Fixes #123 by adding type-hinting functions
This commit is contained in:
parent
85734ab724
commit
03792303bc
9 changed files with 110 additions and 6 deletions
|
|
@ -4,6 +4,9 @@ Only accretive/fixative changes will be made from now on.
|
||||||
|
|
||||||
## Stable Builds
|
## Stable Builds
|
||||||
|
|
||||||
|
* 2020-06-24 -- 1.0.next
|
||||||
|
* Address #123 by adding `next.jdbc.types` namespace, full of auto-generated `as-xxx` functions, one for each of the `java.sql.Types` values.
|
||||||
|
|
||||||
* 2020-06-22 -- 1.0.476
|
* 2020-06-22 -- 1.0.476
|
||||||
* Extend default options behavior to `next.jdbc.sql` functions.
|
* Extend default options behavior to `next.jdbc.sql` functions.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -398,12 +398,14 @@ If you are using [Component](https://github.com/stuartsierra/component), a conne
|
||||||
|
|
||||||
By default, `next.jdbc` relies on the JDBC driver to handle all data type conversions when reading from a result set (to produce Clojure values from SQL values) or setting parameters (to produce SQL values from Clojure values). Sometimes that means that you will get back a database-specific Java object that would need to be manually converted to a Clojure data structure, or that certain database column types require you to manually construct the appropriate database-specific Java object to pass into a SQL operation. You can usually automate those conversions using either the [`ReadableColumn` protocol](/doc/result-set-builders.md#readablecolumn) (for converting database-specific types to Clojure values) or the [`SettableParameter` protocol](/doc/prepared-statements.md#prepared-statement-parameters) (for converting Clojure values to database-specific types).
|
By default, `next.jdbc` relies on the JDBC driver to handle all data type conversions when reading from a result set (to produce Clojure values from SQL values) or setting parameters (to produce SQL values from Clojure values). Sometimes that means that you will get back a database-specific Java object that would need to be manually converted to a Clojure data structure, or that certain database column types require you to manually construct the appropriate database-specific Java object to pass into a SQL operation. You can usually automate those conversions using either the [`ReadableColumn` protocol](/doc/result-set-builders.md#readablecolumn) (for converting database-specific types to Clojure values) or the [`SettableParameter` protocol](/doc/prepared-statements.md#prepared-statement-parameters) (for converting Clojure values to database-specific types).
|
||||||
|
|
||||||
In particular, PostgreSQL does not seem to perform a conversion from `java.util.Date` to a SQL data type automatically. You must `require` the [`next.jdbc.date-time` namespace](https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/api/next.jdbc.date-time) to enable that conversion.
|
In particular, PostgreSQL does not seem to perform a conversion from `java.util.Date` to a SQL data type automatically. You can `require` the [`next.jdbc.date-time` namespace](https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/api/next.jdbc.date-time) to enable that conversion.
|
||||||
|
|
||||||
If you are working with Java Time, some JDBC drivers will automatically convert `java.time.Instant` (and `java.time.LocalDate` and `java.time.LocalDateTime`) to a SQL data type automatically, but others will not. Requiring `next.jdbc.date-time` will enable those automatic conversions for all databases.
|
If you are working with Java Time, some JDBC drivers will automatically convert `java.time.Instant` (and `java.time.LocalDate` and `java.time.LocalDateTime`) to a SQL data type automatically, but others will not. Requiring `next.jdbc.date-time` will enable those automatic conversions for all databases.
|
||||||
|
|
||||||
> Note: `next.jdbc.date-time` also provides functions you can call to enable automatic conversion of SQL date/timestamp types to Clojure data types when reading result sets. If you need specific conversions beyond that to happen automatically, consider extending the `ReadableColumn` protocol, mentioned above.
|
> Note: `next.jdbc.date-time` also provides functions you can call to enable automatic conversion of SQL date/timestamp types to Clojure data types when reading result sets. If you need specific conversions beyond that to happen automatically, consider extending the `ReadableColumn` protocol, mentioned above.
|
||||||
|
|
||||||
|
The `next.jdbc.types` namespace provides over three dozen convenience functions for "type hinting" values so that the JDBC driver might automatically handle some conversions that the default parameter setting function does not. Each function is named for the corresponding SQL type, prefixed by `as-`: `as-bigint`, `as-other`, `as-real`, etc. An example of where this helps is when dealing with PostgreSQL enumerated types: the default behavior, when passed a string that should correspond to an enumerated type, is to throw an exception that `column "..." is of type ... but expression is of type character varying`. You can wrap such strings with `(as-other "...")` which tells PostgreSQL to treat this as `java.sql.Types/OTHER` when setting the parameter.
|
||||||
|
|
||||||
## Processing Database Metadata
|
## Processing Database Metadata
|
||||||
|
|
||||||
JDBC provides several features that let you introspect the database to obtain lists of tables, views, and so on. `next.jdbc` does not provide any specific functions for this but you can easily get this metadata from a `java.sql.Connection` and turn it into Clojure data as follows:
|
JDBC provides several features that let you introspect the database to obtain lists of tables, views, and so on. `next.jdbc` does not provide any specific functions for this but you can easily get this metadata from a `java.sql.Connection` and turn it into Clojure data as follows:
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ You can also extend this protocol via metadata so you can do it on a per-object
|
||||||
(with-meta obj {'next.jdbc.prepare/set-parameter (fn [v ps i]...)})
|
(with-meta obj {'next.jdbc.prepare/set-parameter (fn [v ps i]...)})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `next.jdbc.types` namespace provides functions to wrap values with per-object implementations of `set-parameter` for every standard `java.sql.Types` value. Each is named `as-xxx` corresponding to `java.sql.Types/XXX`.
|
||||||
|
|
||||||
The converse, converting database-specific types to Clojure values is handled by the `ReadableColumn` protocol, discussed in the previous section ([Result Set Builders](/doc/result-set-builders.md#readablecolumn)).
|
The converse, converting database-specific types to Clojure values is handled by the `ReadableColumn` protocol, discussed in the previous section ([Result Set Builders](/doc/result-set-builders.md#readablecolumn)).
|
||||||
|
|
||||||
As noted above, `next.jdbc.prepare/set-parameters` is available for you to call on any existing `PreparedStatement` to set or update the parameters that will be used when the statement is executed:
|
As noted above, `next.jdbc.prepare/set-parameters` is available for you to call on any existing `PreparedStatement` to set or update the parameters that will be used when the statement is executed:
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,27 @@ If you have a query where you want to select where a column is `IN` a sequence o
|
||||||
|
|
||||||
What does this mean for your use of `next.jdbc`? In `plan`, `execute!`, and `execute-one!`, you can use `col = ANY(?)` in the SQL string and a single primitive array parameter, such as `(int-array [1 2 3 4])`. That means that in `next.jdbc.sql`'s functions that take a where clause (`find-by-keys`, `update!`, and `delete!`) you can specify `["col = ANY(?)" (int-array data)]` for what would be a `col IN (?,?,?,,,?)` where clause for other databases and require multiple values.
|
What does this mean for your use of `next.jdbc`? In `plan`, `execute!`, and `execute-one!`, you can use `col = ANY(?)` in the SQL string and a single primitive array parameter, such as `(int-array [1 2 3 4])`. That means that in `next.jdbc.sql`'s functions that take a where clause (`find-by-keys`, `update!`, and `delete!`) you can specify `["col = ANY(?)" (int-array data)]` for what would be a `col IN (?,?,?,,,?)` where clause for other databases and require multiple values.
|
||||||
|
|
||||||
|
PostgreSQL has a SQL extension for defining enumerated types and the default `set-parameter` implementation will not work for those. You can use `next.jdbc.types/as-other` to wrap string values in a way that the JDBC driver will convert them to enumerated type values:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TYPE language AS ENUM('en','fr','de');
|
||||||
|
|
||||||
|
CREATE TABLE person (
|
||||||
|
...
|
||||||
|
speaks language NOT NULL,
|
||||||
|
...
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(require '[next.jdbc.sql :as sql]
|
||||||
|
'[next.jdbc.types :refer [as-other]])
|
||||||
|
|
||||||
|
(sql/insert! ds :person {:speaks (as-other "fr")})
|
||||||
|
```
|
||||||
|
|
||||||
|
That call produces a vector `["fr"]` with metadata that implements `set-parameter` such that `.setObject()` is called with `java.sql.Types/OTHER` which allows PostgreSQL to "convert" the string `"fr"` to the corresponding `language` enumerated type value.
|
||||||
|
|
||||||
### Streaming Result Sets
|
### Streaming Result Sets
|
||||||
|
|
||||||
You can get PostgreSQL to stream very large result sets (when you are reducing over `plan`) by setting the following options:
|
You can get PostgreSQL to stream very large result sets (when you are reducing over `plan`) by setting the following options:
|
||||||
|
|
@ -150,7 +171,7 @@ create table example(
|
||||||
;; => #:example{:tags ["tag1" "tag2"]}
|
;; => #:example{:tags ["tag1" "tag2"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: PostgreSQL JDBC driver supports only 7 primitive array types, but not such as `UUID[]` -
|
> Note: PostgreSQL JDBC driver supports only 7 primitive array types, but not array types like `UUID[]` -
|
||||||
[PostgreSQL™ Extensions to the JDBC API](https://jdbc.postgresql.org/documentation/head/arrays.html).
|
[PostgreSQL™ Extensions to the JDBC API](https://jdbc.postgresql.org/documentation/head/arrays.html).
|
||||||
|
|
||||||
### Working with Date and Time
|
### Working with Date and Time
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,11 @@
|
||||||
|
|
||||||
See also https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/api/next.jdbc.date-time
|
See also https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/api/next.jdbc.date-time
|
||||||
for implementations of `SettableParameter` that provide automatic
|
for implementations of `SettableParameter` that provide automatic
|
||||||
conversion of Java Time objects to SQL data types."
|
conversion of Java Time objects to SQL data types.
|
||||||
|
|
||||||
|
See also https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT/api/next.jdbc.types
|
||||||
|
for `as-xxx` functions that provide per-instance implementations of
|
||||||
|
`SettableParameter` for each of the standard `java.sql.Types` values."
|
||||||
(:require [clojure.java.data :as j]
|
(:require [clojure.java.data :as j]
|
||||||
[next.jdbc.protocols :as p])
|
[next.jdbc.protocols :as p])
|
||||||
(:import (java.sql Connection
|
(:import (java.sql Connection
|
||||||
|
|
|
||||||
39
src/next/jdbc/types.clj
Normal file
39
src/next/jdbc/types.clj
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
;; copyright (c) 2018-2020 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns next.jdbc.types
|
||||||
|
"Provides convenience functions for wrapping values you pass into SQL
|
||||||
|
operations that have per-instance implementations of `SettableParameter`
|
||||||
|
so that `.setObject()` is called with the appropriate `java.sql.Types` value."
|
||||||
|
(:require [clojure.string :as str]
|
||||||
|
[next.jdbc.prepare :as prep])
|
||||||
|
(:import (java.lang.reflect Field Modifier)
|
||||||
|
(java.sql PreparedStatement)))
|
||||||
|
|
||||||
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
(defmacro ^:private all-types
|
||||||
|
[]
|
||||||
|
(let [names
|
||||||
|
(into []
|
||||||
|
(comp (filter #(Modifier/isStatic (.getModifiers ^Field %)))
|
||||||
|
(map #(.getName ^Field %)))
|
||||||
|
(.getDeclaredFields java.sql.Types))]
|
||||||
|
`(do
|
||||||
|
~@(for [n names]
|
||||||
|
(let [as-n (symbol (str "as-"
|
||||||
|
(-> n
|
||||||
|
(str/lower-case)
|
||||||
|
(str/replace "_" "-"))))]
|
||||||
|
`(defn ~as-n
|
||||||
|
~(str "Wrap a Clojure value in a vector with metadata to implement `set-parameter`
|
||||||
|
so that `.setObject()` is called with the `java.sql.Types/" n "` SQL type.")
|
||||||
|
[~'obj]
|
||||||
|
(with-meta [~'obj]
|
||||||
|
{'next.jdbc.prepare/set-parameter
|
||||||
|
(fn [[v#] ^PreparedStatement s# ^long i#]
|
||||||
|
(.setObject s# i# v# ~(symbol "java.sql.Types" n)))})))))))
|
||||||
|
|
||||||
|
(all-types)
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(macroexpand '(all-types)))
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
[next.jdbc.sql :as sql]
|
[next.jdbc.sql :as sql]
|
||||||
[next.jdbc.test-fixtures
|
[next.jdbc.test-fixtures
|
||||||
:refer [with-test-db ds column default-options
|
:refer [with-test-db ds column default-options
|
||||||
derby? jtds? maria? mssql? mysql? postgres? sqlite?]]))
|
derby? jtds? maria? mssql? mysql? postgres? sqlite?]]
|
||||||
|
[next.jdbc.types :refer [as-other as-real as-varchar]]))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
|
@ -79,8 +80,9 @@
|
||||||
:else :FRUIT/ID)]
|
:else :FRUIT/ID)]
|
||||||
(testing "single insert/delete"
|
(testing "single insert/delete"
|
||||||
(is (== 5 (new-key (sql/insert! (ds) :fruit
|
(is (== 5 (new-key (sql/insert! (ds) :fruit
|
||||||
{:name "Kiwi" :appearance "green & fuzzy"
|
{:name (as-varchar "Kiwi")
|
||||||
:cost 100 :grade 99.9}))))
|
:appearance "green & fuzzy"
|
||||||
|
:cost 100 :grade (as-real 99.9)}))))
|
||||||
(is (= 5 (count (sql/query (ds) ["select * from fruit"]))))
|
(is (= 5 (count (sql/query (ds) ["select * from fruit"]))))
|
||||||
(is (= {:next.jdbc/update-count 1}
|
(is (= {:next.jdbc/update-count 1}
|
||||||
(sql/delete! (ds) :fruit {:id 5})))
|
(sql/delete! (ds) :fruit {:id 5})))
|
||||||
|
|
@ -156,3 +158,8 @@
|
||||||
(when (postgres?)
|
(when (postgres?)
|
||||||
(let [data (sql/find-by-keys (ds) :fruit ["id = any(?)" (int-array [1 2 3 4])])]
|
(let [data (sql/find-by-keys (ds) :fruit ["id = any(?)" (int-array [1 2 3 4])])]
|
||||||
(is (= 4 (count data))))))
|
(is (= 4 (count data))))))
|
||||||
|
|
||||||
|
(deftest enum-pg
|
||||||
|
(when (postgres?)
|
||||||
|
(let [r (sql/insert! (ds) :lang_test {:lang (as-other "fr")})]
|
||||||
|
(is (= {:lang_test/lang "fr"} r)))))
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,18 @@
|
||||||
(try
|
(try
|
||||||
(do-commands con [(str "DROP TABLE " fruit)])
|
(do-commands con [(str "DROP TABLE " fruit)])
|
||||||
(catch Exception _))
|
(catch Exception _))
|
||||||
|
(when (postgres?)
|
||||||
|
(try
|
||||||
|
(do-commands con ["DROP TABLE LANG_TEST"])
|
||||||
|
(catch Exception _))
|
||||||
|
(try
|
||||||
|
(do-commands con ["DROP TYPE LANGUAGE"])
|
||||||
|
(catch Exception _))
|
||||||
|
(do-commands con ["CREATE TYPE LANGUAGE AS ENUM('en','fr','de')"])
|
||||||
|
(do-commands con ["
|
||||||
|
CREATE TABLE LANG_TEST (
|
||||||
|
LANG LANGUAGE NOT NULL
|
||||||
|
)"]))
|
||||||
(do-commands con [(str "
|
(do-commands con [(str "
|
||||||
CREATE TABLE " fruit " (
|
CREATE TABLE " fruit " (
|
||||||
ID INTEGER " auto-inc-pk ",
|
ID INTEGER " auto-inc-pk ",
|
||||||
|
|
|
||||||
14
test/next/jdbc/types_test.clj
Normal file
14
test/next/jdbc/types_test.clj
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
;; copyright (c) 2020 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns next.jdbc.types-test
|
||||||
|
"Some tests for the type-assist functions."
|
||||||
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
|
[next.jdbc.types :refer [as-varchar]]))
|
||||||
|
|
||||||
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
(deftest as-varchar-test
|
||||||
|
(let [v (as-varchar "Hello")]
|
||||||
|
(is (= ["Hello"] v))
|
||||||
|
(is (contains? (meta v) 'next.jdbc.prepare/set-parameter))
|
||||||
|
(is (fn? (get (meta v) 'next.jdbc.prepare/set-parameter)))))
|
||||||
Loading…
Reference in a new issue