mirror of
https://github.com/penpot/penpot.git
synced 2026-08-09 22:38:40 +00:00
🐛 Fix list-column CSV ingest and serialize graph session access
COPY failed on any file with container shapes: list-typed DDL columns (shapes UUID[], points STRING[], strokes JSON[], ...) were JSON-encoded in staging CSVs, which Ladybug's list parser rejects. Write Kuzu list literals instead, typed per column. Also: value->clj no longer crashes on LIST/STRUCT values (binding lacks value_get_value support; fall back to string), and the debug session Connection is now guarded by a per-session lock — it was shared unsynchronized between the msgbus sync loop and HTTP query/export handlers, and one lost DETACH DELETE was observed under concurrent refetch load. Signed-off-by: Álvaro Tejero Cantero <alvorithm@teje.ro>
This commit is contained in:
parent
7bb027e172
commit
8036f4a4c7
@ -51,6 +51,43 @@
|
||||
(coll? v) (csv-escape-string (json/encode v))
|
||||
:else (csv-escape-string (str v))))
|
||||
|
||||
(defn- kuzu-list-element
|
||||
"Format one element of a Ladybug LIST column for CSV COPY. Kuzu parses
|
||||
the (CSV-unquoted) field as a list literal: bare values for UUID/number
|
||||
elements, single-quoted strings (backslash-escaped) for STRING/JSON."
|
||||
[elem-type v]
|
||||
(cond
|
||||
(nil? v)
|
||||
"NULL"
|
||||
|
||||
(contains? #{"STRING" "JSON"} elem-type)
|
||||
(let [s (if (coll? v) (json/encode v) (csv-normalize-string (str v)))]
|
||||
(str "'" (-> s
|
||||
(str/replace "\\" "\\\\")
|
||||
(str/replace "'" "\\'"))
|
||||
"'"))
|
||||
|
||||
:else
|
||||
(str v)))
|
||||
|
||||
(defn- kuzu-list-cell
|
||||
"CSV cell for a LIST-typed column (`UUID[]`, `STRING[]`, `JSON[]`, …).
|
||||
JSON-encoding the collection (as `csv-cell` does) is wrong here: Kuzu
|
||||
expects its own list literal, e.g. `[id1,id2]` with bare elements."
|
||||
[ladybug-type v]
|
||||
(let [elem-type (subs ladybug-type 0 (- (count ladybug-type) 2))
|
||||
elems (if (coll? v) (seq v) [v])]
|
||||
(csv-escape-string
|
||||
(str "[" (str/join "," (map #(kuzu-list-element elem-type %) elems)) "]"))))
|
||||
|
||||
(defn- csv-typed-cell
|
||||
[ladybug-type v]
|
||||
(if (and (some? v)
|
||||
(string? ladybug-type)
|
||||
(str/ends-with? ladybug-type "[]"))
|
||||
(kuzu-list-cell ladybug-type v)
|
||||
(csv-cell v)))
|
||||
|
||||
(defn- cypher-file-path
|
||||
[^File file]
|
||||
(-> (.getAbsolutePath file)
|
||||
@ -59,11 +96,13 @@
|
||||
|
||||
(defn- write-node-csv!
|
||||
[^File file table rows]
|
||||
(let [columns (nodes/column-keys table)]
|
||||
(let [columns (nodes/column-keys table)
|
||||
types (mapv #(nodes/column-ladybug-type table %) columns)]
|
||||
(with-open [w (io/writer file :encoding "UTF-8")]
|
||||
(.write w (str (str/join "," (map name columns)) "\n"))
|
||||
(doseq [row rows]
|
||||
(.write w (str (str/join "," (map #(csv-cell (get row %)) columns))
|
||||
(.write w (str (str/join "," (map (fn [k t] (csv-typed-cell t (get row k)))
|
||||
columns types))
|
||||
"\n"))))))
|
||||
|
||||
(defn- write-edge-csv!
|
||||
|
||||
@ -66,8 +66,9 @@
|
||||
(some-> (get @sessions (session-key profile-id))
|
||||
(as-> current
|
||||
(when (= file-id (:file-id current))
|
||||
(let [result (graph.sync/apply-changes!
|
||||
conn (:index current) changes revn)
|
||||
(let [result (locking (:lock current)
|
||||
(graph.sync/apply-changes!
|
||||
conn (:index current) changes revn))
|
||||
sync-at (ct/now)]
|
||||
(swap! sessions assoc-in [(session-key profile-id) :index]
|
||||
(:index result))
|
||||
@ -152,8 +153,12 @@
|
||||
:skip-validation? true)
|
||||
index (graph.sync/build-index file-id (:revn meta) (:projection meta))
|
||||
session
|
||||
;; :lock serializes access to the shared Connection between the
|
||||
;; msgbus sync loop (writes) and HTTP handlers (reads); the Java
|
||||
;; binding gives no thread-safety guarantee for one Connection.
|
||||
(-> {:db db
|
||||
:conn conn
|
||||
:lock (Object.)
|
||||
:file-id file-id
|
||||
:meta meta
|
||||
:index index
|
||||
@ -174,9 +179,10 @@
|
||||
(ex/raise :type :validation
|
||||
:code :missing-query
|
||||
:hint "cypher query is required"))
|
||||
(if-let [{:keys [conn]} (get @sessions (session-key profile-id))]
|
||||
(-> (ladybug/query-on-connection! conn statement)
|
||||
format-query-result)
|
||||
(if-let [{:keys [conn lock]} (get @sessions (session-key profile-id))]
|
||||
(locking (or lock ::no-lock)
|
||||
(-> (ladybug/query-on-connection! conn statement)
|
||||
format-query-result))
|
||||
(ex/raise :type :not-found
|
||||
:code :graph-session-not-loaded
|
||||
:hint "load a file graph before running queries")))
|
||||
@ -220,14 +226,15 @@
|
||||
loaded. Queries the Ladybug database (not the sync index) so the view
|
||||
reflects actual DB state, including drift."
|
||||
[profile-id]
|
||||
(when-let [{:keys [conn file-id index]} (get @sessions (session-key profile-id))]
|
||||
(let [{:keys [nodes] nodes-truncated? :truncated?} (export-nodes conn)
|
||||
{:keys [edges] edges-truncated? :truncated?} (export-edges conn)]
|
||||
{:file-id (str file-id)
|
||||
:revn (:revn index)
|
||||
:truncated (boolean (or nodes-truncated? edges-truncated?))
|
||||
:nodes nodes
|
||||
:edges edges})))
|
||||
(when-let [{:keys [conn lock file-id index]} (get @sessions (session-key profile-id))]
|
||||
(locking (or lock ::no-lock)
|
||||
(let [{:keys [nodes] nodes-truncated? :truncated?} (export-nodes conn)
|
||||
{:keys [edges] edges-truncated? :truncated?} (export-edges conn)]
|
||||
{:file-id (str file-id)
|
||||
:revn (:revn index)
|
||||
:truncated (boolean (or nodes-truncated? edges-truncated?))
|
||||
:nodes nodes
|
||||
:edges edges}))))
|
||||
|
||||
(defn console-context
|
||||
"Build template data for the graph debug console page."
|
||||
|
||||
@ -97,7 +97,13 @@
|
||||
(defn- value->clj
|
||||
[^Value value]
|
||||
(when-not (.isNull value)
|
||||
(let [v (.getValue value)]
|
||||
(let [v (try
|
||||
(.getValue value)
|
||||
(catch Exception _
|
||||
;; LIST/STRUCT values are not supported by the binding's
|
||||
;; getValue (\"value_get_value\"); fall back to the textual
|
||||
;; representation so console queries do not crash.
|
||||
(.toString value)))]
|
||||
(cond
|
||||
(instance? Long v) v
|
||||
(instance? Integer v) (long v)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user