penpot/backend/test/backend_tests/scripts_decode_file_data_test.clj
Andrey Antukh 67643340a2 ✨ Partition file_change by file_id
Partition `file_change` by `HASH (file_id)` in 16 partitions, with the
same bounds as `file_data`, so a join between them is resolved partition
by partition. The primary key becomes `(file_id, id)`, which a
partitioned table needs, and every access filters by `file_id`.

The new table carries a `kind` column (1 snapshot, 2 revision) as a
reserve: the queries still tell them apart by the label, and `kind`
takes over when they are rewritten. The payload of a snapshot stays in
`file_data`; the `data` column is kept for reading old `legacy-db`
payloads.

The four snapshot commands require `file-id`, because it prunes the
partition. The objects GC locks and deletes by the pair, in batches. The
old table is kept as `file_change_legacy` as the way back.

The automatic migration refuses to run above 10 GiB or 2M rows and
points to the runbook; big instances partition online with that runbook,
which copies by id, follows the `updated_at` watermark in delta passes
and checks that no row of the source is missing from the target.

Closes #12416

AI-assisted-by: deepseek-v4.1-flash
2026-11-05 22:40:17 +01:00

94 lines
3.7 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns backend-tests.scripts-decode-file-data-test
(:require
[app.common.json :as json]
[app.common.uuid :as uuid]
[app.scripts.decode-file-data :as dfd]
[app.util.blob :as blob]
[backend-tests.helpers :as th]
[clojure.java.io :as io]
[clojure.test :as t])
(:import
java.util.Base64))
(t/use-fixtures :once th/state-init)
(defn- sample-file-data
[]
(let [p1 (uuid/next)
p2 (uuid/next)]
{:pages [p1 p2]
:pages-index
{p1 {:id p1 :name "Accueil1" :objects {:a 1 :b 2}}
p2 {:id p2 :name "Contact" :objects {:c 3}}}
:components {(uuid/next) {:name "comp"}}
:colors {(uuid/next) {:name "red"}}
:typographies {}}))
(t/deftest sanitize-converts-special-types
(let [id (uuid/next)
out (dfd/sanitize {:id id
:kw :pages-index
:when (java.util.Date. 0)
:tags #{:a :b}
:raw (.getBytes "hi" "UTF-8")})]
(t/is (= (str id) (get out "id")))
(t/is (= "pages-index" (get out "kw")))
(t/is (string? (get out "when")))
(t/is (= ["a" "b"] (sort (get out "tags"))))
(t/is (= {"$bytes" (.encodeToString (Base64/getEncoder) (.getBytes "hi" "UTF-8"))}
(get out "raw")))))
(t/deftest sanitize-keeps-integers-as-integers
(t/is (= 2 (dfd/sanitize 2)))
(t/is (= 2.5 (dfd/sanitize 2.5))))
(t/deftest blob-roundtrip-all-versions-to-json
(doseq [version [1 3 4 5]]
(let [data (sample-file-data)
encoded (blob/encode data {:version version})
payload (dfd/blob->json encoded)
parsed (json/decode payload)]
(t/is (map? parsed) (str "version " version " produces a JSON map"))
(t/is (= 2 (count (get parsed "pages"))) (str "version " version " keeps pages")))))
(t/deftest summary-lists-pages-in-order-with-object-counts
(let [data (sample-file-data)
summary (dfd/summarize-data data)
pages (:pages summary)]
(t/is (= 2 (count pages)))
(t/is (= ["Accueil1" "Contact"] (mapv :name pages)))
(t/is (= [2 1] (mapv :objects pages)))
(t/is (= {:pages 2 :components 1 :colors 1 :typographies 0}
(:stats summary)))
(let [pages-json (json/decode (dfd/file-data-pages-json data))]
(t/is (= ["Accueil1" "Contact"] (mapv #(get % "name") pages-json))))))
(t/deftest cli-decodes-blob-file-to-json
(let [data (sample-file-data)
tmp-in (java.io.File/createTempFile "blob" ".bin")
tmp-out (java.io.File/createTempFile "out" ".json")
_ (.deleteOnExit tmp-in)
_ (.deleteOnExit tmp-out)
_ (with-open [os (io/output-stream tmp-in)]
(.write os ^bytes (blob/encode data {:version 5})))
_ (dfd/-main "--input" (.getAbsolutePath tmp-in)
"--output" (.getAbsolutePath tmp-out)
"--format" "summary")
parsed (json/decode (slurp tmp-out))]
(t/is (== 2 (get-in parsed ["stats" "pages"])))
(t/is (= ["Accueil1" "Contact"] (mapv #(get % "name") (get parsed "pages"))))))
(t/deftest get-snapshot-json-fails-when-the-snapshot-does-not-exist
;; The snapshot lookup returns nil when there is no match, and a nil
;; snapshot used to be printed as an empty JSON instead of failing.
(let [file-id (uuid/next)
snapshot-id (uuid/next)]
(t/is (thrown? clojure.lang.ExceptionInfo
(dfd/get-snapshot-json th/*system* file-id snapshot-id)))))