4

I used next.jdbc library to persist a record to a PostgreSQL table. As you can see below, the values are a LocalDate instance and an Instant instance. The SQL columns are of type DATE and TIMESTAMPTZ respectively.

Required the next.jdbc.date-time namespace to map from Java -> SQL, as mentioned in the docs.

{:id 7, :trade_date #object[java.time.LocalDate 0x6b751cb1 2020-12-22], :created_at #object[java.time.Instant 0x3add5e17 2020-12-22T11:41:07.557790Z]}

When I read the record back (SQL -> Clojure), the representation is as follows

{:id 7, :trade_date #inst "2020-12-21T18:30:00.000-00:00", :created_at #inst "2020-12-22T11:41:07.557790000-00:00"}

So they are read back as #inst literals? I'd like them back as java.time types. Also note that the LocalDate seems to be converted to a UTC timestamp. Here's what I dug up so far..

; inst -> Instant
=> (.toInstant #inst "2020-12-22T11:41:07.557790000-00:00")
#object[java.time.Instant 0x312cc79e "2020-12-22T11:41:07.557Z"]

; inst -> LocalDate
=> (-> #inst "2020-12-21T18:30:00.000-00:00"
            #_=> .toInstant
            #_=> (ZonedDateTime/ofInstant (ZoneId/of "UTC"))
            #_=> (.withZoneSameInstant (ZoneId/systemDefault))
            #_=> (.toLocalDate))
#object[java.time.LocalDate 0x44048462 "2020-12-22"]

Doesn't seem simple / the Clojure way. Is there a better way?

References: Pieced from the answers by Alan Thompson and Basil Bourque

Gishu
  • 134,492
  • 47
  • 225
  • 308

3 Answers3

3

1 hr later. Well that didn't work.

Turns out next.jdbc reconstructs the record with java.sql.* types, which happen to be printed out as #inst literals at the REPL.

Calling (.toInstant sql-date-instance) blows up with an Exception (this is as designed). However armed with this bit of info, it was easy to achieve the end result with. Run the returned vector of maps through this mapper.

(defn mapSqlToTimeTypes [map-with-sql-types]
  (reduce (fn [return-map [k, v]]
            
            (cond
              (instance? java.sql.Timestamp v) (assoc return-map k (.toInstant v))
              (instance? java.sql.Date v) (assoc return-map k (.toLocalDate v))
              :else (assoc return-map k v)))
          {} 
          map-with-sql-types))
Gishu
  • 134,492
  • 47
  • 225
  • 308
3

So #inst is just a tagged literal, by default its underlying type is java.util.Date.

(def my-inst #inst "2018-03-28T10:48:00.000")
(= java.util.Date (class my-inst))
;=> true

But you or any library can change the default reader for #inst, since data-readers is a dynamic var. So at any point in time, depending on the lib you are using #inst may produce different underlying type.

I haven't used nex.jdbc yet myself, but I think that what you need is to call read-as-instant or read-as-local function. See here Also checkout clj-time Clojure library. Also checkout clojure.java-time wrapper around java.time

Simon Polak
  • 1,959
  • 1
  • 13
  • 21
  • These options each do half the job - my table has a UTC timestamp and a local Date. So I'll get either an Instant or a LocalDate via these method calls. Can you give me a few links to understand the #inst literal a bit better.. I have a read a couple of Clojure books but dont remember this being covered. – Gishu Dec 22 '20 at 16:14
  • 1
    @AlanThompson, oh thanks. I'm still old school. Gishu, Read a page on clojure reader here https://clojure.org/reference/reader – Simon Polak Dec 23 '20 at 01:45
1

As a small simplification to the answer from @Gishu, it is often very handy to use postwalk for this type of data conversion. Here is a simple function to convert all keywords to plain strings, for example:

(defn walk-keywords->strings
  "Recursively walks form, converting all keywords to strings. "
  [form]
  (walk/postwalk (fn [item]
                   (if (keyword? item)
                     (t/kw->str item)
                     item))
    form))

This is especially useful when handling web requests & responses, which are usually deeply nested maps & vectors.

Alan Thompson
  • 29,276
  • 6
  • 41
  • 48