🐛 Fix referential integrity data in old files (#9771)

This commit is contained in:
Belén Albeza 2026-05-21 11:18:13 +02:00 committed by GitHub
parent c0e7bfae00
commit 3512a57df7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 111 additions and 1 deletions

View File

@ -38,6 +38,24 @@
(dissoc component :objects))
component)))))
(defn normalize-component-root
"Some old files have shapes with an explicit :component-root false. This is semantically
equivalent to the attribute being absent (instance-root? only treats true as root), but
breaks the subcopy-head? predicate, which expects nil. Remove the explicit false so the
downstream fixers can recognize these shapes as nested copy heads."
[file-data]
(ctf/update-all-shapes
file-data
(fn [shape]
(if (false? (:component-root shape))
(do
(log/warn :msg "Normalizing :component-root false on shape"
:shape-id (:id shape)
:shape-name (:name shape)
:file-id (:id file-data))
{:result :update :updated-shape (dissoc shape :component-root)})
{:result :keep}))))
(defn fix-missing-swap-slots
"Locate shapes that have been swapped (i.e. their shape-ref does not point to the near match) but
they don't have a swap slot. In this case, add one pointing to the near match."

View File

@ -1820,6 +1820,19 @@
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
;; Re-run the 0019 and 0020 fixers after normalizing :component-root.
;; Migrations 0019 and 0020 missed shapes with an explicit :component-root
;; false because subcopy-head? expects nil. Normalize first, then re-run.
(defmethod migrate-data "0022-normalize-component-root-and-resync"
[data _]
(let [libraries (if (:libs data)
(deref (:libs data))
{})]
(-> data
(cfcp/normalize-component-root)
(cfcp/fix-missing-swap-slots libraries)
(cfcp/sync-component-id-with-ref-shape libraries))))
(def available-migrations
(into (d/ordered-set)
["legacy-2"
@ -1898,4 +1911,5 @@
"0018-remove-unneeded-objects-from-components"
"0019-fix-missing-swap-slots"
"0020-sync-component-id-with-near-main"
"0021-fix-shape-svg-attrs"]))
"0021-fix-shape-svg-attrs"
"0022-normalize-component-root-and-resync"]))

View File

@ -114,6 +114,84 @@
(t/is (= expected-diff diff)))))
(t/deftest test-normalize-component-root
(t/testing "nil file should return nil"
(let [file nil
file' (ctf/update-file-data file cfcp/normalize-component-root)]
(t/is (nil? file'))))
(t/testing "empty file should not need any action"
(let [file (thf/sample-file :file1)
file' (ctf/update-file-data file cfcp/normalize-component-root)]
(t/is (empty? (d/map-diff file file')))))
(t/testing "shape with :component-root true should not be modified"
(let [file
(-> (thf/sample-file :file1)
(tho/add-simple-component :component1 :main1-root :main1-child)
(thc/instantiate-component :component1 :copy1-root))
file' (ctf/update-file-data file cfcp/normalize-component-root)]
(t/is (empty? (d/map-diff file file')))))
(t/testing "shape with :component-root false should have it removed"
(let [file
(-> (thf/sample-file :file1)
(tho/add-nested-component-with-copy :component1 :main1-root :main1-child
:component2 :main2-root :nested-head
:copy2-root)
(ths/update-shape :nested-head :component-root false))
file' (ctf/update-file-data file cfcp/normalize-component-root)
shape' (ths/get-shape file' :nested-head)]
(t/is (not (contains? shape' :component-root))))))
(t/deftest test-migration-0022-fixes-component-root-false-mismatch
(t/testing "a nested copy with :component-root false and a stale :component-id is fixed by the full migration sequence"
(let [file
;; Reproduce the legacy corruption: a nested copy head has
;; :component-root false (instead of absent) AND a stale
;; component-id. Migrations 0019/0020 alone don't catch it
;; because subcopy-head? expects nil. After normalize-component-root,
;; sync-component-id-with-ref-shape can repair it.
(-> (thf/sample-file :file1)
(tho/add-nested-component :component1 :main1-root :main1-child
:component2 :main2-root :nested-head)
(thc/instantiate-component :component2 :copy2-root :children-labels [:copy2-nested-head])
(ths/update-shape :copy2-nested-head :component-root false)
(ths/update-shape :copy2-nested-head :component-id (thi/new-id! :wrong-id)))
;; Run the old sequence (0019/0020 without normalization): the bad shape is skipped.
old-sequence
(-> file
(ctf/update-file-data #(cfcp/fix-missing-swap-slots % {}))
(ctf/update-file-data #(cfcp/sync-component-id-with-ref-shape % {})))
old-shape (ths/get-shape old-sequence :copy2-nested-head)
;; Run the new sequence (0022): normalize first, then 0019/0020 logic.
new-sequence
(-> file
(ctf/update-file-data cfcp/normalize-component-root)
(ctf/update-file-data #(cfcp/fix-missing-swap-slots % {}))
(ctf/update-file-data #(cfcp/sync-component-id-with-ref-shape % {})))
new-shape (ths/get-shape new-sequence :copy2-nested-head)]
;; Confirm the old sequence fails to repair (this guards against a future
;; subcopy-head? change accidentally fixing it and masking the regression).
(t/is (= false (:component-root old-shape)))
(t/is (= (thi/id :wrong-id) (:component-id old-shape)))
;; Confirm the new sequence repairs both the explicit false and the bad ref.
(t/is (not (contains? new-shape :component-root)))
(t/is (= (thi/id :component1) (:component-id new-shape))))))
(t/deftest test-fix-missing-swap-slots
(t/testing "nil file should return nil"