datomic query over history - clojure

What is the correct way to query all the properties of a datomic db entity over its history?
For example, with the pull API or pull expressions within a query one can use wildcards to print all the properties of a given entity. However, the same approach does not work for the special history db.
(d/q '[:find [(pull ?e [*]) ...] :where [?e :test/firstName "Bob"]] db-test)
; outputs list of Bob's properties
(d/q '[:find [(pull ?e [*]) ...] :where [?e :test/firstName "Bob"]] (d/history db-test))
; IllegalStateException Can't pull from history

You can use query to return all datoms for a single entity for all of history:
(d/q '[:find ?e ?a ?v ?t ?op
:in $ ?e
:where [?e ?a ?v ?t ?op]]
(d/history (d/db conn)) <Your Entity ID>)

Related

How to find only one record from query in datomic?

I'm doing a query on datomic using datomic.api like the following:
(d/q
'[:find [(pull ?a [*]) ...]
:in $ ?title
:where
[?a :movie/title ?title]]
db title)
This query is returning almost the expected value, but as an array, like this:
[ {:db/id 17592186045442, :movie/title "Test", :movie/year 1984, :movie/director #:db{:id 17592186045439 }} ]
I want this query to return only the first match, and not all the results. What I'm doing wrong?
I found a solution for my specific case. The real issue was that I was not understanding the datomic query correctly.
[:find [(pull ?a [*]) ...]
This part is telling datomic to retrieve more than one result.
I changed the query to the following one:
(d/q
'[:find (pull ?a [*]) .
:in $ ?title
:where
[?a :movie/title ?title]]
db title)
And it worked!
The key thing was to remove the "[" after :find keyword, and switch the "..." for only ".".
If this doesn't work for you, look on the link that #EugenePakhomov posted on the comments: Equivalent of SQL "limit" clause in Datomic
It is documented in the official Datomic documentation:
Find Spec
:find ?a ?b relation (Collection of Lists)
:find [?a …] collection (Collection)
:find [?a ?b] single tuple (List)
:find ?a . single scalar (Scalar Value)

Are nested lookup refs in composite tuples supported in datomic?

I'm using Datomic Ions to develop an application. In my schema I use composite tuples to guarantee uniqueness: shelves have books and the shelf+book combination must be unique. This is my schema:
{:db/ident :shelf/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/value
:db/doc "A shelf is a grouping of books"}
;; Books
{:db/ident :book/shelf
:db/valueType :db.type/ref
:db/isComponent true
:db/cardinality :db.cardinality/one
:db/doc "Shelf this book belongs to"}
{:db/ident :book/id
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "The book identifier"}
;; Shelf + Book combination must be unique
{:db/ident :book/shelf+book
:db/valueType :db.type/tuple
:db/tupleAttrs [:book/shelf :book/id]
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
With the schema above I can do the following pull/queries:
(d/pull db '[*] [:shelf/name "my-shelf"])
Returns: {:db/id 74766790688854, :shelf/name "my-shelf"}
And:
(d/q '[:find ?b ?id
:in $ ?shelf+book
:where [?b :book/shelf+book ?shelf+book]
[?b :book/id ?id]]
db [74766790688854 "book-1"])
Returns: [[101155069755527 "book-1"]].
However I would like to use a lookup ref to resolve the shelf reference in a single query to avoid having to do a separate query to get the shelf reference, something like:
(d/q '[:find ?b ?id
:in $ ?shelf+book
:where
[?b :book/shelf+book ?shelf+book]
[?b :book/id ?id]]
db [[:shelf/name "my-shelf"] "book-1"])
But the above returns []. Is it possible to nest lookup refs like the above example?
I never tried this with composite tuples, but this is how it works in principle:
You can use the pull API inside normal queries like so:
(d/q '[:find ?id (pull ?b [:shelf/name])
:in $ ?shelf+book
:where
[?b :book/shelf+book ?shelf+book]
[?b :book/id ?id]]
db [[:shelf/name "my-shelf"] "book-1"])
I use this a lot for querying graphs inside datomic. Here's an example from my application:
(d/q '[:find
?id
(pull ?t [:db/ident])
(pull ?target-node [:my.graph.node/id
:my.graph.node/label])
:where
[?e :my.graph.edge/id ?id]
[?e :my.graph.edge/type ?t]
[?e :my.graph.edge/target ?target-node]]
db)
Where my.graph.edge/target is a :ref and my.graph.edge/type is the value of an enum.
An example output would be
[[#uuid"8e5fb3a4-1cac-40e2-ab8f-33352b6cabb3"
#:db{:ident :my.graph.edge.type/relationship}
#:my.graph.node{:label "Some node"}]
...]

Error returning a Map instead of a Vector using Datomic

I'm doing some query in Datomic using Clojure, I'm trying to return a Map with keys instead of a Vector, if I don't try to return a Map with the ":keys" keyword in the query it works fine.
I tried to have equal and different names between the :find and :keys.
If I remove the :keys line bellow it works fine.
I'm using [org.clojure/clojure "1.10.0"] with [com.datomic/client-pro "0.8.28"].
(def get-links
'[:find ?e ?url ?description ?createdat ?order ?postedby
:keys e url description createdat order postedby
:in $ ?filter ?skip ?skip-plus-first
:where [?e :link/url ?url]
[?e :link/description ?description]
[?e :link/createdat ?createdat]
[?e :link/postedby ?e2]
[?e :link/order ?order]
[?e2 :user/name ?postedby]
[(.contains ?url ?filter)]
[(> ?order ?skip) ]
[(<= ?order ?skip-plus-first)]])
Here is how I'm calling it:
(d/q get-links db filter skip (+ first skip))
The exact error is:
Execution error (ExceptionInfo) at datomic.client.api.async/ares (async.clj:56).
"Argument :keys in :find is not a variable"
Below is Datomic examples, in their docs.
[:find ?artist-name ?release-name
:keys artist release
:where [?release :release/name ?release-name]
[?release :release/artists ?artist]
[?artist :artist/name ?artist-name]]
I think that you are using an older version of the client that doesn't know the :keys option yet.

Filter by cardinality-many field that contains ALL the values

I’ve got an account model that has interests, which is an array of strings (db.type=string and cardinality=many). For example:
{:id 42
:name "Test"
:interests ["cars" "games" "etc"]
:age 50}
Then another list of interests come from the request. How can I query accounts what have ALL the specified interests?
For example, for ["cars" "games"] I'll get the account #42 since it includes the passed list. But for ["cars" "games" "books"] I won't because "books" is an extra one.
UPD
What I have is:
;; model
{:db/ident :account/interests
:db/valueType :db.type/string
:db/cardinality :db.cardinality/many
:db/index true}
;; a query
[:find ?id
:id $ [?interest ...]
:where
[?a :account/interests ?interest]
[?a :account/id ?id]]
So how should I build a Datomic query?
Try using entities in a query
(d/q '[:find ?e
:in $ ?interests
:where
[?e :interests _]
[(datomic.api/entity $ ?e) ?ent]
[(:interests ?ent) ?ent_interests]
[(subset? ?interests ?ent_interests)]]
(d/db conn)
#{"cars" "games"})
(time (d/q
'[:find ?id
:in $
:where
[?a :account/interests "foo"]
[?a :account/interests "bar"]
[?a :account/id ?id]]
(d/db conn)))
"Elapsed time: 3.771973 msecs"
(time (d/q
'[:find ?id
:in $ ?iii
:where
[(datomic.api/entity $ ?a) ?e]
[(:account/interests ?e) ?interests]
[(clojure.set/superset? ?interests ?iii)]
[?a :account/id ?id]]
(d/db conn)
#{"50 Cent" "Целоваться"}))
"Elapsed time: 169.767354 msecs"
The first one is better.

Retrieve Most Recent Entity from Datomic

I'm interested in entities and their timestamps. Essentially, I want a time-sorted list of entities.
To that end, I've composed the following functions:
(defn return-posts
"grabs all posts from Datomic"
[]
(d/q '[:find ?title ?body ?slug
:where
[?e :post/title ?title]
[?e :post/slug ?slug]
[?e :post/body ?body]] (d/db connection)))
(defn get-postid-from-slug
[slug]
(d/q '[:find ?e
:in $ ?slug
:where [?e :post/slug ?slug]] (d/db connection) slug))
(defn get-post-timestamp
"given an entid, returns the most recent timestamp"
[entid]
(->
(d/q '[:find ?ts
:in $ ?e
:where
[?e _ _ _]
[?e :db/txInstant ?ts]] (d/db connection) entid)
(sort)
(reverse)
(first)))
Which I feel must be a hack rooted in ignorance.
Would someone more well-versed in idiomatic Datomic usage chime in and upgrade my paradigms?
I was bothered by the idea of adding additional timestamps to a database that nominally understands time as a first-class principle and so (after a night of mulling on the approaches outlined by Ulrik Sandberg) evolved the following function:
(defn return-posts
"grabs all posts from Datomic"
[uri]
(d/q '[:find ?title ?body ?slug ?ts
:where
[?e :post/title ?title ?tx]
[?e :post/slug ?slug]
[?e :post/body ?body]
[?tx :db/txInstant ?ts]] (d/db (d/connect uri))))
It's idiomatic in Datalog to omit the binding to the transaction ID itself as we typically don't care. In this situation, we very definitely care and in the words of August Lileaas, wish to "traverse the transaction" (there are situations in which we'd want the post creation time, but for this application the transaction time will suffice for ordering entities).
A notable downside to this approach is that recently edited entries will be bumped up in the list. To that end, I'll have to do something later on in order to get their "first appearance" in Datomic for blog-standard post history.
To summarize:
I have bound the transaction entity ID per "post" entity ID, and then looked up the transaction timestamp with this function for later sorting.
There isn't really a more elegant way to do this than traversing the transactions. This is why I prefer to have a separate domain specific attribute for timestamps, instead of relying on the transaction timestamps from Datomic. One example where this is necessary is merging: let's say you have a wiki, and you want to merge two wiki pages. In that case, you probably want to control the timestamp yourself, and not use the timestamp from the transaction.
I like to have the attributes :created-at and :changed-at. When I transact new entities:
[[:db/add tempid :post/slug "..."]
[:db/add tempid :post/title "A title"]
[:db/add tempid :created-at (java.util.Date.)]
[:db/add tempid :changed-at (java.util.Date.)]]
Then for updates:
[[:db/add post-eid :post/title "An updated title"]
[:db/add post-eid :changed-at (java.util.Date.)]]
That way all I have to do is to read out the :created-at attribute of the entity, which will be ready and waiting in the index.
(defmacro find-one-entity
"Returns entity when query matches, otherwise nil"
[q db & args]
`(when-let [eid# (ffirst (d/q ~q ~db ~#args))]
(d/entity ~db eid#)))
(defn find-post-by-slug
[db slug]
(find-one-entity
'[:find ?e
:in $ ?slug
:where
[?e :post/slug ?slug]]
db
slug))
;; Get timestamp
(:created-at (find-post-by-slug db "my-post-slug"))