Skip to content
Teodor Heggelund edited this page Dec 29, 2018 · 19 revisions

The data model in both Datascript and Datomic is based around atomic facts called datoms.

A datom is a 5-tuple consisting of:

  • Entity ID
  • Attribute
  • Value
  • Transaction ID
  • Whether the fact is being added or retracted

The Entity ID, Attribute and Value for each datom must be provided. The transaction ID is generated.

Setup

To use Datascript, first require the namespaces you need such as datascript.core and datascript.db etc. Then optionally define your database schema and populate it with initial datoms using d/datom as shown below.

(ns ds.core
  (:require [datascript.core :as d]
            [datascript.db :as db]))

;; define schema
(def schema { :aka { :db/cardinality :db.cardinality/many }})

;; populate db with initial datoms
(def datoms #{(d/datom 1 :age  17)
              (d/datom 1 :name "Ivan")})

Create Database

Create an empty database with d/empty-db which can optionally take a schema. Then you can add datoms to the database using d/db-with that takes a list of transactions such as datoms added via :db/add.

(def initial-data [[:db/add 1 :name "Petr"]
                   [:db/add 1 :age 44]
                   [:db/add 2 :name "Ivan"]
                   [:db/add 2 :age 25]])
(def test-db
  (let [db (-> (d/empty-db)
             (d/db-with initial-data))]))

Indexes

When you create the database, you can use :db/index to create indexes for attributes, such as for age in this example: { :age { :db/index true } }. Then when you add atoms such as [:db/add 1 :age 44] they will be indexed accordingly.

See index tests for more examples.

(deftest test-datom-index
  (let [db (-> (d/empty-db { :age { :db/index true }})
               (d/db-with initial-data))]
  ))

Connect

d/create-conn can be used to create a connection to a DB (or schema). Examples taken from conn tests where you can find more usage examples.

(deftest test-conn
  (let [conn (d/create-conn)]

(deftest test-conn-schema
  (let [conn (d/create-conn {:aka { :db/cardinality :db.cardinality/many }})]

Entities

Entities are identified using db/id such as for the datom {:db/id 1, :name "Ivan"}. You can then get an entity by its identity, such as 1 using (d/entity db 1). In the example below we store the entity in the local var e and then test the name of the entity with (is (= (:name e) "Ivan"))

See entity tests for more examples.

(deftest test-entity
  (let [db (-> (d/empty-db)
               (d/db-with [{:db/id 1, :name "Ivan", :age 19}
                           {:db/id 2, :name "Katerina", :sex "female"}]))
        e  (d/entity db 1)]
    (is (= (:name e) "Ivan"))

Queries

For queries you can either use d/q or d/pull just like in Datomic.

Datalog queries in-depth: query and pull

Query

Database queries are performed via d/q, in the form (d/q query db) where the query is an escaped list such as:

'[:find ?e
  :where [?e :name]]
;; query
(deftest test-where
  (let [db (-> (d/empty-db)
               (d/db-with [ { :db/id 1, :name  "Ivan", :age   15 }
                            { :db/id 2, :name  "Petr", :age   37 }
                            { :db/id 3, :name  "Ivan", :age   37 }
                            { :db/id 4, :age 15 }]))]
    (is (= (d/q '[:find ?e
                  :where [?e :name]] db)
           #{[1] [2] [3]}))

You can also use parameterized queries using the special in form. Here we use :in [$ ?attr ?value] to specify how parameters are passed in and then pass the parameters as the last arguments to d/q as :name "Ivan" where :name is inserted for ?attr and "Ivan" for ?value.

(deftest test-q-in
  (let [db (-> (d/empty-db)
               (d/db-with [ { :db/id 1, :name  "Ivan", :age   15 }
                            { :db/id 2, :name  "Petr", :age   37 }
                            { :db/id 3, :name  "Ivan", :age   37 }]))
        query '{:find  [?e]
                :in    [$ ?attr ?value]
                :where [[?e ?attr ?value]]}]
    (is (= (d/q query db :name "Ivan")
           #{[1] [3]}))

Pull

Pull is a declarative way to make hierarchical (and possibly nested) selections of information about entities. Pull applies a pattern to a collection of entities, building a map for each entity.

Pull queries are performed using d/pull in the form: (d/pull db query).

;; define schema
(def ^:private test-schema
  {:name   { :db/valueType :db.type/string }})

;; datoms for DB
(def test-datoms
  (->>
    [[1 :name  "Petr"]]))

;; initialize db with datoms and schema
(def ^:private test-db (d/init-db test-datoms test-schema))

(deftest test-pull-attr-spec
  (is (= {:name "Petr"}
         ;; make a pull query from test-db 
         (d/pull test-db '[:name] 1)))

Filter

d/filter can be used to filter a database, given a filter function such as remove-pass or remove-ivan as shown below.

(deftest test-filter-db
  (let [db (-> (d/empty-db {:aka { :db/cardinality :db.cardinality/many }})
               (d/db-with [{:db/id 1
                            :name  "Petr"
                            :email "[email protected]"
                            :aka   ["I" "Great"]
                            :password "<SECRET>"}

        remove-pass (fn [_ datom] (not= :password (:a datom)))
        remove-ivan (fn [_ datom] (not= 2 (:e datom)))

      (d/filter db remove-pass) #{}
      (d/filter db remove-ivan) #{["<SECRET>"] ["<UNKWOWN>"]}

Transactions

d/transact! is used to transact on a connection, such as adding new datoms via db/add

(deftest test-transact!
  (let [conn (d/create-conn {:aka { :db/cardinality :db.cardinality/many }})]
    (d/transact! conn [[:db/add 1 :name "Ivan"]])

Upsert

Upsert is used to insert/update data.

See upsert tests for examples.

(deftest test-upsert
  (let [db (d/db-with (d/empty-db {:name  { :db/unique :db.unique/identity }
                                   :email { :db/unique :db.unique/identity }})
                      [{:db/id 1 :name "Ivan" :email "@1"}
                       {:db/id 2 :name "Petr" :email "@2"}])
        touched (fn [tx e] (into {} (d/touch (d/entity (:db-after tx) e))))
        tempids (fn [tx] (dissoc (:tempids tx) :db/current-tx))]

    (testing "upsert, no tempid"
      (let [tx (d/with db [{:name "Ivan" :age 35}])]
        (is (= (touched tx 1)
               {:name "Ivan" :email "@1" :age 35}))
        (is (= (tempids tx)
               {}))))

    (testing "upsert by 2 attrs, no tempid"
      (let [tx (d/with db [{:name "Ivan" :email "@1" :age 35}])]
        (is (= (touched tx 1)
               {:name "Ivan" :email "@1" :age 35}))
        (is (= (tempids tx)
               {}))))
    
    (testing "upsert with tempid"
      (let [tx (d/with db [{:db/id -1 :name "Ivan" :age 35}])]
        (is (= (touched tx 1)
               {:name "Ivan" :email "@1" :age 35}))
        (is (= (tempids tx)
               {-1 1}))))
Clone this wiki locally