├── .clj-kondo ├── .gitignore └── config.edn ├── .github └── workflows │ └── ci-cd.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── project.clj ├── resources └── migrations │ ├── 001-alter-schema.edn │ ├── 002-populate.edn │ └── fns │ └── txes.clj ├── src └── dev │ └── gethop │ └── stork.clj └── test └── dev └── gethop └── stork_test.clj /.clj-kondo/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | /rewrite-clj 3 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:skip-comments true 2 | 3 | :linters 4 | {:unsorted-required-namespaces 5 | {:level :warning} 6 | 7 | :refer-all 8 | {:exclude #{clojure.test}} 9 | 10 | :single-key-in 11 | {:level :warning} 12 | 13 | :used-underscored-binding 14 | {:level :warning} 15 | 16 | :redundant-fn-wrapper 17 | {:level :warning} 18 | 19 | :missing-docstring 20 | {:level :info}}} 21 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: ci-cd 2 | on: 3 | push: 4 | paths-ignore: 5 | - "README.md" 6 | - "CONTRIBUTING.md" 7 | - "CHANGELOG.md" 8 | - "LICENSE" 9 | - ".gitignore" 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ubuntu-20.04 14 | env: 15 | LEIN_ROOT: "true" 16 | CLOJARS_USERNAME: ${{ secrets.CLOJARS_USERNAME }} 17 | CLOJARS_PASSWORD: ${{ secrets.CLOJARS_PASSWORD }} 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Install Java 24 | uses: actions/setup-java@v3 25 | with: 26 | distribution: 'temurin' 27 | java-version: '17' 28 | 29 | - name: Install Clojure Tools 30 | uses: DeLaGuardo/setup-clojure@5.1 31 | with: 32 | lein: 2.9.8 33 | 34 | - name: Install clj-kondo 35 | uses: DeLaGuardo/setup-clj-kondo@master 36 | with: 37 | version: '2020.04.05' 38 | 39 | - name: Check formatting 40 | run: lein cljfmt check 41 | 42 | - name: Lint 43 | run: clj-kondo --lint src --lint test && lein eastwood 44 | 45 | - name: Test 46 | run: lein test :all 47 | 48 | - name: Deploy Jar to Clojars 49 | if: contains(github.ref, 'refs/tags/') 50 | run: lein deploy 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | */.DS_Store 3 | .idea 4 | .nrepl-port 5 | /target 6 | /lib 7 | /classes 8 | /checkouts 9 | pom.xml 10 | *.jar 11 | *.class 12 | .lein-deps-sum 13 | .lein-failures 14 | .lein-plugins 15 | stork.iml 16 | pom.xml.asc 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [UNRELEASED] 5 | 6 | ## [0.1.7] - 2022-05-16 7 | 8 | ## Added 9 | 10 | - Moving the repository to [gethop-dev](https://github.com/gethop-dev) organization 11 | - Source code linting using [clj-kondo](https://github.com/clj-kondo/clj-kondo) 12 | - CI/CD solution switch from [TravisCI](https://travis-ci.org/) to [GitHub Actions](Ihttps://github.com/features/actions) 13 | 14 | ## [0.1.5] - 2018-09-17 15 | 16 | ## Added 17 | 18 | - Before running tests, sourcecode will also be Linted. 19 | 20 | ## [0.1.4] - 2018-09-17 21 | 22 | ## Fixed 23 | 24 | - Datomic's reader macro #db/fn was breaking uberjar creation of the project that depends Stork. 25 | Changing that to `d/function` did the trick. 26 | 27 | ## [0.1.3] - 2018-09-17 28 | 29 | ## Added 30 | 31 | - Travis CI integration 32 | - Automatic deployments to Clojars 33 | 34 | [UNRELEASED]: https://github.com/gethop-dev/stork/compare/0.1.7...HEAD 35 | [0.1.7]: https://github.com/gethop-dev/stork/releases/tag/v0.1.7 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![ci-cd](https://github.com/gethop-dev/stork/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/gethop-dev/stork/actions/workflows/ci-cd.yml) 2 | [![Clojars Project](https://img.shields.io/clojars/v/dev.gethop/stork.svg)](https://clojars.org/dev.gethop/stork) 3 | 4 | # Stork 5 | 6 | A Clojure/Datomic migrations library heavily inspired by [avescodes/conformity](https://github.com/avescodes/conformity). 7 | 8 | It consumes migrations defined by explicit id and a transaction data (explicit or evaluated from function) and transacts the data once and only once. 9 | 10 | ## Dependency 11 | 12 | Stork is available on clojars, and can be included in your leiningen `project.clj` by adding the following to `:dependencies`: 13 | 14 | [![Clojars Project](https://clojars.org/dev.gethop/stork/latest-version.svg)](https://clojars.org/gethop-dev/stork) 15 | 16 | 17 | ## Usage 18 | 19 | ### Writing a migration: 20 | 21 | If your migration is going to have an explicit transaction data then all you need is a map that contains unique migration id and the transaction data: 22 | ```clojure 23 | ;; resources/migrations/001-user-entity.edn 24 | {:id :m001/user-entity 25 | :tx-data [{:db/id #db/id [:db.part/db] 26 | :db/ident :user/name 27 | :db/valueType :db.type/string 28 | :db/cardinality :db.cardinality/one 29 | :db.install/_attribute :db.part/db} 30 | {:db/id #db/id [:db.part/db] 31 | :db/ident :user/email 32 | :db/valueType :db.type/string 33 | :db/cardinality :db.cardinality/one 34 | :db.install/_attribute :db.part/db}]} 35 | ``` 36 | 37 | If transaction data necessary for your transaction needs to be calculated first, then you want to use `:tx-data-fn` instead of `:tx-data`. `:tx-data-fn` must be the fully qualified name of the function to invoke. And you need to make sure that the namespace for that function can be found in the application classpath. This function accepts one argument - a Datomic connection: 38 | ```clojure 39 | ;; resources/migrations/001-add-prefix-to-phone-numbers 40 | {:id :m002/add-prefix-to-phone-numbers 41 | :tx-data-fn migrations.fns.m002/add-prefix-to-phone-numbers} 42 | ``` 43 | 44 | ```clojure 45 | ;; resources/migrations/fns/m002.clj 46 | (ns migrations.fns.m002 47 | (:require [datomic.api :as d])) 48 | 49 | ... 50 | 51 | (defn add-prefix-to-phone-numbers [conn] 52 | (mapv 53 | (fn [[u-eid phone]] 54 | [:db/add u-eid :user/phone (str "+48" phone)]) 55 | (get-all-users-eid-phone-pair conn))) 56 | ``` 57 | 58 | ### Running a migration: 59 | 60 | ```clojure 61 | (require '[dev.gethop.stork :as stork]) 62 | 63 | (->> 64 | (stork/read-resource "migrations/001-alter-schema.edn") 65 | (stork/ensure-installed conn)) 66 | ``` 67 | 68 | If there is any problem trying to apply a migration, `stork/ensure-installed` throws ExceptionInfo (and adds relevant details to the exception data map). 69 | 70 | ### To know whether a norm has been installed (e.g. for logging purposes): 71 | 72 | ```clojure 73 | (->> 74 | (stork/read-resource "migrations/001-alter-schema.edn") 75 | (:id) 76 | (stork/installed? db)) 77 | ``` 78 | ## License 79 | 80 | Copyright (c) 2024 Biotz, SL. 81 | 82 | Distributed under the Eclipse Public License, the same as Clojure. 83 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject dev.gethop/stork "0.1.8-SNAPSHOT" 2 | :description "Idempotent and atomic datom transacting for Datomic. Heavily inspired on avescodes/conformity." 3 | :url "http://github.com/gethop-dev/stork" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :min-lein-version "2.9.8" 7 | :plugins [[jonase/eastwood "1.2.3"] 8 | [lein-cljfmt "0.8.0"]] 9 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.10.0"] 10 | [com.datomic/datomic-free "0.9.5697"]] 11 | :source-paths ["dev"]}} 12 | :deploy-repositories [["snapshots" {:url "https://clojars.org/repo" 13 | :username :env/CLOJARS_USERNAME 14 | :password :env/CLOJARS_PASSWORD 15 | :sign-releases false}] 16 | ["releases" {:url "https://clojars.org/repo" 17 | :username :env/CLOJARS_USERNAME 18 | :password :env/CLOJARS_PASSWORD 19 | :sign-releases false}]]) 20 | -------------------------------------------------------------------------------- /resources/migrations/001-alter-schema.edn: -------------------------------------------------------------------------------- 1 | {:id :m001/alter-schema 2 | :tx-data [{:db/ident :life/meaning 3 | :db/doc "meaning of life" 4 | :db/valueType :db.type/long 5 | :db/cardinality :db.cardinality/one 6 | :db/id #db/id[:db.part/db] 7 | :db.install/_attribute :db.part/db}]} 8 | -------------------------------------------------------------------------------- /resources/migrations/002-populate.edn: -------------------------------------------------------------------------------- 1 | {:id :m002/populate-meaning-of-life 2 | :tx-data-fn migrations.fns.txes/populate-meaning-of-life} 3 | -------------------------------------------------------------------------------- /resources/migrations/fns/txes.clj: -------------------------------------------------------------------------------- 1 | (ns migrations.fns.txes 2 | (:require [datomic.api :as d])) 3 | 4 | (defn attr 5 | ([ident] 6 | (attr ident :db.type/string)) 7 | ([ident value-type] 8 | {:db/id (d/tempid :db.part/db) 9 | :db/ident ident 10 | :db/valueType value-type 11 | :db/cardinality :db.cardinality/one 12 | :db.install/_attribute :db.part/db})) 13 | 14 | (defn new-attr [_] 15 | [(attr :test/attribute)]) 16 | 17 | (defn populate-meaning-of-life [_] 18 | [{:db/id (d/tempid :db.part/user) 19 | :life/meaning 42}]) 20 | 21 | (defn txfn-no-args [] 22 | [(attr :test/txfn-no-args)]) 23 | -------------------------------------------------------------------------------- /src/dev/gethop/stork.clj: -------------------------------------------------------------------------------- 1 | (ns dev.gethop.stork 2 | (:require [clojure.edn :as edn] 3 | [clojure.java.io :as io] 4 | [clojure.spec.alpha :as s] 5 | [datomic.api :refer [q db] :as d])) 6 | 7 | (def installed-migrations-attribute :stork/installed-migrations) 8 | (def ensure-migration-tx 9 | "Ident for a function installed in datomic. 10 | See `ensure-migration-should-be-installed`" 11 | :stork/ensure-migration-should-be-installed) 12 | 13 | (def ensure-migration-should-be-installed 14 | "Transaction function to ensure that each migration is executed exactly only 15 | when a migration-id isn't known to had been installed in past." 16 | (d/function 17 | '{:lang :clojure 18 | :params [db inst-migs-attr migration-id tx-data] 19 | :code (when-not (seq (q '[:find ?tx 20 | :in $ ?installed-migrations-attribute ?migration-id 21 | :where 22 | [?tx ?installed-migrations-attribute ?migration-id]] 23 | db inst-migs-attr migration-id)) 24 | (cons {:db/id (d/tempid :db.part/tx) 25 | inst-migs-attr migration-id} 26 | tx-data))})) 27 | 28 | (defn read-resource 29 | "Reads and returns data from a resource containing edn text. 30 | An optional argument allows specifying opts for clojure.edn/read" 31 | ([resource-name] 32 | (read-resource {:readers *data-readers*} resource-name)) 33 | ([opts resource-name] 34 | (->> (io/resource resource-name) 35 | (io/reader) 36 | (java.io.PushbackReader.) 37 | (edn/read opts)))) 38 | 39 | (defn has-attribute? 40 | "Checks if an attribute is installed into db schema." 41 | [db attr-name] 42 | (-> (d/entity db attr-name) 43 | :db.install/_attribute 44 | boolean)) 45 | 46 | (defn has-function? 47 | "Checks if an entity (queried by ident) has a function installed." 48 | [db fn-name] 49 | (-> (d/entity db fn-name) 50 | :db/fn 51 | boolean)) 52 | 53 | (defn ensure-stork-schema 54 | "Makes sure that library vitals are installed into the db: 55 | 1) An attribute to store information about successfully installed migrations. 56 | 2) A custom datomic function for safe migration installing (see `:stork/ensure-migration-should-be-installed-txfn`)" 57 | [conn] 58 | (when-not (has-attribute? (db conn) installed-migrations-attribute) 59 | (d/transact conn [{:db/id (d/tempid :db.part/db) 60 | :db/ident installed-migrations-attribute 61 | :db/valueType :db.type/keyword 62 | :db/cardinality :db.cardinality/one 63 | :db/unique :db.unique/value 64 | :db/index true 65 | :db.install/_attribute :db.part/db}])) 66 | (when-not (has-function? (db conn) ensure-migration-tx) 67 | (d/transact conn [{:db/id (d/tempid :db.part/user) 68 | :db/ident ensure-migration-tx 69 | :db/fn ensure-migration-should-be-installed}]))) 70 | 71 | (defn installed? 72 | "Checks if a migration-id is known to be already installed into the db." 73 | [db migration-id] 74 | (and (has-attribute? db installed-migrations-attribute) 75 | (boolean (q '[:find ?tx . 76 | :in $ ?installed-migrations ?migration-id 77 | :where [?tx ?installed-migrations ?migration-id]] 78 | db installed-migrations-attribute migration-id)))) 79 | 80 | (defn maybe-timeout-synch-schema [conn maybe-timeout] 81 | (if maybe-timeout 82 | (let [result (deref (d/sync-schema conn (d/basis-t (d/db conn))) maybe-timeout ::timed-out)] 83 | (if (= result ::timed-out) 84 | (throw (ex-info 85 | "Timed out calling synch-schema between Stork transactions" 86 | {:timeout maybe-timeout})) 87 | result)) 88 | @(d/sync-schema conn (d/basis-t (d/db conn))))) 89 | 90 | (defn eval-tx-data-fn 91 | "Tries to resolve the symbol that contains the function. 92 | It's evaluation should result in a valid tx-data." 93 | [conn tx-data-fn] 94 | (try 95 | (require (symbol (namespace tx-data-fn))) 96 | {:tx-data ((resolve tx-data-fn) conn)} 97 | (catch Throwable t 98 | (let [reason (.getMessage t) 99 | data {:reason reason}] 100 | (throw (ex-info reason data t)))))) 101 | 102 | (defn complement-migration 103 | "If migration contains tx-data, function returns migration unchanged. 104 | If it contains tx-data-fn instead then it evaluates resolved symbol 105 | and merges the result with migration." 106 | [conn migration] 107 | (let [tx-data-fn (:tx-data-fn migration)] 108 | (cond-> migration 109 | tx-data-fn (merge (eval-tx-data-fn conn tx-data-fn))))) 110 | 111 | (defn handle-tx-data 112 | "Tries to transact tx-data using custom :stork/:stork/ensure-migration-should-be-installed." 113 | [conn migration-id tx-data sync-schema-timeout] 114 | (try 115 | (let [safe-tx [ensure-migration-tx 116 | installed-migrations-attribute 117 | migration-id 118 | tx-data] 119 | _ (maybe-timeout-synch-schema conn sync-schema-timeout) 120 | tx-result @(d/transact conn [safe-tx])] 121 | (when (next (:tx-data tx-result)) 122 | tx-result)) 123 | (catch Throwable t 124 | (let [reason (.getMessage t) 125 | data {:reason reason}] 126 | (throw (ex-info reason data t)))))) 127 | 128 | (defn handle-migration 129 | "Checks if a migration's id is known to be installed already. 130 | If it is, return ::already-installed. 131 | Otherwise transact tx-data (be it explicitly given or evaluated from tx-data-fn) 132 | and return transaction's result." 133 | [conn {:keys [id] :as migration}] 134 | (if (installed? (db conn) id) 135 | ::already-installed 136 | (let [sync-schema-timeout (:stork.setting/sync-schema-timeout migration) 137 | {:keys [tx-data]} (complement-migration conn migration)] 138 | (handle-tx-data conn id tx-data sync-schema-timeout)))) 139 | 140 | (defn tx-data-xor-tx-data-fn? 141 | "Returns true if: 142 | a) tx-data is present and tx-data-fn is not 143 | b) tx-data-fn is present and tx-data is not 144 | Returns false otherwise." 145 | [{:keys [tx-data tx-data-fn]}] 146 | (or (and tx-data (not tx-data-fn)) 147 | (and tx-data-fn (not tx-data)))) 148 | 149 | (s/def ::migration (s/and (s/keys :req-un [::id] 150 | :opt-un [::tx-data ::tx-data-fn]) 151 | tx-data-xor-tx-data-fn?)) 152 | 153 | (defn ensure-installed 154 | "Ensure that migration is installed, be it schema, data or otherwise. 155 | A migration is represented in a format of a map with kv pairs: 156 | 157 | :id - unique identifier of the migration. The id needs to be of type clojure.lang.Keyword. 158 | No migrations having same id will get installed after this one has been installed. 159 | :tx-data - a vector of datoms to be transacted if migration is not installed yet. 160 | :tx-data-fn - an alternative to `:tx-data`. 161 | It's a symbol representing a function that will be ran to produce tx-data. 162 | The function will be ran with one argument - conn. 163 | 164 | If migration hasn't been installed before then function will return with a transaction result. 165 | It will return `::already-installed` otherwise. 166 | Throws ExceptionInfo if the migration cant be transacted, including details about the failure." 167 | [conn migration] 168 | {:pre [(s/valid? ::migration migration)]} 169 | (ensure-stork-schema conn) 170 | (handle-migration conn migration)) 171 | -------------------------------------------------------------------------------- /test/dev/gethop/stork_test.clj: -------------------------------------------------------------------------------- 1 | (ns dev.gethop.stork-test 2 | (:require [clojure.test :refer :all] 3 | [datomic.api :refer [q db] :as d] 4 | [dev.gethop.stork :refer [has-attribute? has-function? installed? 5 | read-resource ensure-installed ensure-migration-tx 6 | ensure-stork-schema installed-migrations-attribute]] 7 | [migrations.fns.txes :refer [new-attr txfn-no-args]])) 8 | 9 | (def uri "datomic:mem://test") 10 | (defn fresh-conn [] 11 | (d/delete-database uri) 12 | (d/create-database uri) 13 | (d/connect uri)) 14 | 15 | (defn attr 16 | ([ident] 17 | (attr ident :db.type/string)) 18 | ([ident value-type] 19 | {:db/id (d/tempid :db.part/db) 20 | :db/ident ident 21 | :db/valueType value-type 22 | :db/cardinality :db.cardinality/one 23 | :db.install/_attribute :db.part/db})) 24 | 25 | (def sample-migration {:id :m001/new-attributes 26 | :tx-data [(attr :test/attribute1) 27 | (attr :test/attribute2)]}) 28 | 29 | (def sample-migration-altered {:id :m001/new-attributes 30 | :tx-data [(attr :test/attribute1) 31 | (attr :test/attribute2) 32 | (attr :test/attribute3)]}) 33 | 34 | (deftest test-ensure-installed 35 | (testing "installs expected migration" 36 | (let [conn (fresh-conn)] 37 | (ensure-installed conn sample-migration) 38 | (is (has-attribute? (db conn) :test/attribute1)) 39 | (is (has-attribute? (db conn) :test/attribute2)))) 40 | 41 | (testing "installing another/altered migration with same migration-id is being ignored" 42 | (let [conn (fresh-conn)] 43 | (ensure-installed conn sample-migration) 44 | (ensure-installed conn sample-migration-altered) 45 | (is (has-attribute? (db conn) :test/attribute1)) 46 | (is (has-attribute? (db conn) :test/attribute2)) 47 | (is (not (has-attribute? (db conn) :test/attribute3))) 48 | (is (= (ensure-installed conn sample-migration) :dev.gethop.stork/already-installed)))) 49 | 50 | (testing "throws exception if migration lacks vital parameters" 51 | (let [conn (fresh-conn)] 52 | (is (thrown? java.lang.AssertionError 53 | (ensure-installed conn {:tx-data [(attr :animal/species)]}))) 54 | (is (thrown? java.lang.AssertionError 55 | (ensure-installed conn {:tx-data-fn new-attr}))) 56 | (is (thrown? java.lang.AssertionError 57 | (ensure-installed conn {:id :m006/creatures-that-live-on-dry-land}))))) 58 | 59 | (testing "throws exception if migration contains both tx-data and tx-data-fn" 60 | (let [conn (fresh-conn)] 61 | (is (thrown? java.lang.AssertionError 62 | (ensure-installed conn {:id :m006/creatures-that-live-on-dry-land 63 | :tx-data [(attr :animal/species)] 64 | :tx-data-fn new-attr}))))) 65 | 66 | (testing "throws exception if migration cannot be transacted" 67 | (let [conn (fresh-conn)] 68 | (is (thrown? clojure.lang.ExceptionInfo 69 | (ensure-installed conn {:id :m002/txfn-cannot-be-executed 70 | :tx-data-fn txfn-no-args})))))) 71 | 72 | (deftest test-migration-installed-to? 73 | (testing "returns truthy if migration is already installed" 74 | (let [conn (fresh-conn)] 75 | (ensure-installed conn sample-migration) 76 | (is (some? (installed? (db conn) :m001/new-attributes))))) 77 | 78 | (testing "returns false if" 79 | (testing "migration has not been installed" 80 | (let [conn (fresh-conn)] 81 | (ensure-stork-schema conn) 82 | (is (false? (installed? (db conn) :m001/new-attributes))))) 83 | 84 | (testing "installed-migrations-attribute does not exist" 85 | (let [conn (fresh-conn)] 86 | (is (and 87 | (false? (has-attribute? (db conn) installed-migrations-attribute)) 88 | (false? (installed? (db conn) :m001/new-attributes)))))))) 89 | 90 | (deftest test-ensure-stork-schema 91 | (testing "it adds the stork schema if it is absent" 92 | (let [conn (fresh-conn) 93 | _ (ensure-stork-schema conn)] 94 | (is (has-attribute? (db conn) installed-migrations-attribute)) 95 | (is (has-function? (db conn) ensure-migration-tx)))) 96 | 97 | (testing "it does nothing if the stork schema exists" 98 | (let [conn (fresh-conn) 99 | count-txes (fn [db] 100 | (-> (q '[:find ?tx 101 | :where [?tx :db/txInstant]] 102 | db) 103 | count)) 104 | _ (ensure-stork-schema conn) 105 | before (count-txes (db conn)) 106 | _ (ensure-stork-schema conn) 107 | after (count-txes (db conn))] 108 | (is (= before after))))) 109 | 110 | (deftest test-loads-migration-from-resource 111 | (testing "loads a datomic schema from edn in a resource" 112 | (let [migration (read-resource "migrations/001-alter-schema.edn") 113 | conn (fresh-conn)] 114 | (is (ensure-installed conn migration)) 115 | (is (installed? (db conn) :m001/alter-schema)) 116 | @(d/transact conn 117 | [{:db/id (d/tempid :db.part/user) 118 | :life/meaning 42}]) 119 | (let [meaning-of-life (d/q '[:find ?meaning . 120 | :where 121 | [_ :life/meaning ?meaning]] 122 | (db conn))] 123 | (is (= meaning-of-life 42))))) 124 | (testing "derive tx-data from from txes-fn reference in a resource" 125 | (let [alter-schema-migration (read-resource "migrations/001-alter-schema.edn") 126 | populate-data-migration (read-resource "migrations/002-populate.edn") 127 | conn (fresh-conn)] 128 | (ensure-installed conn alter-schema-migration) 129 | (ensure-installed conn populate-data-migration) 130 | (let [meaning-of-life (d/q '[:find ?meaning . 131 | :where 132 | [_ :life/meaning ?meaning]] 133 | (db conn))] 134 | (is (= 42 meaning-of-life)))))) 135 | --------------------------------------------------------------------------------