|
| 1 | +(ns datascript.serialize |
| 2 | + (:refer-clojure :exclude [amap]) |
| 3 | + (:require |
| 4 | + [clojure.edn :as edn] |
| 5 | + [clojure.string :as str] |
| 6 | + [datascript.db :as db #?(:cljs :refer-macros :clj :refer) [raise cond+] #?@(:cljs [:refer [Datom]])] |
| 7 | + [me.tonsky.persistent-sorted-set :as set] |
| 8 | + [me.tonsky.persistent-sorted-set.arrays :as arrays]) |
| 9 | + #?(:cljs (:require-macros [datascript.serialize :refer [array dict]])) |
| 10 | + #?(:clj |
| 11 | + (:import |
| 12 | + [datascript.db Datom]))) |
| 13 | + |
| 14 | +(def ^:const marker-kw 0) |
| 15 | +(def ^:const marker-other 1) |
| 16 | + |
| 17 | +(defn- if-cljs [env then else] |
| 18 | + (if (:ns env) then else)) |
| 19 | + |
| 20 | +#?(:clj |
| 21 | + (defmacro array |
| 22 | + "Platform-native array representation (java.util.List on JVM, Array on JS)" |
| 23 | + [& args] |
| 24 | + (if-cljs &env |
| 25 | + (list* 'js* (str "[" (str/join "," (repeat (count args) "~{}")) "]") args) |
| 26 | + `(java.util.List/of ~@args)))) |
| 27 | + |
| 28 | +#?(:clj |
| 29 | + (defmacro dict |
| 30 | + "Platform-native dictionary representation (java.util.Map on JVM, Object on JS)" |
| 31 | + [& args] |
| 32 | + (if-cljs &env |
| 33 | + (list* 'js* (str "{" (str/join "," (repeat (/ (count args) 2) "~{}:~{}")) "}") args) |
| 34 | + `(array-map ~@args)))) |
| 35 | + |
| 36 | +(defn- array-get [d i] |
| 37 | + #?(:clj (.get ^java.util.List d (int i)) |
| 38 | + :cljs (arrays/aget d i))) |
| 39 | + |
| 40 | +(defn- dict-get [d k] |
| 41 | + #?(:clj (.get ^java.util.Map d k) |
| 42 | + :cljs (arrays/aget d k))) |
| 43 | + |
| 44 | +(defn- amap [f xs] |
| 45 | + #?(:clj |
| 46 | + (let [arr (java.util.ArrayList. (count xs))] |
| 47 | + (reduce (fn [idx x] (.add arr (f x)) (inc idx)) 0 xs) |
| 48 | + arr) |
| 49 | + :cljs |
| 50 | + (let [arr (js/Array. (count xs))] |
| 51 | + (reduce (fn [idx x] (arrays/aset arr idx (f x)) (inc idx)) 0 xs) |
| 52 | + arr))) |
| 53 | + |
| 54 | +(defn- amap-indexed [f xs] |
| 55 | + #?(:clj |
| 56 | + (let [arr (java.util.ArrayList. (count xs))] |
| 57 | + (reduce (fn [idx x] (.add arr (f idx x)) (inc idx)) 0 xs) |
| 58 | + arr) |
| 59 | + :cljs |
| 60 | + (let [arr (js/Array. (count xs))] |
| 61 | + (reduce (fn [idx x] (arrays/aset arr idx (f idx x)) (inc idx)) 0 xs) |
| 62 | + arr))) |
| 63 | + |
| 64 | +(defn- attr-comparator |
| 65 | + "Looks for a datom with attribute exactly bigger than the given one" |
| 66 | + [^Datom d1 ^Datom d2] |
| 67 | + (cond |
| 68 | + (nil? (.-a d2)) -1 |
| 69 | + (<= (compare (.-a d1) (.-a d2)) 0) -1 |
| 70 | + true 1)) |
| 71 | + |
| 72 | +(defn- all-attrs |
| 73 | + "All attrs in a DB, distinct, sorted" |
| 74 | + [db] |
| 75 | + (if (empty? (:aevt db)) |
| 76 | + [] |
| 77 | + (loop [attrs (transient [(:a (first (:aevt db)))])] |
| 78 | + (let [attr (nth attrs (dec (count attrs))) |
| 79 | + left (db/datom 0 attr nil) |
| 80 | + right (db/datom db/emax nil nil) |
| 81 | + next-attr (:a (first (set/slice (:aevt db) left right attr-comparator)))] |
| 82 | + (if (some? next-attr) |
| 83 | + (recur (conj! attrs next-attr)) |
| 84 | + (persistent! attrs)))))) |
| 85 | + |
| 86 | +(def ^{:arglists '([kw])} freeze-kw str) |
| 87 | + |
| 88 | +(defn thaw-kw [s] |
| 89 | + (if (str/starts-with? s ":") |
| 90 | + (keyword (subs s 1)) |
| 91 | + s)) |
| 92 | + |
| 93 | +(defn ^:export serializable |
| 94 | + "Converts db into a data structure (not string!) that can be fed to JSON |
| 95 | + serializer of your choice (`js/JSON.stringify` in CLJS, `cheshire.core/generate-string` or |
| 96 | + `jsonista.core/write-value-as-string` in CLJ). |
| 97 | +
|
| 98 | + Options: |
| 99 | +
|
| 100 | + Non-primitive values will be serialized using optional :freeze-fn (`pr-str` by default). |
| 101 | +
|
| 102 | + Serialized structure breakdown: |
| 103 | +
|
| 104 | + count :: number |
| 105 | + tx0 :: number |
| 106 | + max-eid :: number |
| 107 | + max-tx :: number |
| 108 | + schema :: freezed :schema |
| 109 | + attrs :: [keywords ...] |
| 110 | + keywords :: [keywords ...] |
| 111 | + eavt :: [[e a-idx v dtx] ...] |
| 112 | + a-idx :: index in attrs |
| 113 | + v :: (string | number | boolean | [0 <index in keywords>] | [1 <freezed v>]) |
| 114 | + dtx :: tx - tx0 |
| 115 | + aevt :: [<index in eavt> ...] |
| 116 | + avet :: [<index in eavt> ...]" |
| 117 | + ([db] |
| 118 | + (serializable db {})) |
| 119 | + ([db {:keys [freeze-fn] |
| 120 | + :or {freeze-fn pr-str}}] |
| 121 | + (let [attrs (all-attrs db) |
| 122 | + attrs-map (into {} (map vector attrs (range))) |
| 123 | + *kws (volatile! (transient [])) |
| 124 | + *kw-map (volatile! (transient {})) |
| 125 | + write-kw (fn [kw] |
| 126 | + (let [idx (or |
| 127 | + (get @*kw-map kw) |
| 128 | + (let [keywords (vswap! *kws conj! kw) |
| 129 | + idx (dec (count keywords))] |
| 130 | + (vswap! *kw-map assoc! kw idx) |
| 131 | + idx))] |
| 132 | + (array marker-kw idx))) |
| 133 | + write-other (fn [v] (array marker-other (freeze-fn v))) |
| 134 | + write-v (fn [v] |
| 135 | + (cond |
| 136 | + (string? v) v |
| 137 | + #?@(:clj [(ratio? v) (write-other v)]) |
| 138 | + (number? v) v |
| 139 | + (boolean? v) v |
| 140 | + (keyword? v) (write-kw v) |
| 141 | + :else (write-other v))) |
| 142 | + eavt (amap-indexed |
| 143 | + (fn [idx ^Datom d] |
| 144 | + (db/datom-set-idx d idx) |
| 145 | + (let [e (.-e d) |
| 146 | + a (attrs-map (.-a d)) |
| 147 | + v (write-v (.-v d)) |
| 148 | + tx (- (.-tx d) db/tx0)] |
| 149 | + (array e a v tx))) |
| 150 | + (:eavt db)) |
| 151 | + aevt (amap-indexed (fn [_ ^Datom d] (db/datom-get-idx d)) (:aevt db)) |
| 152 | + avet (amap-indexed (fn [_ ^Datom d] (db/datom-get-idx d)) (:avet db)) |
| 153 | + schema (freeze-fn (:schema db)) |
| 154 | + attrs (amap freeze-kw attrs) |
| 155 | + kws (amap freeze-kw (persistent! @*kws))] |
| 156 | + (dict |
| 157 | + "count" (count (:eavt db)) |
| 158 | + "tx0" db/tx0 |
| 159 | + "max-eid" (:max-eid db) |
| 160 | + "max-tx" (:max-tx db) |
| 161 | + "schema" schema |
| 162 | + "attrs" attrs |
| 163 | + "keywords" kws |
| 164 | + "eavt" eavt |
| 165 | + "aevt" aevt |
| 166 | + "avet" avet)))) |
| 167 | + |
| 168 | +(defn ^:export from-serializable |
| 169 | + "Creates db from a data structure (not string!) produced by serializable. |
| 170 | +
|
| 171 | + Non-primitive values will be deserialized using optional :thaw-fn |
| 172 | + (`clojure.edn/read-string` by default). |
| 173 | +
|
| 174 | + :thaw-fn must match :freeze-fn from serializable." |
| 175 | + ([serializable] |
| 176 | + (from-serializable serializable {})) |
| 177 | + ([serializable {:keys [thaw-fn] |
| 178 | + :or {thaw-fn edn/read-string}}] |
| 179 | + (let [tx0 (dict-get serializable "tx0") |
| 180 | + schema (thaw-fn (dict-get serializable "schema")) |
| 181 | + _ (#'db/validate-schema schema) |
| 182 | + attrs (->> (dict-get serializable "attrs") (mapv thaw-kw)) |
| 183 | + keywords (->> (dict-get serializable "keywords") (mapv thaw-kw)) |
| 184 | + eavt (->> (dict-get serializable "eavt") |
| 185 | + (amap (fn [arr] |
| 186 | + (let [e (array-get arr 0) |
| 187 | + a (nth attrs (array-get arr 1)) |
| 188 | + v (array-get arr 2) |
| 189 | + v (cond |
| 190 | + (number? v) v |
| 191 | + (string? v) v |
| 192 | + (boolean? v) v |
| 193 | + (arrays/array? v) |
| 194 | + (let [marker (array-get v 0)] |
| 195 | + (case marker |
| 196 | + marker-kw (array-get keywords (array-get v 1)) |
| 197 | + marker-other (thaw-fn (array-get v 1))))) |
| 198 | + tx (+ tx0 (array-get arr 3))] |
| 199 | + (db/datom e a v tx)))) |
| 200 | + #?(:clj .toArray)) |
| 201 | + aevt (some->> (dict-get serializable "aevt") (amap #(arrays/aget eavt %)) #?(:clj .toArray)) |
| 202 | + avet (some->> (dict-get serializable "avet") (amap #(arrays/aget eavt %)) #?(:clj .toArray))] |
| 203 | + (db/map->DB |
| 204 | + {:schema schema |
| 205 | + :rschema (#'db/rschema (merge db/implicit-schema schema)) |
| 206 | + :eavt (set/from-sorted-array db/cmp-datoms-eavt eavt) |
| 207 | + :aevt (set/from-sorted-array db/cmp-datoms-aevt aevt) |
| 208 | + :avet (set/from-sorted-array db/cmp-datoms-avet avet) |
| 209 | + :max-eid (dict-get serializable "max-eid") |
| 210 | + :max-tx (dict-get serializable "max-tx") |
| 211 | + :hash (atom 0)})))) |
0 commit comments