Skip to content

Commit 0a40283

Browse files
committed
datascript.serialize
1 parent e513e13 commit 0a40283

File tree

4 files changed

+226
-7
lines changed

4 files changed

+226
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# WIP
2+
3+
- New namespace: `datascript.serialize`. Prepares database for fast to/from JSON serialization
4+
15
# 1.1.0
26

37
- [ Breaking ] Remove `ISeqable` from `DB`/`FilteredDB` as it was breaking `keys` #393

src/datascript/db.cljc

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,17 @@
150150

151151
(defprotocol IDatom
152152
(datom-tx [this])
153-
(datom-added [this]))
153+
(datom-added [this])
154+
(datom-get-idx [this])
155+
(datom-set-idx [this value]))
154156

155-
(deftype Datom #?(:clj [^int e a v ^int tx ^:unsynchronized-mutable ^int _hash]
156-
:cljs [^number e a v ^number tx ^:mutable ^number _hash])
157+
(deftype Datom #?(:clj [^int e a v ^int tx ^:unsynchronized-mutable ^int idx ^:unsynchronized-mutable ^int _hash]
158+
:cljs [^number e a v ^number tx ^:mutable ^number idx ^:mutable ^number _hash])
157159
IDatom
158160
(datom-tx [d] (if (pos? tx) tx (- tx)))
159161
(datom-added [d] (pos? tx))
162+
(datom-get-idx [_] idx)
163+
(datom-set-idx [_ value] (set! idx (int value)))
160164

161165
#?@(:cljs
162166
[IHash
@@ -224,9 +228,9 @@
224228
#?(:cljs (goog/exportSymbol "datascript.db.Datom" Datom))
225229

226230
(defn ^Datom datom
227-
([e a v] (Datom. e a v tx0 0))
228-
([e a v tx] (Datom. e a v tx 0))
229-
([e a v tx added] (Datom. e a v (if added tx (- tx)) 0)))
231+
([e a v] (Datom. e a v tx0 0 0))
232+
([e a v tx] (Datom. e a v tx 0 0))
233+
([e a v tx added] (Datom. e a v (if added tx (- tx)) 0 0)))
230234

231235
(defn datom? [x] (instance? Datom x))
232236

src/datascript/externs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ datascript.db.Datom.prototype.e;
99
datascript.db.Datom.prototype.a;
1010
datascript.db.Datom.prototype.v;
1111
datascript.db.Datom.prototype.tx;
12-
12+
datascript.db.Datom.prototype.idx;
1313

1414
datascript.impl = {};
1515
datascript.impl.entity = {};

src/datascript/serialize.cljc

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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

Comments
 (0)