Map valueTypes to SQLite encodings.
In the future, we might add a layer of indirection, hashing values to avoid duplicating storage, or sorting URLs, or handling fulltext indexed values differently, or ...
This commit is contained in:
parent
43423b7d0a
commit
7a90c43a5a
3 changed files with 96 additions and 32 deletions
|
@ -70,14 +70,14 @@
|
||||||
[db]
|
[db]
|
||||||
"TODO: document this interface."))
|
"TODO: document this interface."))
|
||||||
|
|
||||||
;; TODO: handle _?
|
(defn db? [x]
|
||||||
(defn search->sql-clause [pattern]
|
(and (satisfies? IDB x)))
|
||||||
(merge
|
|
||||||
{:select [:*] ;; e :a :v :tx] ;; TODO: generalize columns.
|
(defn- row->Datom [schema row]
|
||||||
:from [:datoms]}
|
(let [e (:e row)
|
||||||
(if-not (empty? pattern)
|
a (:a row)
|
||||||
{:where (cons :and (map #(vector := %1 (if (keyword? %2) (str %2) %2)) [:e :a :v :tx] pattern))} ;; TODO: use schema to process v.
|
v (:v row)]
|
||||||
{})))
|
(Datom. e a (ds/<-SQLite schema a v) (:tx row) (:added row))))
|
||||||
|
|
||||||
(defrecord DB [sqlite-connection schema idents current-tx]
|
(defrecord DB [sqlite-connection schema idents current-tx]
|
||||||
;; idents is map {ident -> entid} of known idents. See http://docs.datomic.com/identity.html#idents.
|
;; idents is map {ident -> entid} of known idents. See http://docs.datomic.com/identity.html#idents.
|
||||||
|
@ -94,33 +94,43 @@
|
||||||
|
|
||||||
;; TODO: use q for searching? Have q use this for searching for a single pattern?
|
;; TODO: use q for searching? Have q use this for searching for a single pattern?
|
||||||
(<eavt [db pattern]
|
(<eavt [db pattern]
|
||||||
(go-pair
|
(let [[e a v tx] pattern
|
||||||
;; TODO: find a better expression of this pattern.
|
v (and v (ds/->SQLite schema a v))] ;; We assume e and a are always given.
|
||||||
(let [rows (<? (->>
|
(go-pair
|
||||||
(search->sql-clause pattern)
|
(->>
|
||||||
(sql/format)
|
{:select [:*] ;; e :a :v :tx] ;; TODO: generalize columns.
|
||||||
(s/all-rows (:sqlite-connection db))))]
|
:from [:datoms]
|
||||||
(mapv #(Datom. (:e %) (:a %) (:v %) (:tx %) true) rows)))) ;; TODO: map values according to schema.
|
:where (cons :and (map #(vector := %1 %2) [:e :a :v :tx] (take-while (comp not nil?) [e a v tx])))} ;; Must drop nils.
|
||||||
|
(sql/format)
|
||||||
|
|
||||||
|
(s/all-rows (:sqlite-connection db))
|
||||||
|
(<?)
|
||||||
|
|
||||||
|
(mapv (partial row->Datom (.-schema db))))))) ;; TODO: understand why (schema db) fails.
|
||||||
|
|
||||||
(<avet [db pattern]
|
(<avet [db pattern]
|
||||||
(go-pair
|
(let [[a v] pattern
|
||||||
;; TODO: find a better expression of this pattern.
|
v (ds/->SQLite schema a v)]
|
||||||
(let [[a v] pattern
|
(go-pair
|
||||||
rows (<? (->>
|
(->>
|
||||||
{:select [:*] :from [:datoms] :where [:and [:= :a a] [:= :v v]]}
|
{:select [:*] :from [:datoms] :where [:and [:= :a a] [:= :v v]]}
|
||||||
(sql/format)
|
(sql/format)
|
||||||
(s/all-rows (:sqlite-connection db))))]
|
|
||||||
(mapv #(Datom. (:e %) (:a %) (:v %) (:tx %) true) rows)))) ;; TODO: map values according to schema.
|
(s/all-rows (:sqlite-connection db))
|
||||||
|
(<?)
|
||||||
|
|
||||||
|
(mapv (partial row->Datom (.-schema db))))))) ;; TODO: understand why (schema db) fails.
|
||||||
|
|
||||||
(<apply-datoms [db datoms]
|
(<apply-datoms [db datoms]
|
||||||
(go-pair
|
(go-pair
|
||||||
(let [exec (partial s/execute! (:sqlite-connection db))]
|
(let [exec (partial s/execute! (:sqlite-connection db))]
|
||||||
;; TODO: batch insert, batch delete.
|
;; TODO: batch insert, batch delete.
|
||||||
(doseq [datom datoms]
|
(doseq [datom datoms]
|
||||||
(let [[e a v tx added] datom]
|
(let [[e a v tx added] datom
|
||||||
|
v (ds/->SQLite (.-schema db) a v)] ;; TODO: understand why (schema db) fails.
|
||||||
;; Append to transaction log.
|
;; Append to transaction log.
|
||||||
(<? (exec
|
(<? (exec
|
||||||
["INSERT INTO transactions VALUES (?, ?, ?, ?, ?)" e a v tx added]))
|
["INSERT INTO transactions VALUES (?, ?, ?, ?, ?)" e a v tx (if added 1 0)]))
|
||||||
;; Update materialized datom view.
|
;; Update materialized datom view.
|
||||||
(if (.-added datom)
|
(if (.-added datom)
|
||||||
(<? (exec
|
(<? (exec
|
||||||
|
@ -144,9 +154,6 @@
|
||||||
|
|
||||||
(close-db [db] (s/close (.-sqlite-connection db))))
|
(close-db [db] (s/close (.-sqlite-connection db))))
|
||||||
|
|
||||||
(defn db? [x]
|
|
||||||
(and (satisfies? IDB x)))
|
|
||||||
|
|
||||||
(defprotocol IConnection
|
(defprotocol IConnection
|
||||||
(close
|
(close
|
||||||
[conn]
|
[conn]
|
||||||
|
@ -215,7 +222,7 @@
|
||||||
(defn <idents [sqlite-connection]
|
(defn <idents [sqlite-connection]
|
||||||
(go-pair
|
(go-pair
|
||||||
(let [rows (<? (->>
|
(let [rows (<? (->>
|
||||||
{:select [:e :v] :from [:datoms] :where [:= :a ":db/ident"]} ;; TODO: don't stringify?
|
{:select [:e :v] :from [:datoms] :where [:= :a ":db/ident"]} ;; TODO: use raw entid.
|
||||||
(sql/format)
|
(sql/format)
|
||||||
(s/all-rows sqlite-connection)))]
|
(s/all-rows sqlite-connection)))]
|
||||||
(into {} (map #(-> {(keyword (:v %)) (:e %)})) rows))))
|
(into {} (map #(-> {(keyword (:v %)) (:e %)})) rows))))
|
||||||
|
@ -228,7 +235,7 @@
|
||||||
(go-pair
|
(go-pair
|
||||||
(when-not (= sqlite-schema/current-version (<? (sqlite-schema/<ensure-current-version sqlite-connection)))
|
(when-not (= sqlite-schema/current-version (<? (sqlite-schema/<ensure-current-version sqlite-connection)))
|
||||||
(raise "Could not ensure current SQLite schema version."))
|
(raise "Could not ensure current SQLite schema version."))
|
||||||
(let [idents (into (<? (<idents sqlite-connection)) {:db/txInstant 100 :db/ident 101 :x 102 :y 103 :name 104 :aka 105})] ;; TODO: pre-populate idents and SQLite tables?
|
(let [idents (into (<? (<idents sqlite-connection)) {:db/txInstant 100 :db/ident 101 :x 102 :y 103 :name 104 :aka 105 :test/kw 106})] ;; TODO: pre-populate idents and SQLite tables?
|
||||||
(map->DB
|
(map->DB
|
||||||
{:sqlite-connection sqlite-connection
|
{:sqlite-connection sqlite-connection
|
||||||
:idents idents
|
:idents idents
|
||||||
|
@ -365,7 +372,7 @@
|
||||||
(vec (for [[op & entity] (:entities report)]
|
(vec (for [[op & entity] (:entities report)]
|
||||||
(into [op] (for [field entity]
|
(into [op] (for [field entity]
|
||||||
(if (lookup-ref? field)
|
(if (lookup-ref? field)
|
||||||
(first (<? (<eavt db field))) ;; TODO improve this
|
(first (<? (<eavt db field))) ;; TODO improve this -- this should be avet, shouldn't it?
|
||||||
field)))))
|
field)))))
|
||||||
(assoc-in report [:entities])))) ;; TODO: meta.
|
(assoc-in report [:entities])))) ;; TODO: meta.
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
;; TODO: consider doing this with a protocol and extending the underlying Clojure(Script) types.
|
;; TODO: consider doing this with a protocol and extending the underlying Clojure(Script) types.
|
||||||
(def value-type-map
|
(def value-type-map
|
||||||
{:db.type/ref { :valid? #(and (integer? %) (pos? %)) :->SQLite identity :<-SQLite identity }
|
{:db.type/ref { :valid? #(and (integer? %) (pos? %)) :->SQLite identity :<-SQLite identity }
|
||||||
:db.type/keyword { :valid? keyword? :->SQLite name :<-SQLite keyword }
|
:db.type/keyword { :valid? keyword? :->SQLite str :<-SQLite #(keyword (subs % 1)) }
|
||||||
:db.type/string { :valid? string? :->SQLite identity :<-SQLite identity }
|
:db.type/string { :valid? string? :->SQLite identity :<-SQLite identity }
|
||||||
:db.type/boolean { :valid? #(instance? Boolean %) :->SQLite #(if % 1 0) :<-SQLite #(if (= % 1) true false) }
|
:db.type/boolean { :valid? #(instance? Boolean %) :->SQLite #(if % 1 0) :<-SQLite #(if (= % 1) true false) }
|
||||||
:db.type/integer { :valid? integer? :->SQLite identity :<-SQLite identity }
|
:db.type/integer { :valid? integer? :->SQLite identity :<-SQLite identity }
|
||||||
|
@ -104,6 +104,31 @@
|
||||||
(raise "Unknown attribute " attr ", expected one of " (sorted-set (keys schema))
|
(raise "Unknown attribute " attr ", expected one of " (sorted-set (keys schema))
|
||||||
{:error :schema/valueType, :attribute attr}))))
|
{:error :schema/valueType, :attribute attr}))))
|
||||||
|
|
||||||
|
(defn ->SQLite [schema attr value]
|
||||||
|
{:pre [(schema? schema)]}
|
||||||
|
(let [schema (.-schema schema)]
|
||||||
|
(if-let [valueType (get-in schema [attr :db/valueType])]
|
||||||
|
(if-let [valid? (get-in value-type-map [valueType :valid?])]
|
||||||
|
(if (valid? value)
|
||||||
|
((get-in value-type-map [valueType :->SQLite]) value)
|
||||||
|
(raise "Invalid value for attribute " attr ", expected " valueType " but got " value
|
||||||
|
{:error :schema/valueType, :attribute attr, :value value}))
|
||||||
|
(raise "Unknown valueType for attribute " attr ", expected one of " (sorted-set (keys value-type-map))
|
||||||
|
{:error :schema/valueType, :attribute attr}))
|
||||||
|
(raise "Unknown attribute " attr ", expected one of " (sorted-set (keys schema))
|
||||||
|
{:error :schema/valueType, :attribute attr}))))
|
||||||
|
|
||||||
|
(defn <-SQLite [schema attr value]
|
||||||
|
{:pre [(schema? schema)]}
|
||||||
|
(let [schema (.-schema schema)]
|
||||||
|
(if-let [valueType (get-in schema [attr :db/valueType])]
|
||||||
|
(if-let [<-SQLite (get-in value-type-map [valueType :<-SQLite])]
|
||||||
|
(<-SQLite value)
|
||||||
|
(raise "Unknown valueType for attribute " attr ", expected one of " (sorted-set (keys value-type-map))
|
||||||
|
{:error :schema/valueType, :attribute attr}))
|
||||||
|
(raise "Unknown attribute " attr ", expected one of " (sorted-set (keys schema))
|
||||||
|
{:error :schema/valueType, :attribute attr}))))
|
||||||
|
|
||||||
(defn- validate-schema [schema]
|
(defn- validate-schema [schema]
|
||||||
(doseq [[a kv] schema]
|
(doseq [[a kv] schema]
|
||||||
(when-not (:db/valueType kv)
|
(when-not (:db/valueType kv)
|
||||||
|
|
|
@ -184,3 +184,35 @@
|
||||||
|
|
||||||
(finally
|
(finally
|
||||||
(<? (dm/close-db db)))))))
|
(<? (dm/close-db db)))))))
|
||||||
|
|
||||||
|
(deftest-async test-valueType-keyword
|
||||||
|
(with-tempfile [t (tempfile)]
|
||||||
|
(let [c (<? (s/<sqlite-connection t))
|
||||||
|
db (<? (dm/<db-with-sqlite-connection c
|
||||||
|
(merge test-schema {:test/kw {:db/unique :db.unique/identity
|
||||||
|
:db/valueType :db.type/keyword}})))
|
||||||
|
conn (dm/connection-with-db db)
|
||||||
|
now -1]
|
||||||
|
(try
|
||||||
|
(let [report (<? (dm/<transact! conn [[:db/add (dm/id-literal :db.part/user -1) :test/kw :test/kw1]] now))
|
||||||
|
eid (get-in report [:tempids (dm/id-literal :db.part/user -1)])]
|
||||||
|
(is (= (<? (<datoms db))
|
||||||
|
#{[eid :test/kw ":test/kw1"]})) ;; Value is raw.
|
||||||
|
|
||||||
|
(testing "Adding the same value compares existing values correctly."
|
||||||
|
(<? (dm/<transact! conn [[:db/add eid :test/kw :test/kw1]] now))
|
||||||
|
(is (= (<? (<datoms db))
|
||||||
|
#{[eid :test/kw ":test/kw1"]}))) ;; Value is raw.
|
||||||
|
|
||||||
|
(testing "Upserting retracts existing value correctly."
|
||||||
|
(<? (dm/<transact! conn [[:db/add eid :test/kw :test/kw2]] now))
|
||||||
|
(is (= (<? (<datoms db))
|
||||||
|
#{[eid :test/kw ":test/kw2"]}))) ;; Value is raw.
|
||||||
|
|
||||||
|
(testing "Retracting compares values correctly."
|
||||||
|
(<? (dm/<transact! conn [[:db/retract eid :test/kw :test/kw2]] now))
|
||||||
|
(is (= (<? (<datoms db))
|
||||||
|
#{}))))
|
||||||
|
|
||||||
|
(finally
|
||||||
|
(<? (dm/close-db db)))))))
|
||||||
|
|
Loading…
Reference in a new issue