From 7e50528788573dfb04fde183240c4caffd7e603e Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Thu, 27 Oct 2016 11:37:02 -0700 Subject: [PATCH 1/7] Add repeated-keys utility. --- src/common/datomish/util.cljc | 19 +++++++++++++++++++ test/datomish/util_test.cljc | 15 +++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/common/datomish/util.cljc b/src/common/datomish/util.cljc index d83e8e4e..c954b150 100644 --- a/src/common/datomish/util.cljc +++ b/src/common/datomish/util.cljc @@ -174,3 +174,22 @@ (let [[k v] (f x)] (assoc! ret k (conj (get ret k []) v)))) (transient {}) coll))) + +(defn repeated-keys + "Takes a seq of maps. + Returns the set of keys that appear in more than one map." + [maps] + (if (not (seq (rest maps))) + #{} + ;; This is a perfect use case for transients, except that + ;; you can't use them for intersection due to CLJ-700. + ;; http://dev.clojure.org/jira/browse/CLJ-700 + (loop [overlapping #{} + seen #{} + key-sets (map (comp set keys) maps)] + (if-let [ks (first key-sets)] + (let [overlap (clojure.set/intersection seen ks)] + (recur (clojure.set/union overlapping overlap) + (clojure.set/union seen ks) + (rest key-sets))) + overlapping)))) diff --git a/test/datomish/util_test.cljc b/test/datomish/util_test.cljc index f1e215b2..1ad00297 100644 --- a/test/datomish/util_test.cljc +++ b/test/datomish/util_test.cljc @@ -51,3 +51,18 @@ (are [m xs] (= m (util/group-by-kv identity xs)) {:a [1 2] :b [3]} [[:a 1] [:a 2] [:b 3]])) + +(deftest test-repeated-keys + (let [abc {:a 1 :b 2 :c 3} + def {:d 1 :e 2 :f 3} + bcd {:b 1 :c 2 :d 3} + efg {:e 1 :f 2 :g 3} + empty {}] + (is (= #{} (util/repeated-keys []))) + (is (= #{} (util/repeated-keys [empty]))) + (is (= #{} (util/repeated-keys [empty empty]))) + (is (= #{} (util/repeated-keys [abc empty empty]))) + (is (= #{} (util/repeated-keys [abc def empty]))) + (is (= #{:b :c} (util/repeated-keys [bcd abc]))) + (is (= #{:b :c :d} (util/repeated-keys [abc def bcd]))) + (is (= #{:b :c :d :e :f :g} (util/repeated-keys [abc efg def efg bcd]))))) From 8e6f8399ae17753e619f0e225de2fbcecfc34cf0 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Tue, 8 Nov 2016 12:09:13 -0800 Subject: [PATCH 2/7] Add Date: Thu, 3 Nov 2016 18:09:27 -0700 Subject: [PATCH 3/7] Expose id-literal? in the API. --- src/common/datomish/api.cljc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/datomish/api.cljc b/src/common/datomish/api.cljc index 2e012f84..ae9ffb7c 100644 --- a/src/common/datomish/api.cljc +++ b/src/common/datomish/api.cljc @@ -35,6 +35,7 @@ (def Date: Fri, 4 Nov 2016 18:51:34 -0700 Subject: [PATCH 4/7] Expose datomish.schema/validate-schema so that schema management can use it. --- src/common/datomish/schema.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/datomish/schema.cljc b/src/common/datomish/schema.cljc index 1ae3967e..8194ed72 100644 --- a/src/common/datomish/schema.cljc +++ b/src/common/datomish/schema.cljc @@ -178,7 +178,7 @@ (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] (when-not (:db/valueType kv) (throw (ex-info (str "Bad attribute specification for " a ": should have {:db/valueType ...}") From 3212be565cc50d1a1f31bff189b87802f27398d4 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Tue, 1 Nov 2016 16:47:47 -0700 Subject: [PATCH 5/7] Allow callers to run functions within the scope of a transaction. This generalizes the transactor loop to allow callers to run an arbitrary function within an `in-transaction!` body. Combined with exposing ` report (assoc-in [:db-after] db-after))))) -(defn- ! token-chan (gensym "transactor-token")) (loop [] (when-let [token ( (! (:listener-source conn) report)) - report)))))] + ;; Default: process the transaction. + (do + (when @(:closed? conn) + ;; Drain enqueued transactions. + (raise "Connection is closed" {:error :transact/connection-closed})) + + (let [db (db conn) + in-transaction-fn + (case sentinel + :sentinel-fn + ;; This is a function that we'd like to run + ;; within a database transaction. See + ;; db/in-transaction! for details. + ;; The function is invoked with two arguments: + ;; the db and a function that takes (db, + ;; tx-data) and transacts it to return a + ;; TxReport. + ;; The function must return a TxReport. + ;; The function must not itself call + ;; `in-transaction!` or `! (:listener-source conn) report)) + report)))))] ;; Even when report is nil (transaction not committed), pair is non-nil. (>! result pair)) (>! token-chan token) From 451f13a0536d819d9038d7fd95f93387aa9612f9 Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Fri, 28 Oct 2016 16:27:42 -0700 Subject: [PATCH 6/7] Add :db.schema/version and :db.schema/attribute. --- src/common/datomish/transact/bootstrap.cljc | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/common/datomish/transact/bootstrap.cljc b/src/common/datomish/transact/bootstrap.cljc index 4bd97765..142e85aa 100644 --- a/src/common/datomish/transact/bootstrap.cljc +++ b/src/common/datomish/transact/bootstrap.cljc @@ -38,6 +38,15 @@ :db/cardinality :db.cardinality/one} :db.alter/attribute {:db/valueType :db.type/ref :db/cardinality :db.cardinality/many} + + :db.schema/version {:db/valueType :db.type/long + :db/cardinality :db.cardinality/one} + + ;; unique-value because an attribute can only belong to a single + ;; schema fragment. + :db.schema/attribute {:db/valueType :db.type/ref + :db/unique :db.unique/value + :db/cardinality :db.cardinality/many} }) (def idents @@ -76,6 +85,8 @@ :db.unique/value 33 :db.unique/identity 34 :db/doc 35 + :db.schema/version 36 ; Fragment -> version. + :db.schema/attribute 37 ; Fragment -> attribute. }) (def parts From d568977fa932d0736aa5a735947b228e5808ae9a Mon Sep 17 00:00:00 2001 From: Richard Newman Date: Tue, 8 Nov 2016 12:09:53 -0800 Subject: [PATCH 7/7] Implement schema management proposal. Fixes #95. --- src/common/datomish/schema_management.cljc | 348 ++++++++++++++++ test/datomish/schema_management_test.cljc | 456 +++++++++++++++++++++ test/datomish/test.cljs | 2 + 3 files changed, 806 insertions(+) create mode 100644 src/common/datomish/schema_management.cljc create mode 100644 test/datomish/schema_management_test.cljc diff --git a/src/common/datomish/schema_management.cljc b/src/common/datomish/schema_management.cljc new file mode 100644 index 00000000..d065bc4f --- /dev/null +++ b/src/common/datomish/schema_management.cljc @@ -0,0 +1,348 @@ +;; 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/. + +(ns datomish.schema-management + #?(:cljs + (:require-macros + [datomish.pair-chan :refer [go-pair datoms [schema-fragment-id [attribute-name attribute-pairs]] + (let [attribute-id (d/id-literal :db.part/user)] + [(assoc + attribute-pairs + :db.install/_attribute :db.part/db + ;; Point back to the fragment. + :db.schema/_attribute schema-fragment-id + :db/id attribute-id + :db/ident attribute-name)])) + +(defn managed-schema-fragment->datoms [{:keys [name version attributes]}] + (let [fragment-id (d/id-literal :db.part/db)] + (conj + (mapcat (partial attribute->datoms fragment-id) attributes) + {:db/id fragment-id + :db/ident name + :db.schema/version version}))) + +(defn symbolic-schema [db] + (:symbolic-schema db)) + +(defn changed-attribute->datoms [schema-fragment-id attribute-name existing new-values] + (let [differences (first (diff new-values existing))] + (when-not (empty? differences) + [(merge + {:db/id (d/id-literal :db.part/user) + :db/ident attribute-name + ;; Point back to the fragment. + :db.schema/_attribute schema-fragment-id + :db.alter/_attribute :db.part/db} + differences)]))) + +(defn changed-schema-fragment->datoms [schema-fragment-id existing-schema name attributes version] + (conj + (mapcat (fn [[attribute-name new-values]] + (let [existing (get existing-schema attribute-name)] + (if existing + (changed-attribute->datoms + schema-fragment-id + attribute-name + existing + new-values) + (attribute->datoms + schema-fragment-id [attribute-name new-values])))) + attributes) + {:db.schema/version version + :db/ident name + :db/id (d/id-literal :db.part/db)})) + +(defn- prepare-schema-application-for-fragments + "Given a non-empty collection of fragments known to be new or outdated, + yield a migration sequence containing the necessary pre/post ops and + transact bodies." + [db + symbolic-schema + schema-fragment-versions + schema-fragment-attributes + {:keys [fragments pre post fragment-pre fragment-post] :as args}] + + (when-let + [body + (mapcat + (fn [{:keys [name version attributes] :as fragment}] + (if-let [existing-version (get schema-fragment-versions name)] + ;; It's a change. + ;; Spit out any pre/post for this fragment, with a + ;; transact of the datoms to effect the change and + ;; bump the schema fragment version in the middle. + (concat + (when-let [fragment-pre-for-this + (get-in fragment-pre [name existing-version])] + [[:call fragment-pre-for-this]]) + [[:transact + (changed-schema-fragment->datoms (d/entid db name) + symbolic-schema + name + attributes + version)]] + (when-let [fragment-post-for-this + (get-in fragment-post [name existing-version])] + [[:call fragment-post-for-this]])) + + ;; It's new! Just do it. + ;; There can't be any fragment pre/post, 'cos there's no previous + ;; version to come from. + [[:transact (managed-schema-fragment->datoms fragment)]])) + fragments)] + + (concat + (when pre [[:call pre]]) + body + (when post [[:call post]])))) + +(defn- symbolic-schema db) + schema-fragment-versions ( version existing-version) + (when (< version existing-version) + (raise "Existing version of " name " is " existing-version + ", which is later than requested " version "." + {:error :schema/outdated-version + :name name + :version version + :existing existing-version}))))) + fragments)] + + (if (empty? to-apply) + (do + (log "No fragments left to apply.") + nil) + (prepare-schema-application-for-fragments + db + symbolic-schema + schema-fragment-versions + schema-fragment-attributes + (assoc args :fragments to-apply))))))) + +(defn !]]]) + #?@(:cljs [[datomish.js-sqlite] + [datomish.pair-chan] + [datomish.test-macros :refer-macros [deftest-async deftest-db]] + [datomish.node-tempfile :refer [tempfile]] + [cljs.test :as t :refer-macros [is are deftest testing async]] + [cljs.core.async :as a :refer [!]]])) + #?(:clj + (:import [clojure.lang ExceptionInfo])) + #?(:clj + (:import [datascript.db DB]))) + +#?(:cljs + (def Throwable js/Error)) + +(def id-schema (d/id-literal :db.part/db)) +(def id-foobar (d/id-literal :db.part/user)) + +(def trivial-schema-managed-fragment-v1 + {:name :com.example.foo + :version 1 + :attributes + {:foo/bar + {:db/cardinality :db.cardinality/one + :db/valueType :db.type/string}}}) + +(def additional-schema-managed-fragment-v7 + {:name :com.example.bar + :version 7 + :attributes + {:bar/noo + {:db/cardinality :db.cardinality/one + :db/unique :db.unique/value + :db/valueType :db.type/long}}}) + +(def additional-schema-managed-fragment-v8 + {:name :com.example.bar + :version 8 + :attributes + {:bar/choo + {:db/cardinality :db.cardinality/many + :db/fulltext true + :db/valueType :db.type/string} + :bar/noo + {:db/cardinality :db.cardinality/one + :db/unique :db.unique/value + :db/valueType :db.type/long}}}) + +(def trivial-schema-managed-fragment-v2 + {:name :com.example.foo + :version 2 + :attributes + {:foo/bar + {:db/cardinality :db.cardinality/many + :db/valueType :db.type/string}}}) + +(def trivial-schema-v1 + (sm/managed-schema-fragment->datoms trivial-schema-managed-fragment-v1)) + +;; TODO +(deftest test-managed-schema-fragment->datoms) + +(defn datoms fragment))) + + (let [current (symbolic-schema db) + [:foo/bar :db/valueType])))) + + (testing "An empty fragment yields no work." + (is (nil? (datoms + additional-schema-managed-fragment-v7)] + [:call "Y"]] + (datoms + additional-schema-managed-fragment-v7)))) + + next-up + {:pre "XX" + :post "YY" + :fragment-pre {:com.example.foo {1 "AA"}} + :fragment-post {:com.example.foo {1 "BB"} + :com.example.bar {7 "CC"}} + :fragments [trivial-schema-managed-fragment-v2 + additional-schema-managed-fragment-v8 + ]} + + counter (atom 0) + pre (atom nil) + post (atom nil) + fragment-pre-foo (atom nil) + fragment-post-foo (atom nil) + fragment-post-bar (atom nil) + + next-up-but-with-functions + {:pre (fn [db _] + (reset! pre (swap! counter inc)) + nil) + :post (fn [db _] + (reset! post (swap! counter inc)) + nil) + :fragment-pre {:com.example.foo + {1 (fn [db _] + (reset! fragment-pre-foo (swap! counter inc)) + nil)}} + :fragment-post {:com.example.foo + {1 (fn [db _] + (reset! fragment-post-foo (swap! counter inc)) + nil)} + :com.example.bar + {7 (fn [db _] + (reset! fragment-post-bar (swap! counter inc)) + nil)}} + :fragments [trivial-schema-managed-fragment-v2 + additional-schema-managed-fragment-v8]}] + + (testing "Make sure our fragment was added correctly." + (let [bar (d/entid db :com.example.bar)] + (is (integer? bar)) + (is (= :com.example.bar (d/ident db bar)))) + + (is (= {:com.example.foo 1 + :com.example.bar 7} + (symbolic-schema @db) [:bar/thoo :db/cardinality])))) + + (is (= (count "hello, world.") ; 13. + (