Compare commits
105 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3b80e6128 | ||
|
|
150a4aa605 | ||
|
|
2a30c5cffd | ||
|
|
0c385c36a6 | ||
|
|
af827c0200 | ||
|
|
0ff09dca21 | ||
|
|
fd3c61b1e6 | ||
|
|
a3fba32605 | ||
|
|
f39f6b3d24 | ||
|
|
99075cd408 | ||
|
|
14b4c3435f | ||
|
|
a40abc285e | ||
|
|
16c1faeaf6 | ||
|
|
d1115c8c42 | ||
|
|
60503dac03 | ||
|
|
a829074b99 | ||
|
|
532d77f372 | ||
|
|
e650137f07 | ||
|
|
9fdc24eab7 | ||
|
|
1642cc04ca | ||
|
|
9d1c35b86c | ||
|
|
ed8ba5402a | ||
|
|
403d156331 | ||
|
|
78894f8ba4 | ||
|
|
0d7b3b60ed | ||
|
|
2214e06cfa | ||
|
|
0d7d58fece | ||
|
|
c2b64dbd54 | ||
|
|
24085c459f | ||
|
|
1062f572d5 | ||
|
|
220957a69f | ||
|
|
c89744b05f | ||
|
|
75dab5d843 | ||
|
|
22e96dcb84 | ||
|
|
9ed335dc8d | ||
|
|
e6d1abf3ec | ||
|
|
946d160409 | ||
|
|
1294e35b98 | ||
|
|
fa6f9040d7 | ||
|
|
3de04761cc | ||
|
|
08c08e7f7b | ||
|
|
56bd2356ac | ||
|
|
c6f86b042e | ||
|
|
a592b7ebae | ||
|
|
d71a3b6423 | ||
|
|
295fd5ddf5 | ||
|
|
f9bfb1248a | ||
|
|
d5f284871b | ||
|
|
2dc72d9254 | ||
|
|
b981357d47 | ||
|
|
801e6c923b | ||
|
|
151b7d8d0b | ||
|
|
87f4224c22 | ||
|
|
4db63f6122 | ||
|
|
0282df7629 | ||
|
|
80748a9593 | ||
|
|
9196b34249 | ||
|
|
b7858c937d | ||
|
|
d1c3291224 | ||
|
|
585a7120a9 | ||
|
|
f43e5490ac | ||
|
|
cad6462c53 | ||
|
|
05cfe1f3fa | ||
|
|
564c43bc79 | ||
|
|
5f0c93642a | ||
|
|
0c50cf28b5 | ||
|
|
ecd950d009 | ||
|
|
bcc6ad85dd | ||
|
|
224333abe6 | ||
|
|
beaaea8f48 | ||
|
|
b2da0156f7 | ||
|
|
f52897adea | ||
|
|
b0a640a101 | ||
|
|
a75468105f | ||
|
|
03117d1160 | ||
|
|
218cf82637 | ||
|
|
64c1f3fefd | ||
|
|
a9058cde18 | ||
|
|
a6eb05ea30 | ||
|
|
b2656120b4 | ||
|
|
4ea3bcf4a3 | ||
|
|
a6ecdee5ac | ||
|
|
fb3188320d | ||
|
|
31ad33dc85 | ||
|
|
26e642096b | ||
|
|
1fed35adc0 | ||
|
|
d6d0edb7a6 | ||
|
|
005ec2ac83 | ||
|
|
1bd4bdedce | ||
|
|
aa7c358cde | ||
|
|
5c9d4795e3 | ||
|
|
212f31d9aa | ||
|
|
35805f1235 | ||
|
|
49a22db52e | ||
|
|
c77f7539c6 | ||
|
|
9e914bc5e1 | ||
|
|
87ea23271e | ||
|
|
eb7dafbe58 | ||
|
|
e2f21e4845 | ||
|
|
05553eee0e | ||
|
|
f03b1ba316 | ||
|
|
4c3e0129bf | ||
|
|
ed95235b7e | ||
|
|
7b32faa7d1 | ||
|
|
3249f31062 |
65 changed files with 2462 additions and 1080 deletions
|
|
@ -0,0 +1,8 @@
|
||||||
|
{:hooks
|
||||||
|
{:analyze-call
|
||||||
|
{next.jdbc/with-transaction
|
||||||
|
hooks.com.github.seancorfield.next-jdbc/with-transaction
|
||||||
|
next.jdbc/with-transaction+options
|
||||||
|
hooks.com.github.seancorfield.next-jdbc/with-transaction+options}}
|
||||||
|
:lint-as {next.jdbc/on-connection clojure.core/with-open
|
||||||
|
next.jdbc/on-connection+options clojure.core/with-open}}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
(ns hooks.com.github.seancorfield.next-jdbc
|
||||||
|
(:require [clj-kondo.hooks-api :as api]))
|
||||||
|
|
||||||
|
(defn with-transaction
|
||||||
|
"Expands (with-transaction [tx expr opts] body)
|
||||||
|
to (let [tx expr] opts body) per clj-kondo examples."
|
||||||
|
[{:keys [:node]}]
|
||||||
|
(let [[binding-vec & body] (rest (:children node))
|
||||||
|
[sym val opts] (:children binding-vec)]
|
||||||
|
(when-not (and sym val)
|
||||||
|
(throw (ex-info "No sym and val provided" {})))
|
||||||
|
(let [new-node (api/list-node
|
||||||
|
(list*
|
||||||
|
(api/token-node 'let)
|
||||||
|
(api/vector-node [sym val])
|
||||||
|
opts
|
||||||
|
body))]
|
||||||
|
{:node new-node})))
|
||||||
|
|
||||||
|
(defn with-transaction+options
|
||||||
|
"Expands (with-transaction+options [tx expr opts] body)
|
||||||
|
to (let [tx expr] opts body) per clj-kondo examples."
|
||||||
|
[{:keys [:node]}]
|
||||||
|
(let [[binding-vec & body] (rest (:children node))
|
||||||
|
[sym val opts] (:children binding-vec)]
|
||||||
|
(when-not (and sym val)
|
||||||
|
(throw (ex-info "No sym and val provided" {})))
|
||||||
|
(let [new-node (api/list-node
|
||||||
|
(list*
|
||||||
|
(api/token-node 'let)
|
||||||
|
(api/vector-node [sym val])
|
||||||
|
opts
|
||||||
|
body))]
|
||||||
|
{:node new-node})))
|
||||||
8
.clj-kondo/imports/com.xtdb/xtdb-api/config.edn
Normal file
8
.clj-kondo/imports/com.xtdb/xtdb-api/config.edn
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{:linters {:xtql/redundant-pipeline {:level :warning}
|
||||||
|
:xtql/redundant-unify {:level :warning}
|
||||||
|
:xtql/unrecognized-operation {:level :error}
|
||||||
|
:xtql/unrecognized-parameter {:level :warning}
|
||||||
|
:xtql/missing-parameter {:level :error}
|
||||||
|
:xtql/type-mismatch {:level :error}
|
||||||
|
:xtql/invalid-arity {:level :error}}
|
||||||
|
:hooks {:analyze-call {xtdb.api/q hooks.xtql/q}}}
|
||||||
567
.clj-kondo/imports/com.xtdb/xtdb-api/hooks/xtql.clj
Normal file
567
.clj-kondo/imports/com.xtdb/xtdb-api/hooks/xtql.clj
Normal file
|
|
@ -0,0 +1,567 @@
|
||||||
|
(ns ^:no-doc hooks.xtql
|
||||||
|
(:require [clj-kondo.hooks-api :as api]))
|
||||||
|
|
||||||
|
(def source-op?
|
||||||
|
#{'from 'rel 'unify})
|
||||||
|
|
||||||
|
(def tail-op?
|
||||||
|
#{'aggregate
|
||||||
|
'limit 'offset
|
||||||
|
'where
|
||||||
|
'order-by
|
||||||
|
'with 'without 'return
|
||||||
|
'unnest})
|
||||||
|
|
||||||
|
(def unify-clause?
|
||||||
|
#{'from 'rel
|
||||||
|
'join 'left-join
|
||||||
|
'unnest
|
||||||
|
'where
|
||||||
|
'with})
|
||||||
|
|
||||||
|
(defn node-map? [node]
|
||||||
|
(contains? #{:map :namespaced-map}
|
||||||
|
(:tag node)))
|
||||||
|
|
||||||
|
(defn node-namespaced-map? [node]
|
||||||
|
(= :namespaced-map (:tag node)))
|
||||||
|
|
||||||
|
(defn map-children [node]
|
||||||
|
(->> (if (node-namespaced-map? node)
|
||||||
|
(-> node :children first)
|
||||||
|
node)
|
||||||
|
:children
|
||||||
|
(partition-all 2)))
|
||||||
|
|
||||||
|
(defn node-vector? [node]
|
||||||
|
(= :vector (:tag node)))
|
||||||
|
|
||||||
|
(defn node-list? [node]
|
||||||
|
(= :list (:tag node)))
|
||||||
|
|
||||||
|
(defn node-symbol? [node]
|
||||||
|
(symbol? (:value node)))
|
||||||
|
|
||||||
|
(defn node-symbol [node]
|
||||||
|
(:value node))
|
||||||
|
|
||||||
|
(defn node-keyword? [node]
|
||||||
|
(keyword? (:k node)))
|
||||||
|
|
||||||
|
(defn node-keyword [node]
|
||||||
|
(:k node))
|
||||||
|
|
||||||
|
(defn node-quote? [node]
|
||||||
|
(= :quote (:tag node)))
|
||||||
|
|
||||||
|
(defn node-op [node]
|
||||||
|
(-> node :children first))
|
||||||
|
|
||||||
|
(declare lint-query)
|
||||||
|
|
||||||
|
(defmulti lint-unify-clause #(-> % node-op node-symbol))
|
||||||
|
(defmulti lint-source-op #(-> % node-op node-symbol))
|
||||||
|
(defmulti lint-tail-op #(-> % node-op node-symbol))
|
||||||
|
|
||||||
|
(defn lint-not-arg-symbol [node]
|
||||||
|
(when (= \$ (-> node node-symbol str first))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "unexpected parameter in binding"
|
||||||
|
:type :xtql/unrecognized-parameter))))
|
||||||
|
|
||||||
|
(defn lint-bind [node]
|
||||||
|
(cond
|
||||||
|
(node-symbol? node)
|
||||||
|
;; TODO: Make own type, should really be a warning
|
||||||
|
(lint-not-arg-symbol node)
|
||||||
|
|
||||||
|
(node-map? node)
|
||||||
|
(doseq [[k _v] (map-children node)]
|
||||||
|
(when-not (node-keyword? k)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "all keys in binding maps must be keywords"
|
||||||
|
:type :xtql/type-mismatch))))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected a symbol or map"
|
||||||
|
:type :xtql/type-mismatch))))
|
||||||
|
|
||||||
|
;; TODO: Lint more unify clauses
|
||||||
|
(defmethod lint-unify-clause :default [node]
|
||||||
|
(when-not (unify-clause? (-> node node-op node-symbol))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (some-> node :children first meta)
|
||||||
|
:message "unrecognized unify clause"
|
||||||
|
:type :xtql/unrecognized-operation))))
|
||||||
|
|
||||||
|
(defmethod lint-unify-clause 'from [node]
|
||||||
|
(lint-source-op node))
|
||||||
|
|
||||||
|
(defmethod lint-unify-clause 'rel [node]
|
||||||
|
(lint-source-op node))
|
||||||
|
|
||||||
|
(defmethod lint-unify-clause 'with [node]
|
||||||
|
(let [opts (-> node :children rest)]
|
||||||
|
(when-not (>= (count opts) 1)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected at least one argument"
|
||||||
|
:type :xtql/invalid-arity)))
|
||||||
|
(doseq [opt opts]
|
||||||
|
(if (node-map? opt)
|
||||||
|
(let [ks (->> opt
|
||||||
|
map-children
|
||||||
|
(map first)
|
||||||
|
(remove node-symbol?))]
|
||||||
|
(doseq [k ks]
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "expected all keys to be symbols in a unify"
|
||||||
|
:type :xtql/type-mismatch))))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opt)
|
||||||
|
:message "opts must be a map"
|
||||||
|
:type :xtql/type-mismatch))))))
|
||||||
|
|
||||||
|
(defn lint-join-clause [node]
|
||||||
|
(let [args (-> node :children rest)]
|
||||||
|
(if-not (= (count args) 2)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected at exactly two arguments"
|
||||||
|
:type :xtql/invalid-arity))
|
||||||
|
(let [[query opts] args]
|
||||||
|
(lint-query query)
|
||||||
|
(cond
|
||||||
|
(node-vector? opts)
|
||||||
|
(->> opts :children (run! lint-bind))
|
||||||
|
(node-map? opts)
|
||||||
|
(let [kvs (map-children opts)
|
||||||
|
ks (->> kvs
|
||||||
|
(map first)
|
||||||
|
(map node-keyword)
|
||||||
|
(remove nil?)
|
||||||
|
(into #{}))]
|
||||||
|
(when-not (contains? ks :bind)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opts)
|
||||||
|
:message "Missing :bind parameter"
|
||||||
|
:type :xtql/missing-parameter)))
|
||||||
|
(doseq [[k v] kvs]
|
||||||
|
(when-not (node-keyword? k)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "All keys in 'opts' must be keywords"
|
||||||
|
:type :xtql/type-mismatch)))
|
||||||
|
(case (node-keyword k)
|
||||||
|
:bind (if (node-vector? v)
|
||||||
|
(->> v :children (run! lint-bind))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta v)
|
||||||
|
:message "expected :bind value to be a vector"
|
||||||
|
:type :xtql/type-mismatch)))
|
||||||
|
:args (if (node-vector? v)
|
||||||
|
;; TODO: Make args specific
|
||||||
|
(->> v :children (run! lint-bind))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta v)
|
||||||
|
:message "expected :args value to be a vector"
|
||||||
|
:type :xtql/type-mismatch)))
|
||||||
|
; else
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "unrecognized parameter"
|
||||||
|
:type :xtql/unrecognized-parameter)))))
|
||||||
|
:else
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "opts must be a map or vector"
|
||||||
|
:type :xtql/type-mismatch)))))))
|
||||||
|
|
||||||
|
(defmethod lint-unify-clause 'join [node]
|
||||||
|
(lint-join-clause node))
|
||||||
|
|
||||||
|
(defmethod lint-unify-clause 'inner-join [node]
|
||||||
|
(lint-join-clause node))
|
||||||
|
|
||||||
|
(defmethod lint-unify-clause 'unnest [node]
|
||||||
|
(let [opts (-> node :children rest)]
|
||||||
|
(when-not (= 1 (count opts))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected at exactly one argument"
|
||||||
|
:type :xtql/invalid-arity)))
|
||||||
|
(let [opt (first opts)]
|
||||||
|
(if (node-map? opt)
|
||||||
|
(doseq [[k _v] (map-children opt)]
|
||||||
|
(when-not (node-symbol? k)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "expected all columns to be symbols"
|
||||||
|
:type :xtql/type-mismatch))))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opt)
|
||||||
|
:message "expected opt to be a map"
|
||||||
|
:type :xtql/type-mismatch))))))
|
||||||
|
|
||||||
|
|
||||||
|
(defmethod lint-source-op :default [node]
|
||||||
|
(let [op (-> node node-op node-symbol)]
|
||||||
|
(if (tail-op? op)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (some-> node :children first meta)
|
||||||
|
:message "tail op in source position"
|
||||||
|
:type :xtql/unrecognized-operation))
|
||||||
|
(when-not (source-op? op)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (some-> node :children first meta)
|
||||||
|
:message "unrecognized source operation"
|
||||||
|
:type :xtql/unrecognized-operation))))))
|
||||||
|
|
||||||
|
(defmethod lint-source-op 'from [node]
|
||||||
|
(let [[_ table opts] (some-> node :children)]
|
||||||
|
(when-not (node-keyword? table)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta table)
|
||||||
|
:message "expected 'table' to be a keyword"
|
||||||
|
:type :xtql/type-mismatch)))
|
||||||
|
(case (:tag opts)
|
||||||
|
:vector (->> opts :children (run! lint-bind))
|
||||||
|
:map
|
||||||
|
(let [kvs (map-children opts)
|
||||||
|
ks (->> kvs
|
||||||
|
(map first)
|
||||||
|
(map node-keyword)
|
||||||
|
(remove nil?)
|
||||||
|
(into #{}))]
|
||||||
|
(when-not (contains? ks :bind)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opts)
|
||||||
|
:message "Missing :bind parameter"
|
||||||
|
:type :xtql/missing-parameter)))
|
||||||
|
(doseq [[k v] kvs]
|
||||||
|
(when-not (node-keyword? k)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "All keys in 'opts' must be keywords"
|
||||||
|
:type :xtql/type-mismatch)))
|
||||||
|
(case (node-keyword k)
|
||||||
|
:bind (if (node-vector? v)
|
||||||
|
(->> v :children (run! lint-bind))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opts)
|
||||||
|
:message "expected :bind value to be a vector"
|
||||||
|
:type :xtql/type-mismatch)))
|
||||||
|
;; TODO
|
||||||
|
:for-valid-time nil
|
||||||
|
;; TODO
|
||||||
|
:for-system-time nil
|
||||||
|
; else
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "unrecognized parameter"
|
||||||
|
:type :xtql/unrecognized-parameter)))))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opts)
|
||||||
|
:message "expected 'opts' to be either a map or vector"
|
||||||
|
:type :xtql/type-mismatch)))))
|
||||||
|
|
||||||
|
(defmethod lint-source-op 'unify [node]
|
||||||
|
(let [[_ & clauses] (some-> node :children)]
|
||||||
|
(doseq [bad-op (remove node-list? clauses)]
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta bad-op)
|
||||||
|
:message "all operations in a unify must be lists"
|
||||||
|
:type :xtql/type-mismatch)))
|
||||||
|
(when (= (count clauses) 1)
|
||||||
|
(let [clause (first clauses)
|
||||||
|
clause-op (-> clause node-op node-symbol)
|
||||||
|
unify-node (some-> node :children first)]
|
||||||
|
(case clause-op
|
||||||
|
from (api/reg-finding!
|
||||||
|
(assoc (meta unify-node)
|
||||||
|
:message "redundant unify"
|
||||||
|
:type :xtql/redundant-unify))
|
||||||
|
rel (api/reg-finding!
|
||||||
|
(assoc (meta unify-node)
|
||||||
|
:message "redundant unify"
|
||||||
|
:type :xtql/redundant-unify))
|
||||||
|
;; TODO: Cover other operators
|
||||||
|
nil)))
|
||||||
|
(->> clauses
|
||||||
|
(filter node-list?)
|
||||||
|
(run! lint-unify-clause))))
|
||||||
|
|
||||||
|
(defmethod lint-source-op 'rel [node]
|
||||||
|
(let [[_ _expr binds] (some-> node :children)]
|
||||||
|
(if (node-vector? binds)
|
||||||
|
(->> binds :children (run! lint-bind))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta binds)
|
||||||
|
:message "expected rel binding to be a vector"
|
||||||
|
:type :xtql/type-mismatch)))))
|
||||||
|
|
||||||
|
;; TODO: Lint more tail ops
|
||||||
|
(defmethod lint-tail-op :default [node]
|
||||||
|
(let [op (-> node node-op node-symbol)]
|
||||||
|
(if (source-op? op)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (some-> node :children first meta)
|
||||||
|
:message "source op in tail position"
|
||||||
|
:type :xtql/unrecognized-operation))
|
||||||
|
(when-not (tail-op? op)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (some-> node :children first meta)
|
||||||
|
:message "unrecognized tail operation"
|
||||||
|
:type :xtql/unrecognized-operation))))))
|
||||||
|
|
||||||
|
(defn lint-keyword [node name]
|
||||||
|
(when-not (node-keyword? node)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message (str "expected '" name "' to be a keyword")
|
||||||
|
:type :xtql/type-mismatch))))
|
||||||
|
|
||||||
|
(defn lint-enum [node name values]
|
||||||
|
;; TODO: Expand to more than just keywords?
|
||||||
|
;; Maybe a `node-value` function?
|
||||||
|
(when-not (contains? values (node-keyword node))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message (str "expected '" name "' to be one of " values)
|
||||||
|
;; TODO: change to different type?
|
||||||
|
:type :xtql/type-mismatch))))
|
||||||
|
|
||||||
|
(defmethod lint-tail-op 'limit [node]
|
||||||
|
(let [opts (-> node :children rest)]
|
||||||
|
(when-not (= 1 (count opts))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected exactly one argument"
|
||||||
|
:type :xtql/invalid-arity)))
|
||||||
|
(when-let [opt (first opts)]
|
||||||
|
(when-not (some-> opt :value int?)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opt)
|
||||||
|
:message "expected limit to be an integer"
|
||||||
|
:type :xtql/type-mismatch))))))
|
||||||
|
|
||||||
|
(defmethod lint-tail-op 'offset [node]
|
||||||
|
(let [opts (-> node :children rest)]
|
||||||
|
(when-not (= 1 (count opts))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected exactly one argument"
|
||||||
|
:type :xtql/invalid-arity)))
|
||||||
|
(when-let [opt (first opts)]
|
||||||
|
(when-not (some-> opt :value int?)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opt)
|
||||||
|
:message "expected offset to be an integer"
|
||||||
|
:type :xtql/type-mismatch))))))
|
||||||
|
|
||||||
|
(defmethod lint-tail-op 'with [node]
|
||||||
|
(let [opts (-> node :children rest)]
|
||||||
|
(when-not (>= (count opts) 1)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected at least one argument"
|
||||||
|
:type :xtql/invalid-arity)))
|
||||||
|
(doseq [opt opts]
|
||||||
|
(cond
|
||||||
|
(node-symbol? opt)
|
||||||
|
(lint-not-arg-symbol opt)
|
||||||
|
(node-map? opt)
|
||||||
|
(let [ks (->> opt
|
||||||
|
map-children
|
||||||
|
(map first)
|
||||||
|
(remove node-keyword?))]
|
||||||
|
(doseq [k ks]
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "expected all keys to be keywords"
|
||||||
|
:type :xtql/type-mismatch))))
|
||||||
|
:else
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opt)
|
||||||
|
:message "opts must be a symbol or map"
|
||||||
|
:type :xtql/type-mismatch))))))
|
||||||
|
|
||||||
|
(defmethod lint-tail-op 'return [node]
|
||||||
|
(let [opts (-> node :children rest)]
|
||||||
|
(when-not (>= (count opts) 1)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected at least one argument"
|
||||||
|
:type :xtql/invalid-arity)))
|
||||||
|
(doseq [opt opts]
|
||||||
|
(cond
|
||||||
|
(node-symbol? opt)
|
||||||
|
(lint-not-arg-symbol opt)
|
||||||
|
(node-map? opt)
|
||||||
|
(let [ks (->> opt
|
||||||
|
map-children
|
||||||
|
(map first)
|
||||||
|
(remove node-keyword?))]
|
||||||
|
(doseq [k ks]
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "expected all keys to be keywords"
|
||||||
|
:type :xtql/type-mismatch))))
|
||||||
|
:else
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opt)
|
||||||
|
:message "opts must be a symbol or map"
|
||||||
|
:type :xtql/type-mismatch))))))
|
||||||
|
|
||||||
|
(defmethod lint-tail-op 'order-by [node]
|
||||||
|
(doseq [opt (-> node :children rest)]
|
||||||
|
(cond
|
||||||
|
(node-symbol? opt)
|
||||||
|
(lint-not-arg-symbol opt)
|
||||||
|
|
||||||
|
(node-map? opt)
|
||||||
|
(let [kvs (map-children opt)
|
||||||
|
ks (->> kvs
|
||||||
|
(map first)
|
||||||
|
(map node-keyword)
|
||||||
|
(remove nil?)
|
||||||
|
(into #{}))]
|
||||||
|
(when-not (contains? ks :val)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opt)
|
||||||
|
:message "Missing :val parameter"
|
||||||
|
:type :xtql/missing-parameter)))
|
||||||
|
(doseq [[k v] kvs]
|
||||||
|
(when-not (node-keyword? k)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "All keys in 'opts' must be keywords"
|
||||||
|
:type :xtql/type-mismatch)))
|
||||||
|
(case (node-keyword k)
|
||||||
|
:val
|
||||||
|
(cond
|
||||||
|
(node-symbol? v)
|
||||||
|
(lint-not-arg-symbol v)
|
||||||
|
(node-keyword? v)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta v)
|
||||||
|
:message "expected :val value to be a symbol or an expression"
|
||||||
|
:type :xtql/type-mismatch)))
|
||||||
|
; else do nothing
|
||||||
|
:dir
|
||||||
|
(if (node-keyword? v)
|
||||||
|
(lint-enum v :dir #{:asc :desc})
|
||||||
|
(lint-keyword v ":dir value"))
|
||||||
|
:nulls
|
||||||
|
(if (node-keyword? v)
|
||||||
|
(lint-enum v :nulls #{:first :last})
|
||||||
|
(lint-keyword v ":nulls value"))
|
||||||
|
; else
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "unrecognized parameter"
|
||||||
|
:type :xtql/unrecognized-parameter)))))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opt)
|
||||||
|
:message "opts must be a symbol or map"
|
||||||
|
:type :xtql/type-mismatch)))))
|
||||||
|
|
||||||
|
(defmethod lint-tail-op 'without [node]
|
||||||
|
(let [columns (-> node :children rest)]
|
||||||
|
(when-not (>= (count columns) 1)
|
||||||
|
;; TODO: Should be a warning really
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected at least one column"
|
||||||
|
:type :xtql/invalid-arity)))
|
||||||
|
(doseq [column columns]
|
||||||
|
(when-not (node-keyword? column)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta column)
|
||||||
|
:message "expected column to be a keyword"
|
||||||
|
:type :xtql/type-mismatch))))))
|
||||||
|
|
||||||
|
(defmethod lint-tail-op 'aggregate [node]
|
||||||
|
(let [opts (-> node :children rest)]
|
||||||
|
(when-not (>= (count opts) 1)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected at least one argument"
|
||||||
|
:type :xtql/invalid-arity)))
|
||||||
|
(doseq [opt opts]
|
||||||
|
(cond
|
||||||
|
(node-symbol? opt)
|
||||||
|
(lint-not-arg-symbol opt)
|
||||||
|
(node-map? opt)
|
||||||
|
(doseq [[k _v] (map-children opt)]
|
||||||
|
(when-not (node-keyword? k)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "expected all keys to be keywords"
|
||||||
|
:type :xtql/type-mismatch))))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opt)
|
||||||
|
:message "expected opts to be a symbol or map"
|
||||||
|
:type :xtql/type-mismatch))))))
|
||||||
|
|
||||||
|
(defmethod lint-tail-op 'unnest [node]
|
||||||
|
(let [opts (-> node :children rest)]
|
||||||
|
(when-not (= 1 (count opts))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta node)
|
||||||
|
:message "expected at exactly one argument"
|
||||||
|
:type :xtql/invalid-arity)))
|
||||||
|
(let [opt (first opts)]
|
||||||
|
(if (node-map? opt)
|
||||||
|
(doseq [[k _v] (map-children opt)]
|
||||||
|
(when-not (node-keyword? k)
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta k)
|
||||||
|
:message "expected all columns to be keywords"
|
||||||
|
:type :xtql/type-mismatch))))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta opt)
|
||||||
|
:message "expected opt to be a map"
|
||||||
|
:type :xtql/type-mismatch))))))
|
||||||
|
|
||||||
|
(defn lint-pipeline [node]
|
||||||
|
(let [[_ & ops] (some-> node :children)]
|
||||||
|
(doseq [bad-op (remove node-list? ops)]
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (meta bad-op)
|
||||||
|
:message "all operations in a pipeline must be lists"
|
||||||
|
:type :xtql/type-mismatch)))
|
||||||
|
(when (= 1 (count ops))
|
||||||
|
(api/reg-finding!
|
||||||
|
(assoc (-> node :children first meta)
|
||||||
|
:message "redundant pipeline"
|
||||||
|
:type :xtql/redundant-pipeline)))
|
||||||
|
(let [first-op (first ops)]
|
||||||
|
(when (node-list? first-op)
|
||||||
|
(lint-source-op (first ops))))
|
||||||
|
(->> ops
|
||||||
|
(drop 1)
|
||||||
|
(filter node-list?)
|
||||||
|
(run! lint-tail-op))))
|
||||||
|
|
||||||
|
(defn lint-query [node]
|
||||||
|
(if (= '-> (node-symbol (-> node :children first)))
|
||||||
|
(lint-pipeline node)
|
||||||
|
(lint-source-op node)))
|
||||||
|
|
||||||
|
;; TODO: Lint other functions that take queries
|
||||||
|
|
||||||
|
(defn q [{:keys [node]}]
|
||||||
|
(let [[_ _node quoted-query] (some-> node :children)]
|
||||||
|
(when (node-quote? quoted-query)
|
||||||
|
(let [query (-> quoted-query :children first)]
|
||||||
|
(lint-query query)))))
|
||||||
6
.clj-kondo/imports/cond_plus/cond_plus/config.edn
Normal file
6
.clj-kondo/imports/cond_plus/cond_plus/config.edn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{:linters {:cond-plus/empty-else {:level :error}
|
||||||
|
:cond-plus/missing-fn {:level :error}
|
||||||
|
:cond-plus/non-final-else {:level :error}
|
||||||
|
:cond-plus/sequence {:level :error}
|
||||||
|
:unresolved-symbol {:exclude [(cond-plus.core/cond+ [=> else])]}}
|
||||||
|
:hooks {:analyze-call {cond-plus.core/cond+ hooks.cond-plus-hook/cond+}}}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
(ns hooks.cond-plus-hook
|
||||||
|
(:require [clj-kondo.hooks-api :as api]))
|
||||||
|
|
||||||
|
(defn analyze-clauses [clauses]
|
||||||
|
(reduce
|
||||||
|
(fn [found-else? clause]
|
||||||
|
;; non-sequence clause
|
||||||
|
(if (not (or (api/list-node? clause)
|
||||||
|
(api/vector-node? clause)))
|
||||||
|
(let [{:keys [row col]} (meta clause)]
|
||||||
|
(api/reg-finding!
|
||||||
|
{:message "must be sequence"
|
||||||
|
:type :cond-plus/sequence
|
||||||
|
:row row
|
||||||
|
:col col})
|
||||||
|
found-else?)
|
||||||
|
(let [[sym arrow fn-expr] (api/sexpr clause)]
|
||||||
|
(cond
|
||||||
|
;; non-final else
|
||||||
|
found-else?
|
||||||
|
(do (api/reg-finding!
|
||||||
|
(merge
|
||||||
|
{:message ":else must be in final position"
|
||||||
|
:type :cond-plus/non-final-else}
|
||||||
|
found-else?))
|
||||||
|
(reduced nil))
|
||||||
|
;; check fn-exprs
|
||||||
|
(and (or (= :> arrow)
|
||||||
|
(= '=> arrow))
|
||||||
|
(nil? fn-expr))
|
||||||
|
(let [{:keys [row col]} (meta clause)]
|
||||||
|
(api/reg-finding!
|
||||||
|
{:message "fn-expr must have third position symbol"
|
||||||
|
:type :cond-plus/missing-fn
|
||||||
|
:row row
|
||||||
|
:col col})
|
||||||
|
found-else?)
|
||||||
|
;; else handling
|
||||||
|
(or (= :else sym)
|
||||||
|
(= 'else sym))
|
||||||
|
(if found-else?
|
||||||
|
(let [{:keys [row col]} (meta clause)]
|
||||||
|
(api/reg-finding!
|
||||||
|
{:message "only one :else clause allowed"
|
||||||
|
:type :cond-plus/empty-else
|
||||||
|
:row row
|
||||||
|
:col col})
|
||||||
|
;; early exit cuz not worth analyzing the rest
|
||||||
|
(reduced nil))
|
||||||
|
(do (when-not arrow
|
||||||
|
(let [{:keys [row col]} (meta clause)]
|
||||||
|
(api/reg-finding!
|
||||||
|
{:message ":else must have a body"
|
||||||
|
:type :cond-plus/empty-else
|
||||||
|
:row row
|
||||||
|
:col col})))
|
||||||
|
;; Store row and col from existing else as we don't throw until
|
||||||
|
;; we've seen a following clause
|
||||||
|
(select-keys (meta clause) [:row :col])))))))
|
||||||
|
nil
|
||||||
|
clauses))
|
||||||
|
|
||||||
|
(defn cond+ [{:keys [node]}]
|
||||||
|
(analyze-clauses (rest (:children node)))
|
||||||
|
node)
|
||||||
23
.clj-kondo/imports/io.github.noahtheduke/lazytest/config.edn
Normal file
23
.clj-kondo/imports/io.github.noahtheduke/lazytest/config.edn
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{:lint-as {lazytest.core/given clojure.core/let
|
||||||
|
lazytest.core/around clojure.core/fn
|
||||||
|
lazytest.core/defdescribe clojure.core/def
|
||||||
|
;; clojure.test interface
|
||||||
|
lazytest.experimental.interfaces.clojure-test/deftest clojure.test/deftest
|
||||||
|
lazytest.experimental.interfaces.clojure-test/testing clojure.test/testing
|
||||||
|
lazytest.experimental.interfaces.clojure-test/is clojure.test/is
|
||||||
|
lazytest.experimental.interfaces.clojure-test/are clojure.test/are
|
||||||
|
;; xunit interface
|
||||||
|
lazytest.experimental.interfaces.xunit/defsuite clojure.core/def
|
||||||
|
;; Expectations v2
|
||||||
|
lazytest.extensions.expectations/defexpect clojure.core/def
|
||||||
|
lazytest.extensions.expectations/from-each clojure.core/for
|
||||||
|
lazytest.extensions.expectations/=? clojure.core/=
|
||||||
|
}
|
||||||
|
:hooks {:analyze-call {;; Expectations v2
|
||||||
|
lazytest.extensions.expectations/more-> hooks.lazytest.expectations/more->
|
||||||
|
lazytest.extensions.expectations/more-of hooks.lazytest.expectations/more-of
|
||||||
|
}}
|
||||||
|
:linters {:clojure-lsp/unused-public-var
|
||||||
|
{:exclude-when-defined-by #{lazytest.core/defdescribe
|
||||||
|
lazytest.experimental.interfaces.xunit/defsuite
|
||||||
|
lazytest.experimental.interfaces.clojure-test/deftest}}}}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
;; Copied from https://github.com/clojure-expectations/clojure-test/blob/b90ed5b24924238b3b16b0bbaaee4c3b05a1268a
|
||||||
|
|
||||||
|
(ns hooks.lazytest.expectations
|
||||||
|
(:require [clj-kondo.hooks-api :as api]))
|
||||||
|
|
||||||
|
(defn more-> [{:keys [node]}]
|
||||||
|
(let [tail (rest (:children node))
|
||||||
|
rewritten
|
||||||
|
(api/list-node
|
||||||
|
(list*
|
||||||
|
(api/token-node 'cond->)
|
||||||
|
(api/token-node 'nil)
|
||||||
|
tail))]
|
||||||
|
{:node rewritten}))
|
||||||
|
|
||||||
|
(defn more-of [{:keys [node]}]
|
||||||
|
(let [bindings (fnext (:children node))
|
||||||
|
pairs (partition 2 (nnext (:children node)))
|
||||||
|
rewritten
|
||||||
|
(api/list-node
|
||||||
|
(list*
|
||||||
|
(api/token-node 'fn)
|
||||||
|
(api/vector-node (vector bindings))
|
||||||
|
(map (fn [[e a]]
|
||||||
|
(api/list-node
|
||||||
|
(list
|
||||||
|
(api/token-node 'lazytest.core/expect)
|
||||||
|
e
|
||||||
|
a)))
|
||||||
|
pairs)))]
|
||||||
|
{:node rewritten}))
|
||||||
4
.clj-kondo/imports/nubank/matcher-combinators/config.edn
Normal file
4
.clj-kondo/imports/nubank/matcher-combinators/config.edn
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{:linters
|
||||||
|
{:unresolved-symbol
|
||||||
|
{:exclude [(cljs.test/is [match? thrown-match?])
|
||||||
|
(clojure.test/is [match? thrown-match?])]}}}
|
||||||
5
.clj-kondo/imports/rewrite-clj/rewrite-clj/config.edn
Normal file
5
.clj-kondo/imports/rewrite-clj/rewrite-clj/config.edn
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{:lint-as
|
||||||
|
{rewrite-clj.zip/subedit-> clojure.core/->
|
||||||
|
rewrite-clj.zip/subedit->> clojure.core/->>
|
||||||
|
rewrite-clj.zip/edit-> clojure.core/->
|
||||||
|
rewrite-clj.zip/edit->> clojure.core/->>}}
|
||||||
1
.clj-kondo/inline-configs/xtdb.api.clj/config.edn
Normal file
1
.clj-kondo/inline-configs/xtdb.api.clj/config.edn
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{:config-in-call {xtdb.api/template {:ignore [:unresolved-symbol :unresolved-namespace]}}}
|
||||||
9
.github/workflows/test-and-release.yml
vendored
9
.github/workflows/test-and-release.yml
vendored
|
|
@ -15,11 +15,11 @@ jobs:
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '11'
|
java-version: '21'
|
||||||
- name: Setup Clojure
|
- name: Setup Clojure
|
||||||
uses: DeLaGuardo/setup-clojure@master
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
with:
|
with:
|
||||||
cli: '1.11.2.1441'
|
cli: '1.12.0.1530'
|
||||||
- name: Cache All The Things
|
- name: Cache All The Things
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -30,11 +30,11 @@ jobs:
|
||||||
~/.cpcache
|
~/.cpcache
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
||||||
- name: Setup Databases
|
- name: Setup Databases
|
||||||
run: docker-compose up -d
|
run: docker compose up -d
|
||||||
env:
|
env:
|
||||||
MYSQL_ROOT_PASSWORD: testing
|
MYSQL_ROOT_PASSWORD: testing
|
||||||
- name: Run MariaDB Tests
|
- name: Run MariaDB Tests
|
||||||
run: clojure -X:test
|
run: clojure -M:test:runner
|
||||||
env:
|
env:
|
||||||
MYSQL_ROOT_PASSWORD: testing
|
MYSQL_ROOT_PASSWORD: testing
|
||||||
NEXT_JDBC_TEST_MYSQL: yes
|
NEXT_JDBC_TEST_MYSQL: yes
|
||||||
|
|
@ -44,6 +44,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
MYSQL_ROOT_PASSWORD: testing
|
MYSQL_ROOT_PASSWORD: testing
|
||||||
NEXT_JDBC_TEST_MYSQL: yes
|
NEXT_JDBC_TEST_MYSQL: yes
|
||||||
|
NEXT_JDBC_TEST_XTDB: yes
|
||||||
NEXT_JDBC_TEST_MSSQL: yes
|
NEXT_JDBC_TEST_MSSQL: yes
|
||||||
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
|
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
|
||||||
- name: Deploy Release
|
- name: Deploy Release
|
||||||
|
|
|
||||||
16
.github/workflows/test-and-snapshot.yml
vendored
16
.github/workflows/test-and-snapshot.yml
vendored
|
|
@ -13,11 +13,11 @@ jobs:
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '11'
|
java-version: '21'
|
||||||
- name: Setup Clojure
|
- name: Setup Clojure
|
||||||
uses: DeLaGuardo/setup-clojure@master
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
with:
|
with:
|
||||||
cli: '1.11.2.1441'
|
cli: '1.12.0.1530'
|
||||||
- name: Cache All The Things
|
- name: Cache All The Things
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -28,11 +28,11 @@ jobs:
|
||||||
~/.cpcache
|
~/.cpcache
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
||||||
- name: Setup Databases
|
- name: Setup Databases
|
||||||
run: docker-compose up -d
|
run: docker compose up -d
|
||||||
env:
|
env:
|
||||||
MYSQL_ROOT_PASSWORD: testing
|
MYSQL_ROOT_PASSWORD: testing
|
||||||
- name: Run MariaDB Tests
|
- name: Run MariaDB Tests
|
||||||
run: clojure -X:test
|
run: clojure -M:test:runner
|
||||||
env:
|
env:
|
||||||
MYSQL_ROOT_PASSWORD: testing
|
MYSQL_ROOT_PASSWORD: testing
|
||||||
NEXT_JDBC_TEST_MYSQL: yes
|
NEXT_JDBC_TEST_MYSQL: yes
|
||||||
|
|
@ -42,6 +42,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
MYSQL_ROOT_PASSWORD: testing
|
MYSQL_ROOT_PASSWORD: testing
|
||||||
NEXT_JDBC_TEST_MYSQL: yes
|
NEXT_JDBC_TEST_MYSQL: yes
|
||||||
|
NEXT_JDBC_TEST_XTDB: yes
|
||||||
NEXT_JDBC_TEST_MSSQL: yes
|
NEXT_JDBC_TEST_MSSQL: yes
|
||||||
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
|
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
|
||||||
- name: Deploy Snapshot
|
- name: Deploy Snapshot
|
||||||
|
|
@ -54,7 +55,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
java: [ '11', '17', '20' ]
|
java: [ '11', '17', '21' ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
|
|
@ -64,15 +65,14 @@ jobs:
|
||||||
- name: Setup Clojure
|
- name: Setup Clojure
|
||||||
uses: DeLaGuardo/setup-clojure@master
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
with:
|
with:
|
||||||
cli: '1.11.2.1441'
|
cli: '1.12.0.1530'
|
||||||
- name: Cache All The Things
|
- name: Cache All The Things
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.m2/repository
|
~/.m2/repository
|
||||||
~/.gitlibs
|
|
||||||
~/.clojure
|
~/.clojure
|
||||||
~/.cpcache
|
~/.cpcache
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: clojure -T:build test
|
run: clojure -T:build:jdk${{ matrix.java }} test
|
||||||
|
|
|
||||||
11
.github/workflows/test.yml
vendored
11
.github/workflows/test.yml
vendored
|
|
@ -7,7 +7,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
java: [ '11', '17', '20' ]
|
java: [ '11', '17', '21' ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
- name: Setup Clojure
|
- name: Setup Clojure
|
||||||
uses: DeLaGuardo/setup-clojure@master
|
uses: DeLaGuardo/setup-clojure@master
|
||||||
with:
|
with:
|
||||||
cli: '1.11.2.1441'
|
cli: '1.12.0.1530'
|
||||||
- name: Cache All The Things
|
- name: Cache All The Things
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -28,19 +28,20 @@ jobs:
|
||||||
~/.cpcache
|
~/.cpcache
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/deps.edn') }}
|
||||||
- name: Setup Databases
|
- name: Setup Databases
|
||||||
run: docker-compose up -d
|
run: docker compose up -d
|
||||||
env:
|
env:
|
||||||
MYSQL_ROOT_PASSWORD: testing
|
MYSQL_ROOT_PASSWORD: testing
|
||||||
- name: Run MariaDB Tests
|
- name: Run MariaDB Tests
|
||||||
run: clojure -X:test
|
run: clojure -M:test:runner
|
||||||
env:
|
env:
|
||||||
MYSQL_ROOT_PASSWORD: testing
|
MYSQL_ROOT_PASSWORD: testing
|
||||||
NEXT_JDBC_TEST_MYSQL: yes
|
NEXT_JDBC_TEST_MYSQL: yes
|
||||||
NEXT_JDBC_TEST_MARIADB: yes
|
NEXT_JDBC_TEST_MARIADB: yes
|
||||||
- name: Run All Tests
|
- name: Run All Tests
|
||||||
run: clojure -X:test
|
run: clojure -M:test:runner:jdk${{ matrix.java }}
|
||||||
env:
|
env:
|
||||||
MYSQL_ROOT_PASSWORD: testing
|
MYSQL_ROOT_PASSWORD: testing
|
||||||
NEXT_JDBC_TEST_MYSQL: yes
|
NEXT_JDBC_TEST_MYSQL: yes
|
||||||
|
NEXT_JDBC_TEST_XTDB: yes
|
||||||
NEXT_JDBC_TEST_MSSQL: yes
|
NEXT_JDBC_TEST_MSSQL: yes
|
||||||
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
|
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,7 +1,8 @@
|
||||||
.calva/output-window/
|
.calva/output-window/
|
||||||
|
.calva/repl.calva-repl
|
||||||
.classpath
|
.classpath
|
||||||
.clj-kondo/.cache
|
.clj-kondo/.cache
|
||||||
.clj-kondo/com.github.seancorfield/next.jdbc
|
.clj-kondo/.lock
|
||||||
.cpcache
|
.cpcache
|
||||||
.eastwood
|
.eastwood
|
||||||
.factorypath
|
.factorypath
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
FROM gitpod/workspace-full
|
|
||||||
|
|
||||||
RUN brew install clojure/tools/clojure@1.10.3.933
|
|
||||||
23
.gitpod.yml
23
.gitpod.yml
|
|
@ -1,23 +0,0 @@
|
||||||
image:
|
|
||||||
file: .gitpod.dockerfile
|
|
||||||
|
|
||||||
vscode:
|
|
||||||
extensions:
|
|
||||||
- betterthantomorrow.calva
|
|
||||||
- mauricioszabo.clover
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: Prepare deps/clover
|
|
||||||
init: |
|
|
||||||
clojure -A:test -P
|
|
||||||
echo 50505 > .socket-repl-port
|
|
||||||
mkdir ~/.config/clover
|
|
||||||
cp .clover/config.cljs ~/.config/clover/
|
|
||||||
- name: Start REPL
|
|
||||||
command: clojure -J-Dclojure.server.repl="{:address \"0.0.0.0\" :port 50505 :accept clojure.core.server/repl}" -A:test
|
|
||||||
- name: See Changes
|
|
||||||
command: code CHANGELOG.md
|
|
||||||
|
|
||||||
github:
|
|
||||||
prebuilds:
|
|
||||||
develop: true
|
|
||||||
6
.joker
6
.joker
|
|
@ -1,6 +0,0 @@
|
||||||
{:known-macros [next.jdbc/with-transaction]
|
|
||||||
:ignored-unused-namespaces [next.jdbc.connection
|
|
||||||
next.jdbc.date-time
|
|
||||||
next.jdbc.prepare
|
|
||||||
next.jdbc.result-set
|
|
||||||
next.jdbc.transaction]}
|
|
||||||
18
.vscode/settings.json
vendored
18
.vscode/settings.json
vendored
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"calva.replConnectSequences": [
|
|
||||||
{
|
|
||||||
"name": "next.jdbc (Jack-In)",
|
|
||||||
"projectType": "deps.edn",
|
|
||||||
"autoSelectForJackIn": true,
|
|
||||||
"menuSelections": {
|
|
||||||
"cljAliases": ["1.12", "dev/repl", "portal", "test"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "next.jdbc (Connect)",
|
|
||||||
"projectType": "deps.edn",
|
|
||||||
"autoSelectForConnect": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"calva.autoConnectRepl": true
|
|
||||||
}
|
|
||||||
41
CHANGELOG.md
41
CHANGELOG.md
|
|
@ -2,6 +2,47 @@
|
||||||
|
|
||||||
Only accretive/fixative changes will be made from now on.
|
Only accretive/fixative changes will be made from now on.
|
||||||
|
|
||||||
|
* 1.3.next in progress
|
||||||
|
* Fix handling of `false` in `clob-column-reader` [#299](https://github.com/seancorfield/next-jdbc/issues/299) via PR [#300](https://github.com/seancorfield/next-jdbc/pull/300) from [@GAumala](https://github.com/GAumala)
|
||||||
|
* Switch tests to LazyTest via PR [#297](https://github.com/seancorfield/next-jdbc/pull/297).
|
||||||
|
* Update dev/test/build deps.
|
||||||
|
|
||||||
|
* 1.3.1002 -- 2025-03-06
|
||||||
|
* Address [#296](https://github.com/seancorfield/next-jdbc/issues/296) by adding an explicit check (and `throw`) for `sql-params` in `next.jdbc` functions.
|
||||||
|
* Address [#295](https://github.com/seancorfield/next-jdbc/issues/295) by providing a way to tell `next.jdbc` that certain options should be passed "as-is" in the `Properties` object when creating a `Connection` -- `:next.jdbc/as-is-properties` accepts a sequence (or set) of keywords, identifying properties that should not be converted to strings.
|
||||||
|
* Fix [#181](https://github.com/seancorfield/next-jdbc/issues/181) (again!) by adding `Wrapped` protocol as a way for `DefaultOptions` and `SQLLogging` to consistently expose the underlying connectable, even when nested.
|
||||||
|
|
||||||
|
* 1.3.994 -- 2025-01-28
|
||||||
|
* Fix [#293](https://github.com/seancorfield/next-jdbc/issues/293) by no longer `locking` on the `Connection` retrieved from a `DataSource`.
|
||||||
|
* Fix documentation examples of `execute-batch!` via PR [#292](https://github.com/seancorfield/next-jdbc/pull/292) from [@devurandom](https://github.com/devurandom).
|
||||||
|
* Update `java.data` to 1.3.113.
|
||||||
|
* Beef up bit/boolean tests and enable them for XTDB.
|
||||||
|
|
||||||
|
* 1.3.981 -- 2024-12-13
|
||||||
|
* Address [#291](https://github.com/seancorfield/next-jdbc/issues/291) by adding an XTDB section to **Tips & Tricks**.
|
||||||
|
* Added XTDB as a supported database for testing via PR [#290](https://github.com/seancorfield/next-jdbc/pull/290). _Note: not all features are tested against XTDB due to several fundamental differences in architecture, mostly around primary key/generated keys and lack of DDL operations (since XTDB is schemaless)._
|
||||||
|
* Update dev/test dependencies.
|
||||||
|
|
||||||
|
* 1.3.967 -- 2024-12-02
|
||||||
|
* Address [#288](https://github.com/seancorfield/next-jdbc/issues/288) by adding speculative support for `:dbtype "xtdb"`.
|
||||||
|
* Fix [#287](https://github.com/seancorfield/next-jdbc/issues/287) by merging user-supplied options over `:return-keys true`.
|
||||||
|
* Fix [#282](https://github.com/seancorfield/next-jdbc/issues/282) by tracking raw `Connection` objects for active TXs, which relaxes several of the conditions around nested transactions.
|
||||||
|
* Replace `assert` calls with proper validation, throwing `IllegalArgumentException` on failure.
|
||||||
|
* Removed (experimental) `:name-fn` option since the driver for it no longer exists (qualified columns names in XTDB).
|
||||||
|
|
||||||
|
* 1.3.955 -- 2024-10-06
|
||||||
|
* Address [#285](https://github.com/seancorfield/next-jdbc/issues/285) by setting the default Clojure version to the earliest supported (1.10.3) to give a better hint to users.
|
||||||
|
* Update PostgreSQL **Tips & Tricks** example code to fix possible NPE. PR [#284](https://github.com/seancorfield/next-jdbc/pull/284) from [@ExNexu](https://github.com/ExNexu).
|
||||||
|
* Address [#283](https://github.com/seancorfield/next-jdbc/issues/283) by adding a note in the documentation, linking to the PostgreSQL bug report about `ANY(array)`.
|
||||||
|
* ~Address [#269](https://github.com/seancorfield/next-jdbc/issues/269) by adding `:name-fn` as an option (primarily for the SQL builder functions, but also for result set processing); the default is `clojure.core/name` but you can now use `next.jdbc.sql.builder/qualified-name` to preserve the qualifier.~ _[This was removed in 1.3.967 since XTDB no longer supports qualified column names]_
|
||||||
|
* Update testing deps; `docker-compose` => `docker compose`.
|
||||||
|
|
||||||
|
* 1.3.939 -- 2024-05-17
|
||||||
|
* Fix [#280](https://github.com/seancorfield/next-jdbc/issues/280) by allowing `-` as well as `_` in `nav` foreign key names.
|
||||||
|
* Address [#279](https://github.com/seancorfield/next-jdbc/issues/279) by adding the missing documentation.
|
||||||
|
* Address [#278](https://github.com/seancorfield/next-jdbc/issues/278) by fixing link in options page.
|
||||||
|
* Update dev dependencies, including testing against Clojure 1.12 Alpha 11.
|
||||||
|
|
||||||
* 1.3.925 -- 2024-03-15
|
* 1.3.925 -- 2024-03-15
|
||||||
* Address [#275](https://github.com/seancorfield/next-jdbc/issues/275) by noting that PostgreSQL may perform additional SQL queries to produce table names used in qualified result set builders.
|
* Address [#275](https://github.com/seancorfield/next-jdbc/issues/275) by noting that PostgreSQL may perform additional SQL queries to produce table names used in qualified result set builders.
|
||||||
* Address [#274](https://github.com/seancorfield/next-jdbc/issues/274) by adding `next.jdbc.sql/aggregate-by-keys` as a convenient wrapper around `find-by-keys` when you want just a single aggregate value back (such as `count`, `max`, etc).
|
* Address [#274](https://github.com/seancorfield/next-jdbc/issues/274) by adding `next.jdbc.sql/aggregate-by-keys` as a convenient wrapper around `find-by-keys` when you want just a single aggregate value back (such as `count`, `max`, etc).
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -1,4 +1,4 @@
|
||||||
# next.jdbc [](https://github.com/seancorfield/next-jdbc/actions/workflows/test.yml) [](https://gitpod.io/#https://github.com/seancorfield/next-jdbc)
|
# next.jdbc [](https://github.com/seancorfield/next-jdbc/actions/workflows/test-and-release.yml) [](https://github.com/seancorfield/next-jdbc/actions/workflows/test-and-snapshot.yml) [](https://github.com/seancorfield/next-jdbc/actions/workflows/test.yml)
|
||||||
|
|
||||||
The next generation of `clojure.java.jdbc`: a new low-level Clojure wrapper for JDBC-based access to databases.
|
The next generation of `clojure.java.jdbc`: a new low-level Clojure wrapper for JDBC-based access to databases.
|
||||||
|
|
||||||
|
|
@ -8,10 +8,11 @@ The next generation of `clojure.java.jdbc`: a new low-level Clojure wrapper for
|
||||||
|
|
||||||
The latest versions on Clojars and on cljdoc:
|
The latest versions on Clojars and on cljdoc:
|
||||||
|
|
||||||
[](https://clojars.org/com.github.seancorfield/next.jdbc)
|
[](https://clojars.org/com.github.seancorfield/next.jdbc)
|
||||||
[](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT)
|
[](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT)
|
||||||
[](https://clojurians.slack.com/app_redirect?channel=sql)
|
[](https://clojurians.slack.com/app_redirect?channel=sql)
|
||||||
[](http://clojurians.net)
|
[](http://clojurians.net)
|
||||||
|
[](https://clojurians.zulipchat.com/#narrow/channel/152063-sql)
|
||||||
|
|
||||||
The documentation on [cljdoc.org](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT) is for the current version of `next.jdbc`:
|
The documentation on [cljdoc.org](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT) is for the current version of `next.jdbc`:
|
||||||
|
|
||||||
|
|
@ -20,11 +21,11 @@ The documentation on [cljdoc.org](https://cljdoc.org/d/com.github.seancorfield/n
|
||||||
* [Migrating from `clojure.java.jdbc`](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/migration-from-clojure-java-jdbc)
|
* [Migrating from `clojure.java.jdbc`](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/migration-from-clojure-java-jdbc)
|
||||||
* Feedback via [issues](https://github.com/seancorfield/next-jdbc/issues) or in the [`#sql` channel on the Clojurians Slack](https://clojurians.slack.com/messages/C1Q164V29/) or the [`#sql` stream on the Clojurians Zulip](https://clojurians.zulipchat.com/#narrow/stream/152063-sql).
|
* Feedback via [issues](https://github.com/seancorfield/next-jdbc/issues) or in the [`#sql` channel on the Clojurians Slack](https://clojurians.slack.com/messages/C1Q164V29/) or the [`#sql` stream on the Clojurians Zulip](https://clojurians.zulipchat.com/#narrow/stream/152063-sql).
|
||||||
|
|
||||||
The documentation on GitHub is for **develop** since the 1.3.925 release -- [see the CHANGELOG](https://github.com/seancorfield/next-jdbc/blob/develop/CHANGELOG.md) and then read the [corresponding updated documentation](https://github.com/seancorfield/next-jdbc/tree/develop/doc) on GitHub if you want. Older versions of `next.jdbc` were published under the `seancorfield` group ID and you can find [older seancorfield/next.jdbc documentation on cljdoc.org](https://cljdoc.org/versions/seancorfield/next.jdbc).
|
The documentation on GitHub is for **develop** since the 1.3.1002 release -- [see the CHANGELOG](https://github.com/seancorfield/next-jdbc/blob/develop/CHANGELOG.md) and then read the [corresponding updated documentation](https://github.com/seancorfield/next-jdbc/tree/develop/doc) on GitHub if you want. Older versions of `next.jdbc` were published under the `seancorfield` group ID and you can find [older seancorfield/next.jdbc documentation on cljdoc.org](https://cljdoc.org/versions/seancorfield/next.jdbc).
|
||||||
|
|
||||||
This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository.
|
This project follows the version scheme MAJOR.MINOR.COMMITS where MAJOR and MINOR provide some relative indication of the size of the change, but do not follow semantic versioning. In general, all changes endeavor to be non-breaking (by moving to new names rather than by breaking existing names). COMMITS is an ever-increasing counter of commits since the beginning of this repository.
|
||||||
|
|
||||||
> Note: every commit to the **develop** branch runs CI (GitHub Actions) and successful runs push a MAJOR.MINOR.999-SNAPSHOT build to Clojars so the very latest version of `next.jdbc` is always available either via that [snapshot on Clojars](https://clojars.org/com.github.seancorfield/next.jdbc) or via a git dependency on the latest SHA.
|
> Note: every commit to the **develop** branch runs CI (GitHub Actions) and successful runs push a MAJOR.MINOR.9999-SNAPSHOT build to Clojars so the very latest version of `next.jdbc` is always available either via that [snapshot on Clojars](https://clojars.org/com.github.seancorfield/next.jdbc) or via a git dependency on the latest SHA.
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
|
|
@ -50,7 +51,7 @@ From a `DataSource`, either you or `next.jdbc` can create a `java.sql.Connection
|
||||||
|
|
||||||
The primary SQL execution API in `next.jdbc` is:
|
The primary SQL execution API in `next.jdbc` is:
|
||||||
* `plan` -- yields an `IReduceInit` that, when reduced with an initial value, executes the SQL statement and then reduces over the `ResultSet` with as little overhead as possible.
|
* `plan` -- yields an `IReduceInit` that, when reduced with an initial value, executes the SQL statement and then reduces over the `ResultSet` with as little overhead as possible.
|
||||||
* `execute!` -- executes the SQL statement and produces a vector of realized hash maps, that use qualified keywords for the column names, of the form `:<table>/<column>`. If you join across multiple tables, the qualified keywords will reflect the originating tables for each of the columns. If the SQL produces named values that do not come from an associated table, a simple, unqualified keyword will be used. The realized hash maps returned by `execute!` are `Datafiable` and thus `Navigable` (see Clojure 1.10's `datafy` and `nav` functions, and tools like [Portal](https://github.com/djblue/portal), [Reveal](https://github.com/vlaaad/reveal), and Cognitect's REBL). Alternatively, you can specify `{:builder-fn rs/as-arrays}` and produce a vector with column names followed by vectors of row values. `rs/as-maps` is the default for `:builder-fn` but there are also `rs/as-unqualified-maps` and `rs/as-unqualified-arrays` if you want unqualified `:<column>` column names (and there are also lower-case variants of all of these).
|
* `execute!` -- executes the SQL statement and produces a vector of realized hash maps, that use qualified keywords for the column names, of the form `:<table>/<column>`. If you join across multiple tables, the qualified keywords will reflect the originating tables for each of the columns. If the SQL produces named values that do not come from an associated table, a simple, unqualified keyword will be used. The realized hash maps returned by `execute!` are `Datafiable` and thus `Navigable` (see Clojure 1.10's `datafy` and `nav` functions, and tools like [Portal](https://github.com/djblue/portal), [Reveal](https://github.com/vlaaad/reveal), and Nubank's Morse -- formerly Cognitect's REBL). Alternatively, you can specify `{:builder-fn rs/as-arrays}` and produce a vector with column names followed by vectors of row values. `rs/as-maps` is the default for `:builder-fn` but there are also `rs/as-unqualified-maps` and `rs/as-unqualified-arrays` if you want unqualified `:<column>` column names (and there are also lower-case variants of all of these).
|
||||||
* `execute-one!` -- executes the SQL or DDL statement and produces a single realized hash map. The realized hash map returned by `execute-one!` is `Datafiable` and thus `Navigable`.
|
* `execute-one!` -- executes the SQL or DDL statement and produces a single realized hash map. The realized hash map returned by `execute-one!` is `Datafiable` and thus `Navigable`.
|
||||||
|
|
||||||
In addition, there are API functions to create `PreparedStatement`s (`prepare`) from `Connection`s, which can be passed to `plan`, `execute!`, or `execute-one!`, and to run code inside a transaction (the `transact` function and the `with-transaction` macro).
|
In addition, there are API functions to create `PreparedStatement`s (`prepare`) from `Connection`s, which can be passed to `plan`, `execute!`, or `execute-one!`, and to run code inside a transaction (the `transact` function and the `with-transaction` macro).
|
||||||
|
|
@ -88,6 +89,6 @@ In addition, convenience functions -- "syntactic sugar" -- are provided to inser
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2018-2021 Sean Corfield
|
Copyright © 2018-2024 Sean Corfield
|
||||||
|
|
||||||
Distributed under the Eclipse Public License version 1.0.
|
Distributed under the Eclipse Public License version 1.0.
|
||||||
|
|
|
||||||
14
build.clj
14
build.clj
|
|
@ -5,29 +5,33 @@
|
||||||
clojure -T:build deploy
|
clojure -T:build deploy
|
||||||
|
|
||||||
Run tests via:
|
Run tests via:
|
||||||
clojure -X:test
|
clojure -M:test:runner
|
||||||
|
|
||||||
For more information, run:
|
For more information, run:
|
||||||
|
|
||||||
clojure -A:deps -T:build help/doc"
|
clojure -A:deps -T:build help/doc"
|
||||||
(:refer-clojure :exclude [test])
|
(:refer-clojure :exclude [test])
|
||||||
(:require [clojure.tools.build.api :as b]
|
(:require [clojure.tools.build.api :as b]
|
||||||
[deps-deploy.deps-deploy :as dd]))
|
[deps-deploy.deps-deploy :as dd]
|
||||||
|
[clojure.string :as str]))
|
||||||
|
|
||||||
(def lib 'com.github.seancorfield/next.jdbc)
|
(def lib 'com.github.seancorfield/next.jdbc)
|
||||||
(defn- the-version [patch] (format "1.3.%s" patch))
|
(defn- the-version [patch] (format "1.3.%s" patch))
|
||||||
(def version (the-version (b/git-count-revs nil)))
|
(def version (the-version (b/git-count-revs nil)))
|
||||||
(def snapshot (the-version "999-SNAPSHOT"))
|
(def snapshot (the-version "9999-SNAPSHOT"))
|
||||||
(def class-dir "target/classes")
|
(def class-dir "target/classes")
|
||||||
|
|
||||||
(defn test "Run all the tests." [opts]
|
(defn test "Run all the tests." [opts]
|
||||||
(doseq [alias [:1.10 :1.11 :1.12]]
|
(doseq [alias [:1.10 :1.11 :1.12]]
|
||||||
(println "\nRunning tests for Clojure" (name alias))
|
(println "\nRunning tests for Clojure" (name alias))
|
||||||
(let [basis (b/create-basis {:aliases [:test alias]})
|
(let [basis (b/create-basis
|
||||||
|
{:aliases (cond-> [:test alias]
|
||||||
|
(str/starts-with? (System/getProperty "java.version") "21")
|
||||||
|
(conj :jdk21))})
|
||||||
cmds (b/java-command
|
cmds (b/java-command
|
||||||
{:basis basis
|
{:basis basis
|
||||||
:main 'clojure.main
|
:main 'clojure.main
|
||||||
:main-args ["-m" "cognitect.test-runner"]})
|
:main-args ["-m" "lazytest.main"]})
|
||||||
{:keys [exit]} (b/process cmds)]
|
{:keys [exit]} (b/process cmds)]
|
||||||
(when-not (zero? exit) (throw (ex-info "Tests failed" {})))))
|
(when-not (zero? exit) (throw (ex-info "Tests failed" {})))))
|
||||||
opts)
|
opts)
|
||||||
|
|
|
||||||
66
deps.edn
66
deps.edn
|
|
@ -1,50 +1,58 @@
|
||||||
{:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}}
|
{:mvn/repos {"sonatype" {:url "https://oss.sonatype.org/content/repositories/snapshots/"}
|
||||||
|
"ossrh-snapshots" {:url "https://s01.oss.sonatype.org/content/repositories/snapshots"}}
|
||||||
:paths ["src" "resources"]
|
:paths ["src" "resources"]
|
||||||
:deps {org.clojure/clojure {:mvn/version "1.11.2"}
|
:deps {org.clojure/clojure {:mvn/version "1.10.3"}
|
||||||
org.clojure/java.data {:mvn/version "1.2.107"}
|
org.clojure/java.data {:mvn/version "1.3.113"}
|
||||||
|
|
||||||
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}}
|
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}}
|
||||||
:aliases
|
:aliases
|
||||||
{;; for help: clojure -A:deps -T:build help/doc
|
{;; for help: clojure -A:deps -T:build help/doc
|
||||||
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.6"}
|
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.7"}
|
||||||
slipset/deps-deploy {:mvn/version "0.2.2"}}
|
slipset/deps-deploy {:mvn/version "0.2.2"}}
|
||||||
:ns-default build}
|
:ns-default build}
|
||||||
|
|
||||||
;; versions to test against:
|
;; versions to test against:
|
||||||
:1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
|
:1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
|
||||||
:1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.2"}}}
|
:1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.4"}}}
|
||||||
:1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0-alpha9"}}}
|
:1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}}
|
||||||
|
|
||||||
;; running tests/checks of various kinds:
|
;; running tests/checks of various kinds:
|
||||||
:test {:extra-paths ["test"] ; can also run clojure -X:test
|
:test {:extra-paths ["test"]
|
||||||
:extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
|
:extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
|
||||||
io.github.cognitect-labs/test-runner
|
io.github.noahtheduke/lazytest {:mvn/version "1.6.1"}
|
||||||
{:git/tag "v0.5.1" :git/sha "dfb30dd"}
|
|
||||||
;; connection pooling
|
;; connection pooling
|
||||||
com.zaxxer/HikariCP {:mvn/version "5.1.0"}
|
com.zaxxer/HikariCP {:mvn/version "6.3.0"}
|
||||||
com.mchange/c3p0 {:mvn/version "0.10.0"}
|
com.mchange/c3p0 {:mvn/version "0.10.1"}
|
||||||
;; JDBC drivers
|
;; JDBC drivers
|
||||||
;; 10.16.x is JDK17+
|
;; 10.16.x is JDK17+
|
||||||
org.apache.derby/derby {:mvn/version "10.15.2.0"}
|
org.apache.derby/derby {:mvn/version "10.15.2.0"}
|
||||||
org.apache.derby/derbyshared {:mvn/version "10.15.2.0"}
|
org.apache.derby/derbyshared {:mvn/version "10.15.2.0"}
|
||||||
org.hsqldb/hsqldb {:mvn/version "2.7.2"}
|
org.hsqldb/hsqldb {:mvn/version "2.7.4"}
|
||||||
com.h2database/h2 {:mvn/version "2.2.224"}
|
com.h2database/h2 {:mvn/version "2.3.232"}
|
||||||
net.sourceforge.jtds/jtds {:mvn/version "1.3.1"}
|
net.sourceforge.jtds/jtds {:mvn/version "1.3.1"}
|
||||||
org.mariadb.jdbc/mariadb-java-client {:mvn/version "3.3.3"}
|
org.mariadb.jdbc/mariadb-java-client {:mvn/version "3.5.2"}
|
||||||
com.mysql/mysql-connector-j {:mvn/version "8.3.0"}
|
com.mysql/mysql-connector-j {:mvn/version "9.2.0"}
|
||||||
org.postgresql/postgresql {:mvn/version "42.7.2"}
|
;; 42.7.4 changes update count (to -1) for stored procs:
|
||||||
io.zonky.test/embedded-postgres {:mvn/version "2.0.6"}
|
org.postgresql/postgresql {:mvn/version "42.7.5"}
|
||||||
io.zonky.test.postgres/embedded-postgres-binaries-darwin-amd64 {:mvn/version "16.2.0"}
|
io.zonky.test/embedded-postgres {:mvn/version "2.1.0"}
|
||||||
io.zonky.test.postgres/embedded-postgres-binaries-linux-amd64 {:mvn/version "16.2.0"}
|
io.zonky.test.postgres/embedded-postgres-binaries-darwin-amd64 {:mvn/version "17.4.0"}
|
||||||
io.zonky.test.postgres/embedded-postgres-binaries-windows-amd64 {:mvn/version "16.2.0"}
|
io.zonky.test.postgres/embedded-postgres-binaries-linux-amd64 {:mvn/version "17.4.0"}
|
||||||
org.xerial/sqlite-jdbc {:mvn/version "3.45.1.0"}
|
io.zonky.test.postgres/embedded-postgres-binaries-windows-amd64 {:mvn/version "17.4.0"}
|
||||||
com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.6.1.jre11"}
|
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
|
||||||
|
com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.10.0.jre11"}
|
||||||
;; use log4j2 to reduce log noise during testing:
|
;; use log4j2 to reduce log noise during testing:
|
||||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.23.0"}
|
org.apache.logging.log4j/log4j-api {:mvn/version "2.24.3"}
|
||||||
;; bridge everything into log4j:
|
;; bridge everything into log4j:
|
||||||
org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.23.0"}
|
org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.24.3"}
|
||||||
org.apache.logging.log4j/log4j-jcl {:mvn/version "2.23.0"}
|
org.apache.logging.log4j/log4j-jcl {:mvn/version "2.24.3"}
|
||||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.23.0"}
|
org.apache.logging.log4j/log4j-jul {:mvn/version "2.24.3"}
|
||||||
org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.23.0"}}
|
org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.24.3"}
|
||||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-info.properties"]
|
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.24.3"}}
|
||||||
:exec-fn cognitect.test-runner.api/test}}}
|
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-info.properties"]}
|
||||||
|
:runner {:main-opts ["-m" "lazytest.main"]}
|
||||||
|
:jdk11 {}
|
||||||
|
:jdk17 {}
|
||||||
|
:jdk21 {:extra-deps {;; only need the XTDB JDBC module:
|
||||||
|
com.xtdb/xtdb-jdbc {:mvn/version "2.0.0-beta7"}}}
|
||||||
|
:jdk24 {:jvm-opts [;; for SQLite on JDK 24 locally
|
||||||
|
"--enable-native-access=ALL-UNNAMED"]}}}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ Any path that calls `get-connection` will accept the following options:
|
||||||
|
|
||||||
If you need additional options set on a connection, you can either use Java interop to set them directly, or provide them as part of the "db spec" hash map passed to `get-datasource` (although then they will apply to _all_ connections obtained from that datasource).
|
If you need additional options set on a connection, you can either use Java interop to set them directly, or provide them as part of the "db spec" hash map passed to `get-datasource` (although then they will apply to _all_ connections obtained from that datasource).
|
||||||
|
|
||||||
|
Additional options passed are set as `java.util.Properties` and, by default, are coerced to strings.
|
||||||
|
If you are working with a driver that requires a non-string value for a property (such as the Snowflake driver), you can provide a `:next.jdbc/as-is-properties` option containing a sequence of options that should be added as-is, rather than coerced to strings.
|
||||||
|
|
||||||
> Note: If `plan`, `execute!`, or `execute-one!` are passed a `DataSource`, a "db spec" hash map, or a JDBC URL string, they will call `get-connection`, so they will accept the above options in those cases.
|
> Note: If `plan`, `execute!`, or `execute-one!` are passed a `DataSource`, a "db spec" hash map, or a JDBC URL string, they will call `get-connection`, so they will accept the above options in those cases.
|
||||||
|
|
||||||
## Generating SQL
|
## Generating SQL
|
||||||
|
|
@ -69,6 +72,7 @@ Any function that might realize a row or a result set will accept:
|
||||||
* `:builder-fn` -- a function that implements the `RowBuilder` and `ResultSetBuilder` protocols; strictly speaking, `plan` and `execute-one!` only need `RowBuilder` to be implemented (and `plan` only needs that if it actually has to realize a row) but most generation functions will implement both for ease of use.
|
* `:builder-fn` -- a function that implements the `RowBuilder` and `ResultSetBuilder` protocols; strictly speaking, `plan` and `execute-one!` only need `RowBuilder` to be implemented (and `plan` only needs that if it actually has to realize a row) but most generation functions will implement both for ease of use.
|
||||||
* `:label-fn` -- if `:builder-fn` is specified as one of `next.jdbc.result-set`'s `as-modified-*` builders, this option must be present and should specify a string-to-string transformation that will be applied to the column label for each returned column name.
|
* `:label-fn` -- if `:builder-fn` is specified as one of `next.jdbc.result-set`'s `as-modified-*` builders, this option must be present and should specify a string-to-string transformation that will be applied to the column label for each returned column name.
|
||||||
* `:qualifier-fn` -- if `:builder-fn` is specified as one of `next.jdbc.result-set`'s `as-modified-*` builders, this option should specify a string-to-string transformation that will be applied to the table name for each returned column name. It will be called with an empty string if the table name is not available. It can be omitted for the `as-unqualified-modified-*` variants.
|
* `:qualifier-fn` -- if `:builder-fn` is specified as one of `next.jdbc.result-set`'s `as-modified-*` builders, this option should specify a string-to-string transformation that will be applied to the table name for each returned column name. It will be called with an empty string if the table name is not available. It can be omitted for the `as-unqualified-modified-*` variants.
|
||||||
|
* `:column-fn` -- if present, applied to each column name before looking up the column in the `ResultSet` to get that column's value.
|
||||||
|
|
||||||
In addition, `execute!` accepts the `:multi-rs true` option to return multiple result sets -- as a vector of result sets.
|
In addition, `execute!` accepts the `:multi-rs true` option to return multiple result sets -- as a vector of result sets.
|
||||||
|
|
||||||
|
|
@ -83,7 +87,7 @@ result set:
|
||||||
* `:schema` -- override the conventions for identifying foreign keys and the related (primary) keys in the tables to which they refer, on a per table/column basis; can also be used to indicate a fk relationship is one-to-many or many-to-many rather than one-to-one or one-to-many,
|
* `:schema` -- override the conventions for identifying foreign keys and the related (primary) keys in the tables to which they refer, on a per table/column basis; can also be used to indicate a fk relationship is one-to-many or many-to-many rather than one-to-one or one-to-many,
|
||||||
* `:schema-opts` -- override the default conventions for identifying foreign keys and the related (primary) keys in the tables to which they refer, as a whole.
|
* `:schema-opts` -- override the default conventions for identifying foreign keys and the related (primary) keys in the tables to which they refer, as a whole.
|
||||||
|
|
||||||
See [`datafy`, `nav`, and `:schema`](/doc/datafy-nav-schema.md) for more details.
|
See [`datafy`, `nav`, and `:schema`](/doc/datafy-nav-and-schema.md) for more details.
|
||||||
|
|
||||||
## Statements & Prepared Statements
|
## Statements & Prepared Statements
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,18 @@ For `:fk-suffix`, the `_` is still permitted and optional in the column name,
|
||||||
so if you specified `:schema-opts {:fk-suffix "fk"}` then `addressfk` and
|
so if you specified `:schema-opts {:fk-suffix "fk"}` then `addressfk` and
|
||||||
`address_fk` would both be treated as foreign keys into the `address` table.
|
`address_fk` would both be treated as foreign keys into the `address` table.
|
||||||
|
|
||||||
The `:pk-fn` can
|
_Note: as of 1.3.939, `-` is permitted in key names (in addition to `_`) so that kebab result set builders work as expected._
|
||||||
|
|
||||||
|
The `:pk-fn` can use the table name to determine the primary key column name
|
||||||
|
for exceptions to the `:pk` value. For example, if you have a table `address`
|
||||||
|
with a primary key column `address_id` instead of `id`, you could use:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
:pk-fn (fn [table pk]
|
||||||
|
(if (= "address" table)
|
||||||
|
"address_id"
|
||||||
|
pk))
|
||||||
|
```
|
||||||
|
|
||||||
The default behavior in the example above is equivalent to this `:schema` value:
|
The default behavior in the example above is equivalent to this `:schema` value:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,8 @@ will use `execute-batch!` under the hood, instead of `execute!`, as follows:
|
||||||
{:batch true})
|
{:batch true})
|
||||||
;; equivalent to
|
;; equivalent to
|
||||||
(jdbc/execute-batch! ds
|
(jdbc/execute-batch! ds
|
||||||
["INSERT INTO address (name,email) VALUES (?,?)"
|
"INSERT INTO address (name,email) VALUES (?,?)"
|
||||||
["Stella" "stella@artois.beer"]
|
[["Stella" "stella@artois.beer"]
|
||||||
["Waldo" "waldo@lagunitas.beer"]
|
["Waldo" "waldo@lagunitas.beer"]
|
||||||
["Aunt Sally" "sour@lagunitas.beer"]]
|
["Aunt Sally" "sour@lagunitas.beer"]]
|
||||||
{:return-keys true :return-generated-keys true})
|
{:return-keys true :return-generated-keys true})
|
||||||
|
|
@ -131,8 +131,8 @@ will use `execute-batch!` under the hood, instead of `execute!`, as follows:
|
||||||
{:batch true})
|
{:batch true})
|
||||||
;; equivalent to
|
;; equivalent to
|
||||||
(jdbc/execute-batch! ds
|
(jdbc/execute-batch! ds
|
||||||
["INSERT INTO address (name,email) VALUES (?,?)"
|
"INSERT INTO address (name,email) VALUES (?,?)"
|
||||||
["Stella" "stella@artois.beer"]
|
[["Stella" "stella@artois.beer"]
|
||||||
["Waldo" "waldo@lagunitas.beer"]
|
["Waldo" "waldo@lagunitas.beer"]
|
||||||
["Aunt Sally" "sour@lagunitas.beer"]]
|
["Aunt Sally" "sour@lagunitas.beer"]]
|
||||||
{:return-keys true :return-generated-keys true})
|
{:return-keys true :return-generated-keys true})
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,26 @@ It is designed to work with Clojure 1.10 or later, supports `datafy`/`nav`, and
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
**You must be using Clojure 1.10 or later.** 1.11.1 is the most recent stable version of Clojure (as of April 5th, 2022).
|
**You must be using Clojure 1.10 or later.** 1.12.0 is the most recent stable version of Clojure (as of March 15th, 2024).
|
||||||
|
|
||||||
You can add `next.jdbc` to your project with either:
|
You can add `next.jdbc` to your project with either:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.925"}
|
com.github.seancorfield/next.jdbc {:mvn/version "1.3.1002"}
|
||||||
```
|
```
|
||||||
for `deps.edn` or:
|
for `deps.edn` or:
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
[com.github.seancorfield/next.jdbc "1.3.925"]
|
[com.github.seancorfield/next.jdbc "1.3.1002"]
|
||||||
```
|
```
|
||||||
for `project.clj` or `build.boot`.
|
for `project.clj` or `build.boot`.
|
||||||
|
|
||||||
**In addition, you will need to add dependencies for the JDBC drivers you wish to use for whatever databases you are using. For example:**
|
**In addition, you will need to add dependencies for the JDBC drivers you wish to use for whatever databases you are using. For example:**
|
||||||
|
|
||||||
* MySQL: `com.mysql/mysql-connector-j {:mvn/version "8.1.0"}` ([search for latest version](https://search.maven.org/artifact/com.mysql/mysql-connector-j))
|
* MySQL: `com.mysql/mysql-connector-j {:mvn/version "9.1.0"}` ([search for latest version](https://search.maven.org/artifact/com.mysql/mysql-connector-j))
|
||||||
* PostgreSQL: `org.postgresql/postgresql {:mvn/version "42.6.0"}` ([search for latest version](https://search.maven.org/artifact/org.postgresql/postgresql))
|
* PostgreSQL: `org.postgresql/postgresql {:mvn/version "42.7.4"}` ([search for latest version](https://search.maven.org/artifact/org.postgresql/postgresql))
|
||||||
* Microsoft SQL Server: `com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.4.1.jre11"}` ([search for latest version](https://search.maven.org/artifact/com.microsoft.sqlserver/mssql-jdbc))
|
* Microsoft SQL Server: `com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.8.1.jre11"}` ([search for latest version](https://search.maven.org/artifact/com.microsoft.sqlserver/mssql-jdbc))
|
||||||
* Sqlite: `org.xerial/sqlite-jdbc {:mvn/version "3.43.0.0"}` ([search for latest version](https://search.maven.org/artifact/org.xerial/sqlite-jdbc))
|
* Sqlite: `org.xerial/sqlite-jdbc {:mvn/version "3.47.1.0"}` ([search for latest version](https://search.maven.org/artifact/org.xerial/sqlite-jdbc))
|
||||||
|
|
||||||
> Note: these are the versions that `next.jdbc` is tested against but there may be more recent versions and those should generally work too -- click the "search for latest version" link to see all available versions of those drivers on Maven Central. You can see the full list of drivers and versions that `next.jdbc` is tested against in [the project's `deps.edn` file](https://github.com/seancorfield/next-jdbc/blob/develop/deps.edn#L10-L27), but many other JDBC drivers for other databases should also work (e.g., Oracle, Red Shift).
|
> Note: these are the versions that `next.jdbc` is tested against but there may be more recent versions and those should generally work too -- click the "search for latest version" link to see all available versions of those drivers on Maven Central. You can see the full list of drivers and versions that `next.jdbc` is tested against in [the project's `deps.edn` file](https://github.com/seancorfield/next-jdbc/blob/develop/deps.edn#L10-L27), but many other JDBC drivers for other databases should also work (e.g., Oracle, Red Shift).
|
||||||
|
|
||||||
|
|
@ -37,9 +37,9 @@ For the examples in this documentation, we will use a local H2 database on disk,
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
;; deps.edn
|
;; deps.edn
|
||||||
{:deps {org.clojure/clojure {:mvn/version "1.11.1"}
|
{:deps {org.clojure/clojure {:mvn/version "1.12.0"}
|
||||||
com.github.seancorfield/next.jdbc {:mvn/version "1.3.925"}
|
com.github.seancorfield/next.jdbc {:mvn/version "1.3.1002"}
|
||||||
com.h2database/h2 {:mvn/version "2.2.224"}}}
|
com.h2database/h2 {:mvn/version "2.3.232"}}}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create & Populate a Database
|
### Create & Populate a Database
|
||||||
|
|
@ -48,7 +48,7 @@ In this REPL session, we'll define an H2 datasource, create a database with a si
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
> clj
|
> clj
|
||||||
Clojure 1.11.1
|
Clojure 1.12.0
|
||||||
user=> (require '[next.jdbc :as jdbc])
|
user=> (require '[next.jdbc :as jdbc])
|
||||||
nil
|
nil
|
||||||
user=> (def db {:dbtype "h2" :dbname "example"})
|
user=> (def db {:dbtype "h2" :dbname "example"})
|
||||||
|
|
@ -487,9 +487,9 @@ Not all databases support using a `PreparedStatement` for every type of SQL oper
|
||||||
First, you need to add the connection pooling library as a dependency, e.g.,
|
First, you need to add the connection pooling library as a dependency, e.g.,
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
|
com.zaxxer/HikariCP {:mvn/version "6.2.1"}
|
||||||
;; or:
|
;; or:
|
||||||
com.mchange/c3p0 {:mvn/version "0.9.5.5"}
|
com.mchange/c3p0 {:mvn/version "0.10.1"}
|
||||||
```
|
```
|
||||||
|
|
||||||
_Check those libraries' documentation for the latest version to use!_
|
_Check those libraries' documentation for the latest version to use!_
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ Several methods on `DatabaseMetaData` return a `ResultSet` object. All of those
|
||||||
|
|
||||||
These are mostly drawn from [Issue #5](https://github.com/seancorfield/next-jdbc/issues/5) although most of the bullets in that issue are described in more detail above.
|
These are mostly drawn from [Issue #5](https://github.com/seancorfield/next-jdbc/issues/5) although most of the bullets in that issue are described in more detail above.
|
||||||
|
|
||||||
* Keyword options no longer end in `?` -- for consistency (in `clojure.java.jdbc`, some flag options ended in `?` and some did not; also some options that ended in `?` accepted non-`Boolean` values, e.g., `:as-arrays?` and `:explain?`),
|
* Keyword options no longer end in `?` -- for consistency (in `clojure.java.jdbc`, some flag options ended in `?` and some did not; also some options that ended in `?` accepted non-`Boolean` values),
|
||||||
* `with-db-connection` has been replaced by just `with-open` containing a call to `get-connection`,
|
* `with-db-connection` has been replaced by just `with-open` containing a call to `get-connection`,
|
||||||
* `with-transaction` can take a `:rollback-only` option, but there is no built-in way to change a transaction to rollback _dynamically_; either throw an exception (all transactions roll back on an exception) or call `.rollback` directly on the `java.sql.Connection` object (see [Manual Rollback Inside a Transactions](/doc/transactions.md#manual-rollback-inside-a-transaction) and the following section about save points),
|
* `with-transaction` can take a `:rollback-only` option, but there is no built-in way to change a transaction to rollback _dynamically_; either throw an exception (all transactions roll back on an exception) or call `.rollback` directly on the `java.sql.Connection` object (see [Manual Rollback Inside a Transactions](/doc/transactions.md#manual-rollback-inside-a-transaction) and the following section about save points),
|
||||||
* `clojure.java.jdbc` implicitly allowed transactions to nest and just silently ignored the inner, nested transactions (so you only really had the top-level, outermost transaction); `next.jdbc` by default assumes you know what you are doing and so an inner (nested) transaction will commit or rollback the work done so far in outer transaction (and then when that outer transaction ends, the remaining work is rolled back or committed); `next.jdbc.transaction/*nested-tx*` is a dynamic var that can be bound to `:ignore` to get similar behavior to `clojure.java.jdbc`.
|
* `clojure.java.jdbc` implicitly allowed transactions to nest and just silently ignored the inner, nested transactions (so you only really had the top-level, outermost transaction); `next.jdbc` by default assumes you know what you are doing and so an inner (nested) transaction will commit or rollback the work done so far in outer transaction (and then when that outer transaction ends, the remaining work is rolled back or committed); `next.jdbc.transaction/*nested-tx*` is a dynamic var that can be bound to `:ignore` to get similar behavior to `clojure.java.jdbc`.
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ be very database-specific. Some database drivers **don't** use the hierarchy
|
||||||
above -- notably PostgreSQL, which has a generic `PSQLException` type
|
above -- notably PostgreSQL, which has a generic `PSQLException` type
|
||||||
with its own subclasses and semantics. See [PostgreSQL JDBC issue #963](https://github.com/pgjdbc/pgjdbc/issues/963)
|
with its own subclasses and semantics. See [PostgreSQL JDBC issue #963](https://github.com/pgjdbc/pgjdbc/issues/963)
|
||||||
for a discussion of the difficulty in adopting the standard JDBC hierarchy
|
for a discussion of the difficulty in adopting the standard JDBC hierarchy
|
||||||
(dating back five years).
|
(dating back to 2017!).
|
||||||
|
|
||||||
The `java.sql.SQLException` class provides `.getErrorCode()` and
|
The `java.sql.SQLException` class provides `.getErrorCode()` and
|
||||||
`.getSQLState()` methods but the values returned by those are
|
`.getSQLState()` methods but the values returned by those are
|
||||||
|
|
@ -237,6 +237,7 @@ common approach, there is also a non-JDBC Clojure/Java driver for PostgreSQL cal
|
||||||
quite a bit faster than using JDBC.
|
quite a bit faster than using JDBC.
|
||||||
|
|
||||||
When you use `:return-keys true` with `execute!` or `execute-one!` (or you use `insert!`), PostgreSQL returns the entire inserted row (unlike nearly every other database that just returns any generated keys!).
|
When you use `:return-keys true` with `execute!` or `execute-one!` (or you use `insert!`), PostgreSQL returns the entire inserted row (unlike nearly every other database that just returns any generated keys!).
|
||||||
|
_[It seems to achieve this by the equivalent of automatically appending `RETURNING *` to your SQL, if necessary.]_
|
||||||
|
|
||||||
The default result set builder for `next.jdbc` is `as-qualified-maps` which
|
The default result set builder for `next.jdbc` is `as-qualified-maps` which
|
||||||
uses the `.getTableName()` method on `ResultSetMetaData` to qualify the
|
uses the `.getTableName()` method on `ResultSetMetaData` to qualify the
|
||||||
|
|
@ -247,7 +248,11 @@ method is called for each query. If you want to avoid those extra queries,
|
||||||
and you can live with unqualified column names, you can use `as-unqualified-maps`
|
and you can live with unqualified column names, you can use `as-unqualified-maps`
|
||||||
as the result set builder instead.
|
as the result set builder instead.
|
||||||
|
|
||||||
If you have a query where you want to select where a column is `IN` a sequence of values, you can use `col = ANY(?)` with a native array of the values instead of `IN (?,?,?,,,?)` and a sequence of values.
|
If you have a query where you want to select where a column is `IN` a sequence of values, you can use `col = ANY(?)` with a native array of the values instead of `IN (?,?,?,,,?)` and a sequence of values. **Be aware of
|
||||||
|
[PostgreSQL bug 17822](https://www.postgresql.org/message-id/flat/17922-1e2e0aeedd294424%40postgresql.org)
|
||||||
|
which can cause pathological performance when the array has a single element!**
|
||||||
|
If you think you might have a single-element array, consider using `UNNEST` and
|
||||||
|
`IN` instead.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|
@ -413,14 +418,12 @@ containing JSON:
|
||||||
(.setValue (->json x)))))
|
(.setValue (->json x)))))
|
||||||
|
|
||||||
(defn <-pgobject
|
(defn <-pgobject
|
||||||
"Transform PGobject containing `json` or `jsonb` value to Clojure
|
"Transform PGobject containing `json` or `jsonb` value to Clojure data."
|
||||||
data."
|
[^PGobject v]
|
||||||
[^org.postgresql.util.PGobject v]
|
|
||||||
(let [type (.getType v)
|
(let [type (.getType v)
|
||||||
value (.getValue v)]
|
value (.getValue v)]
|
||||||
(if (#{"jsonb" "json"} type)
|
(if (#{"jsonb" "json"} type)
|
||||||
(when value
|
(some-> value <-json (with-meta {:pgtype type}))
|
||||||
(with-meta (<-json value) {:pgtype type}))
|
|
||||||
value)))
|
value)))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -572,3 +575,26 @@ If you are using `plan`, you'll most likely be accessing columns by just the lab
|
||||||
|
|
||||||
See also [`datafy`, `nav`, and `:schema` > **SQLite**](/doc/datafy-nav-and-schema.md#sqlite)
|
See also [`datafy`, `nav`, and `:schema` > **SQLite**](/doc/datafy-nav-and-schema.md#sqlite)
|
||||||
for additional caveats on the `next.jdbc.datafy` namespace when using SQLite.
|
for additional caveats on the `next.jdbc.datafy` namespace when using SQLite.
|
||||||
|
|
||||||
|
## XTDB
|
||||||
|
|
||||||
|
XTDB is a bitemporal, schemaless, document-oriented database that presents
|
||||||
|
itself as a PostgreSQL-compatible database, in terms of JDBC. It has a number
|
||||||
|
of SQL extensions, and some differences from common JDBC behavior. See
|
||||||
|
its documentation for details:
|
||||||
|
* [SQL Overview](https://docs.xtdb.com/quickstart/sql-overview.html)
|
||||||
|
* [SQL Queries](https://docs.xtdb.com/reference/main/sql/queries.html)
|
||||||
|
* [SQL Transactions/DML](https://docs.xtdb.com/reference/main/sql/txs.html)
|
||||||
|
|
||||||
|
`next.jdbc` officially supports XTDB as of 1.3.981 but there are some caveats:
|
||||||
|
* You can use `:dbtype "xtdb"` to identify XTDB as the database type.
|
||||||
|
* You must specify `:dbname "xtdb"` in the db-spec hash map or JDBC URL.
|
||||||
|
* XTDB does not support `.getTableName()` so you always get unqualified column names in result sets.
|
||||||
|
* The primary key on all tables is `_id` and it must be specified in all `INSERT` operations (no auto-generated keys).
|
||||||
|
* That means that `next.jdbc.sql/get-by-id` requires the 5-argument call, so that you can specify the `pk-name` as `:_id` and provide an options map.
|
||||||
|
* If you want to use `next.jdbc`'s built-in `datafy` / `nav` functionality, you need to explicitly specify `:schema-opts {:pk "_id"}` to override the default assumption of `id` as the primary key.
|
||||||
|
* DML operations (`INSERT`, `UPDATE`, and `DELETE`) are essentially asynchronous in XTDB and therefore can not return an accurate `next.jdbc/update-count` (so it is always 0).
|
||||||
|
* `INSERT` operations do not return the inserted row (like PostgreSQL does) nor even the provided `_id` primary key.
|
||||||
|
* That means that the `next.jdbc.defer` namespace functions do not work well with XTDB.
|
||||||
|
* `next.jdbc.sql/insert-multi!` returns an empty vector for XTDB (since `INSERT` operations do not return keys or update counts).
|
||||||
|
* The `next.jdbc.result-set/*-kebab-maps` functions (and associated `next.jdbc/*-kebab-opts` option maps) cause leading `_` to be stripped from column names and cannot be used with XTDB (this is inherent in the underlying library that `next.jdbc` relies on -- you can of course write your own custom result set builder function to handle this).
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
version: '2'
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
image: percona:5.7
|
image: percona:5.7
|
||||||
|
|
@ -9,9 +8,14 @@ services:
|
||||||
command:
|
command:
|
||||||
[--character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci]
|
[--character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci]
|
||||||
sqlserver:
|
sqlserver:
|
||||||
image: mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04
|
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||||
environment:
|
environment:
|
||||||
ACCEPT_EULA: Y
|
ACCEPT_EULA: Y
|
||||||
SA_PASSWORD: Str0ngP4ssw0rd
|
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
|
||||||
ports:
|
ports:
|
||||||
- "1433:1433"
|
- "1433:1433"
|
||||||
|
xtdb:
|
||||||
|
image: ghcr.io/xtdb/xtdb:latest
|
||||||
|
# pull_policy: always
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,30 @@
|
||||||
(defn- run-tests [env v]
|
(defn- run-tests [env v]
|
||||||
(when v (println "\nTesting Clojure" v))
|
(when v (println "\nTesting Clojure" v))
|
||||||
(let [{:keys [exit]}
|
(let [{:keys [exit]}
|
||||||
(p/shell {:extra-env env} "clojure" (str "-X"
|
(p/shell {:extra-env env}
|
||||||
|
"clojure"
|
||||||
|
(str "-M"
|
||||||
(when v (str ":" v))
|
(when v (str ":" v))
|
||||||
":test"))]
|
":test:runner"
|
||||||
|
;; jdk21+ adds xtdb:
|
||||||
|
(when (System/getenv "NEXT_JDBC_TEST_XTDB")
|
||||||
|
":jdk21")
|
||||||
|
;; to suppress native access warnings on JDK24:
|
||||||
|
":jdk24")
|
||||||
|
"--output" "dots")]
|
||||||
(when-not (zero? exit)
|
(when-not (zero? exit)
|
||||||
(System/exit exit))))
|
(System/exit exit))))
|
||||||
|
|
||||||
(let [maria? (some #(= "maria" %) *command-line-args*)
|
(let [maria? (some #(= "maria" %) *command-line-args*)
|
||||||
|
xtdb? (some #(= "xtdb" %) *command-line-args*)
|
||||||
all? (some #(= "all" %) *command-line-args*)
|
all? (some #(= "all" %) *command-line-args*)
|
||||||
env
|
env
|
||||||
(cond-> {"NEXT_JDBC_TEST_MSSQL" "yes"
|
(cond-> {"NEXT_JDBC_TEST_MSSQL" "yes"
|
||||||
"NEXT_JDBC_TEST_MYSQL" "yes"
|
"NEXT_JDBC_TEST_MYSQL" "yes"
|
||||||
"MSSQL_SA_PASSWORD" "Str0ngP4ssw0rd"}
|
"MSSQL_SA_PASSWORD" "Str0ngP4ssw0rd"}
|
||||||
maria?
|
maria?
|
||||||
(assoc "NEXT_JDBC_TEST_MARIA" "yes"))]
|
(assoc "NEXT_JDBC_TEST_MARIADB" "yes")
|
||||||
|
xtdb?
|
||||||
|
(assoc "NEXT_JDBC_TEST_XTDB" "yes"))]
|
||||||
(doseq [v (if all? ["1.10" "1.11" "1.12"] [nil])]
|
(doseq [v (if all? ["1.10" "1.11" "1.12"] [nil])]
|
||||||
(run-tests env v)))
|
(run-tests env v)))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2018-2023 Sean Corfield, all rights reserved
|
;; copyright (c) 2018-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc
|
(ns next.jdbc
|
||||||
"The public API of the next generation java.jdbc library.
|
"The public API of the next generation java.jdbc library.
|
||||||
|
|
@ -137,6 +137,7 @@
|
||||||
* `sqlserver`, `mssql` -- `com.microsoft.sqlserver.jdbc.SQLServerDriver` -- `1433`
|
* `sqlserver`, `mssql` -- `com.microsoft.sqlserver.jdbc.SQLServerDriver` -- `1433`
|
||||||
* `timesten:client` -- `com.timesten.jdbc.TimesTenClientDriver`
|
* `timesten:client` -- `com.timesten.jdbc.TimesTenClientDriver`
|
||||||
* `timesten:direct` -- `com.timesten.jdbc.TimesTenDriver`
|
* `timesten:direct` -- `com.timesten.jdbc.TimesTenDriver`
|
||||||
|
* `xtdb` -- `xtdb.jdbc.Driver` -- an XTDB wrapper around `postgresql`
|
||||||
|
|
||||||
For more details about `:dbtype` and `:classname` values, see:
|
For more details about `:dbtype` and `:classname` values, see:
|
||||||
https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/api/next.jdbc.connection#dbtypes"
|
https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/api/next.jdbc.connection#dbtypes"
|
||||||
|
|
@ -176,6 +177,14 @@
|
||||||
[spec user password opts]
|
[spec user password opts]
|
||||||
(p/get-connection spec (assoc opts :user user :password password))))
|
(p/get-connection spec (assoc opts :user user :password password))))
|
||||||
|
|
||||||
|
(defn- ensure-sql-params [sql-params]
|
||||||
|
(when-not (or (nil? sql-params)
|
||||||
|
(and (seqable? sql-params)
|
||||||
|
(or (empty? sql-params)
|
||||||
|
(string? (first sql-params)))))
|
||||||
|
(throw (ex-info "sql-params should be a vector containing a SQL string and any parameters"
|
||||||
|
{:sql-params sql-params}))))
|
||||||
|
|
||||||
(defn prepare
|
(defn prepare
|
||||||
"Given a connection to a database, and a vector containing SQL and any
|
"Given a connection to a database, and a vector containing SQL and any
|
||||||
parameters it needs, return a new `PreparedStatement`.
|
parameters it needs, return a new `PreparedStatement`.
|
||||||
|
|
@ -191,9 +200,11 @@
|
||||||
be passed to prepare."
|
be passed to prepare."
|
||||||
(^java.sql.PreparedStatement
|
(^java.sql.PreparedStatement
|
||||||
[connection sql-params]
|
[connection sql-params]
|
||||||
|
(ensure-sql-params sql-params)
|
||||||
(p/prepare connection sql-params {}))
|
(p/prepare connection sql-params {}))
|
||||||
(^java.sql.PreparedStatement
|
(^java.sql.PreparedStatement
|
||||||
[connection sql-params opts]
|
[connection sql-params opts]
|
||||||
|
(ensure-sql-params sql-params)
|
||||||
(p/prepare connection sql-params opts)))
|
(p/prepare connection sql-params opts)))
|
||||||
|
|
||||||
(defn plan
|
(defn plan
|
||||||
|
|
@ -231,10 +242,12 @@
|
||||||
(p/-execute stmt [] {}))
|
(p/-execute stmt [] {}))
|
||||||
(^clojure.lang.IReduceInit
|
(^clojure.lang.IReduceInit
|
||||||
[connectable sql-params]
|
[connectable sql-params]
|
||||||
|
(ensure-sql-params sql-params)
|
||||||
(p/-execute connectable sql-params
|
(p/-execute connectable sql-params
|
||||||
{:next.jdbc/sql-params sql-params}))
|
{:next.jdbc/sql-params sql-params}))
|
||||||
(^clojure.lang.IReduceInit
|
(^clojure.lang.IReduceInit
|
||||||
[connectable sql-params opts]
|
[connectable sql-params opts]
|
||||||
|
(ensure-sql-params sql-params)
|
||||||
(p/-execute connectable sql-params
|
(p/-execute connectable sql-params
|
||||||
(assoc opts :next.jdbc/sql-params sql-params))))
|
(assoc opts :next.jdbc/sql-params sql-params))))
|
||||||
|
|
||||||
|
|
@ -251,9 +264,11 @@
|
||||||
([stmt]
|
([stmt]
|
||||||
(p/-execute-all stmt [] {}))
|
(p/-execute-all stmt [] {}))
|
||||||
([connectable sql-params]
|
([connectable sql-params]
|
||||||
|
(ensure-sql-params sql-params)
|
||||||
(p/-execute-all connectable sql-params
|
(p/-execute-all connectable sql-params
|
||||||
{:next.jdbc/sql-params sql-params}))
|
{:next.jdbc/sql-params sql-params}))
|
||||||
([connectable sql-params opts]
|
([connectable sql-params opts]
|
||||||
|
(ensure-sql-params sql-params)
|
||||||
(p/-execute-all connectable sql-params
|
(p/-execute-all connectable sql-params
|
||||||
(assoc opts :next.jdbc/sql-params sql-params))))
|
(assoc opts :next.jdbc/sql-params sql-params))))
|
||||||
|
|
||||||
|
|
@ -270,9 +285,11 @@
|
||||||
([stmt]
|
([stmt]
|
||||||
(p/-execute-one stmt [] {}))
|
(p/-execute-one stmt [] {}))
|
||||||
([connectable sql-params]
|
([connectable sql-params]
|
||||||
|
(ensure-sql-params sql-params)
|
||||||
(p/-execute-one connectable sql-params
|
(p/-execute-one connectable sql-params
|
||||||
{:next.jdbc/sql-params sql-params}))
|
{:next.jdbc/sql-params sql-params}))
|
||||||
([connectable sql-params opts]
|
([connectable sql-params opts]
|
||||||
|
(ensure-sql-params sql-params)
|
||||||
(p/-execute-one connectable sql-params
|
(p/-execute-one connectable sql-params
|
||||||
(assoc opts :next.jdbc/sql-params sql-params))))
|
(assoc opts :next.jdbc/sql-params sql-params))))
|
||||||
|
|
||||||
|
|
@ -335,9 +352,9 @@
|
||||||
result))))
|
result))))
|
||||||
params)))
|
params)))
|
||||||
([connectable sql param-groups opts]
|
([connectable sql param-groups opts]
|
||||||
(if (or (instance? java.sql.Connection connectable)
|
(when-not (string? sql)
|
||||||
(and (satisfies? p/Connectable connectable)
|
(throw (IllegalArgumentException. "execute-batch! requires a SQL string")))
|
||||||
(instance? java.sql.Connection (:connectable connectable))))
|
(if (instance? java.sql.Connection (p/unwrap connectable))
|
||||||
(with-open [ps (prepare connectable [sql] opts)]
|
(with-open [ps (prepare connectable [sql] opts)]
|
||||||
(execute-batch! ps param-groups opts))
|
(execute-batch! ps param-groups opts))
|
||||||
(with-open [con (get-connection connectable)]
|
(with-open [con (get-connection connectable)]
|
||||||
|
|
@ -364,13 +381,10 @@
|
||||||
executes the body, and automatically closes it for you."
|
executes the body, and automatically closes it for you."
|
||||||
[[sym connectable] & body]
|
[[sym connectable] & body]
|
||||||
(let [con-sym (vary-meta sym assoc :tag 'java.sql.Connection)]
|
(let [con-sym (vary-meta sym assoc :tag 'java.sql.Connection)]
|
||||||
`(let [con-obj# ~connectable]
|
`(let [con-obj# ~connectable
|
||||||
(cond (instance? java.sql.Connection con-obj#)
|
bare-con# (p/unwrap con-obj#)]
|
||||||
((^{:once true} fn* [~con-sym] ~@body) con-obj#)
|
(if (instance? java.sql.Connection bare-con#)
|
||||||
(and (satisfies? p/Connectable con-obj#)
|
((^{:once true} fn* [~con-sym] ~@body) bare-con#)
|
||||||
(instance? java.sql.Connection (:connectable con-obj#)))
|
|
||||||
((^{:once true} fn* [~con-sym] ~@body) (:connectable con-obj#))
|
|
||||||
:else
|
|
||||||
(with-open [con# (get-connection con-obj#)]
|
(with-open [con# (get-connection con-obj#)]
|
||||||
((^{:once true} fn* [~con-sym] ~@body) con#))))))
|
((^{:once true} fn* [~con-sym] ~@body) con#))))))
|
||||||
|
|
||||||
|
|
@ -402,12 +416,8 @@
|
||||||
with `on-connection`."
|
with `on-connection`."
|
||||||
[[sym connectable] & body]
|
[[sym connectable] & body]
|
||||||
`(let [con-obj# ~connectable]
|
`(let [con-obj# ~connectable]
|
||||||
(cond (instance? java.sql.Connection con-obj#)
|
(if (instance? java.sql.Connection (p/unwrap con-obj#))
|
||||||
((^{:once true} fn* [~sym] ~@body) con-obj#)
|
((^{:once true} fn* [~sym] ~@body) con-obj#)
|
||||||
(and (satisfies? p/Connectable con-obj#)
|
|
||||||
(instance? java.sql.Connection (:connectable con-obj#)))
|
|
||||||
((^{:once true} fn* [~sym] ~@body) con-obj#)
|
|
||||||
:else
|
|
||||||
(with-open [con# (get-connection con-obj#)]
|
(with-open [con# (get-connection con-obj#)]
|
||||||
((^{:once true} fn* [~sym] ~@body)
|
((^{:once true} fn* [~sym] ~@body)
|
||||||
(with-options con# (:options con-obj# {})))))))
|
(with-options con# (:options con-obj# {})))))))
|
||||||
|
|
@ -447,12 +457,19 @@
|
||||||
"Returns true if `next.jdbc` has a currently active transaction in the
|
"Returns true if `next.jdbc` has a currently active transaction in the
|
||||||
current thread, else false.
|
current thread, else false.
|
||||||
|
|
||||||
|
With no arguments, tells you if any transaction is currently active.
|
||||||
|
|
||||||
|
With a `Connection` argument, tells you if a transaction is currently
|
||||||
|
active on that specific connection.
|
||||||
|
|
||||||
Note: transactions are a convention of operations on a `Connection` so
|
Note: transactions are a convention of operations on a `Connection` so
|
||||||
this predicate only reflects `next.jdbc/transact` and `next.jdbc/with-transaction`
|
this predicate only reflects `next.jdbc/transact` and `next.jdbc/with-transaction`
|
||||||
operations -- it does not reflect any other operations on a `Connection`,
|
operations -- it does not reflect any other operations on a `Connection`,
|
||||||
performed via JDBC interop directly."
|
performed via JDBC interop directly."
|
||||||
[]
|
([]
|
||||||
@#'tx/*active-tx*)
|
(boolean (seq @#'tx/*active-tx*)))
|
||||||
|
([con]
|
||||||
|
(contains? @#'tx/*active-tx* con)))
|
||||||
|
|
||||||
(defn with-options
|
(defn with-options
|
||||||
"Given a connectable/transactable object and a set of (default) options
|
"Given a connectable/transactable object and a set of (default) options
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2018-2024 Sean Corfield, all rights reserved
|
;; copyright (c) 2018-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.connection
|
(ns next.jdbc.connection
|
||||||
"Standard implementations of `get-datasource` and `get-connection`.
|
"Standard implementations of `get-datasource` and `get-connection`.
|
||||||
|
|
@ -130,7 +130,9 @@
|
||||||
:host :none}
|
:host :none}
|
||||||
"timesten:direct" {:classname "com.timesten.jdbc.TimesTenDriver"
|
"timesten:direct" {:classname "com.timesten.jdbc.TimesTenDriver"
|
||||||
:dbname-separator ":dsn="
|
:dbname-separator ":dsn="
|
||||||
:host :none}})
|
:host :none}
|
||||||
|
"xtdb" {:classname "xtdb.jdbc.Driver"
|
||||||
|
:port 5432}})
|
||||||
|
|
||||||
(def ^:private driver-cache
|
(def ^:private driver-cache
|
||||||
"An optimization for repeated calls to get-datasource, or for get-connection
|
"An optimization for repeated calls to get-datasource, or for get-connection
|
||||||
|
|
@ -372,9 +374,12 @@
|
||||||
(defn- as-properties
|
(defn- as-properties
|
||||||
"Convert any seq of pairs to a `java.util.Properties` instance."
|
"Convert any seq of pairs to a `java.util.Properties` instance."
|
||||||
^Properties [m]
|
^Properties [m]
|
||||||
(let [p (Properties.)]
|
(let [p (Properties.)
|
||||||
(doseq [[k v] m]
|
as-is (set (:next.jdbc/as-is-properties m))]
|
||||||
(.setProperty p (name k) (str v)))
|
(doseq [[k v] (dissoc m :next.jdbc/as-is-properties)]
|
||||||
|
(if (contains? as-is k)
|
||||||
|
(.put p (name k) v)
|
||||||
|
(.setProperty p (name k) (str v))))
|
||||||
p))
|
p))
|
||||||
|
|
||||||
(defn uri->db-spec
|
(defn uri->db-spec
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2020-2024 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.datafy
|
(ns next.jdbc.datafy
|
||||||
"This namespace provides datafication of several JDBC object types,
|
"This namespace provides datafication of several JDBC object types,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.date-time
|
(ns next.jdbc.date-time
|
||||||
"Optional namespace that extends `next.jdbc.prepare/SettableParameter`
|
"Optional namespace that extends `next.jdbc.prepare/SettableParameter`
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2020-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns ^:no-doc next.jdbc.default-options
|
(ns ^:no-doc next.jdbc.default-options
|
||||||
"Implementation of default options logic."
|
"Implementation of default options logic."
|
||||||
|
|
@ -8,6 +8,10 @@
|
||||||
|
|
||||||
(defrecord DefaultOptions [connectable options])
|
(defrecord DefaultOptions [connectable options])
|
||||||
|
|
||||||
|
(extend-protocol p/Wrapped
|
||||||
|
DefaultOptions
|
||||||
|
(unwrap [this] (p/unwrap (:connectable this))))
|
||||||
|
|
||||||
(extend-protocol p/Sourceable
|
(extend-protocol p/Sourceable
|
||||||
DefaultOptions
|
DefaultOptions
|
||||||
(get-datasource [this]
|
(get-datasource [this]
|
||||||
|
|
|
||||||
94
src/next/jdbc/defer.clj
Normal file
94
src/next/jdbc/defer.clj
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
;; copyright (c) 2024 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns next.jdbc.defer
|
||||||
|
"The idea behind the next.jdbc.defer namespace is to provide a
|
||||||
|
way to defer the execution of a series of SQL statements until
|
||||||
|
a later time, but still provide a way for inserted keys to be
|
||||||
|
used in later SQL statements.
|
||||||
|
|
||||||
|
The principle is to provide a core subset of the next.jdbc
|
||||||
|
and next.jdbc.sql API that produces a data structure that
|
||||||
|
describes a series of SQL operations to be performed, that
|
||||||
|
are held in a dynamic var, and that can be executed at a
|
||||||
|
later time, in a transaction."
|
||||||
|
(:require [next.jdbc :as jdbc]
|
||||||
|
[next.jdbc.sql.builder :refer [for-delete for-insert for-update]]))
|
||||||
|
|
||||||
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
(def ^:private ^:dynamic *deferred* nil)
|
||||||
|
|
||||||
|
(defn execute-one!
|
||||||
|
"Given a vector containing a SQL statement and parameters, defer
|
||||||
|
execution of that statement."
|
||||||
|
([sql-p]
|
||||||
|
(execute-one! sql-p {}))
|
||||||
|
([sql-p opts]
|
||||||
|
(swap! *deferred* conj
|
||||||
|
{:sql-p sql-p
|
||||||
|
:key-fn (or (:key-fn opts) (comp first vals))
|
||||||
|
:key (:key opts)
|
||||||
|
:opts opts})))
|
||||||
|
|
||||||
|
(defn insert!
|
||||||
|
"Given a table name, and a data hash map, defer an insertion of the
|
||||||
|
data as a single row in the database."
|
||||||
|
([table key-map]
|
||||||
|
(insert! table key-map {}))
|
||||||
|
([table key-map opts]
|
||||||
|
(swap! *deferred* conj
|
||||||
|
{:sql-p (for-insert table key-map opts)
|
||||||
|
:key-fn (or (:key-fn opts) (comp first vals))
|
||||||
|
:key (:key opts)
|
||||||
|
:opts opts})))
|
||||||
|
|
||||||
|
(defn update!
|
||||||
|
"Given a table name, a hash map of columns and values to set, and
|
||||||
|
either a hash map of columns and values to search on or a vector
|
||||||
|
of a SQL where clause and parameters, defer an update on the table."
|
||||||
|
([table key-map where-params]
|
||||||
|
(update! table key-map where-params {}))
|
||||||
|
([table key-map where-params opts]
|
||||||
|
(swap! *deferred* conj
|
||||||
|
{:sql-p (for-update table key-map where-params opts)
|
||||||
|
:opts opts})))
|
||||||
|
|
||||||
|
(defn delete!
|
||||||
|
"Given a table name, and either a hash map of columns and values
|
||||||
|
to search on or a vector of a SQL where clause and parameters,
|
||||||
|
defer a delete on the table."
|
||||||
|
([table where-params]
|
||||||
|
(delete! table where-params {}))
|
||||||
|
([table where-params opts]
|
||||||
|
(swap! *deferred* conj
|
||||||
|
{:sql-p (for-delete table where-params opts)
|
||||||
|
:opts opts})))
|
||||||
|
|
||||||
|
(defn deferrable [transactable stmts]
|
||||||
|
(reify clojure.lang.IDeref
|
||||||
|
(deref [_]
|
||||||
|
(let [keys (atom {})]
|
||||||
|
(jdbc/with-transaction [conn transactable]
|
||||||
|
(doseq [{:keys [sql-p key-fn key opts]} @stmts]
|
||||||
|
(let [sql-p
|
||||||
|
(mapv (fn [v]
|
||||||
|
(if (keyword? v)
|
||||||
|
(if (contains? @keys v)
|
||||||
|
(get @keys v)
|
||||||
|
(throw (ex-info (str "Deferred key not found " v)
|
||||||
|
{:key v})))
|
||||||
|
v))
|
||||||
|
sql-p)
|
||||||
|
result (jdbc/execute-one! conn sql-p opts)]
|
||||||
|
(when key
|
||||||
|
(swap! keys assoc key (key-fn result))))))
|
||||||
|
@keys))))
|
||||||
|
|
||||||
|
(defn defer-ops [f]
|
||||||
|
(binding [*deferred* (atom [])]
|
||||||
|
(f)
|
||||||
|
*deferred*))
|
||||||
|
|
||||||
|
(defmacro with-deferred [connectable & body]
|
||||||
|
`(let [conn# ~connectable]
|
||||||
|
(deferrable conn# (defer-ops (^{:once true} fn* [] ~@body)))))
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2020-2024 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.plan
|
(ns next.jdbc.plan
|
||||||
"Some helper functions that make common operations with `next.jdbc/plan`
|
"Some helper functions that make common operations with `next.jdbc/plan`
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2018-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2018-2024 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.prepare
|
(ns next.jdbc.prepare
|
||||||
"Mostly an implementation namespace for how `PreparedStatement` objects are
|
"Mostly an implementation namespace for how `PreparedStatement` objects are
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2018-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2018-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.protocols
|
(ns next.jdbc.protocols
|
||||||
"This is the extensible core of the next generation java.jdbc library.
|
"This is the extensible core of the next generation java.jdbc library.
|
||||||
|
|
@ -63,3 +63,15 @@
|
||||||
:extend-via-metadata true
|
:extend-via-metadata true
|
||||||
(-transact [this body-fn opts]
|
(-transact [this body-fn opts]
|
||||||
"Run the `body-fn` inside a transaction."))
|
"Run the `body-fn` inside a transaction."))
|
||||||
|
|
||||||
|
(defprotocol Wrapped
|
||||||
|
"Protocol for (un)wrapping a `next.jdbc` connectable.
|
||||||
|
|
||||||
|
Implementations are provided for `Object` (identity) and `DefaultOptions`
|
||||||
|
and SQLLogging."
|
||||||
|
(unwrap [this]
|
||||||
|
"Unwrap the connectable to get the underlying connectable."))
|
||||||
|
|
||||||
|
(extend-protocol Wrapped
|
||||||
|
Object
|
||||||
|
(unwrap [this] this))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2019-2023 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.quoted
|
(ns next.jdbc.quoted
|
||||||
"Provides functions for use with the `:table-fn` and `:column-fn` options
|
"Provides functions for use with the `:table-fn` and `:column-fn` options
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@
|
||||||
(mapv (fn [^Integer i] (keyword (.getColumnLabel rsmeta i)))
|
(mapv (fn [^Integer i] (keyword (.getColumnLabel rsmeta i)))
|
||||||
(range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0)))))
|
(range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0)))))
|
||||||
|
|
||||||
|
(defn- validate [expr ^String msg]
|
||||||
|
(when-not expr (throw (IllegalArgumentException. msg))))
|
||||||
|
|
||||||
(defn get-modified-column-names
|
(defn get-modified-column-names
|
||||||
"Given `ResultSetMetaData`, return a vector of modified column names, each
|
"Given `ResultSetMetaData`, return a vector of modified column names, each
|
||||||
qualified by the table from which it came.
|
qualified by the table from which it came.
|
||||||
|
|
@ -66,8 +69,8 @@
|
||||||
[^ResultSetMetaData rsmeta opts]
|
[^ResultSetMetaData rsmeta opts]
|
||||||
(let [qf (:qualifier-fn opts)
|
(let [qf (:qualifier-fn opts)
|
||||||
lf (:label-fn opts)]
|
lf (:label-fn opts)]
|
||||||
(assert qf ":qualifier-fn is required")
|
(validate qf ":qualifier-fn is required")
|
||||||
(assert lf ":label-fn is required")
|
(validate lf ":label-fn is required")
|
||||||
(mapv (fn [^Integer i]
|
(mapv (fn [^Integer i]
|
||||||
(if-let [q (some-> (get-table-name rsmeta i) (qf) (not-empty))]
|
(if-let [q (some-> (get-table-name rsmeta i) (qf) (not-empty))]
|
||||||
(keyword q (-> (.getColumnLabel rsmeta i) (lf)))
|
(keyword q (-> (.getColumnLabel rsmeta i) (lf)))
|
||||||
|
|
@ -81,7 +84,7 @@
|
||||||
Requires the `:label-fn` option."
|
Requires the `:label-fn` option."
|
||||||
[^ResultSetMetaData rsmeta opts]
|
[^ResultSetMetaData rsmeta opts]
|
||||||
(let [lf (:label-fn opts)]
|
(let [lf (:label-fn opts)]
|
||||||
(assert lf ":label-fn is required")
|
(validate lf ":label-fn is required")
|
||||||
(mapv (fn [^Integer i] (keyword (lf (.getColumnLabel rsmeta i))))
|
(mapv (fn [^Integer i] (keyword (lf (.getColumnLabel rsmeta i))))
|
||||||
(range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0))))))
|
(range 1 (inc (if rsmeta (.getColumnCount rsmeta) 0))))))
|
||||||
|
|
||||||
|
|
@ -275,6 +278,10 @@
|
||||||
:qualifier-fn ->kebab-case
|
:qualifier-fn ->kebab-case
|
||||||
:label-fn ->kebab-case)))
|
:label-fn ->kebab-case)))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
(->kebab-case "_id") ;;=> "id"!!
|
||||||
|
)
|
||||||
|
|
||||||
(defn as-unqualified-kebab-maps
|
(defn as-unqualified-kebab-maps
|
||||||
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
|
||||||
that produces bare vectors of hash map rows, with simple, kebab-case keys."
|
that produces bare vectors of hash map rows, with simple, kebab-case keys."
|
||||||
|
|
@ -316,7 +323,7 @@
|
||||||
"An example column-reader that still uses `.getObject` but expands CLOB
|
"An example column-reader that still uses `.getObject` but expands CLOB
|
||||||
columns into strings."
|
columns into strings."
|
||||||
[^ResultSet rs ^ResultSetMetaData _ ^Integer i]
|
[^ResultSet rs ^ResultSetMetaData _ ^Integer i]
|
||||||
(when-let [value (.getObject rs i)]
|
(let [value (.getObject rs i)]
|
||||||
(cond-> value
|
(cond-> value
|
||||||
(instance? Clob value)
|
(instance? Clob value)
|
||||||
(clob->string))))
|
(clob->string))))
|
||||||
|
|
@ -951,14 +958,14 @@
|
||||||
(reify
|
(reify
|
||||||
clojure.lang.IReduceInit
|
clojure.lang.IReduceInit
|
||||||
(reduce [_ f init]
|
(reduce [_ f init]
|
||||||
(reduce-stmt this f init (assoc opts :return-keys true)))
|
(reduce-stmt this f init (merge {:return-keys true} opts)))
|
||||||
r/CollFold
|
r/CollFold
|
||||||
(coll-fold [_ n combinef reducef]
|
(coll-fold [_ n combinef reducef]
|
||||||
(fold-stmt this n combinef reducef (.getConnection this)
|
(fold-stmt this n combinef reducef (.getConnection this)
|
||||||
(assoc opts :return-keys true)))
|
(merge {:return-keys true} opts)))
|
||||||
(toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
|
(toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
|
||||||
(-execute-one [this _ opts]
|
(-execute-one [this _ opts]
|
||||||
(if-let [rs (stmt->result-set this (assoc opts :return-keys true))]
|
(if-let [rs (stmt->result-set this (merge {:return-keys true} opts))]
|
||||||
(let [builder-fn (get opts :builder-fn as-maps)
|
(let [builder-fn (get opts :builder-fn as-maps)
|
||||||
builder (builder-fn rs opts)]
|
builder (builder-fn rs opts)]
|
||||||
(when (.next rs)
|
(when (.next rs)
|
||||||
|
|
@ -969,16 +976,16 @@
|
||||||
(if (:multi-rs opts)
|
(if (:multi-rs opts)
|
||||||
(loop [go (.execute this) acc []]
|
(loop [go (.execute this) acc []]
|
||||||
(if-let [rs (stmt->result-set-update-count
|
(if-let [rs (stmt->result-set-update-count
|
||||||
(.getConnection this) this go (assoc opts :return-keys true))]
|
(.getConnection this) this go (merge {:return-keys true} opts))]
|
||||||
(recur (.getMoreResults this) (conj acc rs))
|
(recur (.getMoreResults this) (conj acc rs))
|
||||||
acc))
|
acc))
|
||||||
(if-let [rs (stmt->result-set this (assoc opts :return-keys true))]
|
(if-let [rs (stmt->result-set this (merge {:return-keys true} opts))]
|
||||||
(datafiable-result-set rs (.getConnection this) opts)
|
(datafiable-result-set rs (.getConnection this) opts)
|
||||||
[{:next.jdbc/update-count (.getUpdateCount this)}])))
|
[{:next.jdbc/update-count (.getUpdateCount this)}])))
|
||||||
|
|
||||||
java.sql.Statement
|
java.sql.Statement
|
||||||
(-execute [this sql-params opts]
|
(-execute [this sql-params opts]
|
||||||
(assert (= 1 (count sql-params))
|
(validate (= 1 (count sql-params))
|
||||||
"Parameters cannot be provided when executing a non-prepared Statement")
|
"Parameters cannot be provided when executing a non-prepared Statement")
|
||||||
(reify
|
(reify
|
||||||
clojure.lang.IReduceInit
|
clojure.lang.IReduceInit
|
||||||
|
|
@ -990,7 +997,7 @@
|
||||||
(.getConnection this) opts))
|
(.getConnection this) opts))
|
||||||
(toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
|
(toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
|
||||||
(-execute-one [this sql-params opts]
|
(-execute-one [this sql-params opts]
|
||||||
(assert (= 1 (count sql-params))
|
(validate (= 1 (count sql-params))
|
||||||
"Parameters cannot be provided when executing a non-prepared Statement")
|
"Parameters cannot be provided when executing a non-prepared Statement")
|
||||||
(if-let [rs (stmt-sql->result-set this (first sql-params))]
|
(if-let [rs (stmt-sql->result-set this (first sql-params))]
|
||||||
(let [builder-fn (get opts :builder-fn as-maps)
|
(let [builder-fn (get opts :builder-fn as-maps)
|
||||||
|
|
@ -1000,12 +1007,12 @@
|
||||||
(.getConnection this) opts)))
|
(.getConnection this) opts)))
|
||||||
{:next.jdbc/update-count (.getUpdateCount this)}))
|
{:next.jdbc/update-count (.getUpdateCount this)}))
|
||||||
(-execute-all [this sql-params opts]
|
(-execute-all [this sql-params opts]
|
||||||
(assert (= 1 (count sql-params))
|
(validate (= 1 (count sql-params))
|
||||||
"Parameters cannot be provided when executing a non-prepared Statement")
|
"Parameters cannot be provided when executing a non-prepared Statement")
|
||||||
(if (:multi-rs opts)
|
(if (:multi-rs opts)
|
||||||
(loop [go (.execute this (first sql-params)) acc []]
|
(loop [go (.execute this (first sql-params)) acc []]
|
||||||
(if-let [rs (stmt->result-set-update-count
|
(if-let [rs (stmt->result-set-update-count
|
||||||
(.getConnection this) this go (assoc opts :return-keys true))]
|
(.getConnection this) this go (merge {:return-keys true} opts))]
|
||||||
(recur (.getMoreResults this) (conj acc rs))
|
(recur (.getMoreResults this) (conj acc rs))
|
||||||
acc))
|
acc))
|
||||||
(if-let [rs (stmt-sql->result-set this (first sql-params))]
|
(if-let [rs (stmt-sql->result-set this (first sql-params))]
|
||||||
|
|
@ -1037,7 +1044,7 @@
|
||||||
(let [fk-suffix (get-in opts [:schema-opts :fk-suffix] "id")
|
(let [fk-suffix (get-in opts [:schema-opts :fk-suffix] "id")
|
||||||
pk (get-in opts [:schema-opts :pk] "id")
|
pk (get-in opts [:schema-opts :pk] "id")
|
||||||
pk-fn (get-in opts [:schema-opts :pk-fn] (constantly (name pk)))
|
pk-fn (get-in opts [:schema-opts :pk-fn] (constantly (name pk)))
|
||||||
[_ table] (re-find (re-pattern (str "(?i)^(.+?)_?"
|
[_ table] (re-find (re-pattern (str "(?i)^(.+?)[-_]?"
|
||||||
(name fk-suffix)
|
(name fk-suffix)
|
||||||
"$"))
|
"$"))
|
||||||
(name col))]
|
(name col))]
|
||||||
|
|
@ -1050,8 +1057,10 @@
|
||||||
(default-schema {} :user_statusid)
|
(default-schema {} :user_statusid)
|
||||||
(default-schema {:schema-opts {:fk-suffix "did"}} :user_id)
|
(default-schema {:schema-opts {:fk-suffix "did"}} :user_id)
|
||||||
(default-schema {:schema-opts {:fk-suffix "did"}} :user_did)
|
(default-schema {:schema-opts {:fk-suffix "did"}} :user_did)
|
||||||
|
(default-schema {:schema-opts {:fk-suffix "did"}} :user-did)
|
||||||
(default-schema {:schema-opts {:fk-suffix "(did|id)"}} :user_id)
|
(default-schema {:schema-opts {:fk-suffix "(did|id)"}} :user_id)
|
||||||
(default-schema {:schema-opts {:fk-suffix "(did|id)"}} :user_did)
|
(default-schema {:schema-opts {:fk-suffix "(did|id)"}} :user_did)
|
||||||
|
(default-schema {:schema-opts {:fk-suffix "(did|id)"}} :user-did)
|
||||||
(default-schema {:schema-opts {:fk-suffix "(did|id)"
|
(default-schema {:schema-opts {:fk-suffix "(did|id)"
|
||||||
:pk :did}} :user_did)
|
:pk :did}} :user_did)
|
||||||
(default-schema {:schema-opts {:fk-suffix "(did|id)"
|
(default-schema {:schema-opts {:fk-suffix "(did|id)"
|
||||||
|
|
@ -1061,6 +1070,13 @@
|
||||||
"id"
|
"id"
|
||||||
pk))}}
|
pk))}}
|
||||||
:user_did)
|
:user_did)
|
||||||
|
(default-schema {:schema-opts {:fk-suffix "(did|id)"
|
||||||
|
:pk :did
|
||||||
|
:pk-fn (fn [table pk]
|
||||||
|
(if (= "user" table)
|
||||||
|
"id"
|
||||||
|
pk))}}
|
||||||
|
:user-did)
|
||||||
(default-schema {:schema-opts {:fk-suffix "(did|id)"
|
(default-schema {:schema-opts {:fk-suffix "(did|id)"
|
||||||
:pk :did
|
:pk :did
|
||||||
:pk-fn (fn [table pk]
|
:pk-fn (fn [table pk]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2019-2023 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.specs
|
(ns next.jdbc.specs
|
||||||
"Specs for the core API of next.jdbc.
|
"Specs for the core API of next.jdbc.
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
:opts (s/? ::opts-map)))
|
:opts (s/? ::opts-map)))
|
||||||
|
|
||||||
(s/fdef jdbc/prepare
|
(s/fdef jdbc/prepare
|
||||||
:args (s/cat :connection ::connection
|
:args (s/cat :connection ::proto-connectable
|
||||||
:sql-params ::sql-params
|
:sql-params ::sql-params
|
||||||
:opts (s/? ::opts-map)))
|
:opts (s/? ::opts-map)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2019-2022 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.sql.builder
|
(ns next.jdbc.sql.builder
|
||||||
"Some utility functions for building SQL strings.
|
"Some utility functions for building SQL strings.
|
||||||
|
|
@ -70,6 +70,9 @@
|
||||||
[key-map opts]
|
[key-map opts]
|
||||||
(as-cols (keys key-map) opts))
|
(as-cols (keys key-map) opts))
|
||||||
|
|
||||||
|
(defn- validate [expr ^String msg]
|
||||||
|
(when-not expr (throw (IllegalArgumentException. msg))))
|
||||||
|
|
||||||
(defn by-keys
|
(defn by-keys
|
||||||
"Given a hash map of column names and values and a clause type
|
"Given a hash map of column names and values and a clause type
|
||||||
(`:set`, `:where`), return a vector of a SQL clause and its parameters.
|
(`:set`, `:where`), return a vector of a SQL clause and its parameters.
|
||||||
|
|
@ -84,7 +87,7 @@
|
||||||
[(conj conds (str e " = ?")) (conj params v)])))
|
[(conj conds (str e " = ?")) (conj params v)])))
|
||||||
[[] []]
|
[[] []]
|
||||||
key-map)]
|
key-map)]
|
||||||
(assert (seq where) "key-map may not be empty")
|
(validate (seq where) "key-map may not be empty")
|
||||||
(into [(str (str/upper-case (safe-name clause)) " "
|
(into [(str (str/upper-case (safe-name clause)) " "
|
||||||
(str/join (if (= :where clause) " AND " ", ") where))]
|
(str/join (if (= :where clause) " AND " ", ") where))]
|
||||||
params)))
|
params)))
|
||||||
|
|
@ -122,7 +125,7 @@
|
||||||
(let [entity-fn (:table-fn opts identity)
|
(let [entity-fn (:table-fn opts identity)
|
||||||
params (as-keys key-map opts)
|
params (as-keys key-map opts)
|
||||||
places (as-? key-map opts)]
|
places (as-? key-map opts)]
|
||||||
(assert (seq key-map) "key-map may not be empty")
|
(validate (seq key-map) "key-map may not be empty")
|
||||||
(into [(str "INSERT INTO " (entity-fn (safe-name table))
|
(into [(str "INSERT INTO " (entity-fn (safe-name table))
|
||||||
" (" params ")"
|
" (" params ")"
|
||||||
" VALUES (" places ")"
|
" VALUES (" places ")"
|
||||||
|
|
@ -144,11 +147,11 @@
|
||||||
If `:suffix` is provided in `opts`, that string is appended to the
|
If `:suffix` is provided in `opts`, that string is appended to the
|
||||||
`INSERT ...` statement."
|
`INSERT ...` statement."
|
||||||
[table cols rows opts]
|
[table cols rows opts]
|
||||||
(assert (apply = (count cols) (map count rows))
|
(validate (apply = (count cols) (map count rows))
|
||||||
"column counts are not consistent across cols and rows")
|
"column counts are not consistent across cols and rows")
|
||||||
;; to avoid generating bad SQL
|
;; to avoid generating bad SQL
|
||||||
(assert (seq cols) "cols may not be empty")
|
(validate (seq cols) "cols may not be empty")
|
||||||
(assert (seq rows) "rows may not be empty")
|
(validate (seq rows) "rows may not be empty")
|
||||||
(let [table-fn (:table-fn opts identity)
|
(let [table-fn (:table-fn opts identity)
|
||||||
batch? (:batch opts)
|
batch? (:batch opts)
|
||||||
params (as-cols cols opts)
|
params (as-cols cols opts)
|
||||||
|
|
@ -195,7 +198,7 @@
|
||||||
[order-by opts]
|
[order-by opts]
|
||||||
(when-not (vector? order-by)
|
(when-not (vector? order-by)
|
||||||
(throw (IllegalArgumentException. ":order-by must be a vector")))
|
(throw (IllegalArgumentException. ":order-by must be a vector")))
|
||||||
(assert (seq order-by) ":order-by may not be empty")
|
(validate (seq order-by) ":order-by may not be empty")
|
||||||
(str "ORDER BY "
|
(str "ORDER BY "
|
||||||
(str/join ", " (map #(for-order-col % opts) order-by))))
|
(str/join ", " (map #(for-order-col % opts) order-by))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2021-2023 Sean Corfield, all rights reserved
|
;; copyright (c) 2021-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns ^:no-doc next.jdbc.sql-logging
|
(ns ^:no-doc next.jdbc.sql-logging
|
||||||
"Implementation of sql-logging logic."
|
"Implementation of sql-logging logic."
|
||||||
|
|
@ -8,6 +8,10 @@
|
||||||
|
|
||||||
(defrecord SQLLogging [connectable sql-logger result-logger options])
|
(defrecord SQLLogging [connectable sql-logger result-logger options])
|
||||||
|
|
||||||
|
(extend-protocol p/Wrapped
|
||||||
|
SQLLogging
|
||||||
|
(unwrap [this] (p/unwrap (:connectable this))))
|
||||||
|
|
||||||
(extend-protocol p/Sourceable
|
(extend-protocol p/Sourceable
|
||||||
SQLLogging
|
SQLLogging
|
||||||
(get-datasource [this]
|
(get-datasource [this]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2018-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2018-2024 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.transaction
|
(ns next.jdbc.transaction
|
||||||
"Implementation of SQL transaction logic.
|
"Implementation of SQL transaction logic.
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
:allow)
|
:allow)
|
||||||
|
|
||||||
(defonce ^:private ^:dynamic ^{:doc "Used to detect nested transactions."}
|
(defonce ^:private ^:dynamic ^{:doc "Used to detect nested transactions."}
|
||||||
*active-tx* false)
|
*active-tx* #{})
|
||||||
|
|
||||||
(def ^:private isolation-levels
|
(def ^:private isolation-levels
|
||||||
"Transaction isolation levels."
|
"Transaction isolation levels."
|
||||||
|
|
@ -112,17 +112,26 @@
|
||||||
(.setReadOnly con old-readonly)
|
(.setReadOnly con old-readonly)
|
||||||
(catch Exception _))))))))
|
(catch Exception _))))))))
|
||||||
|
|
||||||
|
(defn- raw-connection ^Connection [^Connection con]
|
||||||
|
(try ; because some drivers do not implement this :(
|
||||||
|
(if (.isWrapperFor con Connection)
|
||||||
|
(.unwrap con Connection)
|
||||||
|
con)
|
||||||
|
(catch Throwable _ ; to catch AbstractMethodError :(
|
||||||
|
con)))
|
||||||
|
|
||||||
(extend-protocol p/Transactable
|
(extend-protocol p/Transactable
|
||||||
java.sql.Connection
|
java.sql.Connection
|
||||||
(-transact [this body-fn opts]
|
(-transact [this body-fn opts]
|
||||||
|
(let [raw (raw-connection this)]
|
||||||
(cond
|
(cond
|
||||||
(and (not *active-tx*) (= :ignore *nested-tx*))
|
(and (not (contains? *active-tx* raw)) (= :ignore *nested-tx*))
|
||||||
;; #245 do not lock when in c.j.j compatibility mode:
|
;; #245 do not lock when in c.j.j compatibility mode:
|
||||||
(binding [*active-tx* true]
|
(binding [*active-tx* (conj *active-tx* raw)]
|
||||||
(transact* this body-fn opts))
|
(transact* this body-fn opts))
|
||||||
(or (not *active-tx*) (= :allow *nested-tx*))
|
(or (not (contains? *active-tx* raw)) (= :allow *nested-tx*))
|
||||||
(locking this
|
(locking this
|
||||||
(binding [*active-tx* true]
|
(binding [*active-tx* (conj *active-tx* raw)]
|
||||||
(transact* this body-fn opts)))
|
(transact* this body-fn opts)))
|
||||||
(= :ignore *nested-tx*)
|
(= :ignore *nested-tx*)
|
||||||
(body-fn this)
|
(body-fn this)
|
||||||
|
|
@ -132,23 +141,15 @@
|
||||||
(throw (IllegalArgumentException.
|
(throw (IllegalArgumentException.
|
||||||
(str "*nested-tx* ("
|
(str "*nested-tx* ("
|
||||||
*nested-tx*
|
*nested-tx*
|
||||||
") was not :allow, :ignore, or :prohibit")))))
|
") was not :allow, :ignore, or :prohibit"))))))
|
||||||
javax.sql.DataSource
|
javax.sql.DataSource
|
||||||
(-transact [this body-fn opts]
|
(-transact [this body-fn opts]
|
||||||
(cond (or (not *active-tx*) (= :allow *nested-tx*))
|
|
||||||
(binding [*active-tx* true]
|
|
||||||
(with-open [con (p/get-connection this opts)]
|
(with-open [con (p/get-connection this opts)]
|
||||||
(transact* con body-fn opts)))
|
;; this connection is assumed unique so we do not need the active-tx check:
|
||||||
(= :ignore *nested-tx*)
|
(let [raw (raw-connection con)]
|
||||||
(with-open [con (p/get-connection this opts)]
|
;; we don't lock either, per #293:
|
||||||
(body-fn con))
|
(binding [*active-tx* (conj *active-tx* raw)]
|
||||||
(= :prohibit *nested-tx*)
|
(transact* con body-fn opts)))))
|
||||||
(throw (IllegalStateException. "Nested transactions are prohibited"))
|
|
||||||
:else
|
|
||||||
(throw (IllegalArgumentException.
|
|
||||||
(str "*nested-tx* ("
|
|
||||||
*nested-tx*
|
|
||||||
") was not :allow, :ignore, or :prohibit")))))
|
|
||||||
Object
|
Object
|
||||||
(-transact [this body-fn opts]
|
(-transact [this body-fn opts]
|
||||||
(p/-transact (p/get-datasource this) body-fn opts)))
|
(p/-transact (p/get-datasource this) body-fn opts)))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.connection-string-test
|
(ns next.jdbc.connection-string-test
|
||||||
"Tests for the main hash map spec to JDBC URL logic and the get-datasource
|
"Tests for the main hash map spec to JDBC URL logic and the get-datasource
|
||||||
|
|
@ -7,15 +7,17 @@
|
||||||
At some point, the datasource/connection tests should probably be extended
|
At some point, the datasource/connection tests should probably be extended
|
||||||
to accept EDN specs from an external source (environment variables?)."
|
to accept EDN specs from an external source (environment variables?)."
|
||||||
(:require [clojure.string :as str]
|
(:require [clojure.string :as str]
|
||||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
[lazytest.core :refer [around set-ns-context!]]
|
||||||
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
|
||||||
[next.jdbc.connection :as c]
|
[next.jdbc.connection :as c]
|
||||||
[next.jdbc.protocols :as p]
|
[next.jdbc.protocols :as p]
|
||||||
[next.jdbc.specs :as specs]
|
[next.jdbc.specs :as specs]
|
||||||
[next.jdbc.test-fixtures :refer [with-test-db db]]))
|
[next.jdbc.test-fixtures :refer [db with-test-db]])
|
||||||
|
(:import [java.util Properties]))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(use-fixtures :once with-test-db)
|
(set-ns-context! [(around [f] (with-test-db f))])
|
||||||
|
|
||||||
(specs/instrument)
|
(specs/instrument)
|
||||||
|
|
||||||
|
|
@ -39,3 +41,16 @@
|
||||||
(when (and user password)
|
(when (and user password)
|
||||||
(with-open [con (p/get-connection ds {})]
|
(with-open [con (p/get-connection ds {})]
|
||||||
(is (instance? java.sql.Connection con)))))))
|
(is (instance? java.sql.Connection con)))))))
|
||||||
|
|
||||||
|
(deftest property-tests
|
||||||
|
(is (string? (.getProperty ^Properties (#'c/as-properties {:foo [42]}) "foo")))
|
||||||
|
(is (string? (.get ^Properties (#'c/as-properties {:foo [42]}) "foo")))
|
||||||
|
(is (vector? (.get ^Properties (#'c/as-properties
|
||||||
|
{:foo [42]
|
||||||
|
:next.jdbc/as-is-properties [:foo]})
|
||||||
|
"foo")))
|
||||||
|
;; because .getProperty drops non-string values!
|
||||||
|
(is (nil? (.getProperty ^Properties (#'c/as-properties
|
||||||
|
{:foo [42]
|
||||||
|
:next.jdbc/as-is-properties [:foo]})
|
||||||
|
"foo"))))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.connection-test
|
(ns next.jdbc.connection-test
|
||||||
"Tests for the main hash map spec to JDBC URL logic and the get-datasource
|
"Tests for the main hash map spec to JDBC URL logic and the get-datasource
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
At some point, the datasource/connection tests should probably be extended
|
At some point, the datasource/connection tests should probably be extended
|
||||||
to accept EDN specs from an external source (environment variables?)."
|
to accept EDN specs from an external source (environment variables?)."
|
||||||
(:require [clojure.string :as str]
|
(:require [clojure.string :as str]
|
||||||
[clojure.test :refer [deftest is testing]]
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
|
||||||
[next.jdbc.connection :as c]
|
[next.jdbc.connection :as c]
|
||||||
[next.jdbc.protocols :as p])
|
[next.jdbc.protocols :as p])
|
||||||
(:import (com.zaxxer.hikari HikariDataSource)
|
(:import (com.zaxxer.hikari HikariDataSource)
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2020-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.datafy-test
|
(ns next.jdbc.datafy-test
|
||||||
"Tests for the datafy extensions over JDBC types."
|
"Tests for the datafy extensions over JDBC types."
|
||||||
(:require [clojure.datafy :as d]
|
(:require [clojure.datafy :as d]
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
[lazytest.core :refer [around set-ns-context!]]
|
||||||
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
|
||||||
[next.jdbc :as jdbc]
|
[next.jdbc :as jdbc]
|
||||||
[next.jdbc.datafy]
|
[next.jdbc.datafy]
|
||||||
[next.jdbc.result-set :as rs]
|
[next.jdbc.result-set :as rs]
|
||||||
[next.jdbc.specs :as specs]
|
[next.jdbc.specs :as specs]
|
||||||
[next.jdbc.test-fixtures
|
[next.jdbc.test-fixtures
|
||||||
:refer [with-test-db db ds
|
:refer [db derby? ds jtds? mysql? postgres? sqlite? with-test-db
|
||||||
derby? jtds? mysql? postgres? sqlite?]]))
|
xtdb?]]))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(use-fixtures :once with-test-db)
|
(set-ns-context! [(around [f] (with-test-db f))])
|
||||||
|
|
||||||
(specs/instrument)
|
(specs/instrument)
|
||||||
|
|
||||||
|
|
@ -83,6 +84,26 @@
|
||||||
:rowIdLifetime/exception))
|
:rowIdLifetime/exception))
|
||||||
(postgres?) (-> (disj :rowIdLifetime)
|
(postgres?) (-> (disj :rowIdLifetime)
|
||||||
(conj :rowIdLifetime/exception))
|
(conj :rowIdLifetime/exception))
|
||||||
|
(xtdb?) (-> (disj :clientInfoProperties
|
||||||
|
:defaultTransactionIsolation
|
||||||
|
:maxCatalogNameLength
|
||||||
|
:maxColumnNameLength
|
||||||
|
:maxCursorNameLength
|
||||||
|
:maxProcedureNameLength
|
||||||
|
:maxSchemaNameLength
|
||||||
|
:maxTableNameLength
|
||||||
|
:maxUserNameLength
|
||||||
|
:rowIdLifetime)
|
||||||
|
(conj :clientInfoProperties/exception
|
||||||
|
:defaultTransactionIsolation/exception
|
||||||
|
:maxCatalogNameLength/exception
|
||||||
|
:maxColumnNameLength/exception
|
||||||
|
:maxCursorNameLength/exception
|
||||||
|
:maxProcedureNameLength/exception
|
||||||
|
:maxSchemaNameLength/exception
|
||||||
|
:maxTableNameLength/exception
|
||||||
|
:maxUserNameLength/exception
|
||||||
|
:rowIdLifetime/exception))
|
||||||
(sqlite?) (-> (disj :clientInfoProperties :rowIdLifetime)
|
(sqlite?) (-> (disj :clientInfoProperties :rowIdLifetime)
|
||||||
(conj :clientInfoProperties/exception
|
(conj :clientInfoProperties/exception
|
||||||
:rowIdLifetime/exception)))
|
:rowIdLifetime/exception)))
|
||||||
|
|
@ -97,7 +118,8 @@
|
||||||
(let [data (d/datafy (.getMetaData con))]
|
(let [data (d/datafy (.getMetaData con))]
|
||||||
(doseq [k (cond-> #{:catalogs :clientInfoProperties :schemas :tableTypes :typeInfo}
|
(doseq [k (cond-> #{:catalogs :clientInfoProperties :schemas :tableTypes :typeInfo}
|
||||||
(jtds?) (disj :clientInfoProperties)
|
(jtds?) (disj :clientInfoProperties)
|
||||||
(sqlite?) (disj :clientInfoProperties))]
|
(sqlite?) (disj :clientInfoProperties)
|
||||||
|
(xtdb?) (disj :clientInfoProperties))]
|
||||||
(let [rs (d/nav data k nil)]
|
(let [rs (d/nav data k nil)]
|
||||||
(is (vector? rs))
|
(is (vector? rs))
|
||||||
(is (every? map? rs))))))))
|
(is (every? map? rs))))))))
|
||||||
|
|
@ -122,4 +144,5 @@
|
||||||
(.execute ps)
|
(.execute ps)
|
||||||
(.getResultSet ps)
|
(.getResultSet ps)
|
||||||
(.close ps)
|
(.close ps)
|
||||||
(.close con))
|
(.close con)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.date-time-test
|
(ns next.jdbc.date-time-test
|
||||||
"Date/time parameter auto-conversion tests.
|
"Date/time parameter auto-conversion tests.
|
||||||
|
|
@ -6,21 +6,22 @@
|
||||||
These tests contain no assertions. Without requiring `next.jdbc.date-time`
|
These tests contain no assertions. Without requiring `next.jdbc.date-time`
|
||||||
several of the `insert` operations would throw exceptions for some databases
|
several of the `insert` operations would throw exceptions for some databases
|
||||||
so the test here just checks those operations 'succeed'."
|
so the test here just checks those operations 'succeed'."
|
||||||
(:require [clojure.test :refer [deftest is testing use-fixtures]]
|
(:require [lazytest.core :refer [around set-ns-context!]]
|
||||||
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest]]
|
||||||
[next.jdbc :as jdbc]
|
[next.jdbc :as jdbc]
|
||||||
[next.jdbc.date-time] ; to extend SettableParameter to date/time
|
[next.jdbc.date-time] ; to extend SettableParameter to date/time
|
||||||
[next.jdbc.test-fixtures :refer [with-test-db db ds
|
[next.jdbc.test-fixtures :refer [with-test-db ds
|
||||||
mssql?]]
|
mssql? xtdb?]]
|
||||||
[next.jdbc.specs :as specs])
|
[next.jdbc.specs :as specs]))
|
||||||
(:import (java.sql ResultSet)))
|
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(use-fixtures :once with-test-db)
|
(set-ns-context! [(around [f] (with-test-db f))])
|
||||||
|
|
||||||
(specs/instrument)
|
(specs/instrument)
|
||||||
|
|
||||||
(deftest issue-73
|
(deftest issue-73
|
||||||
|
(when-not (xtdb?)
|
||||||
(try
|
(try
|
||||||
(jdbc/execute-one! (ds) ["drop table fruit_time"])
|
(jdbc/execute-one! (ds) ["drop table fruit_time"])
|
||||||
(catch Throwable _))
|
(catch Throwable _))
|
||||||
|
|
@ -46,4 +47,4 @@
|
||||||
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)])
|
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)])
|
||||||
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 2 (java.time.Instant/now)])
|
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 2 (java.time.Instant/now)])
|
||||||
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 3 (java.time.LocalDate/now)])
|
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 3 (java.time.LocalDate/now)])
|
||||||
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 4 (java.time.LocalDateTime/now)]))
|
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 4 (java.time.LocalDateTime/now)])))
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2020-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.default-options-test
|
(ns next.jdbc.default-options-test
|
||||||
"Stub test namespace for default options. Nothing can really be tested
|
"Stub test namespace for default options. Nothing can really be tested
|
||||||
at this level tho'..."
|
at this level tho'..."
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require [next.jdbc.default-options]))
|
||||||
[next.jdbc.default-options :refer :all]))
|
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
|
||||||
55
test/next/jdbc/defer_test.clj
Normal file
55
test/next/jdbc/defer_test.clj
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
;; copyright (c) 2024-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
|
(ns next.jdbc.defer-test
|
||||||
|
"The idea behind the next.jdbc.defer namespace is to provide a
|
||||||
|
way to defer the execution of a series of SQL statements until
|
||||||
|
a later time, but still provide a way for inserted keys to be
|
||||||
|
used in later SQL statements.
|
||||||
|
|
||||||
|
The principle is to provide a core subset of the next.jdbc
|
||||||
|
and next.jdbc.sql API that produces a data structure that
|
||||||
|
describes a series of SQL operations to be performed, that
|
||||||
|
are held in a dynamic var, and that can be executed at a
|
||||||
|
later time, in a transaction."
|
||||||
|
(:require [lazytest.core :refer [around set-ns-context!]]
|
||||||
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
|
||||||
|
[next.jdbc :as jdbc]
|
||||||
|
[next.jdbc.defer :as sut]
|
||||||
|
[next.jdbc.test-fixtures
|
||||||
|
:refer [ds with-test-db xtdb?]]))
|
||||||
|
|
||||||
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
(set-ns-context! [(around [f] (with-test-db f))])
|
||||||
|
|
||||||
|
(deftest basic-test
|
||||||
|
(when-not (xtdb?)
|
||||||
|
(testing "data structures"
|
||||||
|
(is (= [{:sql-p ["INSERT INTO foo (name) VALUES (?)" "Sean"]
|
||||||
|
:key-fn :GENERATED_KEY
|
||||||
|
:key :id
|
||||||
|
:opts {:key-fn :GENERATED_KEY :key :id}}]
|
||||||
|
@(sut/defer-ops
|
||||||
|
#(sut/insert! :foo {:name "Sean"} {:key-fn :GENERATED_KEY :key :id})))))
|
||||||
|
(testing "execution"
|
||||||
|
(let [effects (sut/with-deferred (ds)
|
||||||
|
(sut/insert! :fruit {:name "Mango"} {:key :test}))]
|
||||||
|
(is (= {:test 1} @effects))
|
||||||
|
(is (= 1 (count (jdbc/execute! (ds)
|
||||||
|
["select * from fruit where name = ?"
|
||||||
|
"Mango"])))))
|
||||||
|
(let [effects (sut/with-deferred (ds)
|
||||||
|
(sut/insert! :fruit {:name "Dragonfruit"} {:key :test})
|
||||||
|
(sut/update! :fruit {:cost 123} {:name "Dragonfruit"})
|
||||||
|
(sut/delete! :fruit {:name "Dragonfruit"}))]
|
||||||
|
(is (= {:test 1} @effects))
|
||||||
|
(is (= 0 (count (jdbc/execute! (ds)
|
||||||
|
["select * from fruit where name = ?"
|
||||||
|
"Dragonfruit"])))))
|
||||||
|
(let [effects (sut/with-deferred (ds)
|
||||||
|
(sut/insert! :fruit {:name "Grapefruit" :bad_column 0} {:key :test}))]
|
||||||
|
(is (= :failed (try @effects
|
||||||
|
(catch Exception _ :failed))))
|
||||||
|
(is (= 0 (count (jdbc/execute! (ds)
|
||||||
|
["select * from fruit where name = ?"
|
||||||
|
"Grapefruit"]))))))))
|
||||||
|
|
@ -1,23 +1,25 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.optional-test
|
(ns next.jdbc.optional-test
|
||||||
"Test namespace for the optional builder functions."
|
"Test namespace for the optional builder functions."
|
||||||
(:require [clojure.string :as str]
|
(:require [clojure.string :as str]
|
||||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
[lazytest.core :refer [around set-ns-context!]]
|
||||||
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
|
||||||
[next.jdbc.optional :as opt]
|
[next.jdbc.optional :as opt]
|
||||||
[next.jdbc.protocols :as p]
|
[next.jdbc.protocols :as p]
|
||||||
[next.jdbc.test-fixtures :refer [with-test-db ds column
|
[next.jdbc.test-fixtures :refer [col-kw column default-options ds index
|
||||||
default-options]])
|
with-test-db]])
|
||||||
(:import (java.sql ResultSet ResultSetMetaData)))
|
(:import
|
||||||
|
(java.sql ResultSet ResultSetMetaData)))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(use-fixtures :once with-test-db)
|
(set-ns-context! [(around [f] (with-test-db f))])
|
||||||
|
|
||||||
(deftest test-map-row-builder
|
(deftest test-map-row-builder
|
||||||
(testing "default row builder"
|
(testing "default row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 1]
|
[(str "select * from fruit where " (index) " = ?") 1]
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:builder-fn opt/as-maps))]
|
:builder-fn opt/as-maps))]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
|
|
@ -26,7 +28,7 @@
|
||||||
(is (= "Apple" ((column :FRUIT/NAME) row)))))
|
(is (= "Apple" ((column :FRUIT/NAME) row)))))
|
||||||
(testing "unqualified row builder"
|
(testing "unqualified row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 2]
|
[(str "select * from fruit where " (index) " = ?") 2]
|
||||||
{:builder-fn opt/as-unqualified-maps})]
|
{:builder-fn opt/as-unqualified-maps})]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (not (contains? row (column :COST))))
|
(is (not (contains? row (column :COST))))
|
||||||
|
|
@ -34,23 +36,23 @@
|
||||||
(is (= "Banana" ((column :NAME) row)))))
|
(is (= "Banana" ((column :NAME) row)))))
|
||||||
(testing "lower-case row builder"
|
(testing "lower-case row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 3]
|
[(str "select * from fruit where " (index) " = ?") 3]
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:builder-fn opt/as-lower-maps))]
|
:builder-fn opt/as-lower-maps))]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (not (contains? row :fruit/appearance)))
|
(is (not (contains? row (col-kw :fruit/appearance))))
|
||||||
(is (= 3 (:fruit/id row)))
|
(is (= 3 ((col-kw :fruit/id) row)))
|
||||||
(is (= "Peach" (:fruit/name row)))))
|
(is (= "Peach" ((col-kw :fruit/name) row)))))
|
||||||
(testing "unqualified lower-case row builder"
|
(testing "unqualified lower-case row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 4]
|
[(str "select * from fruit where " (index) " = ?") 4]
|
||||||
{:builder-fn opt/as-unqualified-lower-maps})]
|
{:builder-fn opt/as-unqualified-lower-maps})]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (= 4 (:id row)))
|
(is (= 4 ((col-kw :id) row)))
|
||||||
(is (= "Orange" (:name row)))))
|
(is (= "Orange" ((col-kw :name) row)))))
|
||||||
(testing "custom row builder"
|
(testing "custom row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 3]
|
[(str "select * from fruit where " (index) " = ?") 3]
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:builder-fn opt/as-modified-maps
|
:builder-fn opt/as-modified-maps
|
||||||
:label-fn str/lower-case
|
:label-fn str/lower-case
|
||||||
|
|
@ -61,13 +63,13 @@
|
||||||
(is (= "Peach" ((column :FRUIT/name) row))))))
|
(is (= "Peach" ((column :FRUIT/name) row))))))
|
||||||
|
|
||||||
(defn- default-column-reader
|
(defn- default-column-reader
|
||||||
[^ResultSet rs ^ResultSetMetaData rsmeta ^Integer i]
|
[^ResultSet rs ^ResultSetMetaData _ ^Integer i]
|
||||||
(.getObject rs i))
|
(.getObject rs i))
|
||||||
|
|
||||||
(deftest test-map-row-adapter
|
(deftest test-map-row-adapter
|
||||||
(testing "default row builder"
|
(testing "default row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 1]
|
[(str "select * from fruit where " (index) " = ?") 1]
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:builder-fn (opt/as-maps-adapter
|
:builder-fn (opt/as-maps-adapter
|
||||||
opt/as-maps
|
opt/as-maps
|
||||||
|
|
@ -78,7 +80,7 @@
|
||||||
(is (= "Apple" ((column :FRUIT/NAME) row)))))
|
(is (= "Apple" ((column :FRUIT/NAME) row)))))
|
||||||
(testing "unqualified row builder"
|
(testing "unqualified row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 2]
|
[(str "select * from fruit where " (index) " = ?") 2]
|
||||||
{:builder-fn (opt/as-maps-adapter
|
{:builder-fn (opt/as-maps-adapter
|
||||||
opt/as-unqualified-maps
|
opt/as-unqualified-maps
|
||||||
default-column-reader)})]
|
default-column-reader)})]
|
||||||
|
|
@ -88,27 +90,27 @@
|
||||||
(is (= "Banana" ((column :NAME) row)))))
|
(is (= "Banana" ((column :NAME) row)))))
|
||||||
(testing "lower-case row builder"
|
(testing "lower-case row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 3]
|
[(str "select * from fruit where " (index) " = ?") 3]
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:builder-fn (opt/as-maps-adapter
|
:builder-fn (opt/as-maps-adapter
|
||||||
opt/as-lower-maps
|
opt/as-lower-maps
|
||||||
default-column-reader)))]
|
default-column-reader)))]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (not (contains? row :fruit/appearance)))
|
(is (not (contains? row (col-kw :fruit/appearance))))
|
||||||
(is (= 3 (:fruit/id row)))
|
(is (= 3 ((col-kw :fruit/id) row)))
|
||||||
(is (= "Peach" (:fruit/name row)))))
|
(is (= "Peach" ((col-kw :fruit/name) row)))))
|
||||||
(testing "unqualified lower-case row builder"
|
(testing "unqualified lower-case row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 4]
|
[(str "select * from fruit where " (index) " = ?") 4]
|
||||||
{:builder-fn (opt/as-maps-adapter
|
{:builder-fn (opt/as-maps-adapter
|
||||||
opt/as-unqualified-lower-maps
|
opt/as-unqualified-lower-maps
|
||||||
default-column-reader)})]
|
default-column-reader)})]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (= 4 (:id row)))
|
(is (= 4 ((col-kw :id) row)))
|
||||||
(is (= "Orange" (:name row)))))
|
(is (= "Orange" ((col-kw :name) row)))))
|
||||||
(testing "custom row builder"
|
(testing "custom row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 3]
|
[(str "select * from fruit where " (index) " = ?") 3]
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:builder-fn (opt/as-maps-adapter
|
:builder-fn (opt/as-maps-adapter
|
||||||
opt/as-modified-maps
|
opt/as-modified-maps
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,75 @@
|
||||||
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2020-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.plan-test
|
(ns next.jdbc.plan-test
|
||||||
"Tests for the plan helpers."
|
"Tests for the plan helpers."
|
||||||
(:require [clojure.test :refer [deftest is use-fixtures]]
|
(:require [lazytest.core :refer [around]]
|
||||||
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest is]]
|
||||||
[next.jdbc.plan :as plan]
|
[next.jdbc.plan :as plan]
|
||||||
[next.jdbc.specs :as specs]
|
[next.jdbc.specs :as specs]
|
||||||
[next.jdbc.test-fixtures
|
[next.jdbc.test-fixtures
|
||||||
:refer [with-test-db ds]]
|
:refer [with-test-db ds col-kw index]]
|
||||||
[clojure.string :as str]))
|
[clojure.string :as str]))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
;; around each test because of the folding tests using 1,000 rows
|
|
||||||
(use-fixtures :each with-test-db)
|
|
||||||
|
|
||||||
(specs/instrument)
|
(specs/instrument)
|
||||||
|
|
||||||
(deftest select-one!-tests
|
(deftest select-one!-tests
|
||||||
(is (= {:id 1}
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(plan/select-one! (ds) [:id] ["select * from fruit order by id"])))
|
(is (= {(col-kw :id) 1}
|
||||||
|
(plan/select-one! (ds) [(col-kw :id)] [(str "select * from fruit order by " (index))])))
|
||||||
(is (= 1
|
(is (= 1
|
||||||
(plan/select-one! (ds) :id ["select * from fruit order by id"])))
|
(plan/select-one! (ds) (col-kw :id) [(str "select * from fruit order by " (index))])))
|
||||||
(is (= "Banana"
|
(is (= "Banana"
|
||||||
(plan/select-one! (ds) :name ["select * from fruit where id = ?" 2])))
|
(plan/select-one! (ds) :name [(str "select * from fruit where " (index) " = ?") 2])))
|
||||||
(is (= [1 "Apple"]
|
(is (= [1 "Apple"]
|
||||||
(plan/select-one! (ds) (juxt :id :name)
|
(plan/select-one! (ds) (juxt (col-kw :id) :name)
|
||||||
["select * from fruit order by id"])))
|
[(str "select * from fruit order by " (index))])))
|
||||||
(is (= {:id 1 :name "Apple"}
|
(is (= {(col-kw :id) 1 :name "Apple"}
|
||||||
(plan/select-one! (ds) #(select-keys % [:id :name])
|
(plan/select-one! (ds) #(select-keys % [(col-kw :id) :name])
|
||||||
["select * from fruit order by id"]))))
|
[(str "select * from fruit order by " (index))]))))
|
||||||
|
|
||||||
(deftest select-vector-tests
|
(deftest select-vector-tests
|
||||||
(is (= [{:id 1} {:id 2} {:id 3} {:id 4}]
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(plan/select! (ds) [:id] ["select * from fruit order by id"])))
|
(is (= [{(col-kw :id) 1} {(col-kw :id) 2} {(col-kw :id) 3} {(col-kw :id) 4}]
|
||||||
|
(plan/select! (ds) [(col-kw :id)] [(str "select * from fruit order by " (index))])))
|
||||||
(is (= [1 2 3 4]
|
(is (= [1 2 3 4]
|
||||||
(plan/select! (ds) :id ["select * from fruit order by id"])))
|
(plan/select! (ds) (col-kw :id) [(str "select * from fruit order by " (index))])))
|
||||||
(is (= ["Banana"]
|
(is (= ["Banana"]
|
||||||
(plan/select! (ds) :name ["select * from fruit where id = ?" 2])))
|
(plan/select! (ds) :name [(str "select * from fruit where " (index) " = ?") 2])))
|
||||||
(is (= [[2 "Banana"]]
|
(is (= [[2 "Banana"]]
|
||||||
(plan/select! (ds) (juxt :id :name)
|
(plan/select! (ds) (juxt (col-kw :id) :name)
|
||||||
["select * from fruit where id = ?" 2])))
|
[(str "select * from fruit where " (index) " = ?") 2])))
|
||||||
(is (= [{:id 2 :name "Banana"}]
|
(is (= [{(col-kw :id) 2 :name "Banana"}]
|
||||||
(plan/select! (ds) [:id :name]
|
(plan/select! (ds) [(col-kw :id) :name]
|
||||||
["select * from fruit where id = ?" 2]))))
|
[(str "select * from fruit where " (index) " = ?") 2]))))
|
||||||
|
|
||||||
(deftest select-set-tests
|
(deftest select-set-tests
|
||||||
(is (= #{{:id 1} {:id 2} {:id 3} {:id 4}}
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(plan/select! (ds) [:id] ["select * from fruit order by id"]
|
(is (= #{{(col-kw :id) 1} {(col-kw :id) 2} {(col-kw :id) 3} {(col-kw :id) 4}}
|
||||||
|
(plan/select! (ds) [(col-kw :id)] [(str "select * from fruit order by " (index))]
|
||||||
{:into #{}})))
|
{:into #{}})))
|
||||||
(is (= #{1 2 3 4}
|
(is (= #{1 2 3 4}
|
||||||
(plan/select! (ds) :id ["select * from fruit order by id"]
|
(plan/select! (ds) (col-kw :id) [(str "select * from fruit order by " (index))]
|
||||||
{:into #{}}))))
|
{:into #{}}))))
|
||||||
|
|
||||||
(deftest select-map-tests
|
(deftest select-map-tests
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(is (= {1 "Apple", 2 "Banana", 3 "Peach", 4 "Orange"}
|
(is (= {1 "Apple", 2 "Banana", 3 "Peach", 4 "Orange"}
|
||||||
(plan/select! (ds) (juxt :id :name) ["select * from fruit order by id"]
|
(plan/select! (ds) (juxt (col-kw :id) :name) [(str "select * from fruit order by " (index))]
|
||||||
{:into {}}))))
|
{:into {}}))))
|
||||||
|
|
||||||
(deftest select-issue-227
|
(deftest select-issue-227
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(is (= ["Apple"]
|
(is (= ["Apple"]
|
||||||
(plan/select! (ds) :name ["select * from fruit where id = ?" 1]
|
(plan/select! (ds) :name [(str "select * from fruit where " (index) " = ?") 1]
|
||||||
{:column-fn #(str/replace % "-" "_")})))
|
{:column-fn #(str/replace % "-" "_")})))
|
||||||
(is (= ["Apple"]
|
(is (= ["Apple"]
|
||||||
(plan/select! (ds) :foo/name ["select * from fruit where id = ?" 1]
|
(plan/select! (ds) :foo/name [(str "select * from fruit where " (index) " = ?") 1]
|
||||||
{:column-fn #(str/replace % "-" "_")})))
|
{:column-fn #(str/replace % "-" "_")})))
|
||||||
(is (= ["Apple"]
|
(is (= ["Apple"]
|
||||||
(plan/select! (ds) #(get % "name") ["select * from fruit where id = ?" 1]
|
(plan/select! (ds) #(get % "name") [(str "select * from fruit where " (index) " = ?") 1]
|
||||||
{:column-fn #(str/replace % "-" "_")})))
|
{:column-fn #(str/replace % "-" "_")})))
|
||||||
(is (= [["Apple"]]
|
(is (= [["Apple"]]
|
||||||
(plan/select! (ds) (juxt :name) ["select * from fruit where id = ?" 1]
|
(plan/select! (ds) (juxt :name) [(str "select * from fruit where " (index) " = ?") 1]
|
||||||
{:column-fn #(str/replace % "-" "_")}))))
|
{:column-fn #(str/replace % "-" "_")}))))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.prepare-test
|
(ns next.jdbc.prepare-test
|
||||||
"Stub test namespace for PreparedStatement creation etc.
|
"Stub test namespace for PreparedStatement creation etc.
|
||||||
|
|
@ -8,20 +8,22 @@
|
||||||
|
|
||||||
The tests for the deprecated version of `execute-batch!` are here
|
The tests for the deprecated version of `execute-batch!` are here
|
||||||
as a guard against regressions."
|
as a guard against regressions."
|
||||||
(:require [clojure.test :refer [deftest is testing use-fixtures]]
|
(:require [lazytest.core :refer [around set-ns-context!]]
|
||||||
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
|
||||||
[next.jdbc :as jdbc]
|
[next.jdbc :as jdbc]
|
||||||
[next.jdbc.test-fixtures
|
[next.jdbc.test-fixtures
|
||||||
:refer [with-test-db ds jtds? mssql? sqlite?]]
|
:refer [with-test-db ds jtds? mssql? sqlite? xtdb?]]
|
||||||
[next.jdbc.prepare :as prep]
|
[next.jdbc.prepare :as prep]
|
||||||
[next.jdbc.specs :as specs]))
|
[next.jdbc.specs :as specs]))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(use-fixtures :once with-test-db)
|
(set-ns-context! [(around [f] (with-test-db f))])
|
||||||
|
|
||||||
(specs/instrument)
|
(specs/instrument)
|
||||||
|
|
||||||
(deftest execute-batch-tests
|
(deftest execute-batch-tests
|
||||||
|
(when-not (xtdb?)
|
||||||
(testing "simple batch insert"
|
(testing "simple batch insert"
|
||||||
(is (= [1 1 1 1 1 1 1 1 1 13]
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
(jdbc/with-transaction [t (ds) {:rollback-only true}]
|
(jdbc/with-transaction [t (ds) {:rollback-only true}]
|
||||||
|
|
@ -120,4 +122,4 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
;; Derby and SQLite only return one generated key per batch so there
|
;; Derby and SQLite only return one generated key per batch so there
|
||||||
;; are only three keys, plus the overall count here:
|
;; are only three keys, plus the overall count here:
|
||||||
(is (< 3 (count results))))
|
(is (< 3 (count results))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))))
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.protocols-test
|
(ns next.jdbc.protocols-test
|
||||||
"Stub test namespace for low-level protocols. Nothing can really be tested
|
"Stub test namespace for low-level protocols. Nothing can really be tested
|
||||||
at this level tho'..."
|
at this level tho'..."
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require [next.jdbc.protocols]))
|
||||||
[next.jdbc.protocols :refer :all]))
|
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,30 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.quoted-test
|
(ns next.jdbc.quoted-test
|
||||||
"Basic tests for quoting strategies. These are also tested indirectly
|
"Basic tests for quoting strategies. These are also tested indirectly
|
||||||
via the next.jdbc.sql tests."
|
via the next.jdbc.sql tests."
|
||||||
(:require [clojure.test :refer [deftest are testing]]
|
(:require [lazytest.core :refer [defdescribe describe it expect]]
|
||||||
[next.jdbc.quoted :refer [ansi mysql sql-server oracle postgres
|
[next.jdbc.quoted :refer [ansi mysql sql-server oracle postgres
|
||||||
schema]]))
|
schema]]))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(deftest basic-quoting
|
(def ^:private quote-fns [ansi mysql sql-server oracle postgres])
|
||||||
(are [quote-fn quoted] (= (quote-fn "x") quoted)
|
|
||||||
ansi "\"x\""
|
|
||||||
mysql "`x`"
|
|
||||||
sql-server "[x]"
|
|
||||||
oracle "\"x\""
|
|
||||||
postgres "\"x\""))
|
|
||||||
|
|
||||||
(deftest schema-quoting
|
(defdescribe quoted-functionality
|
||||||
(testing "verify non-schema behavior"
|
(describe "base quoting"
|
||||||
(are [quote-fn quoted] (= (quote-fn "x.y") quoted)
|
(it "should correctly quote simple names"
|
||||||
ansi "\"x.y\""
|
(doseq [[f e] (map vector quote-fns
|
||||||
mysql "`x.y`"
|
["\"x\"" "`x`" "[x]" "\"x\"" "\"x\""])]
|
||||||
sql-server "[x.y]"
|
(expect (= (f "x") e)))))
|
||||||
oracle "\"x.y\""
|
(describe "dotted name quoting"
|
||||||
postgres "\"x.y\""))
|
(describe "basic quoting"
|
||||||
(testing "verify schema behavior"
|
(it "should quote dotted names 'as-is'"
|
||||||
(are [quote-fn quoted] (= ((schema quote-fn) "x.y") quoted)
|
(doseq [[f e] (map vector quote-fns
|
||||||
ansi "\"x\".\"y\""
|
["\"x.y\"" "`x.y`" "[x.y]" "\"x.y\"" "\"x.y\""])]
|
||||||
mysql "`x`.`y`"
|
(expect (= (f "x.y") e)))))
|
||||||
sql-server "[x].[y]"
|
(describe "schema quoting"
|
||||||
oracle "\"x\".\"y\""
|
(it "should split and quote dotted names with schema"
|
||||||
postgres "\"x\".\"y\"")))
|
(doseq [[f e] (map vector quote-fns
|
||||||
|
["\"x\".\"y\"" "`x`.`y`" "[x].[y]" "\"x\".\"y\"" "\"x\".\"y\""])]
|
||||||
|
(expect (= ((schema f) "x.y") e)))))))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.result-set-test
|
(ns next.jdbc.result-set-test
|
||||||
"Test namespace for the result set functions.
|
"Test namespace for the result set functions.
|
||||||
|
|
@ -8,18 +8,19 @@
|
||||||
(:require [clojure.core.protocols :as core-p]
|
(:require [clojure.core.protocols :as core-p]
|
||||||
[clojure.datafy :as d]
|
[clojure.datafy :as d]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
[lazytest.core :refer [around set-ns-context!]]
|
||||||
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
|
||||||
[next.jdbc.protocols :as p]
|
[next.jdbc.protocols :as p]
|
||||||
[next.jdbc.result-set :as rs]
|
[next.jdbc.result-set :as rs]
|
||||||
[next.jdbc.specs :as specs]
|
[next.jdbc.specs :as specs]
|
||||||
[next.jdbc.test-fixtures :refer [with-test-db ds column
|
[next.jdbc.test-fixtures :refer [with-test-db ds column index col-kw
|
||||||
default-options
|
default-options
|
||||||
derby? mssql? mysql? postgres?]])
|
derby? mssql? mysql? postgres? xtdb?]])
|
||||||
(:import (java.sql ResultSet ResultSetMetaData)))
|
(:import (java.sql ResultSet ResultSetMetaData)))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(use-fixtures :once with-test-db)
|
(set-ns-context! [(around [f] (with-test-db f))])
|
||||||
|
|
||||||
(specs/instrument)
|
(specs/instrument)
|
||||||
|
|
||||||
|
|
@ -27,7 +28,9 @@
|
||||||
(testing "default schema"
|
(testing "default schema"
|
||||||
(let [connectable (ds)
|
(let [connectable (ds)
|
||||||
test-row (rs/datafiable-row {:TABLE/FRUIT_ID 1} connectable
|
test-row (rs/datafiable-row {:TABLE/FRUIT_ID 1} connectable
|
||||||
(default-options))
|
(cond-> (default-options)
|
||||||
|
(xtdb?)
|
||||||
|
(assoc :schema-opts {:pk "_id"})))
|
||||||
data (d/datafy test-row)
|
data (d/datafy test-row)
|
||||||
v (get data :TABLE/FRUIT_ID)]
|
v (get data :TABLE/FRUIT_ID)]
|
||||||
;; check datafication is sane
|
;; check datafication is sane
|
||||||
|
|
@ -40,7 +43,10 @@
|
||||||
(let [connectable (ds)
|
(let [connectable (ds)
|
||||||
test-row (rs/datafiable-row {:foo/bar 2} connectable
|
test-row (rs/datafiable-row {:foo/bar 2} connectable
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:schema {:foo/bar :fruit/id}))
|
:schema {:foo/bar
|
||||||
|
(if (xtdb?)
|
||||||
|
:fruit/_id
|
||||||
|
:fruit/id)}))
|
||||||
data (d/datafy test-row)
|
data (d/datafy test-row)
|
||||||
v (get data :foo/bar)]
|
v (get data :foo/bar)]
|
||||||
;; check datafication is sane
|
;; check datafication is sane
|
||||||
|
|
@ -53,7 +59,10 @@
|
||||||
(let [connectable (ds)
|
(let [connectable (ds)
|
||||||
test-row (rs/datafiable-row {:foo/bar 3} connectable
|
test-row (rs/datafiable-row {:foo/bar 3} connectable
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:schema {:foo/bar [:fruit/id]}))
|
:schema {:foo/bar
|
||||||
|
[(if (xtdb?)
|
||||||
|
:fruit/_id
|
||||||
|
:fruit/id)]}))
|
||||||
data (d/datafy test-row)
|
data (d/datafy test-row)
|
||||||
v (get data :foo/bar)]
|
v (get data :foo/bar)]
|
||||||
;; check datafication is sane
|
;; check datafication is sane
|
||||||
|
|
@ -67,7 +76,7 @@
|
||||||
(let [connectable (ds)
|
(let [connectable (ds)
|
||||||
test-row (rs/datafiable-row {:foo/bar 2} connectable
|
test-row (rs/datafiable-row {:foo/bar 2} connectable
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:schema {:foo/bar [:fruit :id]}))
|
:schema {:foo/bar [:fruit (col-kw :id)]}))
|
||||||
data (d/datafy test-row)
|
data (d/datafy test-row)
|
||||||
v (get data :foo/bar)]
|
v (get data :foo/bar)]
|
||||||
;; check datafication is sane
|
;; check datafication is sane
|
||||||
|
|
@ -79,7 +88,7 @@
|
||||||
(let [connectable (ds)
|
(let [connectable (ds)
|
||||||
test-row (rs/datafiable-row {:foo/bar 3} connectable
|
test-row (rs/datafiable-row {:foo/bar 3} connectable
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:schema {:foo/bar [:fruit :id :many]}))
|
:schema {:foo/bar [:fruit (col-kw :id) :many]}))
|
||||||
data (d/datafy test-row)
|
data (d/datafy test-row)
|
||||||
v (get data :foo/bar)]
|
v (get data :foo/bar)]
|
||||||
;; check datafication is sane
|
;; check datafication is sane
|
||||||
|
|
@ -93,7 +102,7 @@
|
||||||
(deftest test-map-row-builder
|
(deftest test-map-row-builder
|
||||||
(testing "default row builder"
|
(testing "default row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 1]
|
[(str "select * from fruit where " (index) " = ?") 1]
|
||||||
(default-options))]
|
(default-options))]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (contains? row (column :FRUIT/GRADE)))
|
(is (contains? row (column :FRUIT/GRADE)))
|
||||||
|
|
@ -101,7 +110,7 @@
|
||||||
(is (= 1 ((column :FRUIT/ID) row)))
|
(is (= 1 ((column :FRUIT/ID) row)))
|
||||||
(is (= "Apple" ((column :FRUIT/NAME) row))))
|
(is (= "Apple" ((column :FRUIT/NAME) row))))
|
||||||
(let [rs (p/-execute-all (ds)
|
(let [rs (p/-execute-all (ds)
|
||||||
["select * from fruit order by id"]
|
[(str "select * from fruit order by " (index))]
|
||||||
(default-options))]
|
(default-options))]
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
(is (= 1 ((column :FRUIT/ID) (first rs))))
|
(is (= 1 ((column :FRUIT/ID) (first rs))))
|
||||||
|
|
@ -110,7 +119,7 @@
|
||||||
(is (= "Orange" ((column :FRUIT/NAME) (last rs))))))
|
(is (= "Orange" ((column :FRUIT/NAME) (last rs))))))
|
||||||
(testing "unqualified row builder"
|
(testing "unqualified row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 2]
|
[(str "select * from fruit where " (index) " = ?") 2]
|
||||||
{:builder-fn rs/as-unqualified-maps})]
|
{:builder-fn rs/as-unqualified-maps})]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (contains? row (column :COST)))
|
(is (contains? row (column :COST)))
|
||||||
|
|
@ -119,34 +128,35 @@
|
||||||
(is (= "Banana" ((column :NAME) row)))))
|
(is (= "Banana" ((column :NAME) row)))))
|
||||||
(testing "lower-case row builder"
|
(testing "lower-case row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 3]
|
[(str "select * from fruit where " (index) " = ?") 3]
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:builder-fn rs/as-lower-maps))]
|
:builder-fn rs/as-lower-maps))]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (contains? row :fruit/appearance))
|
(is (contains? row (col-kw :fruit/appearance)))
|
||||||
(is (nil? (:fruit/appearance row)))
|
(is (nil? ((col-kw :fruit/appearance) row)))
|
||||||
(is (= 3 (:fruit/id row)))
|
(is (= 3 ((col-kw :fruit/id) row)))
|
||||||
(is (= "Peach" (:fruit/name row)))))
|
(is (= "Peach" ((col-kw :fruit/name) row)))))
|
||||||
(testing "unqualified lower-case row builder"
|
(testing "unqualified lower-case row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 4]
|
[(str "select * from fruit where " (index) " = ?") 4]
|
||||||
{:builder-fn rs/as-unqualified-lower-maps})]
|
{:builder-fn rs/as-unqualified-lower-maps})]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (= 4 (:id row)))
|
(is (= 4 ((col-kw :id) row)))
|
||||||
(is (= "Orange" (:name row)))))
|
(is (= "Orange" ((col-kw :name) row)))))
|
||||||
(testing "kebab-case row builder"
|
(testing "kebab-case row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select id,name,appearance as looks_like from fruit where id = ?" 3]
|
[(str "select " (index) ",name,appearance as looks_like from fruit where " (index) " = ?") 3]
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:builder-fn rs/as-kebab-maps))]
|
:builder-fn rs/as-kebab-maps))]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (contains? row :fruit/looks-like))
|
(is (contains? row (col-kw :fruit/looks-like)))
|
||||||
(is (nil? (:fruit/looks-like row)))
|
(is (nil? ((col-kw :fruit/looks-like) row)))
|
||||||
(is (= 3 (:fruit/id row)))
|
;; kebab-case strips leading _ from _id (XTDB):
|
||||||
(is (= "Peach" (:fruit/name row)))))
|
(is (= 3 ((if (xtdb?) :id :fruit/id) row)))
|
||||||
|
(is (= "Peach" ((col-kw :fruit/name) row)))))
|
||||||
(testing "unqualified kebab-case row builder"
|
(testing "unqualified kebab-case row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select id,name,appearance as looks_like from fruit where id = ?" 4]
|
[(str "select " (index) ",name,appearance as looks_like from fruit where " (index) " = ?") 4]
|
||||||
{:builder-fn rs/as-unqualified-kebab-maps})]
|
{:builder-fn rs/as-unqualified-kebab-maps})]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (contains? row :looks-like))
|
(is (contains? row :looks-like))
|
||||||
|
|
@ -155,7 +165,7 @@
|
||||||
(is (= "Orange" (:name row)))))
|
(is (= "Orange" (:name row)))))
|
||||||
(testing "custom row builder 1"
|
(testing "custom row builder 1"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select fruit.*, id + 100 as newid from fruit where id = ?" 3]
|
[(str "select fruit.*, " (index) " + 100 as newid from fruit where " (index) " = ?") 3]
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:builder-fn rs/as-modified-maps
|
:builder-fn rs/as-modified-maps
|
||||||
:label-fn str/lower-case
|
:label-fn str/lower-case
|
||||||
|
|
@ -168,7 +178,7 @@
|
||||||
(is (= "Peach" ((column :FRUIT/name) row)))))
|
(is (= "Peach" ((column :FRUIT/name) row)))))
|
||||||
(testing "custom row builder 2"
|
(testing "custom row builder 2"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select fruit.*, id + 100 as newid from fruit where id = ?" 3]
|
[(str "select fruit.*, " (index) " + 100 as newid from fruit where " (index) " = ?") 3]
|
||||||
(assoc (default-options)
|
(assoc (default-options)
|
||||||
:builder-fn rs/as-modified-maps
|
:builder-fn rs/as-modified-maps
|
||||||
:label-fn str/lower-case
|
:label-fn str/lower-case
|
||||||
|
|
@ -176,12 +186,12 @@
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (contains? row :vegetable/appearance))
|
(is (contains? row :vegetable/appearance))
|
||||||
(is (nil? (:vegetable/appearance row)))
|
(is (nil? (:vegetable/appearance row)))
|
||||||
(is (= 3 (:vegetable/id row)))
|
(is (= 3 ((if (xtdb?) :vegetable/_id :vegetable/id) row)))
|
||||||
(is (= 103 (:vegetable/newid row))) ; constant qualifier here
|
(is (= 103 (:vegetable/newid row))) ; constant qualifier here
|
||||||
(is (= "Peach" (:vegetable/name row)))))
|
(is (= "Peach" (:vegetable/name row)))))
|
||||||
(testing "adapted row builder"
|
(testing "adapted row builder"
|
||||||
(let [row (p/-execute-one (ds)
|
(let [row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 3]
|
[(str "select * from fruit where " (index) " = ?") 3]
|
||||||
(assoc
|
(assoc
|
||||||
(default-options)
|
(default-options)
|
||||||
:builder-fn (rs/as-maps-adapter
|
:builder-fn (rs/as-maps-adapter
|
||||||
|
|
@ -207,7 +217,7 @@
|
||||||
(fn [^ResultSet rs _ ^Integer i]
|
(fn [^ResultSet rs _ ^Integer i]
|
||||||
(.getObject rs i)))
|
(.getObject rs i)))
|
||||||
row (p/-execute-one (ds)
|
row (p/-execute-one (ds)
|
||||||
["select * from fruit where id = ?" 3]
|
[(str "select * from fruit where " (index) " = ?") 3]
|
||||||
(assoc
|
(assoc
|
||||||
(default-options)
|
(default-options)
|
||||||
:builder-fn (rs/as-maps-adapter
|
:builder-fn (rs/as-maps-adapter
|
||||||
|
|
@ -236,7 +246,7 @@
|
||||||
(testing "row-numbers on bare abstraction"
|
(testing "row-numbers on bare abstraction"
|
||||||
(is (= [1 2 3]
|
(is (= [1 2 3]
|
||||||
(into [] (map rs/row-number)
|
(into [] (map rs/row-number)
|
||||||
(p/-execute (ds) ["select * from fruit where id < ?" 4]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
|
||||||
;; we do not need a real builder here...
|
;; we do not need a real builder here...
|
||||||
(cond-> {:builder-fn (constantly nil)}
|
(cond-> {:builder-fn (constantly nil)}
|
||||||
(derby?)
|
(derby?)
|
||||||
|
|
@ -247,7 +257,7 @@
|
||||||
(is (= [1 2 3]
|
(is (= [1 2 3]
|
||||||
(into [] (comp (map #(rs/datafiable-row % (ds) {}))
|
(into [] (comp (map #(rs/datafiable-row % (ds) {}))
|
||||||
(map rs/row-number))
|
(map rs/row-number))
|
||||||
(p/-execute (ds) ["select * from fruit where id < ?" 4]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
|
||||||
;; ...but datafiable-row requires a real builder
|
;; ...but datafiable-row requires a real builder
|
||||||
(cond-> {:builder-fn rs/as-arrays}
|
(cond-> {:builder-fn rs/as-arrays}
|
||||||
(derby?)
|
(derby?)
|
||||||
|
|
@ -257,7 +267,7 @@
|
||||||
|
|
||||||
(deftest test-column-names
|
(deftest test-column-names
|
||||||
(testing "column-names on bare abstraction"
|
(testing "column-names on bare abstraction"
|
||||||
(is (= #{"id" "appearance" "grade" "cost" "name"}
|
(is (= #{(index) "appearance" "grade" "cost" "name"}
|
||||||
(reduce (fn [_ row]
|
(reduce (fn [_ row]
|
||||||
(-> row
|
(-> row
|
||||||
(->> (rs/column-names)
|
(->> (rs/column-names)
|
||||||
|
|
@ -265,11 +275,11 @@
|
||||||
(set)
|
(set)
|
||||||
(reduced))))
|
(reduced))))
|
||||||
nil
|
nil
|
||||||
(p/-execute (ds) ["select * from fruit where id < ?" 4]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
|
||||||
;; column-names require a real builder
|
;; column-names require a real builder
|
||||||
{:builder-fn rs/as-arrays})))))
|
{:builder-fn rs/as-arrays})))))
|
||||||
(testing "column-names on realized row"
|
(testing "column-names on realized row"
|
||||||
(is (= #{"id" "appearance" "grade" "cost" "name"}
|
(is (= #{(index) "appearance" "grade" "cost" "name"}
|
||||||
(reduce (fn [_ row]
|
(reduce (fn [_ row]
|
||||||
(-> row
|
(-> row
|
||||||
(rs/datafiable-row (ds) {})
|
(rs/datafiable-row (ds) {})
|
||||||
|
|
@ -278,7 +288,7 @@
|
||||||
(set)
|
(set)
|
||||||
(reduced))))
|
(reduced))))
|
||||||
nil
|
nil
|
||||||
(p/-execute (ds) ["select * from fruit where id < ?" 4]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
|
||||||
{:builder-fn rs/as-arrays}))))))
|
{:builder-fn rs/as-arrays}))))))
|
||||||
|
|
||||||
(deftest test-over-partition-all
|
(deftest test-over-partition-all
|
||||||
|
|
@ -299,31 +309,31 @@
|
||||||
(testing "no row builder is used"
|
(testing "no row builder is used"
|
||||||
(is (= [true]
|
(is (= [true]
|
||||||
(into [] (map map?) ; it looks like a real map now
|
(into [] (map map?) ; it looks like a real map now
|
||||||
(p/-execute (ds) ["select * from fruit where id = ?" 1]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
|
||||||
{:builder-fn (constantly nil)}))))
|
{:builder-fn (constantly nil)}))))
|
||||||
(is (= ["Apple"]
|
(is (= ["Apple"]
|
||||||
(into [] (map :name) ; keyword selection works
|
(into [] (map :name) ; keyword selection works
|
||||||
(p/-execute (ds) ["select * from fruit where id = ?" 1]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
|
||||||
{:builder-fn (constantly nil)}))))
|
{:builder-fn (constantly nil)}))))
|
||||||
(is (= [[2 [:name "Banana"]]]
|
(is (= [[2 [:name "Banana"]]]
|
||||||
(into [] (map (juxt #(get % "id") ; get by string key works
|
(into [] (map (juxt #(get % (index)) ; get by string key works
|
||||||
#(find % :name))) ; get MapEntry works
|
#(find % :name))) ; get MapEntry works
|
||||||
(p/-execute (ds) ["select * from fruit where id = ?" 2]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 2]
|
||||||
{:builder-fn (constantly nil)}))))
|
{:builder-fn (constantly nil)}))))
|
||||||
(is (= [{:id 3 :name "Peach"}]
|
(is (= [{(col-kw :id) 3 :name "Peach"}]
|
||||||
(into [] (map #(select-keys % [:id :name])) ; select-keys works
|
(into [] (map #(select-keys % [(col-kw :id) :name])) ; select-keys works
|
||||||
(p/-execute (ds) ["select * from fruit where id = ?" 3]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 3]
|
||||||
{:builder-fn (constantly nil)}))))
|
{:builder-fn (constantly nil)}))))
|
||||||
(is (= [[:orange 4]]
|
(is (= [[:orange 4]]
|
||||||
(into [] (map #(vector (if (contains? % :name) ; contains works
|
(into [] (map #(vector (if (contains? % :name) ; contains works
|
||||||
(keyword (str/lower-case (:name %)))
|
(keyword (str/lower-case (:name %)))
|
||||||
:unnamed)
|
:unnamed)
|
||||||
(get % :id 0))) ; get with not-found works
|
(get % (col-kw :id) 0))) ; get with not-found works
|
||||||
(p/-execute (ds) ["select * from fruit where id = ?" 4]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 4]
|
||||||
{:builder-fn (constantly nil)}))))
|
{:builder-fn (constantly nil)}))))
|
||||||
(is (= [{}]
|
(is (= [{}]
|
||||||
(into [] (map empty) ; return empty map without building
|
(into [] (map empty) ; return empty map without building
|
||||||
(p/-execute (ds) ["select * from fruit where id = ?" 1]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
|
||||||
{:builder-fn (constantly nil)})))))
|
{:builder-fn (constantly nil)})))))
|
||||||
(testing "count does not build a map"
|
(testing "count does not build a map"
|
||||||
(let [count-builder (fn [_1 _2]
|
(let [count-builder (fn [_1 _2]
|
||||||
|
|
@ -331,7 +341,7 @@
|
||||||
(column-count [_] 13)))]
|
(column-count [_] 13)))]
|
||||||
(is (= [13]
|
(is (= [13]
|
||||||
(into [] (map count) ; count relies on columns, not row fields
|
(into [] (map count) ; count relies on columns, not row fields
|
||||||
(p/-execute (ds) ["select * from fruit where id = ?" 1]
|
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
|
||||||
{:builder-fn count-builder}))))))
|
{:builder-fn count-builder}))))))
|
||||||
(testing "assoc, dissoc, cons, seq, and = build maps"
|
(testing "assoc, dissoc, cons, seq, and = build maps"
|
||||||
(is (map? (reduce (fn [_ row] (reduced (assoc row :x 1)))
|
(is (map? (reduce (fn [_ row] (reduced (assoc row :x 1)))
|
||||||
|
|
@ -417,7 +427,7 @@
|
||||||
(defn fruit-builder [^ResultSet rs ^ResultSetMetaData rsmeta]
|
(defn fruit-builder [^ResultSet rs ^ResultSetMetaData rsmeta]
|
||||||
(reify
|
(reify
|
||||||
rs/RowBuilder
|
rs/RowBuilder
|
||||||
(->row [_] (->Fruit (.getObject rs "id")
|
(->row [_] (->Fruit (.getObject rs ^String (index))
|
||||||
(.getObject rs "name")
|
(.getObject rs "name")
|
||||||
(.getObject rs "appearance")
|
(.getObject rs "appearance")
|
||||||
(.getObject rs "cost")
|
(.getObject rs "cost")
|
||||||
|
|
@ -434,7 +444,7 @@
|
||||||
(valAt [this k] (get this k nil))
|
(valAt [this k] (get this k nil))
|
||||||
(valAt [this k not-found]
|
(valAt [this k not-found]
|
||||||
(case k
|
(case k
|
||||||
:cols [:id :name :appearance :cost :grade]
|
:cols [(col-kw :id) :name :appearance :cost :grade]
|
||||||
:rsmeta rsmeta
|
:rsmeta rsmeta
|
||||||
not-found))))
|
not-found))))
|
||||||
|
|
||||||
|
|
@ -467,7 +477,7 @@
|
||||||
metadata))))
|
metadata))))
|
||||||
|
|
||||||
(deftest clob-reading
|
(deftest clob-reading
|
||||||
(when-not (or (mssql?) (mysql?) (postgres?)) ; no clob in these
|
(when-not (or (mssql?) (mysql?) (postgres?) (xtdb?)) ; no clob in these
|
||||||
(with-open [con (p/get-connection (ds) {})]
|
(with-open [con (p/get-connection (ds) {})]
|
||||||
(try
|
(try
|
||||||
(p/-execute-one con ["DROP TABLE CLOBBER"] {})
|
(p/-execute-one con ["DROP TABLE CLOBBER"] {})
|
||||||
|
|
@ -497,10 +507,10 @@ CREATE TABLE CLOBBER (
|
||||||
(testing "get n on bare abstraction over arrays"
|
(testing "get n on bare abstraction over arrays"
|
||||||
(is (= [1 2 3]
|
(is (= [1 2 3]
|
||||||
(into [] (map #(get % 0))
|
(into [] (map #(get % 0))
|
||||||
(p/-execute (ds) ["select id from fruit where id < ?" 4]
|
(p/-execute (ds) [(str "select " (index) " from fruit where " (index) " < ? order by " (index)) 4]
|
||||||
{:builder-fn rs/as-arrays})))))
|
{:builder-fn rs/as-arrays})))))
|
||||||
(testing "nth on bare abstraction over arrays"
|
(testing "nth on bare abstraction over arrays"
|
||||||
(is (= [1 2 3]
|
(is (= [1 2 3]
|
||||||
(into [] (map #(nth % 0))
|
(into [] (map #(nth % 0))
|
||||||
(p/-execute (ds) ["select id from fruit where id < ?" 4]
|
(p/-execute (ds) [(str "select " (index) " from fruit where " (index) " < ? order by " (index)) 4]
|
||||||
{:builder-fn rs/as-arrays}))))))
|
{:builder-fn rs/as-arrays}))))))
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.specs-test
|
(ns next.jdbc.specs-test
|
||||||
"Stub test namespace for the specs.
|
"Stub test namespace for the specs.
|
||||||
|
|
||||||
The specs are used (and 'tested') as part of the tests for the
|
The specs are used (and 'tested') as part of the tests for the
|
||||||
next.jdbc and next.jdbc.sql namespaces."
|
next.jdbc and next.jdbc.sql namespaces."
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require [next.jdbc.specs]))
|
||||||
[next.jdbc.specs :refer :all]))
|
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.sql.builder-test
|
(ns next.jdbc.sql.builder-test
|
||||||
"Tests for the SQL string building functions in next.jdbc.sql.builder."
|
"Tests for the SQL string building functions in next.jdbc.sql.builder."
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require [lazytest.experimental.interfaces.clojure-test :refer [deftest is testing thrown?]]
|
||||||
[next.jdbc.quoted :refer [mysql sql-server]]
|
[next.jdbc.quoted :refer [mysql sql-server]]
|
||||||
[next.jdbc.sql.builder :as builder]))
|
[next.jdbc.sql.builder :as builder]))
|
||||||
|
|
||||||
|
|
@ -11,9 +11,13 @@
|
||||||
(deftest test-by-keys
|
(deftest test-by-keys
|
||||||
(testing ":where clause"
|
(testing ":where clause"
|
||||||
(is (= (builder/by-keys {:a nil :b 42 :c "s"} :where {})
|
(is (= (builder/by-keys {:a nil :b 42 :c "s"} :where {})
|
||||||
|
["WHERE a IS NULL AND b = ? AND c = ?" 42 "s"]))
|
||||||
|
(is (= (builder/by-keys {:q/a nil :q/b 42 :q/c "s"} :where {})
|
||||||
["WHERE a IS NULL AND b = ? AND c = ?" 42 "s"])))
|
["WHERE a IS NULL AND b = ? AND c = ?" 42 "s"])))
|
||||||
(testing ":set clause"
|
(testing ":set clause"
|
||||||
(is (= (builder/by-keys {:a nil :b 42 :c "s"} :set {})
|
(is (= (builder/by-keys {:a nil :b 42 :c "s"} :set {})
|
||||||
|
["SET a = ?, b = ?, c = ?" nil 42 "s"]))
|
||||||
|
(is (= (builder/by-keys {:q/a nil :q/b 42 :q/c "s"} :set {})
|
||||||
["SET a = ?, b = ?, c = ?" nil 42 "s"]))))
|
["SET a = ?, b = ?, c = ?" nil 42 "s"]))))
|
||||||
|
|
||||||
(deftest test-as-cols
|
(deftest test-as-cols
|
||||||
|
|
@ -22,14 +26,24 @@
|
||||||
(is (= (builder/as-cols [[:a :aa] :b ["count(*)" :c]] {})
|
(is (= (builder/as-cols [[:a :aa] :b ["count(*)" :c]] {})
|
||||||
"a AS aa, b, count(*) AS c"))
|
"a AS aa, b, count(*) AS c"))
|
||||||
(is (= (builder/as-cols [[:a :aa] :b ["count(*)" :c]] {:column-fn mysql})
|
(is (= (builder/as-cols [[:a :aa] :b ["count(*)" :c]] {:column-fn mysql})
|
||||||
|
"`a` AS `aa`, `b`, count(*) AS `c`"))
|
||||||
|
(is (= (builder/as-cols [:q/a :q/b :q/c] {})
|
||||||
|
"a, b, c"))
|
||||||
|
(is (= (builder/as-cols [[:q/a :q/aa] :q/b ["count(*)" :q/c]] {})
|
||||||
|
"a AS aa, b, count(*) AS c"))
|
||||||
|
(is (= (builder/as-cols [[:q/a :q/aa] :q/b ["count(*)" :q/c]] {:column-fn mysql})
|
||||||
"`a` AS `aa`, `b`, count(*) AS `c`")))
|
"`a` AS `aa`, `b`, count(*) AS `c`")))
|
||||||
|
|
||||||
(deftest test-as-keys
|
(deftest test-as-keys
|
||||||
(is (= (builder/as-keys {:a nil :b 42 :c "s"} {})
|
(is (= (builder/as-keys {:a nil :b 42 :c "s"} {})
|
||||||
|
"a, b, c"))
|
||||||
|
(is (= (builder/as-keys {:q/a nil :q/b 42 :q/c "s"} {})
|
||||||
"a, b, c")))
|
"a, b, c")))
|
||||||
|
|
||||||
(deftest test-as-?
|
(deftest test-as-?
|
||||||
(is (= (builder/as-? {:a nil :b 42 :c "s"} {})
|
(is (= (builder/as-? {:a nil :b 42 :c "s"} {})
|
||||||
|
"?, ?, ?"))
|
||||||
|
(is (= (builder/as-? {:q/a nil :q/b 42 :q/c "s"} {})
|
||||||
"?, ?, ?")))
|
"?, ?, ?")))
|
||||||
|
|
||||||
(deftest test-for-query
|
(deftest test-for-query
|
||||||
|
|
@ -45,6 +59,18 @@
|
||||||
{:id nil}
|
{:id nil}
|
||||||
{:table-fn sql-server :column-fn mysql
|
{:table-fn sql-server :column-fn mysql
|
||||||
:suffix "FOR UPDATE"})
|
:suffix "FOR UPDATE"})
|
||||||
|
["SELECT * FROM [user] WHERE `id` IS NULL FOR UPDATE"]))
|
||||||
|
(is (= (builder/for-query
|
||||||
|
:t/user
|
||||||
|
{:q/id 9}
|
||||||
|
{:table-fn sql-server :column-fn mysql :order-by [:x/a [:x/b :desc]]})
|
||||||
|
["SELECT * FROM [user] WHERE `id` = ? ORDER BY `a`, `b` DESC" 9]))
|
||||||
|
(is (= (builder/for-query :t/user {:q/id nil} {:table-fn sql-server :column-fn mysql})
|
||||||
|
["SELECT * FROM [user] WHERE `id` IS NULL"]))
|
||||||
|
(is (= (builder/for-query :t/user
|
||||||
|
{:q/id nil}
|
||||||
|
{:table-fn sql-server :column-fn mysql
|
||||||
|
:suffix "FOR UPDATE"})
|
||||||
["SELECT * FROM [user] WHERE `id` IS NULL FOR UPDATE"])))
|
["SELECT * FROM [user] WHERE `id` IS NULL FOR UPDATE"])))
|
||||||
(testing "by where clause"
|
(testing "by where clause"
|
||||||
(is (= (builder/for-query
|
(is (= (builder/for-query
|
||||||
|
|
@ -112,17 +138,27 @@
|
||||||
:user
|
:user
|
||||||
{:opt nil :id 9}
|
{:opt nil :id 9}
|
||||||
{:table-fn sql-server :column-fn mysql})
|
{:table-fn sql-server :column-fn mysql})
|
||||||
|
["DELETE FROM [user] WHERE `opt` IS NULL AND `id` = ?" 9]))
|
||||||
|
(is (= (builder/for-delete
|
||||||
|
:t/user
|
||||||
|
{:q/opt nil :q/id 9}
|
||||||
|
{:table-fn sql-server :column-fn mysql})
|
||||||
["DELETE FROM [user] WHERE `opt` IS NULL AND `id` = ?" 9])))
|
["DELETE FROM [user] WHERE `opt` IS NULL AND `id` = ?" 9])))
|
||||||
(testing "by where clause"
|
(testing "by where clause"
|
||||||
(is (= (builder/for-delete
|
(is (= (builder/for-delete
|
||||||
:user
|
:user
|
||||||
["id = ? and opt is null" 9]
|
["id = ? and opt is null" 9]
|
||||||
{:table-fn sql-server :column-fn mysql})
|
{:table-fn sql-server :column-fn mysql})
|
||||||
|
["DELETE FROM [user] WHERE id = ? and opt is null" 9]))
|
||||||
|
(is (= (builder/for-delete
|
||||||
|
:t/user
|
||||||
|
["id = ? and opt is null" 9]
|
||||||
|
{:table-fn sql-server :column-fn mysql})
|
||||||
["DELETE FROM [user] WHERE id = ? and opt is null" 9]))))
|
["DELETE FROM [user] WHERE id = ? and opt is null" 9]))))
|
||||||
|
|
||||||
(deftest test-for-update
|
(deftest test-for-update
|
||||||
(testing "empty example (would be a SQL error)"
|
(testing "empty example (would be a SQL error)"
|
||||||
(is (thrown? AssertionError ; changed in #44
|
(is (thrown? IllegalArgumentException
|
||||||
(builder/for-update :user
|
(builder/for-update :user
|
||||||
{:status 42}
|
{:status 42}
|
||||||
{}
|
{}
|
||||||
|
|
@ -132,6 +168,11 @@
|
||||||
{:status 42}
|
{:status 42}
|
||||||
{:id 9}
|
{:id 9}
|
||||||
{:table-fn sql-server :column-fn mysql})
|
{:table-fn sql-server :column-fn mysql})
|
||||||
|
["UPDATE [user] SET `status` = ? WHERE `id` = ?" 42 9]))
|
||||||
|
(is (= (builder/for-update :t/user
|
||||||
|
{:q/status 42}
|
||||||
|
{:q/id 9}
|
||||||
|
{:table-fn sql-server :column-fn mysql})
|
||||||
["UPDATE [user] SET `status` = ? WHERE `id` = ?" 42 9])))
|
["UPDATE [user] SET `status` = ? WHERE `id` = ?" 42 9])))
|
||||||
(testing "by where clause, with nil set value"
|
(testing "by where clause, with nil set value"
|
||||||
(is (= (builder/for-update :user
|
(is (= (builder/for-update :user
|
||||||
|
|
@ -145,6 +186,10 @@
|
||||||
(is (= (builder/for-insert :user
|
(is (= (builder/for-insert :user
|
||||||
{:id 9 :status 42 :opt nil}
|
{:id 9 :status 42 :opt nil}
|
||||||
{:table-fn sql-server :column-fn mysql})
|
{:table-fn sql-server :column-fn mysql})
|
||||||
|
["INSERT INTO [user] (`id`, `status`, `opt`) VALUES (?, ?, ?)" 9 42 nil]))
|
||||||
|
(is (= (builder/for-insert :t/user
|
||||||
|
{:q/id 9 :q/status 42 :q/opt nil}
|
||||||
|
{:table-fn sql-server :column-fn mysql})
|
||||||
["INSERT INTO [user] (`id`, `status`, `opt`) VALUES (?, ?, ?)" 9 42 nil])))
|
["INSERT INTO [user] (`id`, `status`, `opt`) VALUES (?, ?, ?)" 9 42 nil])))
|
||||||
(testing "multi-row insert (normal mode)"
|
(testing "multi-row insert (normal mode)"
|
||||||
(is (= (builder/for-insert-multi :user
|
(is (= (builder/for-insert-multi :user
|
||||||
|
|
@ -153,6 +198,13 @@
|
||||||
[35 "world"]
|
[35 "world"]
|
||||||
[64 "dollars"]]
|
[64 "dollars"]]
|
||||||
{:table-fn sql-server :column-fn mysql})
|
{:table-fn sql-server :column-fn mysql})
|
||||||
|
["INSERT INTO [user] (`id`, `status`) VALUES (?, ?), (?, ?), (?, ?)" 42 "hello" 35 "world" 64 "dollars"]))
|
||||||
|
(is (= (builder/for-insert-multi :t/user
|
||||||
|
[:q/id :q/status]
|
||||||
|
[[42 "hello"]
|
||||||
|
[35 "world"]
|
||||||
|
[64 "dollars"]]
|
||||||
|
{:table-fn sql-server :column-fn mysql})
|
||||||
["INSERT INTO [user] (`id`, `status`) VALUES (?, ?), (?, ?), (?, ?)" 42 "hello" 35 "world" 64 "dollars"])))
|
["INSERT INTO [user] (`id`, `status`) VALUES (?, ?), (?, ?), (?, ?)" 42 "hello" 35 "world" 64 "dollars"])))
|
||||||
(testing "multi-row insert (batch mode)"
|
(testing "multi-row insert (batch mode)"
|
||||||
(is (= (builder/for-insert-multi :user
|
(is (= (builder/for-insert-multi :user
|
||||||
|
|
@ -161,4 +213,11 @@
|
||||||
[35 "world"]
|
[35 "world"]
|
||||||
[64 "dollars"]]
|
[64 "dollars"]]
|
||||||
{:table-fn sql-server :column-fn mysql :batch true})
|
{:table-fn sql-server :column-fn mysql :batch true})
|
||||||
|
["INSERT INTO [user] (`id`, `status`) VALUES (?, ?)" [42 "hello"] [35 "world"] [64 "dollars"]]))
|
||||||
|
(is (= (builder/for-insert-multi :t/user
|
||||||
|
[:q/id :q/status]
|
||||||
|
[[42 "hello"]
|
||||||
|
[35 "world"]
|
||||||
|
[64 "dollars"]]
|
||||||
|
{:table-fn sql-server :column-fn mysql :batch true})
|
||||||
["INSERT INTO [user] (`id`, `status`) VALUES (?, ?)" [42 "hello"] [35 "world"] [64 "dollars"]]))))
|
["INSERT INTO [user] (`id`, `status`) VALUES (?, ?)" [42 "hello"] [35 "world"] [64 "dollars"]]))))
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.sql-test
|
(ns next.jdbc.sql-test
|
||||||
"Tests for the syntactic sugar SQL functions."
|
"Tests for the syntactic sugar SQL functions."
|
||||||
(:require [clojure.test :refer [deftest is testing use-fixtures]]
|
(:require [lazytest.core :refer [around set-ns-context!]]
|
||||||
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing thrown?]]
|
||||||
[next.jdbc :as jdbc]
|
[next.jdbc :as jdbc]
|
||||||
[next.jdbc.specs :as specs]
|
[next.jdbc.specs :as specs]
|
||||||
[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 [col-kw column default-options derby? ds index jtds?
|
||||||
derby? jtds? maria? mssql? mysql? postgres? sqlite?]]
|
maria? mssql? mysql? postgres? sqlite? with-test-db xtdb?]]
|
||||||
[next.jdbc.types :refer [as-other as-real as-varchar]]))
|
[next.jdbc.types :refer [as-other as-real as-varchar]]))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(use-fixtures :once with-test-db)
|
(set-ns-context! [(around [f] (with-test-db f))])
|
||||||
|
|
||||||
(specs/instrument)
|
(specs/instrument)
|
||||||
|
|
||||||
(deftest test-query
|
(deftest test-query
|
||||||
(let [ds-opts (jdbc/with-options (ds) (default-options))
|
(let [ds-opts (jdbc/with-options (ds) (default-options))
|
||||||
rs (sql/query ds-opts ["select * from fruit order by id"])]
|
rs (sql/query ds-opts [(str "select * from fruit order by " (index))])]
|
||||||
(is (= 4 (count rs)))
|
(is (= 4 (count rs)))
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
(is (every? meta rs))
|
(is (every? meta rs))
|
||||||
|
|
@ -34,10 +35,10 @@
|
||||||
(if (or (mysql?) (sqlite?))
|
(if (or (mysql?) (sqlite?))
|
||||||
{:limit 2 :offset 1}
|
{:limit 2 :offset 1}
|
||||||
{:offset 1 :fetch 2})
|
{:offset 1 :fetch 2})
|
||||||
:columns [:ID
|
:columns [(col-kw :ID)
|
||||||
["CASE WHEN grade > 91 THEN 'ok ' ELSE 'bad' END"
|
["CASE WHEN grade > 91 THEN 'ok ' ELSE 'bad' END"
|
||||||
:QUALITY]]
|
:QUALITY]]
|
||||||
:order-by [:id]))]
|
:order-by [(col-kw :id)]))]
|
||||||
(is (= 2 (count rs)))
|
(is (= 2 (count rs)))
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
(is (every? meta rs))
|
(is (every? meta rs))
|
||||||
|
|
@ -67,17 +68,18 @@
|
||||||
(is (= 1 count-v)))
|
(is (= 1 count-v)))
|
||||||
(let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" :all)]
|
(let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" :all)]
|
||||||
(is (= 4 count-v)))
|
(is (= 4 count-v)))
|
||||||
(let [max-id (sql/aggregate-by-keys ds-opts :fruit "max(id)" :all)]
|
(let [max-id (sql/aggregate-by-keys ds-opts :fruit (str "max(" (index) ")") :all)]
|
||||||
(is (= 4 max-id)))
|
(is (= 4 max-id)))
|
||||||
|
(when-not (xtdb?) ; XTDB does not support min/max on strings?
|
||||||
(let [min-name (sql/aggregate-by-keys ds-opts :fruit "min(name)" :all)]
|
(let [min-name (sql/aggregate-by-keys ds-opts :fruit "min(name)" :all)]
|
||||||
(is (= "Apple" min-name)))
|
(is (= "Apple" min-name))))
|
||||||
(is (thrown? IllegalArgumentException
|
(is (thrown? IllegalArgumentException
|
||||||
(sql/aggregate-by-keys ds-opts :fruit "count(*)" :all {:columns []})))))
|
(sql/aggregate-by-keys ds-opts :fruit "count(*)" :all {:columns []})))))
|
||||||
|
|
||||||
(deftest test-get-by-id
|
(deftest test-get-by-id
|
||||||
(let [ds-opts (jdbc/with-options (ds) (default-options))]
|
(let [ds-opts (jdbc/with-options (ds) (default-options))]
|
||||||
(is (nil? (sql/get-by-id ds-opts :fruit -1)))
|
(is (nil? (sql/get-by-id ds-opts :fruit -1 (col-kw :id) {})))
|
||||||
(let [row (sql/get-by-id ds-opts :fruit 3)]
|
(let [row (sql/get-by-id ds-opts :fruit 3 (col-kw :id) {})]
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (= "Peach" ((column :FRUIT/NAME) row))))
|
(is (= "Peach" ((column :FRUIT/NAME) row))))
|
||||||
(let [row (sql/get-by-id ds-opts :fruit "juicy" :appearance {})]
|
(let [row (sql/get-by-id ds-opts :fruit "juicy" :appearance {})]
|
||||||
|
|
@ -88,23 +90,28 @@
|
||||||
(is (map? row))
|
(is (map? row))
|
||||||
(is (= 2 ((column :FRUIT/ID) row))))))
|
(is (= 2 ((column :FRUIT/ID) row))))))
|
||||||
|
|
||||||
|
(defn- update-count [n]
|
||||||
|
(if (xtdb?)
|
||||||
|
{:next.jdbc/update-count 0}
|
||||||
|
{:next.jdbc/update-count n}))
|
||||||
|
|
||||||
(deftest test-update!
|
(deftest test-update!
|
||||||
(let [ds-opts (jdbc/with-options (ds) (default-options))]
|
(let [ds-opts (jdbc/with-options (ds) (default-options))]
|
||||||
(try
|
(try
|
||||||
(is (= {:next.jdbc/update-count 1}
|
(is (= (update-count 1)
|
||||||
(sql/update! ds-opts :fruit {:appearance "brown"} {:id 2})))
|
(sql/update! ds-opts :fruit {:appearance "brown"} {(col-kw :id) 2})))
|
||||||
(is (= "brown" ((column :FRUIT/APPEARANCE)
|
(is (= "brown" ((column :FRUIT/APPEARANCE)
|
||||||
(sql/get-by-id ds-opts :fruit 2))))
|
(sql/get-by-id ds-opts :fruit 2 (col-kw :id) {}))))
|
||||||
(finally
|
(finally
|
||||||
(sql/update! ds-opts :fruit {:appearance "yellow"} {:id 2})))
|
(sql/update! ds-opts :fruit {:appearance "yellow"} {(col-kw :id) 2})))
|
||||||
(try
|
(try
|
||||||
(is (= {:next.jdbc/update-count 1}
|
(is (= (update-count 1)
|
||||||
(sql/update! ds-opts :fruit {:appearance "green"}
|
(sql/update! ds-opts :fruit {:appearance "green"}
|
||||||
["name = ?" "Banana"])))
|
["name = ?" "Banana"])))
|
||||||
(is (= "green" ((column :FRUIT/APPEARANCE)
|
(is (= "green" ((column :FRUIT/APPEARANCE)
|
||||||
(sql/get-by-id ds-opts :fruit 2))))
|
(sql/get-by-id ds-opts :fruit 2 (col-kw :id) {}))))
|
||||||
(finally
|
(finally
|
||||||
(sql/update! ds-opts :fruit {:appearance "yellow"} {:id 2})))))
|
(sql/update! ds-opts :fruit {:appearance "yellow"} {(col-kw :id) 2})))))
|
||||||
|
|
||||||
(deftest test-insert-delete
|
(deftest test-insert-delete
|
||||||
(let [new-key (cond (derby?) :1
|
(let [new-key (cond (derby?) :1
|
||||||
|
|
@ -113,18 +120,24 @@
|
||||||
(mssql?) :GENERATED_KEYS
|
(mssql?) :GENERATED_KEYS
|
||||||
(mysql?) :GENERATED_KEY
|
(mysql?) :GENERATED_KEY
|
||||||
(postgres?) :fruit/id
|
(postgres?) :fruit/id
|
||||||
|
;; XTDB does not return the generated key so we fix it
|
||||||
|
;; to be the one we insert here, and then fake it in all
|
||||||
|
;; the other tests.
|
||||||
|
(xtdb?) (constantly 5)
|
||||||
: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 (as-varchar "Kiwi")
|
(cond-> {:name (as-varchar "Kiwi")
|
||||||
:appearance "green & fuzzy"
|
:appearance "green & fuzzy"
|
||||||
:cost 100 :grade (as-real 99.9)}
|
:cost 100 :grade (as-real 99.9)}
|
||||||
|
(xtdb?)
|
||||||
|
(assoc :_id 5))
|
||||||
{:suffix
|
{:suffix
|
||||||
(when (sqlite?)
|
(when (sqlite?)
|
||||||
"RETURNING *")}))))
|
"RETURNING *")}))))
|
||||||
(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 (= (update-count 1)
|
||||||
(sql/delete! (ds) :fruit {:id 5})))
|
(sql/delete! (ds) :fruit {(col-kw :id) 5})))
|
||||||
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
|
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
|
||||||
(testing "multiple insert/delete"
|
(testing "multiple insert/delete"
|
||||||
(is (= (cond (derby?)
|
(is (= (cond (derby?)
|
||||||
|
|
@ -133,23 +146,28 @@
|
||||||
[8M]
|
[8M]
|
||||||
(maria?)
|
(maria?)
|
||||||
[6]
|
[6]
|
||||||
|
(xtdb?)
|
||||||
|
[]
|
||||||
:else
|
:else
|
||||||
[6 7 8])
|
[6 7 8])
|
||||||
(mapv new-key
|
(mapv new-key
|
||||||
(sql/insert-multi! (ds) :fruit
|
(sql/insert-multi! (ds) :fruit
|
||||||
[:name :appearance :cost :grade]
|
(cond->> [:name :appearance :cost :grade]
|
||||||
[["Kiwi" "green & fuzzy" 100 99.9]
|
(xtdb?) (cons :_id))
|
||||||
|
(cond->> [["Kiwi" "green & fuzzy" 100 99.9]
|
||||||
["Grape" "black" 10 50]
|
["Grape" "black" 10 50]
|
||||||
["Lemon" "yellow" 20 9.9]]
|
["Lemon" "yellow" 20 9.9]]
|
||||||
|
(xtdb?)
|
||||||
|
(map cons [6 7 8]))
|
||||||
{:suffix
|
{:suffix
|
||||||
(when (sqlite?)
|
(when (sqlite?)
|
||||||
"RETURNING *")}))))
|
"RETURNING *")}))))
|
||||||
(is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
|
(is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
|
||||||
(is (= {:next.jdbc/update-count 1}
|
(is (= (update-count 1)
|
||||||
(sql/delete! (ds) :fruit {:id 6})))
|
(sql/delete! (ds) :fruit {(col-kw :id) 6})))
|
||||||
(is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
|
(is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
|
||||||
(is (= {:next.jdbc/update-count 2}
|
(is (= (update-count 2)
|
||||||
(sql/delete! (ds) :fruit ["id > ?" 4])))
|
(sql/delete! (ds) :fruit [(str (index) " > ?") 4])))
|
||||||
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
|
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
|
||||||
(testing "multiple insert/delete with sequential cols/rows" ; per #43
|
(testing "multiple insert/delete with sequential cols/rows" ; per #43
|
||||||
(is (= (cond (derby?)
|
(is (= (cond (derby?)
|
||||||
|
|
@ -158,23 +176,28 @@
|
||||||
[11M]
|
[11M]
|
||||||
(maria?)
|
(maria?)
|
||||||
[9]
|
[9]
|
||||||
|
(xtdb?)
|
||||||
|
[]
|
||||||
:else
|
:else
|
||||||
[9 10 11])
|
[9 10 11])
|
||||||
(mapv new-key
|
(mapv new-key
|
||||||
(sql/insert-multi! (ds) :fruit
|
(sql/insert-multi! (ds) :fruit
|
||||||
'(:name :appearance :cost :grade)
|
(cond->> '(:name :appearance :cost :grade)
|
||||||
'(("Kiwi" "green & fuzzy" 100 99.9)
|
(xtdb?) (cons :_id))
|
||||||
|
(cond->> '(("Kiwi" "green & fuzzy" 100 99.9)
|
||||||
("Grape" "black" 10 50)
|
("Grape" "black" 10 50)
|
||||||
("Lemon" "yellow" 20 9.9))
|
("Lemon" "yellow" 20 9.9))
|
||||||
|
(xtdb?)
|
||||||
|
(map cons [9 10 11]))
|
||||||
{:suffix
|
{:suffix
|
||||||
(when (sqlite?)
|
(when (sqlite?)
|
||||||
"RETURNING *")}))))
|
"RETURNING *")}))))
|
||||||
(is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
|
(is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
|
||||||
(is (= {:next.jdbc/update-count 1}
|
(is (= (update-count 1)
|
||||||
(sql/delete! (ds) :fruit {:id 9})))
|
(sql/delete! (ds) :fruit {(col-kw :id) 9})))
|
||||||
(is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
|
(is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
|
||||||
(is (= {:next.jdbc/update-count 2}
|
(is (= (update-count 2)
|
||||||
(sql/delete! (ds) :fruit ["id > ?" 4])))
|
(sql/delete! (ds) :fruit [(str (index) " > ?") 4])))
|
||||||
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
|
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
|
||||||
(testing "multiple insert/delete with maps"
|
(testing "multiple insert/delete with maps"
|
||||||
(is (= (cond (derby?)
|
(is (= (cond (derby?)
|
||||||
|
|
@ -183,11 +206,13 @@
|
||||||
[14M]
|
[14M]
|
||||||
(maria?)
|
(maria?)
|
||||||
[12]
|
[12]
|
||||||
|
(xtdb?)
|
||||||
|
[]
|
||||||
:else
|
:else
|
||||||
[12 13 14])
|
[12 13 14])
|
||||||
(mapv new-key
|
(mapv new-key
|
||||||
(sql/insert-multi! (ds) :fruit
|
(sql/insert-multi! (ds) :fruit
|
||||||
[{:name "Kiwi"
|
(cond->> [{:name "Kiwi"
|
||||||
:appearance "green & fuzzy"
|
:appearance "green & fuzzy"
|
||||||
:cost 100
|
:cost 100
|
||||||
:grade 99.9}
|
:grade 99.9}
|
||||||
|
|
@ -199,15 +224,17 @@
|
||||||
:appearance "yellow"
|
:appearance "yellow"
|
||||||
:cost 20
|
:cost 20
|
||||||
:grade 9.9}]
|
:grade 9.9}]
|
||||||
|
(xtdb?)
|
||||||
|
(map #(assoc %2 :_id %1) [12 13 14]))
|
||||||
{:suffix
|
{:suffix
|
||||||
(when (sqlite?)
|
(when (sqlite?)
|
||||||
"RETURNING *")}))))
|
"RETURNING *")}))))
|
||||||
(is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
|
(is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
|
||||||
(is (= {:next.jdbc/update-count 1}
|
(is (= (update-count 1)
|
||||||
(sql/delete! (ds) :fruit {:id 12})))
|
(sql/delete! (ds) :fruit {(col-kw :id) 12})))
|
||||||
(is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
|
(is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
|
||||||
(is (= {:next.jdbc/update-count 2}
|
(is (= (update-count 2)
|
||||||
(sql/delete! (ds) :fruit ["id > ?" 10])))
|
(sql/delete! (ds) :fruit [(str (index) " > ?") 10])))
|
||||||
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
|
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
|
||||||
(testing "empty insert-multi!" ; per #44 and #264
|
(testing "empty insert-multi!" ; per #44 and #264
|
||||||
(is (= [] (sql/insert-multi! (ds) :fruit
|
(is (= [] (sql/insert-multi! (ds) :fruit
|
||||||
|
|
@ -255,7 +282,7 @@
|
||||||
|
|
||||||
(deftest array-in
|
(deftest array-in
|
||||||
(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 [(str (index) " = any(?)") (int-array [1 2 3 4])])]
|
||||||
(is (= 4 (count data))))))
|
(is (= 4 (count data))))))
|
||||||
|
|
||||||
(deftest enum-pg
|
(deftest enum-pg
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2024 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.test-fixtures
|
(ns next.jdbc.test-fixtures
|
||||||
"Multi-database testing fixtures."
|
"Multi-database testing fixtures."
|
||||||
|
|
@ -64,16 +64,27 @@
|
||||||
(def ^:private test-jtds
|
(def ^:private test-jtds
|
||||||
(when (System/getenv "NEXT_JDBC_TEST_MSSQL") test-jtds-map))
|
(when (System/getenv "NEXT_JDBC_TEST_MSSQL") test-jtds-map))
|
||||||
|
|
||||||
|
(def ^:private test-xtdb-map {:dbtype "xtdb" :dbname "xtdb"})
|
||||||
|
|
||||||
|
(def ^:private test-xtdb
|
||||||
|
(when (and (System/getenv "NEXT_JDBC_TEST_XTDB")
|
||||||
|
;; only if we're on jdk21+
|
||||||
|
(str/starts-with? (System/getProperty "java.version") "2"))
|
||||||
|
test-xtdb-map))
|
||||||
|
|
||||||
(def ^:private test-db-specs
|
(def ^:private test-db-specs
|
||||||
(cond-> [test-derby test-h2-mem test-h2 test-hsql test-sqlite]
|
(cond-> [test-derby test-h2-mem test-h2 test-hsql test-sqlite]
|
||||||
test-postgres (conj test-postgres)
|
test-postgres (conj test-postgres)
|
||||||
test-mysql (conj test-mysql)
|
test-mysql (conj test-mysql)
|
||||||
test-mssql (conj test-mssql test-jtds)))
|
test-mssql (conj test-mssql test-jtds)
|
||||||
|
test-xtdb (conj test-xtdb)))
|
||||||
|
|
||||||
(def ^:private test-db-spec (atom nil))
|
(def ^:private test-db-spec (atom nil))
|
||||||
|
|
||||||
(defn derby? [] (= "derby" (:dbtype @test-db-spec)))
|
(defn derby? [] (= "derby" (:dbtype @test-db-spec)))
|
||||||
|
|
||||||
|
(defn h2? [] (str/starts-with? (:dbtype @test-db-spec) "h2"))
|
||||||
|
|
||||||
(defn hsqldb? [] (= "hsqldb" (:dbtype @test-db-spec)))
|
(defn hsqldb? [] (= "hsqldb" (:dbtype @test-db-spec)))
|
||||||
|
|
||||||
(defn jtds? [] (= "jtds" (:dbtype @test-db-spec)))
|
(defn jtds? [] (= "jtds" (:dbtype @test-db-spec)))
|
||||||
|
|
@ -86,19 +97,34 @@
|
||||||
|
|
||||||
(defn postgres? [] (= "embedded-postgres" (:dbtype @test-db-spec)))
|
(defn postgres? [] (= "embedded-postgres" (:dbtype @test-db-spec)))
|
||||||
|
|
||||||
|
(defn xtdb? [] (= "xtdb" (:dbtype @test-db-spec)))
|
||||||
|
|
||||||
(defn sqlite? [] (= "sqlite" (:dbtype @test-db-spec)))
|
(defn sqlite? [] (= "sqlite" (:dbtype @test-db-spec)))
|
||||||
|
|
||||||
(defn stored-proc? [] (not (#{"derby" "h2" "h2:mem" "sqlite"} (:dbtype @test-db-spec))))
|
(defn stored-proc? [] (not (#{"derby" "h2" "h2:mem" "sqlite" "xtdb"}
|
||||||
|
(:dbtype @test-db-spec))))
|
||||||
|
|
||||||
(defn column [k]
|
(defn column [k]
|
||||||
(let [n (namespace k)]
|
(let [n (namespace k)]
|
||||||
(keyword (when n (cond (postgres?) (str/lower-case n)
|
(keyword (when n (cond (postgres?) (str/lower-case n)
|
||||||
(mssql?) (str/lower-case n)
|
(mssql?) (str/lower-case n)
|
||||||
(mysql?) (str/lower-case n)
|
(mysql?) (str/lower-case n)
|
||||||
|
(xtdb?) nil
|
||||||
:else n))
|
:else n))
|
||||||
(cond (postgres?) (str/lower-case (name k))
|
(cond (postgres?) (str/lower-case (name k))
|
||||||
|
(xtdb?) (let [c (str/lower-case (name k))]
|
||||||
|
(if (= "id" c) "_id" c))
|
||||||
:else (name k)))))
|
:else (name k)))))
|
||||||
|
|
||||||
|
(defn index []
|
||||||
|
(if (xtdb?) "_id" "id"))
|
||||||
|
|
||||||
|
(defn col-kw [k]
|
||||||
|
(if (xtdb?)
|
||||||
|
(let [n (name k)]
|
||||||
|
(if (= "id" (str/lower-case n)) :_id (keyword n)))
|
||||||
|
k))
|
||||||
|
|
||||||
(defn default-options []
|
(defn default-options []
|
||||||
(if (mssql?) ; so that we get table names back from queries
|
(if (mssql?) ; so that we get table names back from queries
|
||||||
{:result-type :scroll-insensitive :concurrency :read-only}
|
{:result-type :scroll-insensitive :concurrency :read-only}
|
||||||
|
|
@ -156,6 +182,31 @@
|
||||||
:else
|
:else
|
||||||
"AUTO_INCREMENT PRIMARY KEY")]
|
"AUTO_INCREMENT PRIMARY KEY")]
|
||||||
(with-open [con (jdbc/get-connection (ds))]
|
(with-open [con (jdbc/get-connection (ds))]
|
||||||
|
(if (xtdb?) ; no DDL for creation
|
||||||
|
(do
|
||||||
|
(try
|
||||||
|
(do-commands con ["ERASE FROM fruit WHERE true"])
|
||||||
|
(catch Throwable _))
|
||||||
|
(try
|
||||||
|
(do-commands con ["ERASE FROM btest WHERE true"])
|
||||||
|
(catch Throwable _))
|
||||||
|
(sql/insert-multi! con :fruit
|
||||||
|
[:_id :name :appearance :cost]
|
||||||
|
[[1 "Apple" "red" 59]]
|
||||||
|
{:return-keys false})
|
||||||
|
(sql/insert-multi! con :fruit
|
||||||
|
[:_id :name :appearance :grade]
|
||||||
|
[[2 "Banana" "yellow" 92.2]]
|
||||||
|
{:return-keys false})
|
||||||
|
(sql/insert-multi! con :fruit
|
||||||
|
[:_id :name :cost :grade]
|
||||||
|
[[3 "Peach" 139 90.0]]
|
||||||
|
{:return-keys false})
|
||||||
|
(sql/insert-multi! con :fruit
|
||||||
|
[:_id :name :appearance :cost :grade]
|
||||||
|
[[4 "Orange" "juicy" 89 88.6]]
|
||||||
|
{:return-keys false}))
|
||||||
|
(do
|
||||||
(when (stored-proc?)
|
(when (stored-proc?)
|
||||||
(try
|
(try
|
||||||
(jdbc/execute-one! con ["DROP PROCEDURE FRUITP"])
|
(jdbc/execute-one! con ["DROP PROCEDURE FRUITP"])
|
||||||
|
|
@ -231,14 +282,14 @@ CREATE PROCEDURE FRUITP" (cond (hsqldb?) "() READS SQL DATA DYNAMIC RESULT SETS
|
||||||
["Banana" "yellow" nil 92.2]
|
["Banana" "yellow" nil 92.2]
|
||||||
["Peach" nil 139 90.0]
|
["Peach" nil 139 90.0]
|
||||||
["Orange" "juicy" 89 88.6]]
|
["Orange" "juicy" 89 88.6]]
|
||||||
{:return-keys false})
|
{:return-keys false})))
|
||||||
(t)))))
|
(t)))))
|
||||||
|
|
||||||
(create-clojure-test)
|
(create-clojure-test)
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
;; this is a convenience to bring next.jdbc's test dependencies
|
;; this is a convenience to bring next.jdbc's test dependencies
|
||||||
;; into any REPL running Clojure 1.12.0 Alpha 2's new add-libs API
|
;; into any REPL running Clojure 1.12.0's new add-libs API
|
||||||
;; which allows me to develop and test next.jdbc inside my work's
|
;; which allows me to develop and test next.jdbc inside my work's
|
||||||
;; "everything" REPL environment
|
;; "everything" REPL environment
|
||||||
(require '[clojure.repl.deps :refer [add-libs]]
|
(require '[clojure.repl.deps :refer [add-libs]]
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.transaction-test
|
(ns next.jdbc.transaction-test
|
||||||
"Stub test namespace for transaction handling."
|
"Stub test namespace for transaction handling."
|
||||||
(:require [clojure.test :refer [deftest is testing use-fixtures]]
|
(:require [next.jdbc.specs :as specs]
|
||||||
[next.jdbc :as jdbc]
|
[next.jdbc.transaction]))
|
||||||
[next.jdbc.specs :as specs]
|
|
||||||
[next.jdbc.test-fixtures :refer [with-test-db db ds column
|
|
||||||
default-options
|
|
||||||
derby? mssql? mysql? postgres?]]
|
|
||||||
[next.jdbc.transaction :as tx]))
|
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(use-fixtures :once with-test-db)
|
|
||||||
|
|
||||||
(specs/instrument)
|
(specs/instrument)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
;; copyright (c) 2020-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2020-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc.types-test
|
(ns next.jdbc.types-test
|
||||||
"Some tests for the type-assist functions."
|
"Some tests for the type-assist functions."
|
||||||
(:require [clojure.test :refer [deftest is]]
|
(:require [lazytest.core :refer [defdescribe describe it expect]]
|
||||||
[next.jdbc.types :refer [as-varchar]]))
|
[next.jdbc.types :refer [as-varchar]]))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
(deftest as-varchar-test
|
(defdescribe as-varchar-tests
|
||||||
(let [v (as-varchar "Hello")]
|
(let [v (as-varchar "Hello")]
|
||||||
(is (= "Hello" (v)))
|
(describe "produces a function"
|
||||||
(is (contains? (meta v) 'next.jdbc.prepare/set-parameter))
|
(it "yields the original value when invoked"
|
||||||
(is (fn? (get (meta v) 'next.jdbc.prepare/set-parameter)))))
|
(expect (fn? v))
|
||||||
|
(expect (= "Hello" (v)))))
|
||||||
|
(describe "carries metadata"
|
||||||
|
(it "has a `set-parameter` function"
|
||||||
|
(expect (contains? (meta v) 'next.jdbc.prepare/set-parameter))
|
||||||
|
(expect (fn? (get (meta v) 'next.jdbc.prepare/set-parameter)))))))
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,46 @@
|
||||||
;; copyright (c) 2019-2021 Sean Corfield, all rights reserved
|
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved
|
||||||
|
|
||||||
(ns next.jdbc-test
|
(ns next.jdbc-test
|
||||||
"Basic tests for the primary API of `next.jdbc`."
|
"Basic tests for the primary API of `next.jdbc`."
|
||||||
(:require [clojure.core.reducers :as r]
|
(:require
|
||||||
|
[clojure.core.reducers :as r]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[clojure.test :refer [deftest is testing use-fixtures]]
|
[lazytest.core :refer [around defdescribe it ok?]]
|
||||||
|
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing
|
||||||
|
thrown?]]
|
||||||
[next.jdbc :as jdbc]
|
[next.jdbc :as jdbc]
|
||||||
[next.jdbc.connection :as c]
|
[next.jdbc.connection :as c]
|
||||||
[next.jdbc.test-fixtures
|
|
||||||
:refer [with-test-db db ds column
|
|
||||||
default-options stored-proc?
|
|
||||||
derby? hsqldb? jtds? mssql? mysql? postgres? sqlite?]]
|
|
||||||
[next.jdbc.prepare :as prep]
|
[next.jdbc.prepare :as prep]
|
||||||
[next.jdbc.result-set :as rs]
|
[next.jdbc.result-set :as rs]
|
||||||
[next.jdbc.specs :as specs]
|
[next.jdbc.specs :as specs]
|
||||||
|
[next.jdbc.test-fixtures
|
||||||
|
:refer [col-kw column db default-options derby? ds h2? hsqldb?
|
||||||
|
index jtds? mssql? mysql? postgres? sqlite? stored-proc?
|
||||||
|
with-test-db xtdb?]]
|
||||||
[next.jdbc.types :as types])
|
[next.jdbc.types :as types])
|
||||||
(:import (com.zaxxer.hikari HikariDataSource)
|
(:import
|
||||||
(com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource)
|
(com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource)
|
||||||
|
(com.zaxxer.hikari HikariDataSource)
|
||||||
(java.sql ResultSet ResultSetMetaData)))
|
(java.sql ResultSet ResultSetMetaData)))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
;; around each test because of the folding tests using 1,000 rows
|
|
||||||
(use-fixtures :each with-test-db)
|
|
||||||
|
|
||||||
(specs/instrument)
|
(specs/instrument)
|
||||||
|
|
||||||
(deftest spec-tests
|
(defdescribe spec-tests
|
||||||
|
"sanity checks on instrumented function calls"
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(let [db-spec {:dbtype "h2:mem" :dbname "clojure_test"}]
|
(let [db-spec {:dbtype "h2:mem" :dbname "clojure_test"}]
|
||||||
;; some sanity checks on instrumented function calls:
|
(it "succeeds with a basic db-spec"
|
||||||
(jdbc/get-datasource db-spec)
|
(ok? #(jdbc/get-datasource db-spec))
|
||||||
(jdbc/get-connection db-spec)
|
(ok? #(jdbc/get-connection db-spec)))
|
||||||
;; and again with options:
|
|
||||||
(let [db-spec' (jdbc/with-options db-spec {})]
|
(let [db-spec' (jdbc/with-options db-spec {})]
|
||||||
(jdbc/get-datasource db-spec')
|
(it "succeeds with an option-wrapped db-spec"
|
||||||
(jdbc/get-connection db-spec'))))
|
(ok? #(jdbc/get-datasource db-spec'))
|
||||||
|
(ok? #(jdbc/get-connection db-spec'))))))
|
||||||
|
|
||||||
(deftest basic-tests
|
(deftest basic-tests
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
;; use ds-opts instead of (ds) anywhere you want default options applied:
|
;; use ds-opts instead of (ds) anywhere you want default options applied:
|
||||||
(let [ds-opts (jdbc/with-options (ds) (default-options))]
|
(let [ds-opts (jdbc/with-options (ds) (default-options))]
|
||||||
(testing "plan"
|
(testing "plan"
|
||||||
|
|
@ -60,27 +64,27 @@
|
||||||
(jdbc/execute-one!
|
(jdbc/execute-one!
|
||||||
ds-opts
|
ds-opts
|
||||||
["select * from fruit where appearance = ?" "red"]))))
|
["select * from fruit where appearance = ?" "red"]))))
|
||||||
(is (= "red" (:fruit/looks-like
|
(is (= "red" ((col-kw :fruit/looks-like)
|
||||||
(jdbc/execute-one!
|
(jdbc/execute-one!
|
||||||
ds-opts
|
ds-opts
|
||||||
["select appearance as looks_like from fruit where id = ?" 1]
|
[(str "select appearance as looks_like from fruit where " (index) " = ?") 1]
|
||||||
jdbc/snake-kebab-opts))))
|
jdbc/snake-kebab-opts))))
|
||||||
(let [ds' (jdbc/with-options ds-opts jdbc/snake-kebab-opts)]
|
(let [ds' (jdbc/with-options ds-opts jdbc/snake-kebab-opts)]
|
||||||
(is (= "red" (:fruit/looks-like
|
(is (= "red" ((col-kw :fruit/looks-like)
|
||||||
(jdbc/execute-one!
|
(jdbc/execute-one!
|
||||||
ds'
|
ds'
|
||||||
["select appearance as looks_like from fruit where id = ?" 1])))))
|
[(str "select appearance as looks_like from fruit where " (index) " = ?") 1])))))
|
||||||
(jdbc/with-transaction+options [ds' (jdbc/with-options ds-opts jdbc/snake-kebab-opts)]
|
(jdbc/with-transaction+options [ds' (jdbc/with-options ds-opts jdbc/snake-kebab-opts)]
|
||||||
(is (= (merge (default-options) jdbc/snake-kebab-opts)
|
(is (= (merge (default-options) jdbc/snake-kebab-opts)
|
||||||
(:options ds')))
|
(:options ds')))
|
||||||
(is (= "red" (:fruit/looks-like
|
(is (= "red" ((col-kw :fruit/looks-like)
|
||||||
(jdbc/execute-one!
|
(jdbc/execute-one!
|
||||||
ds'
|
ds'
|
||||||
["select appearance as looks_like from fruit where id = ?" 1])))))
|
[(str "select appearance as looks_like from fruit where " (index) " = ?") 1])))))
|
||||||
(is (= "red" (:looks-like
|
(is (= "red" (:looks-like
|
||||||
(jdbc/execute-one!
|
(jdbc/execute-one!
|
||||||
ds-opts
|
ds-opts
|
||||||
["select appearance as looks_like from fruit where id = ?" 1]
|
[(str "select appearance as looks_like from fruit where " (index) " = ?") 1]
|
||||||
jdbc/unqualified-snake-kebab-opts)))))
|
jdbc/unqualified-snake-kebab-opts)))))
|
||||||
(testing "execute!"
|
(testing "execute!"
|
||||||
(let [rs (jdbc/execute!
|
(let [rs (jdbc/execute!
|
||||||
|
|
@ -95,7 +99,7 @@
|
||||||
(is (= 1 ((column :FRUIT/ID) (first rs)))))
|
(is (= 1 ((column :FRUIT/ID) (first rs)))))
|
||||||
(let [rs (jdbc/execute!
|
(let [rs (jdbc/execute!
|
||||||
ds-opts
|
ds-opts
|
||||||
["select * from fruit order by id"]
|
[(str "select * from fruit order by " (index))]
|
||||||
{:builder-fn rs/as-maps})]
|
{:builder-fn rs/as-maps})]
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
(is (every? meta rs))
|
(is (every? meta rs))
|
||||||
|
|
@ -104,22 +108,23 @@
|
||||||
(is (= 4 ((column :FRUIT/ID) (last rs)))))
|
(is (= 4 ((column :FRUIT/ID) (last rs)))))
|
||||||
(let [rs (jdbc/execute!
|
(let [rs (jdbc/execute!
|
||||||
ds-opts
|
ds-opts
|
||||||
["select * from fruit order by id"]
|
[(str "select * from fruit order by " (index))]
|
||||||
{:builder-fn rs/as-arrays})]
|
{:builder-fn rs/as-arrays})]
|
||||||
(is (every? vector? rs))
|
(is (every? vector? rs))
|
||||||
(is (= 5 (count rs)))
|
(is (= 5 (count rs)))
|
||||||
(is (every? #(= 5 (count %)) rs))
|
(is (every? #(= 5 (count %)) rs))
|
||||||
;; columns come first
|
;; columns come first
|
||||||
(is (every? qualified-keyword? (first rs)))
|
(is (every? (if (xtdb?) keyword? qualified-keyword?) (first rs)))
|
||||||
;; :FRUIT/ID should be first column
|
;; :FRUIT/ID should be first column
|
||||||
(is (= (column :FRUIT/ID) (ffirst rs)))
|
(is (= (column :FRUIT/ID) (ffirst rs)))
|
||||||
;; and all its corresponding values should be ints
|
;; and all its corresponding values should be ints
|
||||||
(is (every? int? (map first (rest rs))))
|
(is (every? int? (map first (rest rs))))
|
||||||
(is (every? string? (map second (rest rs))))))
|
(let [n (max (.indexOf ^java.util.List (first rs) :name) 1)]
|
||||||
|
(is (every? string? (map #(nth % n) (rest rs)))))))
|
||||||
(testing "execute! with adapter"
|
(testing "execute! with adapter"
|
||||||
(let [rs (jdbc/execute! ; test again, with adapter and lower columns
|
(let [rs (jdbc/execute! ; test again, with adapter and lower columns
|
||||||
ds-opts
|
ds-opts
|
||||||
["select * from fruit order by id"]
|
[(str "select * from fruit order by " (index))]
|
||||||
{:builder-fn (rs/as-arrays-adapter
|
{:builder-fn (rs/as-arrays-adapter
|
||||||
rs/as-lower-arrays
|
rs/as-lower-arrays
|
||||||
(fn [^ResultSet rs _ ^Integer i]
|
(fn [^ResultSet rs _ ^Integer i]
|
||||||
|
|
@ -128,16 +133,17 @@
|
||||||
(is (= 5 (count rs)))
|
(is (= 5 (count rs)))
|
||||||
(is (every? #(= 5 (count %)) rs))
|
(is (every? #(= 5 (count %)) rs))
|
||||||
;; columns come first
|
;; columns come first
|
||||||
(is (every? qualified-keyword? (first rs)))
|
(is (every? (if (xtdb?) keyword? qualified-keyword?) (first rs)))
|
||||||
;; :fruit/id should be first column
|
;; :fruit/id should be first column
|
||||||
(is (= :fruit/id (ffirst rs)))
|
(is (= (col-kw :fruit/id) (ffirst rs)))
|
||||||
;; and all its corresponding values should be ints
|
;; and all its corresponding values should be ints
|
||||||
(is (every? int? (map first (rest rs))))
|
(is (every? int? (map first (rest rs))))
|
||||||
(is (every? string? (map second (rest rs))))))
|
(let [n (max (.indexOf ^java.util.List (first rs) :name) 1)]
|
||||||
|
(is (every? string? (map #(nth % n) (rest rs)))))))
|
||||||
(testing "execute! with unqualified"
|
(testing "execute! with unqualified"
|
||||||
(let [rs (jdbc/execute!
|
(let [rs (jdbc/execute!
|
||||||
(ds)
|
(ds)
|
||||||
["select * from fruit order by id"]
|
[(str "select * from fruit order by " (index))]
|
||||||
{:builder-fn rs/as-unqualified-maps})]
|
{:builder-fn rs/as-unqualified-maps})]
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
(is (every? meta rs))
|
(is (every? meta rs))
|
||||||
|
|
@ -146,7 +152,7 @@
|
||||||
(is (= 4 ((column :ID) (last rs)))))
|
(is (= 4 ((column :ID) (last rs)))))
|
||||||
(let [rs (jdbc/execute!
|
(let [rs (jdbc/execute!
|
||||||
ds-opts
|
ds-opts
|
||||||
["select * from fruit order by id"]
|
[(str "select * from fruit order by " (index))]
|
||||||
{:builder-fn rs/as-unqualified-arrays})]
|
{:builder-fn rs/as-unqualified-arrays})]
|
||||||
(is (every? vector? rs))
|
(is (every? vector? rs))
|
||||||
(is (= 5 (count rs)))
|
(is (= 5 (count rs)))
|
||||||
|
|
@ -157,11 +163,12 @@
|
||||||
(is (= (column :ID) (ffirst rs)))
|
(is (= (column :ID) (ffirst rs)))
|
||||||
;; and all its corresponding values should be ints
|
;; and all its corresponding values should be ints
|
||||||
(is (every? int? (map first (rest rs))))
|
(is (every? int? (map first (rest rs))))
|
||||||
(is (every? string? (map second (rest rs))))))
|
(let [n (max (.indexOf ^java.util.List (first rs) :name) 1)]
|
||||||
|
(is (every? string? (map #(nth % n) (rest rs)))))))
|
||||||
(testing "execute! with :max-rows / :maxRows"
|
(testing "execute! with :max-rows / :maxRows"
|
||||||
(let [rs (jdbc/execute!
|
(let [rs (jdbc/execute!
|
||||||
ds-opts
|
ds-opts
|
||||||
["select * from fruit order by id"]
|
[(str "select * from fruit order by " (index))]
|
||||||
{:max-rows 2})]
|
{:max-rows 2})]
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
(is (every? meta rs))
|
(is (every? meta rs))
|
||||||
|
|
@ -170,7 +177,7 @@
|
||||||
(is (= 2 ((column :FRUIT/ID) (last rs)))))
|
(is (= 2 ((column :FRUIT/ID) (last rs)))))
|
||||||
(let [rs (jdbc/execute!
|
(let [rs (jdbc/execute!
|
||||||
ds-opts
|
ds-opts
|
||||||
["select * from fruit order by id"]
|
[(str "select * from fruit order by " (index))]
|
||||||
{:statement {:maxRows 2}})]
|
{:statement {:maxRows 2}})]
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
(is (every? meta rs))
|
(is (every? meta rs))
|
||||||
|
|
@ -182,7 +189,7 @@
|
||||||
(let [rs (with-open [con (jdbc/get-connection (ds))
|
(let [rs (with-open [con (jdbc/get-connection (ds))
|
||||||
ps (jdbc/prepare
|
ps (jdbc/prepare
|
||||||
con
|
con
|
||||||
["select * from fruit order by id"]
|
[(str "select * from fruit order by " (index))]
|
||||||
(default-options))]
|
(default-options))]
|
||||||
(jdbc/execute! ps))]
|
(jdbc/execute! ps))]
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
|
|
@ -194,7 +201,7 @@
|
||||||
(let [rs (with-open [con (jdbc/get-connection (ds))
|
(let [rs (with-open [con (jdbc/get-connection (ds))
|
||||||
ps (jdbc/prepare
|
ps (jdbc/prepare
|
||||||
con
|
con
|
||||||
["select * from fruit where id = ?"]
|
[(str "select * from fruit where " (index) " = ?")]
|
||||||
(default-options))]
|
(default-options))]
|
||||||
(jdbc/execute! (prep/set-parameters ps [4]) nil {}))]
|
(jdbc/execute! (prep/set-parameters ps [4]) nil {}))]
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
|
|
@ -205,7 +212,7 @@
|
||||||
;; default options do not flow over get-connection
|
;; default options do not flow over get-connection
|
||||||
(let [rs (with-open [con (jdbc/get-connection (ds))]
|
(let [rs (with-open [con (jdbc/get-connection (ds))]
|
||||||
(jdbc/execute! (prep/statement con (default-options))
|
(jdbc/execute! (prep/statement con (default-options))
|
||||||
["select * from fruit order by id"]))]
|
[(str "select * from fruit order by " (index))]))]
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
(is (every? meta rs))
|
(is (every? meta rs))
|
||||||
(is (= 4 (count rs)))
|
(is (= 4 (count rs)))
|
||||||
|
|
@ -214,11 +221,12 @@
|
||||||
;; default options do not flow over get-connection
|
;; default options do not flow over get-connection
|
||||||
(let [rs (with-open [con (jdbc/get-connection (ds))]
|
(let [rs (with-open [con (jdbc/get-connection (ds))]
|
||||||
(jdbc/execute! (prep/statement con (default-options))
|
(jdbc/execute! (prep/statement con (default-options))
|
||||||
["select * from fruit where id = 4"]))]
|
[(str "select * from fruit where " (index) " = 4")]))]
|
||||||
(is (every? map? rs))
|
(is (every? map? rs))
|
||||||
(is (every? meta rs))
|
(is (every? meta rs))
|
||||||
(is (= 1 (count rs)))
|
(is (= 1 (count rs)))
|
||||||
(is (= 4 ((column :FRUIT/ID) (first rs))))))
|
(is (= 4 ((column :FRUIT/ID) (first rs))))))
|
||||||
|
(when-not (xtdb?)
|
||||||
(testing "transact"
|
(testing "transact"
|
||||||
(is (= [{:next.jdbc/update-count 1}]
|
(is (= [{:next.jdbc/update-count 1}]
|
||||||
(jdbc/transact (ds)
|
(jdbc/transact (ds)
|
||||||
|
|
@ -233,6 +241,7 @@ VALUES ('Pear', 'green', 49, 47)
|
||||||
(is (= [{:next.jdbc/update-count 1}]
|
(is (= [{:next.jdbc/update-count 1}]
|
||||||
(jdbc/with-transaction [t (ds) {:rollback-only true}]
|
(jdbc/with-transaction [t (ds) {:rollback-only true}]
|
||||||
(is (jdbc/active-tx?) "should be in a transaction")
|
(is (jdbc/active-tx?) "should be in a transaction")
|
||||||
|
(is (jdbc/active-tx? t) "connection should be in a transaction")
|
||||||
(jdbc/execute! t ["
|
(jdbc/execute! t ["
|
||||||
INSERT INTO fruit (name, appearance, cost, grade)
|
INSERT INTO fruit (name, appearance, cost, grade)
|
||||||
VALUES ('Pear', 'green', 49, 47)
|
VALUES ('Pear', 'green', 49, 47)
|
||||||
|
|
@ -244,6 +253,7 @@ VALUES ('Pear', 'green', 49, 47)
|
||||||
(is (= [{:next.jdbc/update-count 1}]
|
(is (= [{:next.jdbc/update-count 1}]
|
||||||
(jdbc/with-transaction [t con {:rollback-only true}]
|
(jdbc/with-transaction [t con {:rollback-only true}]
|
||||||
(is (jdbc/active-tx?) "should be in a transaction")
|
(is (jdbc/active-tx?) "should be in a transaction")
|
||||||
|
(is (jdbc/active-tx? t) "connection should be in a transaction")
|
||||||
(jdbc/execute! t ["
|
(jdbc/execute! t ["
|
||||||
INSERT INTO fruit (name, appearance, cost, grade)
|
INSERT INTO fruit (name, appearance, cost, grade)
|
||||||
VALUES ('Pear', 'green', 49, 47)
|
VALUES ('Pear', 'green', 49, 47)
|
||||||
|
|
@ -258,6 +268,7 @@ INSERT INTO fruit (name, appearance, cost, grade)
|
||||||
VALUES ('Pear', 'green', 49, 47)
|
VALUES ('Pear', 'green', 49, 47)
|
||||||
"])
|
"])
|
||||||
(is (jdbc/active-tx?) "should be in a transaction")
|
(is (jdbc/active-tx?) "should be in a transaction")
|
||||||
|
(is (jdbc/active-tx? t) "connection should be in a transaction")
|
||||||
(throw (ex-info "abort" {})))))
|
(throw (ex-info "abort" {})))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
(is (not (jdbc/active-tx?)) "should not be in a transaction")
|
(is (not (jdbc/active-tx?)) "should not be in a transaction")
|
||||||
|
|
@ -270,6 +281,7 @@ INSERT INTO fruit (name, appearance, cost, grade)
|
||||||
VALUES ('Pear', 'green', 49, 47)
|
VALUES ('Pear', 'green', 49, 47)
|
||||||
"])
|
"])
|
||||||
(is (jdbc/active-tx?) "should be in a transaction")
|
(is (jdbc/active-tx?) "should be in a transaction")
|
||||||
|
(is (jdbc/active-tx? t) "connection should be in a transaction")
|
||||||
(throw (ex-info "abort" {})))))
|
(throw (ex-info "abort" {})))))
|
||||||
(is (= 4 (count (jdbc/execute! con ["select * from fruit"]))))
|
(is (= 4 (count (jdbc/execute! con ["select * from fruit"]))))
|
||||||
(is (= ac (.getAutoCommit con))))))
|
(is (= ac (.getAutoCommit con))))))
|
||||||
|
|
@ -283,6 +295,7 @@ VALUES ('Pear', 'green', 49, 47)
|
||||||
(.rollback t)
|
(.rollback t)
|
||||||
;; still in a next.jdbc TX even tho' we rolled back!
|
;; still in a next.jdbc TX even tho' we rolled back!
|
||||||
(is (jdbc/active-tx?) "should be in a transaction")
|
(is (jdbc/active-tx?) "should be in a transaction")
|
||||||
|
(is (jdbc/active-tx? t) "connection should be in a transaction")
|
||||||
result))))
|
result))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
(is (not (jdbc/active-tx?)) "should not be in a transaction")
|
(is (not (jdbc/active-tx?)) "should not be in a transaction")
|
||||||
|
|
@ -309,6 +322,7 @@ VALUES ('Pear', 'green', 49, 47)
|
||||||
(.rollback t save-point)
|
(.rollback t save-point)
|
||||||
;; still in a next.jdbc TX even tho' we rolled back to a save point!
|
;; still in a next.jdbc TX even tho' we rolled back to a save point!
|
||||||
(is (jdbc/active-tx?) "should be in a transaction")
|
(is (jdbc/active-tx?) "should be in a transaction")
|
||||||
|
(is (jdbc/active-tx? t) "connection should be in a transaction")
|
||||||
result))))
|
result))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
(is (not (jdbc/active-tx?)) "should not be in a transaction")
|
(is (not (jdbc/active-tx?)) "should not be in a transaction")
|
||||||
|
|
@ -348,11 +362,12 @@ VALUES ('Pear', 'green', 49, 47)
|
||||||
(.rollback t save-point)
|
(.rollback t save-point)
|
||||||
result))))
|
result))))
|
||||||
(is (= 4 (count (jdbc/execute! con ["select * from fruit"]))))
|
(is (= 4 (count (jdbc/execute! con ["select * from fruit"]))))
|
||||||
(is (= ac (.getAutoCommit con)))))))
|
(is (= ac (.getAutoCommit con))))))))
|
||||||
|
|
||||||
(deftest issue-146
|
(deftest issue-146
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
;; since we use an embedded PostgreSQL data source, we skip this:
|
;; since we use an embedded PostgreSQL data source, we skip this:
|
||||||
(when-not (or (postgres?)
|
(when-not (or (postgres?) (xtdb?)
|
||||||
;; and now we skip MS SQL because we can't use the db-spec
|
;; and now we skip MS SQL because we can't use the db-spec
|
||||||
;; we'd need to build the jdbcUrl with encryption turned off:
|
;; we'd need to build the jdbcUrl with encryption turned off:
|
||||||
(and (mssql?) (not (jtds?))))
|
(and (mssql?) (not (jtds?))))
|
||||||
|
|
@ -467,6 +482,7 @@ VALUES ('Pear', 'green', 49, 47)
|
||||||
|
|
||||||
#_
|
#_
|
||||||
(deftest duplicate-insert-test
|
(deftest duplicate-insert-test
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
;; this is primarily a look at exception types/information for #226
|
;; this is primarily a look at exception types/information for #226
|
||||||
(try
|
(try
|
||||||
(jdbc/execute! (ds) ["
|
(jdbc/execute! (ds) ["
|
||||||
|
|
@ -489,14 +505,42 @@ VALUES ('Pear', 'green', 49, 47)
|
||||||
"\n\t" (ex-message t)))))
|
"\n\t" (ex-message t)))))
|
||||||
|
|
||||||
(deftest bool-tests
|
(deftest bool-tests
|
||||||
|
{:context [(around [f] (with-test-db f))]} ;; Ensure the test database is used
|
||||||
|
(testing (str "bool-tests for " (:dbtype (db)))
|
||||||
|
(let [lit-t (cond (hsqldb?) "(1=1)" (mssql?) "1" :else "TRUE")
|
||||||
|
lit-f (cond (hsqldb?) "(1=0)" (mssql?) "0" :else "FALSE")]
|
||||||
|
(when-not (or (hsqldb?) (derby?))
|
||||||
|
(testing "literal TRUE select"
|
||||||
|
(is (= {(column :V) (if (or (sqlite?) (mysql?) (mssql?)) 1 true)}
|
||||||
|
(jdbc/execute-one! (ds) [(str "SELECT " lit-t " AS V")]))))
|
||||||
|
(testing "literal FALSE select"
|
||||||
|
(is (= {(column :V) (if (or (sqlite?) (mysql?) (mssql?)) 0 false)}
|
||||||
|
(jdbc/execute-one! (ds) [(str "SELECT " lit-f " AS V")])))))
|
||||||
|
(testing "literal TRUE insert"
|
||||||
|
(jdbc/execute-one!
|
||||||
|
(ds)
|
||||||
|
[(str "insert into btest (" (if (xtdb?) "_id" "name") ",is_it,twiddle)"
|
||||||
|
" values ('lit_t'," lit-t ","
|
||||||
|
(if (or (derby?) (sqlite?) (h2?) (mssql?) (xtdb?)) "1" "b'1'")
|
||||||
|
")")]))
|
||||||
|
(testing "literal FALSE insert"
|
||||||
|
(jdbc/execute-one!
|
||||||
|
(ds)
|
||||||
|
[(str "insert into btest (" (if (xtdb?) "_id" "name") ",is_it,twiddle)"
|
||||||
|
" values ('lit_f'," lit-f ","
|
||||||
|
(if (or (derby?) (sqlite?) (h2?) (mssql?) (xtdb?)) "0" "b'0'")
|
||||||
|
")")]))
|
||||||
|
(testing "BIT/BOOLEAN value insertion"
|
||||||
(doseq [[n b] [["zero" 0] ["one" 1] ["false" false] ["true" true]]
|
(doseq [[n b] [["zero" 0] ["one" 1] ["false" false] ["true" true]]
|
||||||
:let [v-bit (if (number? b) b (if b 1 0))
|
:let [v-bit (if (number? b) b (if b 1 0))
|
||||||
v-bool (if (number? b) (pos? b) b)]]
|
v-bool (if (number? b) (pos? b) b)]]
|
||||||
(jdbc/execute-one!
|
(jdbc/execute-one!
|
||||||
(ds)
|
(ds)
|
||||||
["insert into btest (name,is_it,twiddle) values (?,?,?)"
|
[(str "insert into btest ("
|
||||||
|
(if (xtdb?) "_id" "name")
|
||||||
|
",is_it,twiddle) values (?,?,?)")
|
||||||
n
|
n
|
||||||
(if (postgres?)
|
(if (or (postgres?) (xtdb?))
|
||||||
(types/as-boolean b)
|
(types/as-boolean b)
|
||||||
b) ; 0, 1, false, true are all acceptable
|
b) ; 0, 1, false, true are all acceptable
|
||||||
(cond (hsqldb?)
|
(cond (hsqldb?)
|
||||||
|
|
@ -504,18 +548,20 @@ VALUES ('Pear', 'green', 49, 47)
|
||||||
(postgres?)
|
(postgres?)
|
||||||
(types/as-other v-bit) ; really postgres??
|
(types/as-other v-bit) ; really postgres??
|
||||||
:else
|
:else
|
||||||
v-bit)]))
|
v-bit)])))
|
||||||
|
(testing "BOOLEAN results from SELECT"
|
||||||
(let [data (jdbc/execute! (ds) ["select * from btest"]
|
(let [data (jdbc/execute! (ds) ["select * from btest"]
|
||||||
(default-options))]
|
(default-options))]
|
||||||
(if (sqlite?)
|
(if (sqlite?)
|
||||||
(is (every? number? (map (column :BTEST/IS_IT) data)))
|
(is (every? number? (map (column :BTEST/IS_IT) data)))
|
||||||
(is (every? boolean? (map (column :BTEST/IS_IT) data))))
|
(is (every? boolean? (map (column :BTEST/IS_IT) data))))
|
||||||
(if (or (sqlite?) (derby?))
|
(if (or (sqlite?) (derby?) (xtdb?))
|
||||||
(is (every? number? (map (column :BTEST/TWIDDLE) data)))
|
(is (every? number? (map (column :BTEST/TWIDDLE) data)))
|
||||||
(is (every? boolean? (map (column :BTEST/TWIDDLE) data)))))
|
(is (every? boolean? (map (column :BTEST/TWIDDLE) data))))))
|
||||||
|
(testing "BOOLEAN read column by index"
|
||||||
(let [data (jdbc/execute! (ds) ["select * from btest"]
|
(let [data (jdbc/execute! (ds) ["select * from btest"]
|
||||||
(cond-> (default-options)
|
(cond-> (default-options)
|
||||||
(sqlite?)
|
(or (sqlite?) (xtdb?))
|
||||||
(assoc :builder-fn
|
(assoc :builder-fn
|
||||||
(rs/builder-adapter
|
(rs/builder-adapter
|
||||||
rs/as-maps
|
rs/as-maps
|
||||||
|
|
@ -523,8 +569,11 @@ VALUES ('Pear', 'green', 49, 47)
|
||||||
(let [rsm ^ResultSetMetaData (:rsmeta builder)]
|
(let [rsm ^ResultSetMetaData (:rsmeta builder)]
|
||||||
(rs/read-column-by-index
|
(rs/read-column-by-index
|
||||||
;; we only use bit and bool for
|
;; we only use bit and bool for
|
||||||
;; sqlite (not boolean)
|
;; sqlite (not boolean), and
|
||||||
(if (#{"BIT" "BOOL"} (.getColumnTypeName rsm i))
|
;; int8 and bool for xtdb:
|
||||||
|
(if (#{"BIT" "BOOL"
|
||||||
|
"int8" "bool"}
|
||||||
|
(.getColumnTypeName rsm i))
|
||||||
(.getBoolean rs i)
|
(.getBoolean rs i)
|
||||||
(.getObject rs i))
|
(.getObject rs i))
|
||||||
rsm
|
rsm
|
||||||
|
|
@ -532,19 +581,22 @@ VALUES ('Pear', 'green', 49, 47)
|
||||||
(is (every? boolean? (map (column :BTEST/IS_IT) data)))
|
(is (every? boolean? (map (column :BTEST/IS_IT) data)))
|
||||||
(if (derby?)
|
(if (derby?)
|
||||||
(is (every? number? (map (column :BTEST/TWIDDLE) data)))
|
(is (every? number? (map (column :BTEST/TWIDDLE) data)))
|
||||||
(is (every? boolean? (map (column :BTEST/TWIDDLE) data)))))
|
(is (every? boolean? (map (column :BTEST/TWIDDLE) data))))))
|
||||||
|
(testing "BOOLEAN via coercion"
|
||||||
(let [data (reduce (fn [acc row]
|
(let [data (reduce (fn [acc row]
|
||||||
(conj acc (cond-> (select-keys row [:is_it :twiddle])
|
(conj acc (cond-> (select-keys row [:is_it :twiddle])
|
||||||
(sqlite?)
|
(sqlite?)
|
||||||
(update :is_it pos?)
|
(update :is_it pos?)
|
||||||
(or (sqlite?) (derby?))
|
(or (sqlite?) (derby?) (xtdb?))
|
||||||
(update :twiddle pos?))))
|
(update :twiddle pos?))))
|
||||||
[]
|
[]
|
||||||
(jdbc/plan (ds) ["select * from btest"]))]
|
(jdbc/plan (ds) ["select * from btest"]))]
|
||||||
(is (every? boolean? (map :is_it data)))
|
(is (every? boolean? (map :is_it data)))
|
||||||
(is (every? boolean? (map :twiddle data)))))
|
(is (every? boolean? (map :twiddle data))))))))
|
||||||
|
|
||||||
(deftest execute-batch-tests
|
(deftest execute-batch-tests
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
|
(when-not (xtdb?)
|
||||||
(testing "simple batch insert"
|
(testing "simple batch insert"
|
||||||
(is (= [1 1 1 1 1 1 1 1 1 13]
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
(jdbc/with-transaction [t (ds) {:rollback-only true}]
|
(jdbc/with-transaction [t (ds) {:rollback-only true}]
|
||||||
|
|
@ -643,9 +695,11 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
;; Derby and SQLite only return one generated key per batch so there
|
;; Derby and SQLite only return one generated key per batch so there
|
||||||
;; are only three keys, plus the overall count here:
|
;; are only three keys, plus the overall count here:
|
||||||
(is (< 3 (count results))))
|
(is (< 3 (count results))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))))
|
||||||
|
|
||||||
(deftest execute-batch-connectable-tests
|
(deftest execute-batch-connectable-tests
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
|
(when-not (xtdb?)
|
||||||
(testing "simple batch insert"
|
(testing "simple batch insert"
|
||||||
(is (= [1 1 1 1 1 1 1 1 1 13]
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
(try
|
(try
|
||||||
|
|
@ -663,7 +717,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
{})]
|
{})]
|
||||||
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
(finally
|
(finally
|
||||||
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
||||||
(testing "batch with-options"
|
(testing "batch with-options"
|
||||||
(is (= [1 1 1 1 1 1 1 1 1 13]
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
|
|
@ -682,12 +736,19 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
{})]
|
{})]
|
||||||
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
(finally
|
(finally
|
||||||
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
||||||
(testing "batch with-logging"
|
(testing "batch with-logging"
|
||||||
|
(let [tracker (atom {:opts 0, :log1 0 :log2 0})
|
||||||
|
;; opts and log2 never get invoked with batch/prepare -- because
|
||||||
|
;; no fn opts are invoked on that path, and no post-logging is done:
|
||||||
|
track-fn (fn ([k] (fn [& _] (swap! tracker update k inc)))
|
||||||
|
([k f] (fn [& args] (swap! tracker update k inc) (apply f args))))]
|
||||||
|
(is (= {:opts 0, :log1 0 :log2 0} @tracker))
|
||||||
(is (= [1 1 1 1 1 1 1 1 1 13]
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
(try
|
(try
|
||||||
(let [result (jdbc/execute-batch! (jdbc/with-logging (ds) println println)
|
;; wrapping a non-connection will lose the wrapper:
|
||||||
|
(let [result (jdbc/execute-batch! (jdbc/with-options (ds) {:builder-fn (track-fn :opts rs/as-maps)})
|
||||||
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
|
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
|
||||||
[["fruit1" "one"]
|
[["fruit1" "one"]
|
||||||
["fruit2" "two"]
|
["fruit2" "two"]
|
||||||
|
|
@ -701,8 +762,109 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
{})]
|
{})]
|
||||||
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
(finally
|
(finally
|
||||||
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
(is (= {:opts 0, :log1 0 :log2 0} @tracker))
|
||||||
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
|
(try
|
||||||
|
;; wrapping a non-connection will lose the wrapper:
|
||||||
|
(let [result (jdbc/execute-batch! (jdbc/with-logging (ds) {:builder-fn (track-fn :opts rs/as-maps)})
|
||||||
|
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
|
||||||
|
[["fruit1" "one"]
|
||||||
|
["fruit2" "two"]
|
||||||
|
["fruit3" "three"]
|
||||||
|
["fruit4" "four"]
|
||||||
|
["fruit5" "five"]
|
||||||
|
["fruit6" "six"]
|
||||||
|
["fruit7" "seven"]
|
||||||
|
["fruit8" "eight"]
|
||||||
|
["fruit9" "nine"]]
|
||||||
|
{})]
|
||||||
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
|
(finally
|
||||||
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
|
(is (= {:opts 0, :log1 0 :log2 0} @tracker))
|
||||||
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
|
(try
|
||||||
|
(with-open [con (jdbc/get-connection (ds))]
|
||||||
|
(let [result (jdbc/execute-batch! (jdbc/with-options con {:builder-fn (track-fn :opts rs/as-maps)})
|
||||||
|
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
|
||||||
|
[["fruit1" "one"]
|
||||||
|
["fruit2" "two"]
|
||||||
|
["fruit3" "three"]
|
||||||
|
["fruit4" "four"]
|
||||||
|
["fruit5" "five"]
|
||||||
|
["fruit6" "six"]
|
||||||
|
["fruit7" "seven"]
|
||||||
|
["fruit8" "eight"]
|
||||||
|
["fruit9" "nine"]]
|
||||||
|
{})]
|
||||||
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
||||||
|
(finally
|
||||||
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
|
(is (= {:opts 0, :log1 0 :log2 0} @tracker))
|
||||||
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
|
(try
|
||||||
|
(with-open [con (jdbc/get-connection (ds))]
|
||||||
|
(let [result (jdbc/execute-batch! (jdbc/with-logging con (track-fn :log1) (track-fn :log2))
|
||||||
|
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
|
||||||
|
[["fruit1" "one"]
|
||||||
|
["fruit2" "two"]
|
||||||
|
["fruit3" "three"]
|
||||||
|
["fruit4" "four"]
|
||||||
|
["fruit5" "five"]
|
||||||
|
["fruit6" "six"]
|
||||||
|
["fruit7" "seven"]
|
||||||
|
["fruit8" "eight"]
|
||||||
|
["fruit9" "nine"]]
|
||||||
|
{})]
|
||||||
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
||||||
|
(finally
|
||||||
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
|
(is (= {:opts 0, :log1 1 :log2 0} @tracker))
|
||||||
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
|
(try
|
||||||
|
(with-open [con (jdbc/get-connection (ds))]
|
||||||
|
(let [result (jdbc/execute-batch! (jdbc/with-options
|
||||||
|
(jdbc/with-logging con (track-fn :log1) (track-fn :log2))
|
||||||
|
{:builder-fn (track-fn :opts rs/as-maps)})
|
||||||
|
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
|
||||||
|
[["fruit1" "one"]
|
||||||
|
["fruit2" "two"]
|
||||||
|
["fruit3" "three"]
|
||||||
|
["fruit4" "four"]
|
||||||
|
["fruit5" "five"]
|
||||||
|
["fruit6" "six"]
|
||||||
|
["fruit7" "seven"]
|
||||||
|
["fruit8" "eight"]
|
||||||
|
["fruit9" "nine"]]
|
||||||
|
{})]
|
||||||
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
||||||
|
(finally
|
||||||
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
|
(is (= {:opts 0, :log1 2 :log2 0} @tracker))
|
||||||
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
|
(try
|
||||||
|
(with-open [con (jdbc/get-connection (ds))]
|
||||||
|
(let [result (jdbc/execute-batch! (jdbc/with-logging
|
||||||
|
(jdbc/with-options con
|
||||||
|
{:builder-fn (track-fn :opts rs/as-maps)})
|
||||||
|
(track-fn :log1) (track-fn :log2))
|
||||||
|
"INSERT INTO fruit (name, appearance) VALUES (?,?)"
|
||||||
|
[["fruit1" "one"]
|
||||||
|
["fruit2" "two"]
|
||||||
|
["fruit3" "three"]
|
||||||
|
["fruit4" "four"]
|
||||||
|
["fruit5" "five"]
|
||||||
|
["fruit6" "six"]
|
||||||
|
["fruit7" "seven"]
|
||||||
|
["fruit8" "eight"]
|
||||||
|
["fruit9" "nine"]]
|
||||||
|
{})]
|
||||||
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
||||||
|
(finally
|
||||||
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
|
(is (= {:opts 0, :log1 3 :log2 0} @tracker))
|
||||||
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))
|
||||||
(testing "small batch insert"
|
(testing "small batch insert"
|
||||||
(is (= [1 1 1 1 1 1 1 1 1 13]
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
(try
|
(try
|
||||||
|
|
@ -720,7 +882,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
{:batch-size 3})]
|
{:batch-size 3})]
|
||||||
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
(finally
|
(finally
|
||||||
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
||||||
(testing "big batch insert"
|
(testing "big batch insert"
|
||||||
(is (= [1 1 1 1 1 1 1 1 1 13]
|
(is (= [1 1 1 1 1 1 1 1 1 13]
|
||||||
|
|
@ -739,7 +901,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
{:batch-size 8})]
|
{:batch-size 8})]
|
||||||
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
(finally
|
(finally
|
||||||
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
|
||||||
(testing "large batch insert"
|
(testing "large batch insert"
|
||||||
(when-not (or (jtds?) (sqlite?))
|
(when-not (or (jtds?) (sqlite?))
|
||||||
|
|
@ -760,7 +922,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
:large true})]
|
:large true})]
|
||||||
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
(finally
|
(finally
|
||||||
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))
|
||||||
(testing "return generated keys"
|
(testing "return generated keys"
|
||||||
(when-not (or (mssql?) (sqlite?))
|
(when-not (or (mssql?) (sqlite?))
|
||||||
|
|
@ -784,26 +946,31 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
:return-generated-keys true})]
|
:return-generated-keys true})]
|
||||||
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
|
||||||
(finally
|
(finally
|
||||||
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))]
|
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))]
|
||||||
(is (= 13 (last results)))
|
(is (= 13 (last results)))
|
||||||
(is (every? map? (butlast results)))
|
(is (every? map? (butlast results)))
|
||||||
;; Derby and SQLite only return one generated key per batch so there
|
;; Derby and SQLite only return one generated key per batch so there
|
||||||
;; are only three keys, plus the overall count here:
|
;; are only three keys, plus the overall count here:
|
||||||
(is (< 3 (count results))))
|
(is (< 3 (count results))))
|
||||||
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))))
|
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))))
|
||||||
|
|
||||||
(deftest folding-test
|
(deftest folding-test
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(jdbc/execute-one! (ds) ["delete from fruit"])
|
(jdbc/execute-one! (ds) ["delete from fruit"])
|
||||||
|
(if (xtdb?)
|
||||||
|
(with-open [con (jdbc/get-connection (ds))
|
||||||
|
ps (jdbc/prepare con ["insert into fruit(_id,name) values (?,?)"])]
|
||||||
|
(jdbc/execute-batch! ps (mapv #(vector % (str "Fruit-" %)) (range 1 1001))))
|
||||||
(with-open [con (jdbc/get-connection (ds))
|
(with-open [con (jdbc/get-connection (ds))
|
||||||
ps (jdbc/prepare con ["insert into fruit(name) values (?)"])]
|
ps (jdbc/prepare con ["insert into fruit(name) values (?)"])]
|
||||||
(jdbc/execute-batch! ps (mapv #(vector (str "Fruit-" %)) (range 1 1001))))
|
(jdbc/execute-batch! ps (mapv #(vector (str "Fruit-" %)) (range 1 1001)))))
|
||||||
(testing "foldable result set"
|
(testing "foldable result set"
|
||||||
(testing "from a Connection"
|
(testing "from a Connection"
|
||||||
(let [result
|
(let [result
|
||||||
(with-open [con (jdbc/get-connection (ds))]
|
(with-open [con (jdbc/get-connection (ds))]
|
||||||
(r/foldcat
|
(r/foldcat
|
||||||
(r/map (column :FRUIT/NAME)
|
(r/map (column :FRUIT/NAME)
|
||||||
(jdbc/plan con ["select * from fruit order by id"]
|
(jdbc/plan con [(str "select * from fruit order by " (index))]
|
||||||
(default-options)))))]
|
(default-options)))))]
|
||||||
(is (= 1000 (count result)))
|
(is (= 1000 (count result)))
|
||||||
(is (= "Fruit-1" (first result)))
|
(is (= "Fruit-1" (first result)))
|
||||||
|
|
@ -815,7 +982,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
(try
|
(try
|
||||||
(r/fold n r/cat r/append!
|
(r/fold n r/cat r/append!
|
||||||
(r/map (column :FRUIT/NAME)
|
(r/map (column :FRUIT/NAME)
|
||||||
(jdbc/plan (ds) ["select * from fruit order by id"]
|
(jdbc/plan (ds) [(str "select * from fruit order by " (index))]
|
||||||
(default-options))))
|
(default-options))))
|
||||||
(catch java.util.concurrent.RejectedExecutionException _
|
(catch java.util.concurrent.RejectedExecutionException _
|
||||||
[]))]
|
[]))]
|
||||||
|
|
@ -826,7 +993,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
(let [result
|
(let [result
|
||||||
(with-open [con (jdbc/get-connection (ds))
|
(with-open [con (jdbc/get-connection (ds))
|
||||||
stmt (jdbc/prepare con
|
stmt (jdbc/prepare con
|
||||||
["select * from fruit order by id"]
|
[(str "select * from fruit order by " (index))]
|
||||||
(default-options))]
|
(default-options))]
|
||||||
(r/foldcat
|
(r/foldcat
|
||||||
(r/map (column :FRUIT/NAME)
|
(r/map (column :FRUIT/NAME)
|
||||||
|
|
@ -840,15 +1007,16 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
stmt (prep/statement con (default-options))]
|
stmt (prep/statement con (default-options))]
|
||||||
(r/foldcat
|
(r/foldcat
|
||||||
(r/map (column :FRUIT/NAME)
|
(r/map (column :FRUIT/NAME)
|
||||||
(jdbc/plan stmt ["select * from fruit order by id"]
|
(jdbc/plan stmt [(str "select * from fruit order by " (index))]
|
||||||
(default-options)))))]
|
(default-options)))))]
|
||||||
(is (= 1000 (count result)))
|
(is (= 1000 (count result)))
|
||||||
(is (= "Fruit-1" (first result)))
|
(is (= "Fruit-1" (first result)))
|
||||||
(is (= "Fruit-1000" (last result)))))))
|
(is (= "Fruit-1000" (last result)))))))
|
||||||
|
|
||||||
(deftest connection-tests
|
(deftest connection-tests
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(testing "datasource via jdbcUrl"
|
(testing "datasource via jdbcUrl"
|
||||||
(when-not (postgres?)
|
(when-not (or (postgres?) (xtdb?))
|
||||||
(let [[url etc] (#'c/spec->url+etc (db))
|
(let [[url etc] (#'c/spec->url+etc (db))
|
||||||
ds (jdbc/get-datasource (assoc etc :jdbcUrl url))]
|
ds (jdbc/get-datasource (assoc etc :jdbcUrl url))]
|
||||||
(cond (derby?) (is (= {:create true} etc))
|
(cond (derby?) (is (= {:create true} etc))
|
||||||
|
|
@ -873,6 +1041,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
(is (instance? java.sql.Connection con)))))))
|
(is (instance? java.sql.Connection con)))))))
|
||||||
|
|
||||||
(deftest multi-rs
|
(deftest multi-rs
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(when (mssql?)
|
(when (mssql?)
|
||||||
(testing "script with multiple result sets"
|
(testing "script with multiple result sets"
|
||||||
(let [multi-rs
|
(let [multi-rs
|
||||||
|
|
@ -904,9 +1073,9 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
{:multi-rs true})
|
{:multi-rs true})
|
||||||
zero-updates [{:next.jdbc/update-count 0}]]
|
zero-updates [{:next.jdbc/update-count 0}]]
|
||||||
(cond (postgres?) ; does not support multiple result sets yet
|
(cond (postgres?) ; does not support multiple result sets yet
|
||||||
(do
|
;; 4.7.3 (and earlier?) returned the fake zero-updates
|
||||||
(is (= 1 (count multi-rs)))
|
;; 4.7.4 returns -1 for update count and an empty result set
|
||||||
(is (= zero-updates (first multi-rs))))
|
(is (= 0 (count multi-rs)))
|
||||||
(hsqldb?)
|
(hsqldb?)
|
||||||
(do
|
(do
|
||||||
(is (= 3 (count multi-rs)))
|
(is (= 3 (count multi-rs)))
|
||||||
|
|
@ -921,6 +1090,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
(println 'call-proc (:dbtype (db)) (ex-message t) (some-> t (ex-cause) (ex-message))))))))
|
(println 'call-proc (:dbtype (db)) (ex-message t) (some-> t (ex-cause) (ex-message))))))))
|
||||||
|
|
||||||
(deftest plan-misuse
|
(deftest plan-misuse
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(let [s (pr-str (jdbc/plan (ds) ["select * from fruit"]))]
|
(let [s (pr-str (jdbc/plan (ds) ["select * from fruit"]))]
|
||||||
(is (re-find #"missing reduction" s)))
|
(is (re-find #"missing reduction" s)))
|
||||||
(let [s (pr-str (into [] (jdbc/plan (ds) ["select * from fruit"])))]
|
(let [s (pr-str (into [] (jdbc/plan (ds) ["select * from fruit"])))]
|
||||||
|
|
@ -931,17 +1101,18 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
(let [s (pr-str (into [] (take 3) (jdbc/plan (ds) ["select * from fruit"]
|
(let [s (pr-str (into [] (take 3) (jdbc/plan (ds) ["select * from fruit"]
|
||||||
(default-options))))]
|
(default-options))))]
|
||||||
(is (or (re-find #"missing `map` or `reduce`" s)
|
(is (or (re-find #"missing `map` or `reduce`" s)
|
||||||
(re-find #"(?i)^\[#:fruit\{.*:id.*\}\]$" s))))
|
(re-find #"(?i)^\[(#:fruit)?\{.*:_?id.*\}\]$" s))))
|
||||||
(is (every? #(re-find #"(?i)^#:fruit\{.*:id.*\}$" %)
|
(is (every? #(re-find #"(?i)^(#:fruit)?\{.*:_?id.*\}$" %)
|
||||||
(into [] (map str) (jdbc/plan (ds) ["select * from fruit"]
|
(into [] (map str) (jdbc/plan (ds) ["select * from fruit"]
|
||||||
(default-options)))))
|
(default-options)))))
|
||||||
(is (every? #(re-find #"(?i)^#:fruit\{.*:id.*\}$" %)
|
(is (every? #(re-find #"(?i)^(#:fruit)?\{.*:_?id.*\}$" %)
|
||||||
(into [] (map pr-str) (jdbc/plan (ds) ["select * from fruit"]
|
(into [] (map pr-str) (jdbc/plan (ds) ["select * from fruit"]
|
||||||
(default-options)))))
|
(default-options)))))
|
||||||
(is (thrown? IllegalArgumentException
|
(is (thrown? IllegalArgumentException
|
||||||
(doall (take 3 (jdbc/plan (ds) ["select * from fruit"]))))))
|
(doall (take 3 (jdbc/plan (ds) ["select * from fruit"]))))))
|
||||||
|
|
||||||
(deftest issue-204
|
(deftest issue-204
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(testing "against a Connection"
|
(testing "against a Connection"
|
||||||
(is (seq (with-open [con (jdbc/get-connection (ds))]
|
(is (seq (with-open [con (jdbc/get-connection (ds))]
|
||||||
(jdbc/on-connection
|
(jdbc/on-connection
|
||||||
|
|
@ -962,6 +1133,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
|
||||||
(jdbc/execute! x ["select * from fruit"]))))))
|
(jdbc/execute! x ["select * from fruit"]))))))
|
||||||
|
|
||||||
(deftest issue-256
|
(deftest issue-256
|
||||||
|
{:context [(around [f] (with-test-db f))]}
|
||||||
(testing "against a Connection"
|
(testing "against a Connection"
|
||||||
(is (seq (with-open [con (jdbc/get-connection (ds))]
|
(is (seq (with-open [con (jdbc/get-connection (ds))]
|
||||||
(jdbc/on-connection+options
|
(jdbc/on-connection+options
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue