diff --git a/src/datomish/db.cljc b/src/datomish/db.cljc index 83e1cfbd..b0c61a6d 100644 --- a/src/datomish/db.cljc +++ b/src/datomish/db.cljc @@ -209,7 +209,15 @@ (defn id-literal? [x] (and (instance? TempId x))) -(defrecord TxReport [db-before db-after entities tx-data tempids]) +(defrecord TxReport [db-before ;; The DB before the transaction. + db-after ;; The DB after the transaction. + entities ;; The set of entities (like [:db/add e a v tx]) processed. + tx-data ;; The set of datoms applied to the database, like (Datom. e a v tx added). + tempids ;; The map from id-literal -> numeric entid. + added-parts ;; The set of parts added during the transaction via :db.part/db :db.install/part. + added-idents ;; The map of idents -> entid added during the transaction, via e :db/ident ident. + added-attributes ;; The map of schema attributes (ident -> schema fragment) added during the transaction, via :db.part/db :db.install/attribute. + ]) (defn- report? [x] (and (instance? TxReport x))) @@ -234,8 +242,10 @@ ;; TODO: implement support for DB parts? (def tx0 0x2000000) -(def default-schema - { +(def ^{:private true} bootstrap-symbolic-schema + {:db/ident {:db/valueType :db.type/keyword + :db/cardinality :db.cardinality/one + :db/unique :db.unique/identity} :db.install/partition {:db/valueType :db.type/ref :db/cardinality :db.cardinality/many} :db.install/valueType {:db/valueType :db.type/ref @@ -247,10 +257,7 @@ ;; :db/cardinality :db.cardinality/many} :db/txInstant {:db/valueType :db.type/integer :db/cardinality :db.cardinality/one - :db/index true} - :db/ident {:db/valueType :db.type/keyword - :db/cardinality :db.cardinality/one - :db/unique :db.unique/identity} + } ;; :db/index true} TODO: Handle this using SQLite protocol. :db/valueType {:db/valueType :db.type/ref :db/cardinality :db.cardinality/one} :db/cardinality {:db/valueType :db.type/ref @@ -267,13 +274,90 @@ :db/cardinality :db.cardinality/one} }) +(def ^{:private true} bootstrap-idents + {:db/ident 1 + :db.part/db 2 + :db/txInstant 3 + :db.install/partition 4 + :db.install/valueType 5 + :db.install/attribute 6 + :db/valueType 7 + :db/cardinality 8 + :db/unique 9 + :db/isComponent 10 + :db/index 11 + :db/fulltext 12 + :db/noHistory 13 + :db/add 14 + :db/retract 15 + :db.part/tx 16 + :db.part/user 17 + :db/excise 18 + :db.excise/attrs 19 + :db.excise/beforeT 20 + :db.excise/before 21 + :db.alter/attribute 22 + :db.type/ref 23 + :db.type/keyword 24 + :db.type/integer 25 ;; TODO: :db.type/long, to match Datomic? + :db.type/string 26 + :db.type/boolean 27 + :db.type/instant 28 + :db.type/bytes 29 + :db.cardinality/one 30 + :db.cardinality/many 31 + :db.unique/value 32 + :db.unique/identity 33}) + +(defn- bootstrap-tx-data [] + (concat + (map (fn [[ident entid]] [:db/add entid :db/ident ident]) bootstrap-idents) + (map (fn [[ident attrs]] (assoc attrs :db/id ident)) bootstrap-symbolic-schema) + (map (fn [[ident attrs]] [:db/add :db.part/db :db.install/attribute (get bootstrap-idents ident)]) bootstrap-symbolic-schema) ;; TODO: fail if nil. + )) + (defn (integer entid), like {:db/ident 0}." + + (let [<-SQLite (get-in ds/value-type-map [:db.type/keyword :<-SQLite])] ;; TODO: make this a protocol. + (go-pair + (let [rows (> + {:select [:ident :entid] :from [:idents]} + (sql/format) + (s/all-rows sqlite-connection)))] + (into {} (map (fn [row] [(<-SQLite (:ident row)) (:entid row)])) rows))))) + +(defn > - {:select [:e :v] :from [:datoms] :where [:= :a ":db/ident"]} ;; TODO: use raw entid. - (sql/format) - (s/all-rows sqlite-connection)))] - (into {} (map #(-> {(keyword (:v %)) (:e %)})) rows)))) + (let [rows ( (map (keyword attribute -> keyword value)), like + {:db/ident {:db/cardinality :db.cardinality/one}}." + + (let [<-SQLite (get-in ds/value-type-map [:db.type/keyword :<-SQLite])] ;; TODO: make this a protocol. + (go-pair + (->> + (->> + {:select [:ident :attr :value] :from [:schema]} + (sql/format) + (s/all-rows sqlite-connection)) + (DB - {:sqlite-connection sqlite-connection - :idents idents - :symbolic-schema symbolic-schema - :schema (ds/schema (into {} (map (fn [[k v]] [(k idents) v]) symbolic-schema))) - :current-tx tx0}))))) + + (let [current-tx (= current-tx 0) + current-tx (max current-tx tx0)] + (when-not bootstrapped + ;; We need to bootstrap the DB. + (let [fail-alter-ident (fn [old new] (if-not (= old new) + (raise "Altering idents is not yet supported, got " new " altering existing ident " old + {:error :schema/alter-idents :old old :new new}) + new)) + fail-alter-attr (fn [old new] (if-not (= old new) + (raise "Altering schema attributes is not yet supported, got " new " altering existing schema attribute " old + {:error :schema/alter-schema :old old :new new}) + new))] + (-> (map->DB + {:sqlite-connection sqlite-connection + :idents bootstrap-idents + :symbolic-schema bootstrap-symbolic-schema + :schema (ds/schema (into {} (map (fn [[k v]] [(k bootstrap-idents) v]) bootstrap-symbolic-schema))) ;; TODO: fail if ident missing. + :current-tx current-tx}) + ;; We use DB + {:sqlite-connection sqlite-connection + :idents idents + :symbolic-schema symbolic-schema + :schema (ds/schema (into {} (map (fn [[k v]] [(k idents) v]) symbolic-schema))) ;; TODO: fail if ident missing. + :current-tx (inc current-tx)})))))) (defn connection-with-db [db] (map->Connection {:current-db (atom db)})) @@ -688,22 +812,22 @@ (defn- is-ident? [db [_ a & _]] (= a (get-in db [:idents :db/ident]))) -(defn process-db-ident-assertions +(defn collect-db-ident-assertions "Transactions may add idents, install new partitions, and install new schema attributes. - Handle :db/ident assertions here." + Collect :db/ident assertions into :added-idents here." [db report] {:pre [(db? db) (report? report)]} ;; TODO: use q to filter the report! - (let [original-db db + (let [original-report report tx-data (:tx-data report) original-ident-assertions (filter (partial is-ident? db) tx-data)] - (loop [db original-db + (loop [report original-report ident-assertions original-ident-assertions] (let [[ia & ias] ident-assertions] (cond (nil? ia) - db + report (not (:added ia)) (raise "Retracting a :db/ident is not yet supported, got " ia @@ -713,13 +837,8 @@ :else ;; Added. (let [ident (:v ia)] - ;; TODO: accept re-assertions? - (when (get-in db [:idents ident]) - (raise "Re-asserting a :db/ident is not yet supported, got " ia - {:error :schema/idents - :op ia })) (if (keyword? ident) - (recur (assoc-in db [:idents ident] (:e ia)) ias) + (recur (assoc-in report [:added-idents ident] (:e ia)) ias) (raise "Cannot assert a :db/ident with a non-keyword value, got " ia {:error :schema/idents :op ia })))))))) @@ -735,51 +854,91 @@ (symbolicate tx) added))) -(defn process-db-install-assertions +(defn collect-db-install-assertions "Transactions may add idents, install new partitions, and install new schema attributes. - Handle [:db.part/db :db.install/attribute] assertions here." + Collect [:db.part/db :db.install/attribute] assertions here." [db report] {:pre [(db? db) (report? report)]} ;; TODO: be more efficient; symbolicating each datom is expensive! (let [datoms (map (partial symbolicate-datom db) (:tx-data report)) - schema-fragment (datomish.schema-changes/datoms->schema-fragment datoms) - fail (fn [old new] (raise "Altering schema elements is not yet supported, got " new " altering existing schema element " old - {:error :schema/alter-schema :old old :new new}))] + schema-fragment (datomish.schema-changes/datoms->schema-fragment datoms)] + (assoc-in report [:added-attributes] schema-fragment))) - (if (empty? schema-fragment) - db - (let [symbolic-schema (merge-with fail (:symbolic-schema db) schema-fragment) - schema (ds/schema (into {} (map (fn [[k v]] [(k (idents db)) v]) symbolic-schema)))] - (assoc db - :symbolic-schema symbolic-schema - :schema schema))))) - -(defn TxReport - {:db-before db - :db-after db - ;; :current-tx current-tx - :entities tx-data - :tx-data [] - :tempids {}}))) - db-after (-> - db + (let [->SQLite (get-in ds/value-type-map [:db.type/keyword :->SQLite]) ;; TODO: make this a protocol. + exec (partial s/execute! (:sqlite-connection db))] + ;; TODO: batch insert. + (doseq [[ident entid] added-idents] + (SQLite ident) entid])))) + db)) - (SQLite (get-in ds/value-type-map [:db.type/keyword :->SQLite]) ;; TODO: make this a protocol. + exec (partial s/execute! (:sqlite-connection db))] + ;; TODO: batch insert. + (doseq [[ident attr-map] fragment] + (doseq [[attr value] attr-map] + (SQLite ident) (->SQLite attr) (->SQLite value)]))))) + db)) - (> + (map->TxReport + {:db-before db + :db-after db + ;; :current-tx current-tx + :entities tx-data + :tx-data [] + :tempids {} + :added-parts {} + :added-idents {} + :added-attributes {} + }) - (process-db-ident-assertions report) + ( + db + + ( report (assoc-in [:db-after] db-after))))) +(defn- ident; fail if not possible. 5. Return the map, with ident keys. @@ -55,6 +56,6 @@ db-avs (into {} (map ->av (filter db-*? datoms)))] ;; TODO: get ident from existing datom, to allow [:db.part/db :db.install/attribute existing-id]. (if-let [ident (:db/ident db-avs)] - [ident db-avs] + [ident (dissoc db-avs :db/ident)] (raise ":db.install/attribute requires :db/ident, got " db-avs " for " e {:error :schema/db-install :op db-avs})))))))))) diff --git a/src/datomish/sqlite_schema.cljc b/src/datomish/sqlite_schema.cljc index f107b211..cc6066f4 100644 --- a/src/datomish/sqlite_schema.cljc +++ b/src/datomish/sqlite_schema.cljc @@ -25,11 +25,24 @@ "CREATE INDEX aevt ON datoms (a, e)" ;; No v -- that's an opt-in index. "CREATE UNIQUE INDEX avet ON datoms (a, v, e) WHERE index_avet = 1" ;; Opt-in index: only if a has :db/index true. "CREATE UNIQUE INDEX vaet ON datoms (v, a, e) WHERE index_vaet = 1" ;; Opt-in index: only if a has :db/valueType :db.type/ref - "CREATE UNIQUE INDEX unique_value ON datoms (v) WHERE unique_value = 1" ;; TODO. - "CREATE UNIQUE INDEX unique_identity ON datoms (a, v) WHERE unique_identity = 1" ;; TODO. + + ;; TODO: possibly remove this index. :db.unique/value should be asserted by the transactor in + ;; all cases, but the index may speed up some of SQLite's query planning. For now, it services + ;; to validate the transactor implementation. + "CREATE UNIQUE INDEX unique_value ON datoms (v) WHERE unique_value = 1" + ;; TODO: possibly remove this index. :db.unique/identity should be asserted by the transactor in + ;; all cases, but the index may speed up some of SQLite's query planning. For now, it serves to + ;; validate the transactor implementation. + "CREATE UNIQUE INDEX unique_identity ON datoms (a, v) WHERE unique_identity = 1" + "CREATE TABLE transactions (e INTEGER NOT NULL, a SMALLINT NOT NULL, v BLOB NOT NULL, tx INTEGER NOT NULL, added TINYINT NOT NULL DEFAULT 1)" "CREATE INDEX tx ON transactions (tx)" - "CREATE TABLE attributes (name TEXT NOT NULL PRIMARY KEY, a INTEGER UNIQUE NOT NULL)"]) + + ;; Materialized views of the schema. + "CREATE TABLE idents (ident TEXT NOT NULL PRIMARY KEY, entid INTEGER UNIQUE NOT NULL)" + "CREATE TABLE schema (ident TEXT NOT NULL, attr TEXT NOT NULL, value TEXT NOT NULL, FOREIGN KEY (ident) REFERENCES idents (ident))" + "CREATE INDEX unique_schema ON schema (ident, attr, value)" + ]) (defn > - (s/all-rows (:sqlite-connection db) ["SELECT e, a, v, tx FROM datoms"]) + (s/all-rows (:sqlite-connection db) ["SELECT e, a, v, tx FROM datoms WHERE tx >= ?" tx]) (> - (s/all-rows (:sqlite-connection db) ["SELECT e, a, v, tx, added FROM transactions ORDER BY tx ASC, e, a, v, added"]) + (s/all-rows (:sqlite-connection db) ["SELECT e, a, v, tx, added FROM transactions WHERE tx >= ? ORDER BY tx ASC, e, a, v, added" tx]) (