feat: per-annotation schema version + migration framework
Each stored annotation carries :v (schema-version, currently 1). tl.scene gains a transformer registry and migrate fn that chains version upgrades on load; save-group and open-draft stamp the current version. Empty transformer set at v1 — bump schema-version and add a transformer when the model changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7e47ec6a2d
commit
9c4b56dbc6
3 changed files with 40 additions and 8 deletions
|
|
@ -368,7 +368,8 @@
|
|||
;; fresh annotation would reuse a prior gid and clobber it on merge.
|
||||
(assoc-in [:scene :groups (keyword (str "ann-" (random-uuid)))]
|
||||
{:type :annotation :parent (peek (get-in db [:view :stack]))
|
||||
:draft :new :name "" :color "#4e8fc2" :marks []})
|
||||
:draft :new :name "" :color "#4e8fc2" :marks []
|
||||
:v scene/schema-version})
|
||||
(assoc-in [:view :pt] :new))))
|
||||
|
||||
(rf/reg-event-db ::edit-draft (fn [db [_ gid]] (-> db (assoc-in [:scene :groups gid :draft] :edit)
|
||||
|
|
@ -399,7 +400,8 @@
|
|||
(rf/reg-event-fx
|
||||
::save-group
|
||||
(fn [{:keys [db]} [_ gid g orig]]
|
||||
(let [g (editable-group g)
|
||||
(let [g (cond-> (editable-group g)
|
||||
(= :annotation (:type g)) (assoc :v scene/schema-version))
|
||||
patch (group-patch orig g)
|
||||
root? (nil? (:parent g)) ; the root timeline persists whole
|
||||
id (get-in db [:project :id])] ; (no diff: it'd lose :type/:marks)
|
||||
|
|
|
|||
|
|
@ -257,16 +257,37 @@
|
|||
(get-in m [:end :ref]) (update-in [:end :ref] keyword)
|
||||
(:track m) (update :track keyword)))
|
||||
|
||||
;; --- schema versioning ----------------------------------------------------
|
||||
;; Every stored annotation carries :v, its schema version. When the data model
|
||||
;; changes, bump schema-version and add a transformer that upgrades the previous
|
||||
;; version's shape to the new one; old annotations migrate forward on load.
|
||||
|
||||
(def schema-version 1) ; current annotation schema — bump on any model change
|
||||
|
||||
;; transformers[n] upgrades a schema-v(n) annotation to v(n+1) (and must set
|
||||
;; :v (n+1)). Empty at v1; e.g. {1 (fn [g] (-> g (rename-key …) (assoc :v 2)))}.
|
||||
(def ^:private transformers {})
|
||||
|
||||
(defn migrate
|
||||
"Upgrade annotation `g` to the current schema-version by chaining transformers.
|
||||
Pre-versioned data (no :v) is treated as the original v1."
|
||||
[g]
|
||||
(loop [g (update g :v #(or % 1))]
|
||||
(if (>= (:v g) schema-version)
|
||||
(assoc g :v schema-version)
|
||||
(recur ((transformers (:v g)) g)))))
|
||||
|
||||
(defn restore-annotations
|
||||
"Re-keywordize the fields that lose their keyword-ness through JSON: a saved
|
||||
annotation round-trips with string :type/:parent and string mark :refs, so
|
||||
put them back before merging into the (keyword-keyed) scene."
|
||||
"Re-keywordize the fields that lose their keyword-ness through JSON (string
|
||||
:type/:parent and mark :refs), then migrate each annotation to the current
|
||||
schema version, before merging into the (keyword-keyed) scene."
|
||||
[anns]
|
||||
(into {} (map (fn [[gid g]]
|
||||
[gid (-> g
|
||||
(update :type keyword)
|
||||
(update :parent keyword)
|
||||
(update :marks #(mapv restore-mark (or % []))))]))
|
||||
(update :marks #(mapv restore-mark (or % [])))
|
||||
migrate)]))
|
||||
anns))
|
||||
|
||||
(defn marks->rows
|
||||
|
|
|
|||
|
|
@ -256,9 +256,9 @@
|
|||
(deftest annotations-survive-json-roundtrip
|
||||
(testing "restore-annotations is the exact inverse of the JSON wire trip — guards
|
||||
against any keyword-valued field (id/ref/type/parent/track) being missed"
|
||||
(let [anns {:ann-p {:type :annotation :parent :root :name "p" :color "#abc" :content "hi"
|
||||
(let [anns {:ann-p {:v 1 :type :annotation :parent :root :name "p" :color "#abc" :content "hi"
|
||||
:marks [{:id :m-1 :start {:ref :clip-a :at 0} :end {:ref :clip-a :at -1} :track :t0}]}
|
||||
:ann-c {:type :annotation :parent :ann-p :name "c"
|
||||
:ann-c {:v 1 :type :annotation :parent :ann-p :name "c"
|
||||
:marks [{:id :m-2 :start {:ref :m-1 :at 0} :end {:ref :m-1 :at 5}}]}}]
|
||||
(is (= anns (s/restore-annotations (json-roundtrip anns)))))))
|
||||
|
||||
|
|
@ -363,6 +363,15 @@
|
|||
(is (contains? gids :b))
|
||||
(is (not (contains? gids :orphan)))))))
|
||||
|
||||
(deftest schema-migration
|
||||
(testing "pre-versioned annotations are treated as v1 (the current schema)"
|
||||
(is (= s/schema-version (:v (s/migrate {:type :annotation :name "x"})))))
|
||||
(testing "migrate is idempotent at the current version"
|
||||
(is (= s/schema-version (:v (s/migrate {:v s/schema-version :name "x"})))))
|
||||
(testing "restore-annotations stamps :v on every annotation"
|
||||
(is (= s/schema-version
|
||||
(:v (get (s/restore-annotations {"a" {:type "annotation" :name "x"}}) "a"))))))
|
||||
|
||||
(deftest seg-point-and-link-local-round-trip
|
||||
(testing "a clip-scoped point resolves back to the same ctx-local frame"
|
||||
(let [seg (some #(when (= :clip-b (:mark %)) %) (s/content-segments base :root))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue