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.
|
;; fresh annotation would reuse a prior gid and clobber it on merge.
|
||||||
(assoc-in [:scene :groups (keyword (str "ann-" (random-uuid)))]
|
(assoc-in [:scene :groups (keyword (str "ann-" (random-uuid)))]
|
||||||
{:type :annotation :parent (peek (get-in db [:view :stack]))
|
{: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))))
|
(assoc-in [:view :pt] :new))))
|
||||||
|
|
||||||
(rf/reg-event-db ::edit-draft (fn [db [_ gid]] (-> db (assoc-in [:scene :groups gid :draft] :edit)
|
(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
|
(rf/reg-event-fx
|
||||||
::save-group
|
::save-group
|
||||||
(fn [{:keys [db]} [_ gid g orig]]
|
(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)
|
patch (group-patch orig g)
|
||||||
root? (nil? (:parent g)) ; the root timeline persists whole
|
root? (nil? (:parent g)) ; the root timeline persists whole
|
||||||
id (get-in db [:project :id])] ; (no diff: it'd lose :type/:marks)
|
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)
|
(get-in m [:end :ref]) (update-in [:end :ref] keyword)
|
||||||
(:track m) (update :track 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
|
(defn restore-annotations
|
||||||
"Re-keywordize the fields that lose their keyword-ness through JSON: a saved
|
"Re-keywordize the fields that lose their keyword-ness through JSON (string
|
||||||
annotation round-trips with string :type/:parent and string mark :refs, so
|
:type/:parent and mark :refs), then migrate each annotation to the current
|
||||||
put them back before merging into the (keyword-keyed) scene."
|
schema version, before merging into the (keyword-keyed) scene."
|
||||||
[anns]
|
[anns]
|
||||||
(into {} (map (fn [[gid g]]
|
(into {} (map (fn [[gid g]]
|
||||||
[gid (-> g
|
[gid (-> g
|
||||||
(update :type keyword)
|
(update :type keyword)
|
||||||
(update :parent keyword)
|
(update :parent keyword)
|
||||||
(update :marks #(mapv restore-mark (or % []))))]))
|
(update :marks #(mapv restore-mark (or % [])))
|
||||||
|
migrate)]))
|
||||||
anns))
|
anns))
|
||||||
|
|
||||||
(defn marks->rows
|
(defn marks->rows
|
||||||
|
|
|
||||||
|
|
@ -256,9 +256,9 @@
|
||||||
(deftest annotations-survive-json-roundtrip
|
(deftest annotations-survive-json-roundtrip
|
||||||
(testing "restore-annotations is the exact inverse of the JSON wire trip — guards
|
(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"
|
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}]}
|
: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}}]}}]
|
:marks [{:id :m-2 :start {:ref :m-1 :at 0} :end {:ref :m-1 :at 5}}]}}]
|
||||||
(is (= anns (s/restore-annotations (json-roundtrip anns)))))))
|
(is (= anns (s/restore-annotations (json-roundtrip anns)))))))
|
||||||
|
|
||||||
|
|
@ -363,6 +363,15 @@
|
||||||
(is (contains? gids :b))
|
(is (contains? gids :b))
|
||||||
(is (not (contains? gids :orphan)))))))
|
(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
|
(deftest seg-point-and-link-local-round-trip
|
||||||
(testing "a clip-scoped point resolves back to the same ctx-local frame"
|
(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))
|
(let [seg (some #(when (= :clip-b (:mark %)) %) (s/content-segments base :root))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue