Skip to content

Commit

Permalink
feat: schema migration
Browse files Browse the repository at this point in the history
- schema migration as norm namespace in datahike
- Closes #13
  • Loading branch information
TimoKramer committed Jan 23, 2023
1 parent 11adbeb commit c477a5d
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 0 deletions.
83 changes: 83 additions & 0 deletions src/datahike/norm/norm.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
(ns datahike.norm.norm
(:require
[clojure.java.io :as io]
[clojure.string :as string]
[taoensso.timbre :as log]
[datahike.api :as d]))

(defn attribute-installed? [conn attr]
(some? (d/entity @conn [:db/ident attr])))

(defn ensure-norm-attribute! [conn]
(if-not (attribute-installed? conn :tx/norm)
(:db-after (d/transact conn {:tx-data [{:db/ident :tx/norm
:db/valueType :db.type/keyword
:db/cardinality :db.cardinality/one}]}))
@conn))

(defn norm-installed? [db norm]
(->> {:query '[:find (count ?t)
:in $ ?tn
:where
[_ :tx/norm ?tn ?t]]
:args [db norm]}
d/q
first
some?))

(defn read-norm-files! [norms-folder]
(let [folder (io/file norms-folder)]
(if (.exists folder)
(let [migration-files (file-seq folder)
xf (comp
(filter #(re-find #".edn" (.getPath %)))
(map (fn [migration-file]
(-> (.getPath migration-file)
slurp
read-string
(update :norm (fn [norm] (or norm
(-> (.getName migration-file)
(string/replace #"\.edn" "")
keyword))))))))]
(sort-by :norm (into [] xf migration-files)))
(throw
(ex-info
(format "Norms folder %s does not exist." norms-folder)
{:folder norms-folder})))))

(defn neutral-fn [_] [])

(defn ensure-norms!
([conn]
(ensure-norms! conn (io/resource "migrations")))
([conn migrations]
(let [db (ensure-norm-attribute! conn)
norm-list (cond
(string? migrations) (read-norm-files! migrations)
(vector? migrations) migrations)]
(log/info "Checking migrations ...")
(doseq [{:keys [norm tx-data tx-fn]
:or {tx-data []
tx-fn #'neutral-fn}}
norm-list]
(log/info "Checking migration" norm)
(when-not (norm-installed? db norm)
(log/info "Run migration" norm)
(->> (d/transact conn {:tx-data (vec (concat [{:tx/norm norm}]
tx-data
(tx-fn conn)))})
(log/info "Done")))))))

(comment
(d/delete-database {:store {:backend :file
:path "/tmp/file-example"}})
(d/create-database {:store {:backend :file
:path "/tmp/file-example"}})
(def conn (d/connect {:store {:backend :file
:path "/tmp/file-example"}}))
(ensure-norms! conn "test/resources")
(def norm-list (read-norm-files! "test/resources"))
(norm-installed? (d/db conn) (:norm (first norm-list)))
(d/transact conn {:tx-data [{:foo "foo"}]}))


122 changes: 122 additions & 0 deletions test/datahike/norm/norm_test.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
(ns datahike.norm.norm-test
(:require [clojure.test :refer [deftest is]]
[clojure.string :as s]
[datahike.api :as d]
[datahike.norm :as sut]))

(defn create-test-db []
(let [id (apply str
(for [_i (range 8)]
(char (+ (rand 26) 65))))]
(d/create-database {:store {:backend :mem
:id id}})
(d/connect {:store {:backend :mem
:id id}})))

(deftest simple-test
(let [conn (create-test-db)
_ (sut/ensure-norms! conn "test/datahike/norm/resources")]
(is (= #:db{:valueType :db.type/string, :cardinality :db.cardinality/one, :doc "foo", :ident :foo}
(-> (d/schema (d/db conn))
:foo
(dissoc :db/id))))
(is (= #:db{:valueType :db.type/string, :cardinality :db.cardinality/one, :doc "Simpsons character name", :ident :character/name}
(-> (d/schema (d/db conn))
:character/name
(dissoc :db/id))))
(is (= #:db{:ident :tx/norm, :valueType :db.type/keyword, :cardinality :db.cardinality/one}
(-> (d/schema (d/db conn))
:tx/norm
(dissoc :db/id))))))

(deftest tx-fn-test
(let [conn (create-test-db)
_ (sut/ensure-norms! conn "test/datahike/norm/resources")
_ (d/transact conn {:tx-data [{:foo "upper-case"}
{:foo "Grossbuchstaben"}]})
test-fn (fn [conn]
(-> (for [[eid value] (d/q '[:find ?e ?v
:where
[?e :foo ?v]]
(d/db conn))]
[:db/add eid
:foo (s/upper-case value)])
vec))
test-norm [{:norm :test-norm-1,
:tx-fn test-fn}]
_ (sut/ensure-norms! conn test-norm)]
(is (= #{["GROSSBUCHSTABEN"] ["UPPER-CASE"]}
(d/q '[:find ?v
:where
[_ :foo ?v]]
(d/db conn))))))

(deftest tx-and-fn-test
(let [conn (create-test-db)
_ (sut/ensure-norms! conn "test/datahike/norm/resources")
_ (d/transact conn {:tx-data [{:character/name "Homer Simpson"}
{:character/name "Marge Simpson"}]})
margehomer (d/q '[:find [?e ...]
:where
[?e :character/name]]
(d/db conn))
tx-data [{:db/doc "Simpsons children reference"
:db/ident :character/child
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}]
tx-fn (fn [conn]
(-> (for [[eid] (d/q '[:find ?e
:where
[?e :character/name]
(or-join [?e]
[?e :character/name "Homer Simpson"]
[?e :character/name "Marge Simpson"])]
(d/db conn))]
{:db/id eid
:character/child [{:character/name "Bart Simpson"}
{:character/name "Lisa Simpson"}
{:character/name "Maggie Simpson"}]})
vec))
test-norm [{:norm :test-norm-2
:tx-data tx-data
:tx-fn tx-fn}]]
(sut/ensure-norms! conn test-norm)
(is (= [#:character{:name "Marge Simpson",
:child
[#:character{:name "Bart Simpson"}
#:character{:name "Lisa Simpson"}
#:character{:name "Maggie Simpson"}]}
#:character{:name "Homer Simpson",
:child
[#:character{:name "Bart Simpson"}
#:character{:name "Lisa Simpson"}
#:character{:name "Maggie Simpson"}]}]
(d/pull-many (d/db conn) '[:character/name {:character/child [:character/name]}] margehomer)))))

(comment
(def conn (create-test-db))
(sut/ensure-norms! conn "test/resources")
(d/transact conn {:tx-data [{:character/name "Homer Simpson"}
{:character/name "Marge Simpson"}]})
(def margehomer (-> (d/q '[:find [?e ...]
:where
[?e :character/name]]
(d/db conn))))
(d/transact conn {:tx-data [{:db/doc "Simpsons children reference"
:db/ident :character/child
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}]})
(d/transact conn (-> (for [[eid] (d/q '[:find ?e
:where
[?e :character/name]
(or-join [?e]
[?e :character/name "Homer Simpson"]
[?e :character/name "Marge Simpson"])]
(d/db conn))]
{:db/id eid
:character/child [{:character/name "Bart Simpson"}
{:character/name "Lisa Simpson"}
{:character/name "Maggie Simpson"}]})
vec))

(d/pull-many (d/db conn) '[:character/name {:character/child [:character/name]}] margehomer))
6 changes: 6 additions & 0 deletions test/datahike/norm/resources/001-a1-example.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{:norm :a1-example
:tx-data [{:db/doc "foo"
:db/ident :foo
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}]
:tx-fn io.replikativ.garantie/neutral-fn}
5 changes: 5 additions & 0 deletions test/datahike/norm/resources/002-a2-example.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{:norm :a2-example
:tx-data [{:db/doc "Simpsons character name"
:db/ident :character/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}]}

0 comments on commit c477a5d

Please sign in to comment.