From 7b6aa69e31227df7de4c4db88139594cd479f89d Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Thu, 14 Nov 2019 17:10:49 -0800 Subject: [PATCH] Fixes #73 by adding optional namespace to extend SettableParameter --- CHANGELOG.md | 1 + src/next/jdbc/date_time.clj | 47 +++++++++++++++++++++++++++++++ test/next/jdbc/date_time_test.clj | 46 ++++++++++++++++++++++++++++++ test/next/jdbc_test.clj | 16 ++--------- 4 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 src/next/jdbc/date_time.clj create mode 100644 test/next/jdbc/date_time_test.clj diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3bb81..5e4a1fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The following changes have been committed to the **master** branch since the 1.0 * Fix #75 by adding support for `java.sql.Statement` to `plan`, `execute!`, and `execute-one!`. * Address #74 by making several small changes to satisfy Eastwood. +* Fix #73 by providing a new, optional namespace `next.jdbc.date-time` that can be required if your database driver needs assistance converting `java.util.Date` (PostgreSQL!) or the Java Time types to SQL `timestamp` (or SQL `date`/`time`). * Fix link to **All The Options** in **Migration from `clojure.java.jdbc`**. PR #71 (@laurio). * Address #70 by adding **CLOB & BLOB SQL Types** to the **Tips & Tricks** section of **Friendly SQL Functions** and by adding `next.jdbc.result-set/clob-column-reader` and `next.jdbc.result-set/clob->string` helper to make it easier to deal with `CLOB` column data. * Clarify what `execute!` and `execute-one!` produce when the result set is empty (`[]` and `nil` respectively, and there are now tests for this). Similarly for `find-by-keys` and `get-by-id`. diff --git a/src/next/jdbc/date_time.clj b/src/next/jdbc/date_time.clj new file mode 100644 index 0000000..c07d121 --- /dev/null +++ b/src/next/jdbc/date_time.clj @@ -0,0 +1,47 @@ +;; copyright (c) 2019 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). + + Some databases support a wide variety of date/time type conversions. + Other databases need a bit of help. You should only require this + namespace if you database does not support these conversions automatically. + + * 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: + * `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." + (:require [next.jdbc.prepare :as p]) + (:import (java.sql PreparedStatement Timestamp) + (java.time Instant LocalDate LocalDateTime))) + +(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))))) diff --git a/test/next/jdbc/date_time_test.clj b/test/next/jdbc/date_time_test.clj new file mode 100644 index 0000000..2466a7c --- /dev/null +++ b/test/next/jdbc/date_time_test.clj @@ -0,0 +1,46 @@ +;; copyright (c) 2019 Sean Corfield, all rights reserved + +(ns next.jdbc.date-time-test + "Date/time parameter auto-conversion tests. + + These tests contain no assertions. Without requiring `next.jdbc.date-time` + several of the `insert` operations would throw exceptions for some databases + so the test here just checks those operations 'succeed'." + (:require [clojure.test :refer [deftest is testing use-fixtures]] + [next.jdbc :as jdbc] + [next.jdbc.date-time] ; to extend SettableParameter to date/time + [next.jdbc.test-fixtures :refer [with-test-db db ds]] + [next.jdbc.specs :as specs]) + (:import (java.sql ResultSet))) + +(set! *warn-on-reflection* true) + +(use-fixtures :once with-test-db) + +(specs/instrument) + +(deftest issue-73 + (try + (jdbc/execute-one! (ds) ["drop table temp_table"]) + (catch Throwable _)) + (jdbc/execute-one! (ds) ["create table temp_table (id int not null, deadline timestamp not null)"]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 1 (java.util.Date.)]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 2 (java.time.Instant/now)]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 3 (java.time.LocalDate/now)]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 4 (java.time.LocalDateTime/now)]) + (try + (jdbc/execute-one! (ds) ["drop table temp_table"]) + (catch Throwable _)) + (jdbc/execute-one! (ds) ["create table temp_table (id int not null, deadline time not null)"]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 1 (java.util.Date.)]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 2 (java.time.Instant/now)]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 3 (java.time.LocalDate/now)]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 4 (java.time.LocalDateTime/now)]) + (try + (jdbc/execute-one! (ds) ["drop table temp_table"]) + (catch Throwable _)) + (jdbc/execute-one! (ds) ["create table temp_table (id int not null, deadline date not null)"]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 1 (java.util.Date.)]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 2 (java.time.Instant/now)]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 3 (java.time.LocalDate/now)]) + (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 4 (java.time.LocalDateTime/now)])) diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index a7b7c42..eadc7f6 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -1,7 +1,7 @@ ;; copyright (c) 2019 Sean Corfield, all rights reserved (ns next.jdbc-test - "Not exactly a test suite -- more a series of examples." + "Basic tests for the primary API of `next.jdbc`." (:require [clojure.string :as str] [clojure.test :refer [deftest is testing use-fixtures]] [next.jdbc :as jdbc] @@ -11,7 +11,7 @@ [next.jdbc.prepare :as prep] [next.jdbc.result-set :as rs] [next.jdbc.specs :as specs]) - (:import (java.sql ResultSet ResultSetMetaData))) + (:import (java.sql ResultSet))) (set! *warn-on-reflection* true) @@ -236,15 +236,3 @@ VALUES ('Pear', 'green', 49, 47) (into [] (map pr-str) (jdbc/plan (ds) ["select * from fruit"])))) (is (thrown? IllegalArgumentException (doall (take 3 (jdbc/plan (ds) ["select * from fruit"])))))) - -(deftest issue-73 - ;; only postgresql cannot convert a java.util.Date to a timestamp column... - (try - (jdbc/execute-one! (ds) ["drop table temp_table"]) - (catch Throwable _)) - (jdbc/execute-one! (ds) ["create table temp_table (id int not null, deadline timestamp not null)"]) - (try - (jdbc/execute-one! (ds) ["insert into temp_table (id, deadline) values (?,?)" 1 (java.util.Date.)]) - (catch Throwable t - (println "Issue #73:" (db) "cannot convert java.util.Date to timestamp by default?") - (println (ex-message t)))))