;; copyright (c) 2019-2021 Sean Corfield, all rights reserved

(ns next.jdbc.date-time
  "Optional namespace that extends `next.jdbc.prepare/SettableParameter`
  to various date/time types so that they will all be treated as SQL
  timestamps (which also supports date and time column types) and has
  functions to extend `next.jdbc.result-set/ReadableColumn`.

  Simply requiring this namespace will extend the `SettableParameter` protocol
  to the four types listed below.

  In addition, there are several `read-as-*` functions here that will
  extend `next.jdbc.result-set/ReadableColumn` to allow `java.sql.Date`
  and `java.sql.Timestamp` columns to be read as (converted to) various
  Java Time types automatically. The expectation is that you will call at
  most one of these, at application startup, to enable the behavior you want.

  Database support for Java Time:
  * H2 and SQLite support conversion of Java Time (`Instant`, `LocalDate`,
    `LocalDateTime`) out of the box,
  * Nearly all databases support conversion of `java.util.Date` out of
    the box -- except PostgreSQL apparently!

  Types supported by this namespace:
  * `java.time.Instant`
  * `java.time.LocalDate`
  * `java.time.LocalDateTime`
  * `java.util.Date` -- mainly for PostgreSQL

  PostgreSQL does not seem able to convert `java.util.Date` to a SQL
  timestamp by default (every other database can!) so you'll probably
  need to require this namespace, even if you don't use Java Time, when
  working with PostgreSQL."
  (:require [next.jdbc.prepare :as p]
            [next.jdbc.result-set :as rs])
  (:import (java.sql PreparedStatement Timestamp)))

(set! *warn-on-reflection* true)

(extend-protocol p/SettableParameter
  ;; Java Time type conversion:
  java.time.Instant
  (set-parameter [^java.time.Instant v ^PreparedStatement s ^long i]
    (.setTimestamp s i (Timestamp/from v)))
  java.time.LocalDate
  (set-parameter [^java.time.LocalDate v ^PreparedStatement s ^long i]
    (.setTimestamp s i (Timestamp/valueOf (.atStartOfDay v))))
  java.time.LocalDateTime
  (set-parameter [^java.time.LocalDateTime v ^PreparedStatement s ^long i]
    (.setTimestamp s i (Timestamp/valueOf v)))

  ;; this is just to help PostgreSQL:
  java.util.Date
  (set-parameter [^java.util.Date v ^PreparedStatement s ^long i]
    (.setTimestamp s i (Timestamp/from (.toInstant v))))
  ;; but avoid unnecessary conversions for SQL Date and Timestamp:
  java.sql.Date
  (set-parameter [^java.sql.Date v ^PreparedStatement s ^long i]
    (.setDate s i v))
  java.sql.Timestamp
  (set-parameter [^java.sql.Timestamp v ^PreparedStatement s ^long i]
    (.setTimestamp s i v)))

(defn read-as-instant
  "After calling this function, `next.jdbc.result-set/ReadableColumn`
  will be extended to (`java.sql.Date` and) `java.sql.Timestamp` so that any
  timestamp columns will automatically be read as `java.time.Instant`.

  Note that `java.sql.Date` columns will still be returned as-is because they
  cannot be converted to an instant (they lack a time component)."
  []
  (extend-protocol rs/ReadableColumn
    java.sql.Date
    (read-column-by-label [^java.sql.Date v _]     v)
    (read-column-by-index [^java.sql.Date v _2 _3] v)
    java.sql.Timestamp
    (read-column-by-label [^java.sql.Timestamp v _]     (.toInstant v))
    (read-column-by-index [^java.sql.Timestamp v _2 _3] (.toInstant v))))

(defn read-as-local
  "After calling this function, `next.jdbc.result-set/ReadableColumn`
  will be extended to `java.sql.Date` and `java.sql.Timestamp` so that any
  date or timestamp columns will automatically be read as `java.time.LocalDate`
  or `java.time.LocalDateTime` respectively."
  []
  (extend-protocol rs/ReadableColumn
    java.sql.Date
    (read-column-by-label [^java.sql.Date v _]     (.toLocalDate v))
    (read-column-by-index [^java.sql.Date v _2 _3] (.toLocalDate v))
    java.sql.Timestamp
    (read-column-by-label [^java.sql.Timestamp v _]     (.toLocalDateTime v))
    (read-column-by-index [^java.sql.Timestamp v _2 _3] (.toLocalDateTime v))))

(defn read-as-default
  "After calling this function, `next.jdbc.result-set/ReadableColumn`
  will be extended to `java.sql.Date` and `java.sql.Timestamp` so that any
  date or timestamp columns will be read as-is. This is provided for
  completeness, to undo the effects of `read-as-instant` or `read-as-local`."
  []
  (extend-protocol rs/ReadableColumn
    java.sql.Date
    (read-column-by-label [^java.sql.Date v _]     v)
    (read-column-by-index [^java.sql.Date v _2 _3] v)
    java.sql.Timestamp
    (read-column-by-label [^java.sql.Timestamp v _]     v)
    (read-column-by-index [^java.sql.Timestamp v _2 _3] v)))
