Record the applied transforms in GraphMeta

Ingestion is a partial port, so a graph this backend writes has had only
some of the pipeline applied. Rather than have a reader infer which from
a build version, the build writes it down: `GraphMeta.transforms` names
every transform id applied, projection-time denormalizations included,
and the parity consumer computes the complement and runs only that.

The ids cross a language boundary as data, so they are kebab-case
strings rather than keywords and must stay byte-identical to the
consumer's own list.

This stays off `graph-backend`. Nothing in this repository reads the
column, and an unread column is weight a reviewer is right to question.

AI-assisted-by: mixed models
This commit is contained in:
Álvaro Tejero Cantero 2026-08-07 01:43:21 +02:00
parent 13229dd94e
commit 6bd1af4f4a
No known key found for this signature in database
2 changed files with 43 additions and 7 deletions

View File

@ -59,9 +59,11 @@
(graph.arrow/load-projection! conn {:nodes nodes :edges edges} allocator) (graph.arrow/load-projection! conn {:nodes nodes :edges edges} allocator)
(ladybug/exec-on-connection! conn ["CHECKPOINT;"]) (ladybug/exec-on-connection! conn ["CHECKPOINT;"])
(let [transforms (projection.transforms/apply-transforms! system conn data file)] (let [transforms (projection.transforms/apply-transforms! system conn data file)]
;; Written last: its presence doubles as the build-complete marker. ;; Written last: its presence doubles as the build-complete marker,
(graph.meta/write! conn {:file-id file-id ;; and it is what the parity consumer reads to know what is left.
:revn (:revn file)}) (graph.meta/write! conn {:file-id file-id
:revn (:revn file)
:transform-ids (:ids transforms)})
{:file-id file-id {:file-id file-id
:revn (:revn file) :revn (:revn file)
:name (or (:name data) (:name file)) :name (or (:name data) (:name file))

View File

@ -5,13 +5,20 @@
;; Copyright (c) KALEIDOS INC Sucursal en España SL ;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.graph.meta (ns app.graph.meta
"`GraphMeta`: the graph's own account of who built it and from what. "`GraphMeta`: the graph's own account of who built it, from what, and what
it did.
A projected graph is a cache of a file at a revision, built by a known A projected graph is a cache of a file at a revision, built by a known
schema. The row records both, so a reader can decide whether to reuse the schema. The row records both, so a reader can decide whether to reuse the
database or rebuild it: a `schema_version` that no longer matches the database or rebuild it: a `schema_version` that no longer matches the
registry, or a `source_revn` behind the file's, means the cache is stale. registry, or a `source_revn` behind the file's, means the cache is stale.
Ingestion is a partial port, so a graph can arrive with any subset of the
pipeline applied. `transforms` names what this build did, and the parity
consumer computes the complement and runs only that in Python. The ids cross
a language boundary as data, so they are kebab-case strings rather than
keywords and must stay byte-identical to the consumer's own list.
The row is written *last* in a build, so its presence also marks the build The row is written *last* in a build, so its presence also marks the build
complete. complete.
@ -21,7 +28,8 @@
(:require (:require
[app.common.time :as ct] [app.common.time :as ct]
[app.graph.ladybug :as ladybug] [app.graph.ladybug :as ladybug]
[app.graph.schema.nodes :as nodes]) [app.graph.schema.nodes :as nodes]
[clojure.string :as str])
(:import (:import
com.ladybugdb.Connection)) com.ladybugdb.Connection))
@ -41,12 +49,37 @@
"`producer_version` STRING, " "`producer_version` STRING, "
"`schema_version` STRING, " "`schema_version` STRING, "
"`source_revn` INT64, " "`source_revn` INT64, "
"`transforms` STRING[], "
"`built_at` TIMESTAMP, " "`built_at` TIMESTAMP, "
"PRIMARY KEY (`source_file_id`));")) "PRIMARY KEY (`source_file_id`));"))
(def projection-transforms
"Transform ids this backend satisfies while *projecting*, before any
transform pass runs.
A reader cares whether the result is in the graph, not how it got there. The
consumer derives `IsChildOf` from persisted `shapes` arrays in a later pass;
`app.graph.project.document/project-shape-ids` emits the edges during the
tree walk. Same id, same observable graph. The two denormalizations are the
same case."
["add-document"
"link-contained-shapes"
"denormalize-page-id"
"denormalize-component-id"])
(defn- format-transforms
[ids]
(str "[" (->> (sort (distinct ids))
(map ladybug/format-string)
(str/join ", "))
"]"))
(defn write! (defn write!
"Record what this build produced for `file-id`." "Record what this build produced for `file-id`.
[^Connection conn {:keys [file-id revn]}]
`transform-ids` are the ids applied *on top of* `projection-transforms`, so
a caller only names what its transform pass did."
[^Connection conn {:keys [file-id revn transform-ids]}]
(ladybug/exec-on-connection! conn [ddl]) (ladybug/exec-on-connection! conn [ddl])
(ladybug/exec-on-connection! (ladybug/exec-on-connection!
conn conn
@ -55,5 +88,6 @@
"m.producer_version = " (ladybug/format-string (or (System/getenv "PENPOT_BUILD") "devenv")) ", " "m.producer_version = " (ladybug/format-string (or (System/getenv "PENPOT_BUILD") "devenv")) ", "
"m.schema_version = " (ladybug/format-string nodes/schema-version) ", " "m.schema_version = " (ladybug/format-string nodes/schema-version) ", "
"m.source_revn = " (ladybug/format-int (or revn 0)) ", " "m.source_revn = " (ladybug/format-int (or revn 0)) ", "
"m.transforms = " (format-transforms (concat projection-transforms transform-ids)) ", "
"m.built_at = " (ladybug/format-timestamp (ct/now)) ";")])) "m.built_at = " (ladybug/format-timestamp (ct/now)) ";")]))