Welcome! Please see the About page for a little more info on how this works.

+1 vote
ago in On-Prem by

does datomic pro print and read queries during datomic.api/q, maybe as part of its query cache? we’ve changed the #inst reader in our project to return java.time.Instant and now a java.util.Date we pass to a query comes out as a java.time.Instant. Is that expected & documented somewhere?

(require '[datomic.api])

;; this is demonstrating an issue we ran into where overwriting the
;; `#inst` reader broke `datomic.api/q` in surprising ways.

(def schema
  [{:db/ident :exit/datetime
    :db/valueType :db.type/instant
    :db/cardinality :db.cardinality/one}])

(def conn
  (datomic.api/connect (doto "datomic:mem://inst-java-time-readers" datomic.api/create-database)))

@(datomic.api/transact conn schema)

(def db+data
  (:db-after
   @(datomic.api/transact conn [{:exit/datetime #inst "2024"}
                                {:exit/datetime #inst "2025"}
                                {:exit/datetime #inst "2026"}])))

;; this works
(count (datomic.api/q [:find '?f
                       :where
                       '[?f :exit/datetime ?exit]
                       [(list '< '?exit (java.util.Date.))]]
                      db+data))


;; fails silently (returns incorrect number of results)
(binding [*data-readers* (assoc *data-readers* 'inst (fn [cs] (java.util.Date/.toInstant (clojure.instant/read-instant-date cs))))]
  (count (datomic.api/q [:find '?f
                         :where
                         '[?f :exit/datetime ?exit]
                         [(list '< '?exit (java.util.Date.))]]
                        db+data)))

;; fails loudly with processing rule: (q__124130 ?f), message:
;; processing clause: [?f :exit/datetime ?exit], message:
;; java.lang.ClassCastException: class java.time.Instant cannot be
;; cast to class java.util.Date (java.time.Instant and java.util.Date
;; are in module java.base of loader 'bootstrap')
(binding [*data-readers* (assoc *data-readers* 'inst (fn [cs] (java.util.Date/.toInstant (clojure.instant/read-instant-date cs))))]
  (count (datomic.api/q [:find '?f
                         :where
                         '[?f :exit/datetime ?exit]
                         [(list java.util.Date/.before '?exit (java.util.Date.))]]
                        db+data)))

;; My guess is that this is related to datomic's [query
;; caching](https://docs.datomic.com/query/query-executing.html#query-caching)
;; because once a query has been cached, a sucessive call with the
;; same query value and a bound #inst reader succeeds:
(let [query [:find '?f
             :where
             '[?f :exit/datetime ?exit]
             [(list java.util.Date/.before '?exit (java.util.Date.))]]]
  (= (count (datomic.api/q query db+data))
     (binding [*data-readers* (assoc *data-readers* 'inst (fn [cs] (java.util.Date/.toInstant (clojure.instant/read-instant-date cs))))]
       (count (datomic.api/q query db+data)))))

1 Answer

+2 votes
ago by

Favilla answered this on Clojurians slack:

Function expressions in queries are compiled with eval. In your case it would be evaluating something like (clojure.core/fn [?exit] (java.util.Date/.before ?exit <j.u.Date-object>) )
But for reasons I don't know, clojure eval invokes the reader on the date object:

(def dr-instant
  (assoc *data-readers* 'inst (fn [cs] (java.util.Date/.toInstant (clojure.instant/read-instant-date cs)))))

(let [expr [(java.util.Date.)]]
  (-> (binding [*data-readers* dr-instant]
        (eval expr))
      first
      class))
;; => java.time.Instant

(let [expr (list 'do (java.util.Date.))]
  (-> (binding [*data-readers* dr-instant]
        (eval expr))
      class))
;; => java.util.Date

(let [expr (java.util.Date.)]
  (-> (binding [*data-readers* dr-instant]
        (eval expr))
      class))
;; => java.util.Date
...