mirror of
https://github.com/penpot/penpot.git
synced 2026-11-07 02:20:36 +00:00
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
94 lines
3.7 KiB
Clojure
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)))))
|