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

+14 votes
in Datomic by

Support for other types from java.Time: Year, YearMonth, LocalTime etc.

1 Answer

0 votes
edited by

The lack of java.time support is getting more and more annoying as our code-base grows.
It turned out that even passing a YearMonth value to a transaction function, which would convert it to java.util.Date is not possible.

I've tried to extend the transit reader & writer handlers, but it was not sufficient, because somewhere along the way the YearMonth instance has been converted into a com.cognitect.transit.impl.TaggedValueImpl and stayed like that when it reached our transaction function.

The same transaction worked with d/with though, because it doesn't seem to involve marshalling and unmarshalling.

I've even checked within datomic.client.impl.shared.io/marshal, that the marshalled byte-array, when unmarshalled, is equal to the input (m)

(defn- marshal
  "Encodes val as Transit data in msgpack. Returns a map with keys:

  :bytes - a Java byte array of the marshalled data
  :length - the length in bytes of the marshalled data.

  Note that for efficiency's sake the byte array may be longer than
  the returned length."
  (clojure.pprint/pprint m)
  (let [stm (BytesOutputStream.)
        w (transit/writer stm :msgpack {:handlers write-handlers})]
    (transit/write w m)
    (let [unmarshalled (unmarshal (java.io.ByteArrayInputStream. (.internalBuffer stm)))]
      (clojure.pprint/pprint [unmarshalled (= m unmarshalled)]))
    {:bytes  (.internalBuffer stm)
     :length (.length stm)}))

the result:

{:t 3857,
 :next-t 3858,
 :db-name "predict",
 :database-id "6652bfa3-2e73-445f-8379-2448f824cbf3",
 :tx-id #uuid "25c92f68-a931-44b7-b96d-c4be8fa147fb",
 [[ginoco.datomic.component/loophole #time/year-month "2000-01"]],
 :op :transact}

[{:t 3857,
  :next-t 3858,
  :db-name "predict",
  :database-id "6652bfa3-2e73-445f-8379-2448f824cbf3",
  :tx-id #uuid "25c92f68-a931-44b7-b96d-c4be8fa147fb",
  [[ginoco.datomic.component/loophole #time/year-month "2000-01"]],
  :op :transact}
edited by
here is the repro code i was using, but it relies on the specifics of our system.
when i tried with Datomic Local on an in-memory DB, i couldn't reproduce the issue, because i suspect some of the layers are left out in that implementation.

(ns repl.java-time
    [cognitect.transit :as transit]
    [datomic.client.api :as d]
    [datomic.client.impl.shared.io :as datomic-io]
    [gini.common :refer :all]
    [ginoco.datomic.component :as dc]
    [ginoco.repl :refer :all]
    [richelieu.core :as ri]
    [tick.core :as t])
  (:import (java.io ByteArrayInputStream)
           (java.time YearMonth)))

;;; Passing YearMonth in arguments for Datomic tx fns
(def java-time-transit-read-handlers
  {"year-month" (transit/read-handler
                  (comp (flip ? "year-month reader")

(alter-var-root #'datomic-io/read-handlers
                #(merge % java-time-transit-read-handlers))

(def java-time-transit-write-handlers
  {YearMonth (transit/write-handler "year-month" str str str)})

(alter-var-root #'datomic-io/write-handlers
                #(merge % java-time-transit-write-handlers))

(defn year-month-tx [year-month]
  {:tx-data [[`dc/loophole (t/year-month year-month)]]})

  (-> time-literals.read-write/tags)

  (ri/advice d/transact)
  (ri/unadvise-var #'d/transact #'ginoco.datomic.api/transact-instants)

  (-> (year-month-tx "2000-01") (->> (d/transact (:conn (gini-db)))) :tx-data)

  ;=== datomic.client.impl.shared.io/marshal ===
  ; at datomic.client.impl.shared.io$marshal.invokeStatic(io.clj:47)
  ;{:database-id "6652bfa3-2e73-445f-8379-2448f824cbf3",
  ; :db-name "predict",
  ; :next-t 3854,
  ; :op :transact,
  ; :t 3853,
  ; :tx-data [[ginoco.datomic.component/loophole
  ;            #<java.time.YearMonth@3aa12edb 2000-01>]],
  ; :tx-id #uuid "8a085222-2672-46a9-8042-1747da42ee8d"}
  ;=== LOOPHOLE ===
  ; at ginoco.datomic.component$loophole.invokeStatic(component.clj:485)
  ;{:class #<Class@19b1fd6c com.cognitect.transit.impl.TaggedValueImpl>,
  ; :rep "2000-01",
  ; :tag "year-month"}
  ;[#datom[13194139537166 50 #inst"2023-11-03T05:39:14.147-00:00" 13194139537166 true]
  ; #datom[83562883714830 10 :test/entity 13194139537166 true]
  ; #datom[83562883714830 63 "com.cognitect.transit.impl.TaggedValueImpl@b1ff6900"
  ;          13194139537166 true]]

  (ri/advise-var #'d/transact #'ginoco.datomic.api/transact-instants)

  (d/transact (:conn (gini-db)) {:tx-data [[:db/retractEntity :test/entity]]})

  (-> (year-month-tx "2000-01") (->> (d/with (d/with-db (:conn (gini-db))))) :tx-data)

  ;=== LOOPHOLE ===
  ; at ginoco.datomic.component$loophole.invokeStatic(component.clj:485)
  ;{:class #<Class@7438645b java.time.YearMonth>,
  ; :leapYear true,
  ; :month #<java.time.Month@66d4e153 JANUARY>,
  ; :monthValue 1,
  ; :year 2000}
  ;[#datom[13194139537166 50 #inst"2023-11-03T05:36:43.530-00:00" 13194139537166 true]
  ; #datom[83562883714830 10 :test/entity 13194139537166 true]
  ; #datom[83562883714830 63 "2000-01" 13194139537166 true]]


  (-> (t/year-month "2000-10")
      (#'datomic-io/marshal) :bytes (ByteArrayInputStream.)

  ;=== datomic.client.impl.shared.io/marshal ===
  ; at datomic.client.impl.shared.io$marshal.invokeStatic(io.clj:47)
  ;#<java.time.YearMonth@188506f8 2000-10>
  ;=== year-month reader ===
  ; at gini.common$flip$fn__231.invoke(common.cljc:37)
  ;#<java.time.YearMonth@69f0f656 2000-10>
  ;=> #time/year-month"2000-10"


the `loophole` transaction function i've used was this:

(defn loophole [_db-val year-month]
  (-> year-month bean (? "LOOPHOLE"))
  [{:db/ident :test/entity :db/doc (-> year-month str)}])

where `?` is just a fancy version of `(fn [x comment] (-> x (doto clojure.pprint/pprint)))`.
Welcome to the Datomic Knowledgebase, where you can make features requests, ask questions and receive answers from other members of the community.