-
-
Notifications
You must be signed in to change notification settings - Fork 308
API overview
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.
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 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))]))
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))]
))
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 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"))
For queries you can either use d/q
or d/pull
just like in Datomic.
Datalog queries in-depth: query and pull
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 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)))
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>"]}
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 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}))))