Compare commits

..

No commits in common. "develop" and "v1.3.967" have entirely different histories.

53 changed files with 1042 additions and 2132 deletions

View file

@ -1,8 +0,0 @@
{: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}}}

View file

@ -1,567 +0,0 @@
(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)))))

View file

@ -1,6 +0,0 @@
{: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+}}}

View file

@ -1,65 +0,0 @@
(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)

View file

@ -1,23 +0,0 @@
{: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}}}}

View file

@ -1,31 +0,0 @@
;; 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}))

View file

@ -1,4 +0,0 @@
{:linters
{:unresolved-symbol
{:exclude [(cljs.test/is [match? thrown-match?])
(clojure.test/is [match? thrown-match?])]}}}

View file

@ -1,5 +0,0 @@
{: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/->>}}

View file

@ -1 +0,0 @@
{:config-in-call {xtdb.api/template {:ignore [:unresolved-symbol :unresolved-namespace]}}}

View file

@ -15,11 +15,11 @@ jobs:
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '11'
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1530' cli: '1.12.0.1479'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@ -34,7 +34,7 @@ jobs:
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
- name: Run MariaDB Tests - name: Run MariaDB Tests
run: clojure -M:test:runner run: clojure -X:test
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
NEXT_JDBC_TEST_MYSQL: yes NEXT_JDBC_TEST_MYSQL: yes
@ -44,7 +44,6 @@ 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

View file

@ -13,11 +13,11 @@ jobs:
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '11'
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1530' cli: '1.12.0.1479'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@ -32,7 +32,7 @@ jobs:
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
- name: Run MariaDB Tests - name: Run MariaDB Tests
run: clojure -M:test:runner run: clojure -X:test
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
NEXT_JDBC_TEST_MYSQL: yes NEXT_JDBC_TEST_MYSQL: yes
@ -42,7 +42,6 @@ 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
@ -65,14 +64,15 @@ jobs:
- name: Setup Clojure - name: Setup Clojure
uses: DeLaGuardo/setup-clojure@master uses: DeLaGuardo/setup-clojure@master
with: with:
cli: '1.12.0.1530' cli: '1.12.0.1479'
- 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:jdk${{ matrix.java }} test run: clojure -T:build test

View file

@ -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.12.0.1530' cli: '1.12.0.1479'
- name: Cache All The Things - name: Cache All The Things
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@ -32,16 +32,15 @@ jobs:
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
- name: Run MariaDB Tests - name: Run MariaDB Tests
run: clojure -M:test:runner run: clojure -X:test
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 -M:test:runner:jdk${{ matrix.java }} run: clojure -X:test
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

9
.gitignore vendored
View file

@ -1,8 +1,11 @@
*.class
*.jar
*.swp
*~
.calva/output-window/ .calva/output-window/
.calva/repl.calva-repl .calva/repl.calva-repl
.classpath .classpath
.clj-kondo/.cache .clj-kondo/.cache
.clj-kondo/.lock
.cpcache .cpcache
.eastwood .eastwood
.factorypath .factorypath
@ -20,10 +23,6 @@
.settings .settings
.socket-repl-port .socket-repl-port
.sw* .sw*
*.class
*.jar
*.swp
*~
/checkouts /checkouts
/classes /classes
/clojure_test_* /clojure_test_*

3
.gitpod.dockerfile Normal file
View file

@ -0,0 +1,3 @@
FROM gitpod/workspace-full
RUN brew install clojure/tools/clojure@1.12.0.1479

23
.gitpod.yml Normal file
View file

@ -0,0 +1,23 @@
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 Normal file
View file

@ -0,0 +1,6 @@
{: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]}

View file

@ -2,27 +2,6 @@
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 * 1.3.967 -- 2024-12-02
* Address [#288](https://github.com/seancorfield/next-jdbc/issues/288) by adding speculative support for `:dbtype "xtdb"`. * 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 [#287](https://github.com/seancorfield/next-jdbc/issues/287) by merging user-supplied options over `:return-keys true`.

View file

@ -8,11 +8,10 @@ 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:
[![Clojars](https://img.shields.io/badge/clojars-com.github.seancorfield/next.jdbc_1.3.1002-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABjFBMVEUAAAAdCh0qDikdChwAAAAnDSY0EjM2FjUnDiYnDSYnDSYpDigyEDEEAQRGNUb///////8mDSYAAAAAAAAAAAAFAgUqEyoAAAAAAAAAAAAFAgUAAABXU1c2FjVMx+dQx+f///////9Nx+b////4/f6y4vRPt+RQtOT///9Qt+P///8oDSey4vRQr9/////3/P5hzelNx+dNx+dNx+f///8AAAAuDy0zETIAAAAoDScAAAAAAAARBREAAAAvDy40ETMwEC9gSF+Ne42ilKKuoK6Rg5B5ZXlaP1o4Gzf///9nTWZ4YncyEDF/bn/8/Pz9/P339/c1FTUlDCRRM1AbCRtlS2QyEDEuDy1gRWAxEDAzETIwEC/g4OAvDy40EjOaiZorDiq9sbzNyM3UzdQyEDE0ETMzETKflZ/UzdQ5Fzmu4fNYyuhNx+dPt+RLu9xQyOhBbo81GTuW2vCo4PJNx+c4MFE5N1lHiLFEhKQyEDGDboMzETI5Fjh5bXje2d57aHrIw8jc2NyWhJUrDioxe9o4AAAAPnRSTlMAkf+IAQj9+e7n6e31RtqAD/QAAAED+A0ZEQ8DwvkLBsmcR4aG8+cdAD6C8/MC94eP+qoTrgH+/wj1HA8eEvpXOCUAAAABYktHRA8YugDZAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wcHFjou4Z/shwAAAUpJREFUOMul0/VTwzAUB/AAwyW4y3B3h8EDNuTh7u6UDHcd8I+TbHSjWdrjju/1h77kc+3Lu5aQvyakF/r6B5wu1+DQMEBomLRtG0EpozYDCEccA4iIjIqOiY0bB5iYxHgZ4FQCpYneKmmal0aQPMOXZnUAvJhLkbpInf8NFtKCTrGImK6DJcTlDGl/BXGV6oCsrSNIYAM3aQDwl2xJYBtBB5lZAuyYgWzY3YMcNcjN2wc4EGMEFTg8+hlyfgEenygAj71Q9FBExH0wKC4p1bRTJlJWXqEAVNM05ovbXfkPAHBmAUQPAGaAsXMBLiwA8z3h0gRcsWsObuAWLJu8Awb3ZoB5T8EvS/CgBo9Y5Z8TPwXBJwlUI9Ia/yRrEZ8lID71Olrf0MiamkkL4kurDEjba+C/e2sninR0wrsH8eMTvrqIWbodjh7jyjdtCY3Aniz4jwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wNy0wN1QyMjo1ODo0NiswMjowMCgWtSoAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDctMDdUMjI6NTg6NDYrMDI6MDBZSw2WAAAAAElFTkSuQmCC)](https://clojars.org/com.github.seancorfield/next.jdbc) [![Clojars](https://img.shields.io/badge/clojars-com.github.seancorfield/next.jdbc_1.3.967-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABjFBMVEUAAAAdCh0qDikdChwAAAAnDSY0EjM2FjUnDiYnDSYnDSYpDigyEDEEAQRGNUb///////8mDSYAAAAAAAAAAAAFAgUqEyoAAAAAAAAAAAAFAgUAAABXU1c2FjVMx+dQx+f///////9Nx+b////4/f6y4vRPt+RQtOT///9Qt+P///8oDSey4vRQr9/////3/P5hzelNx+dNx+dNx+f///8AAAAuDy0zETIAAAAoDScAAAAAAAARBREAAAAvDy40ETMwEC9gSF+Ne42ilKKuoK6Rg5B5ZXlaP1o4Gzf///9nTWZ4YncyEDF/bn/8/Pz9/P339/c1FTUlDCRRM1AbCRtlS2QyEDEuDy1gRWAxEDAzETIwEC/g4OAvDy40EjOaiZorDiq9sbzNyM3UzdQyEDE0ETMzETKflZ/UzdQ5Fzmu4fNYyuhNx+dPt+RLu9xQyOhBbo81GTuW2vCo4PJNx+c4MFE5N1lHiLFEhKQyEDGDboMzETI5Fjh5bXje2d57aHrIw8jc2NyWhJUrDioxe9o4AAAAPnRSTlMAkf+IAQj9+e7n6e31RtqAD/QAAAED+A0ZEQ8DwvkLBsmcR4aG8+cdAD6C8/MC94eP+qoTrgH+/wj1HA8eEvpXOCUAAAABYktHRA8YugDZAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wcHFjou4Z/shwAAAUpJREFUOMul0/VTwzAUB/AAwyW4y3B3h8EDNuTh7u6UDHcd8I+TbHSjWdrjju/1h77kc+3Lu5aQvyakF/r6B5wu1+DQMEBomLRtG0EpozYDCEccA4iIjIqOiY0bB5iYxHgZ4FQCpYneKmmal0aQPMOXZnUAvJhLkbpInf8NFtKCTrGImK6DJcTlDGl/BXGV6oCsrSNIYAM3aQDwl2xJYBtBB5lZAuyYgWzY3YMcNcjN2wc4EGMEFTg8+hlyfgEenygAj71Q9FBExH0wKC4p1bRTJlJWXqEAVNM05ovbXfkPAHBmAUQPAGaAsXMBLiwA8z3h0gRcsWsObuAWLJu8Awb3ZoB5T8EvS/CgBo9Y5Z8TPwXBJwlUI9Ia/yRrEZ8lID71Olrf0MiamkkL4kurDEjba+C/e2sninR0wrsH8eMTvrqIWbodjh7jyjdtCY3Aniz4jwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wNy0wN1QyMjo1ODo0NiswMjowMCgWtSoAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDctMDdUMjI6NTg6NDYrMDI6MDBZSw2WAAAAAElFTkSuQmCC)](https://clojars.org/com.github.seancorfield/next.jdbc)
[![cljdoc](https://cljdoc.org/badge/com.github.seancorfield/next.jdbc?1.3.1002)](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT) [![cljdoc](https://cljdoc.org/badge/com.github.seancorfield/next.jdbc?1.3.967)](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT)
[![Slack](https://img.shields.io/badge/slack-next.jdbc-orange.svg?logo=slack)](https://clojurians.slack.com/app_redirect?channel=sql) [![Slack](https://img.shields.io/badge/slack-next.jdbc-orange.svg?logo=slack)](https://clojurians.slack.com/app_redirect?channel=sql)
[![Join Slack](https://img.shields.io/badge/slack-join_clojurians-orange.svg?logo=slack)](http://clojurians.net) [![Join Slack](https://img.shields.io/badge/slack-join_clojurians-orange.svg?logo=slack)](http://clojurians.net)
[![Zulip](https://img.shields.io/badge/zulip-next.jdbc-orange.svg?logo=zulip)](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`:
@ -21,11 +20,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.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). The documentation on GitHub is for **develop** since the 1.3.967 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.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. > 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.
## Motivation ## Motivation
@ -51,7 +50,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 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!` -- 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-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).

View file

@ -5,33 +5,29 @@
clojure -T:build deploy clojure -T:build deploy
Run tests via: Run tests via:
clojure -M:test:runner clojure -X:test
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 "9999-SNAPSHOT")) (def snapshot (the-version "999-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 (let [basis (b/create-basis {:aliases [:test alias]})
{: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" "lazytest.main"]}) :main-args ["-m" "cognitect.test-runner"]})
{: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)

View file

@ -1,13 +1,12 @@
{: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.10.3"} :deps {org.clojure/clojure {:mvn/version "1.10.3"}
org.clojure/java.data {:mvn/version "1.3.113"} org.clojure/java.data {:mvn/version "1.2.107"}
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.10.7"} :build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.5"}
slipset/deps-deploy {:mvn/version "0.2.2"}} slipset/deps-deploy {:mvn/version "0.2.2"}}
:ns-default build} :ns-default build}
@ -17,42 +16,36 @@
:1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}} :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"] :test {:extra-paths ["test"] ; can also run clojure -X: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.noahtheduke/lazytest {:mvn/version "1.6.1"} io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}
;; connection pooling ;; connection pooling
com.zaxxer/HikariCP {:mvn/version "6.3.0"} com.zaxxer/HikariCP {:mvn/version "6.0.0"}
com.mchange/c3p0 {:mvn/version "0.10.1"} 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.4"} org.hsqldb/hsqldb {:mvn/version "2.7.3"}
com.h2database/h2 {:mvn/version "2.3.232"} 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.5.2"} org.mariadb.jdbc/mariadb-java-client {:mvn/version "3.4.1"}
com.mysql/mysql-connector-j {:mvn/version "9.2.0"} com.mysql/mysql-connector-j {:mvn/version "9.0.0"}
;; 42.7.4 changes update count (to -1) for stored procs: ;; 42.7.4 changes update count (to -1) for stored procs:
org.postgresql/postgresql {:mvn/version "42.7.5"} org.postgresql/postgresql {:mvn/version "42.7.4"}
io.zonky.test/embedded-postgres {:mvn/version "2.1.0"} io.zonky.test/embedded-postgres {:mvn/version "2.0.7"}
io.zonky.test.postgres/embedded-postgres-binaries-darwin-amd64 {:mvn/version "17.4.0"} io.zonky.test.postgres/embedded-postgres-binaries-darwin-amd64 {:mvn/version "17.0.0"}
io.zonky.test.postgres/embedded-postgres-binaries-linux-amd64 {:mvn/version "17.4.0"} io.zonky.test.postgres/embedded-postgres-binaries-linux-amd64 {:mvn/version "17.0.0"}
io.zonky.test.postgres/embedded-postgres-binaries-windows-amd64 {:mvn/version "17.4.0"} io.zonky.test.postgres/embedded-postgres-binaries-windows-amd64 {:mvn/version "17.0.0"}
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"} org.xerial/sqlite-jdbc {:mvn/version "3.46.1.3"}
com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.10.0.jre11"} com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.8.1.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.24.3"} org.apache.logging.log4j/log4j-api {:mvn/version "2.24.0"}
;; bridge everything into log4j: ;; bridge everything into log4j:
org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.24.3"} org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.24.0"}
org.apache.logging.log4j/log4j-jcl {:mvn/version "2.24.3"} org.apache.logging.log4j/log4j-jcl {:mvn/version "2.24.0"}
org.apache.logging.log4j/log4j-jul {:mvn/version "2.24.3"} org.apache.logging.log4j/log4j-jul {:mvn/version "2.24.0"}
org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.24.3"} org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.24.0"}}
org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.24.3"}} :jvm-opts ["-Dlog4j2.configurationFile=log4j2-info.properties"]
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-info.properties"]} :exec-fn cognitect.test-runner.api/test}}}
: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"]}}}

View file

@ -33,9 +33,6 @@ 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

View file

@ -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})

View file

@ -11,21 +11,21 @@ It is designed to work with Clojure 1.10 or later, supports `datafy`/`nav`, and
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.1002"} com.github.seancorfield/next.jdbc {:mvn/version "1.3.967"}
``` ```
for `deps.edn` or: for `deps.edn` or:
```clojure ```clojure
[com.github.seancorfield/next.jdbc "1.3.1002"] [com.github.seancorfield/next.jdbc "1.3.967"]
``` ```
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 "9.1.0"}` ([search for latest version](https://search.maven.org/artifact/com.mysql/mysql-connector-j)) * 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))
* PostgreSQL: `org.postgresql/postgresql {:mvn/version "42.7.4"}` ([search for latest version](https://search.maven.org/artifact/org.postgresql/postgresql)) * PostgreSQL: `org.postgresql/postgresql {:mvn/version "42.6.0"}` ([search for latest version](https://search.maven.org/artifact/org.postgresql/postgresql))
* 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)) * 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))
* Sqlite: `org.xerial/sqlite-jdbc {:mvn/version "3.47.1.0"}` ([search for latest version](https://search.maven.org/artifact/org.xerial/sqlite-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))
> 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).
@ -38,8 +38,8 @@ 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.12.0"} {:deps {org.clojure/clojure {:mvn/version "1.12.0"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.1002"} com.github.seancorfield/next.jdbc {:mvn/version "1.3.967"}
com.h2database/h2 {:mvn/version "2.3.232"}}} com.h2database/h2 {:mvn/version "2.2.224"}}}
``` ```
### Create & Populate a Database ### Create & Populate a Database
@ -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 "6.2.1"} com.zaxxer/HikariCP {:mvn/version "5.0.1"}
;; or: ;; or:
com.mchange/c3p0 {:mvn/version "0.10.1"} com.mchange/c3p0 {:mvn/version "0.9.5.5"}
``` ```
_Check those libraries' documentation for the latest version to use!_ _Check those libraries' documentation for the latest version to use!_

View file

@ -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 to 2017!). (dating back five years).
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,7 +237,6 @@ 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
@ -575,26 +574,3 @@ 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).

View file

@ -14,8 +14,3 @@ services:
MSSQL_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"

View file

@ -5,30 +5,19 @@
(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} (p/shell {:extra-env env} "clojure" (str "-X"
"clojure" (when v (str ":" v))
(str "-M" ":test"))]
(when v (str ":" v))
":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_MARIADB" "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)))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2018-2025 Sean Corfield, all rights reserved ;; copyright (c) 2018-2024 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.
@ -177,14 +177,6 @@
[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`.
@ -199,13 +191,11 @@
See the list of options above (in the namespace docstring) for what can See the list of options above (in the namespace docstring) for what can
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
"General SQL execution function (for working with result sets). "General SQL execution function (for working with result sets).
@ -238,18 +228,16 @@
(or they can be different, depending on how you want the row to be built, (or they can be different, depending on how you want the row to be built,
and how you want any subsequent lazy navigation to be handled)." and how you want any subsequent lazy navigation to be handled)."
(^clojure.lang.IReduceInit (^clojure.lang.IReduceInit
[stmt] [stmt]
(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))))
(defn execute! (defn execute!
"General SQL execution function. "General SQL execution function.
@ -264,11 +252,9 @@
([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))))
@ -285,11 +271,9 @@
([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))))
@ -352,9 +336,9 @@
result)))) result))))
params))) params)))
([connectable sql param-groups opts] ([connectable sql param-groups opts]
(when-not (string? sql) (if (or (instance? java.sql.Connection connectable)
(throw (IllegalArgumentException. "execute-batch! requires a SQL string"))) (and (satisfies? p/Connectable connectable)
(if (instance? java.sql.Connection (p/unwrap connectable)) (instance? java.sql.Connection (:connectable 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)]
@ -381,12 +365,15 @@
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]
bare-con# (p/unwrap con-obj#)] (cond (instance? java.sql.Connection con-obj#)
(if (instance? java.sql.Connection bare-con#) ((^{:once true} fn* [~con-sym] ~@body) con-obj#)
((^{:once true} fn* [~con-sym] ~@body) bare-con#) (and (satisfies? p/Connectable con-obj#)
(with-open [con# (get-connection con-obj#)] (instance? java.sql.Connection (:connectable con-obj#)))
((^{:once true} fn* [~con-sym] ~@body) con#)))))) ((^{:once true} fn* [~con-sym] ~@body) (:connectable con-obj#))
:else
(with-open [con# (get-connection con-obj#)]
((^{:once true} fn* [~con-sym] ~@body) con#))))))
(defmacro on-connection+options (defmacro on-connection+options
"Given a connectable object, assumed to be wrapped with options, gets "Given a connectable object, assumed to be wrapped with options, gets
@ -416,11 +403,15 @@
with `on-connection`." with `on-connection`."
[[sym connectable] & body] [[sym connectable] & body]
`(let [con-obj# ~connectable] `(let [con-obj# ~connectable]
(if (instance? java.sql.Connection (p/unwrap con-obj#)) (cond (instance? java.sql.Connection con-obj#)
((^{:once true} fn* [~sym] ~@body) con-obj#) ((^{:once true} fn* [~sym] ~@body) con-obj#)
(with-open [con# (get-connection con-obj#)] (and (satisfies? p/Connectable con-obj#)
((^{:once true} fn* [~sym] ~@body) (instance? java.sql.Connection (:connectable con-obj#)))
(with-options con# (:options con-obj# {}))))))) ((^{:once true} fn* [~sym] ~@body) con-obj#)
:else
(with-open [con# (get-connection con-obj#)]
((^{:once true} fn* [~sym] ~@body)
(with-options con# (:options con-obj# {})))))))
(defn transact (defn transact
"Given a transactable object and a function (taking a `Connection`), "Given a transactable object and a function (taking a `Connection`),

View file

@ -1,4 +1,4 @@
;; copyright (c) 2018-2025 Sean Corfield, all rights reserved ;; copyright (c) 2018-2024 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`.
@ -374,12 +374,9 @@
(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.)]
as-is (set (:next.jdbc/as-is-properties m))] (doseq [[k v] m]
(doseq [[k v] (dissoc m :next.jdbc/as-is-properties)] (.setProperty p (name k) (str v)))
(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

View file

@ -1,4 +1,4 @@
;; copyright (c) 2020-2025 Sean Corfield, all rights reserved ;; copyright (c) 2020-2024 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,10 +8,6 @@
(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]

View file

@ -1,4 +1,4 @@
;; copyright (c) 2018-2025 Sean Corfield, all rights reserved ;; copyright (c) 2018-2024 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,15 +63,3 @@
: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))

View file

@ -278,10 +278,6 @@
: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."
@ -323,7 +319,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]
(let [value (.getObject rs i)] (when-let [value (.getObject rs i)]
(cond-> value (cond-> value
(instance? Clob value) (instance? Clob value)
(clob->string)))) (clob->string))))

View file

@ -110,7 +110,7 @@
:opts (s/? ::opts-map))) :opts (s/? ::opts-map)))
(s/fdef jdbc/prepare (s/fdef jdbc/prepare
:args (s/cat :connection ::proto-connectable :args (s/cat :connection ::connection
:sql-params ::sql-params :sql-params ::sql-params
:opts (s/? ::opts-map))) :opts (s/? ::opts-map)))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2021-2025 Sean Corfield, all rights reserved ;; copyright (c) 2021-2024 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,10 +8,6 @@
(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]

View file

@ -145,11 +145,7 @@
javax.sql.DataSource javax.sql.DataSource
(-transact [this body-fn opts] (-transact [this body-fn opts]
(with-open [con (p/get-connection this opts)] (with-open [con (p/get-connection this opts)]
;; this connection is assumed unique so we do not need the active-tx check: (p/-transact con body-fn opts)))
(let [raw (raw-connection con)]
;; we don't lock either, per #293:
(binding [*active-tx* (conj *active-tx* raw)]
(transact* con body-fn opts)))))
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)))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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,17 +7,15 @@
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]
[lazytest.core :refer [around set-ns-context!]] [clojure.test :refer [deftest is testing use-fixtures]]
[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 [db with-test-db]]) [next.jdbc.test-fixtures :refer [with-test-db db]]))
(:import [java.util Properties]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(set-ns-context! [(around [f] (with-test-db f))]) (use-fixtures :once with-test-db)
(specs/instrument) (specs/instrument)
@ -41,16 +39,3 @@
(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"))))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]] [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)

View file

@ -1,22 +1,21 @@
;; copyright (c) 2020-2025 Sean Corfield, all rights reserved ;; copyright (c) 2020-2024 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]
[lazytest.core :refer [around set-ns-context!]] [clojure.test :refer [deftest is testing use-fixtures]]
[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 [db derby? ds jtds? mysql? postgres? sqlite? with-test-db :refer [with-test-db db ds
xtdb?]])) derby? jtds? mysql? postgres? sqlite?]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(set-ns-context! [(around [f] (with-test-db f))]) (use-fixtures :once with-test-db)
(specs/instrument) (specs/instrument)
@ -84,26 +83,6 @@
: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)))
@ -118,8 +97,7 @@
(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))))))))
@ -144,5 +122,4 @@
(.execute ps) (.execute ps)
(.getResultSet ps) (.getResultSet ps)
(.close ps) (.close ps)
(.close con) (.close con))
)

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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,45 +6,44 @@
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 [lazytest.core :refer [around set-ns-context!]] (:require [clojure.test :refer [deftest is testing use-fixtures]]
[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 ds [next.jdbc.test-fixtures :refer [with-test-db db ds
mssql? xtdb?]] mssql?]]
[next.jdbc.specs :as specs])) [next.jdbc.specs :as specs])
(:import (java.sql ResultSet)))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(set-ns-context! [(around [f] (with-test-db f))]) (use-fixtures :once with-test-db)
(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 _)) (jdbc/execute-one! (ds) [(str "create table fruit_time (id int not null, deadline "
(jdbc/execute-one! (ds) [(str "create table fruit_time (id int not null, deadline " (if (mssql?) "datetime" "timestamp")
(if (mssql?) "datetime" "timestamp") " not null)")])
" not null)")]) (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)]) (try
(try (jdbc/execute-one! (ds) ["drop table fruit_time"])
(jdbc/execute-one! (ds) ["drop table fruit_time"]) (catch Throwable _))
(catch Throwable _)) (jdbc/execute-one! (ds) ["create table fruit_time (id int not null, deadline time not null)"])
(jdbc/execute-one! (ds) ["create table fruit_time (id int not null, deadline time not null)"]) (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)]) (try
(try (jdbc/execute-one! (ds) ["drop table fruit_time"])
(jdbc/execute-one! (ds) ["drop table fruit_time"]) (catch Throwable _))
(catch Throwable _)) (jdbc/execute-one! (ds) ["create table fruit_time (id int not null, deadline date not null)"])
(jdbc/execute-one! (ds) ["create table fruit_time (id int not null, deadline date not null)"]) (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)])))

View file

@ -1,8 +1,9 @@
;; copyright (c) 2020-2025 Sean Corfield, all rights reserved ;; copyright (c) 2020-2024 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 [next.jdbc.default-options])) (:require [clojure.test :refer [deftest is testing]]
[next.jdbc.default-options :refer :all]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)

View file

@ -1,4 +1,4 @@
;; copyright (c) 2024-2025 Sean Corfield, all rights reserved ;; copyright (c) 2024 Sean Corfield, all rights reserved
(ns next.jdbc.defer-test (ns next.jdbc.defer-test
"The idea behind the next.jdbc.defer namespace is to provide a "The idea behind the next.jdbc.defer namespace is to provide a
@ -11,45 +11,43 @@
describes a series of SQL operations to be performed, that describes a series of SQL operations to be performed, that
are held in a dynamic var, and that can be executed at a are held in a dynamic var, and that can be executed at a
later time, in a transaction." later time, in a transaction."
(:require [lazytest.core :refer [around set-ns-context!]] (:require [clojure.test :refer [deftest is testing use-fixtures]]
[lazytest.experimental.interfaces.clojure-test :refer [deftest is testing]]
[next.jdbc :as jdbc] [next.jdbc :as jdbc]
[next.jdbc.defer :as sut] [next.jdbc.defer :as sut]
[next.jdbc.test-fixtures [next.jdbc.test-fixtures
:refer [ds with-test-db xtdb?]])) :refer [ds with-test-db]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(set-ns-context! [(around [f] (with-test-db f))]) (use-fixtures :once with-test-db)
(deftest basic-test (deftest basic-test
(when-not (xtdb?) (testing "data structures"
(testing "data structures" (is (= [{:sql-p ["INSERT INTO foo (name) VALUES (?)" "Sean"]
(is (= [{:sql-p ["INSERT INTO foo (name) VALUES (?)" "Sean"] :key-fn :GENERATED_KEY
:key-fn :GENERATED_KEY :key :id
:key :id :opts {:key-fn :GENERATED_KEY :key :id}}]
:opts {:key-fn :GENERATED_KEY :key :id}}] @(sut/defer-ops
@(sut/defer-ops #(sut/insert! :foo {:name "Sean"} {:key-fn :GENERATED_KEY :key :id})))))
#(sut/insert! :foo {:name "Sean"} {:key-fn :GENERATED_KEY :key :id}))))) (testing "execution"
(testing "execution" (let [effects (sut/with-deferred (ds)
(let [effects (sut/with-deferred (ds) (sut/insert! :fruit {:name "Mango"} {:key :test}))]
(sut/insert! :fruit {:name "Mango"} {:key :test}))] (is (= {:test 1} @effects))
(is (= {:test 1} @effects)) (is (= 1 (count (jdbc/execute! (ds)
(is (= 1 (count (jdbc/execute! (ds) ["select * from fruit where name = ?"
["select * from fruit where name = ?" "Mango"])))))
"Mango"]))))) (let [effects (sut/with-deferred (ds)
(let [effects (sut/with-deferred (ds) (sut/insert! :fruit {:name "Dragonfruit"} {:key :test})
(sut/insert! :fruit {:name "Dragonfruit"} {:key :test}) (sut/update! :fruit {:cost 123} {:name "Dragonfruit"})
(sut/update! :fruit {:cost 123} {:name "Dragonfruit"}) (sut/delete! :fruit {:name "Dragonfruit"}))]
(sut/delete! :fruit {:name "Dragonfruit"}))] (is (= {:test 1} @effects))
(is (= {:test 1} @effects)) (is (= 0 (count (jdbc/execute! (ds)
(is (= 0 (count (jdbc/execute! (ds) ["select * from fruit where name = ?"
["select * from fruit where name = ?" "Dragonfruit"])))))
"Dragonfruit"]))))) (let [effects (sut/with-deferred (ds)
(let [effects (sut/with-deferred (ds) (sut/insert! :fruit {:name "Grapefruit" :bad_column 0} {:key :test}))]
(sut/insert! :fruit {:name "Grapefruit" :bad_column 0} {:key :test}))] (is (= :failed (try @effects
(is (= :failed (try @effects (catch Exception _ :failed))))
(catch Exception _ :failed)))) (is (= 0 (count (jdbc/execute! (ds)
(is (= 0 (count (jdbc/execute! (ds) ["select * from fruit where name = ?"
["select * from fruit where name = ?" "Grapefruit"])))))))
"Grapefruit"]))))))))

View file

@ -1,25 +1,23 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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]
[lazytest.core :refer [around set-ns-context!]] [clojure.test :refer [deftest is testing use-fixtures]]
[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 [col-kw column default-options ds index [next.jdbc.test-fixtures :refer [with-test-db ds column
with-test-db]]) default-options]])
(:import (:import (java.sql ResultSet ResultSetMetaData)))
(java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(set-ns-context! [(around [f] (with-test-db f))]) (use-fixtures :once with-test-db)
(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)
[(str "select * from fruit where " (index) " = ?") 1] ["select * from fruit where id = ?" 1]
(assoc (default-options) (assoc (default-options)
:builder-fn opt/as-maps))] :builder-fn opt/as-maps))]
(is (map? row)) (is (map? row))
@ -28,7 +26,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)
[(str "select * from fruit where " (index) " = ?") 2] ["select * from fruit where id = ?" 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))))
@ -36,23 +34,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)
[(str "select * from fruit where " (index) " = ?") 3] ["select * from fruit where id = ?" 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 (col-kw :fruit/appearance)))) (is (not (contains? row :fruit/appearance)))
(is (= 3 ((col-kw :fruit/id) row))) (is (= 3 (:fruit/id row)))
(is (= "Peach" ((col-kw :fruit/name) row))))) (is (= "Peach" (: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)
[(str "select * from fruit where " (index) " = ?") 4] ["select * from fruit where id = ?" 4]
{:builder-fn opt/as-unqualified-lower-maps})] {:builder-fn opt/as-unqualified-lower-maps})]
(is (map? row)) (is (map? row))
(is (= 4 ((col-kw :id) row))) (is (= 4 (:id row)))
(is (= "Orange" ((col-kw :name) row))))) (is (= "Orange" (:name row)))))
(testing "custom row builder" (testing "custom row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
[(str "select * from fruit where " (index) " = ?") 3] ["select * from fruit where id = ?" 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
@ -63,13 +61,13 @@
(is (= "Peach" ((column :FRUIT/name) row)))))) (is (= "Peach" ((column :FRUIT/name) row))))))
(defn- default-column-reader (defn- default-column-reader
[^ResultSet rs ^ResultSetMetaData _ ^Integer i] [^ResultSet rs ^ResultSetMetaData rsmeta ^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)
[(str "select * from fruit where " (index) " = ?") 1] ["select * from fruit where id = ?" 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
@ -80,7 +78,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)
[(str "select * from fruit where " (index) " = ?") 2] ["select * from fruit where id = ?" 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)})]
@ -90,27 +88,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)
[(str "select * from fruit where " (index) " = ?") 3] ["select * from fruit where id = ?" 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 (col-kw :fruit/appearance)))) (is (not (contains? row :fruit/appearance)))
(is (= 3 ((col-kw :fruit/id) row))) (is (= 3 (:fruit/id row)))
(is (= "Peach" ((col-kw :fruit/name) row))))) (is (= "Peach" (: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)
[(str "select * from fruit where " (index) " = ?") 4] ["select * from fruit where id = ?" 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 ((col-kw :id) row))) (is (= 4 (:id row)))
(is (= "Orange" ((col-kw :name) row))))) (is (= "Orange" (:name row)))))
(testing "custom row builder" (testing "custom row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
[(str "select * from fruit where " (index) " = ?") 3] ["select * from fruit where id = ?" 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

View file

@ -1,75 +1,72 @@
;; copyright (c) 2020-2025 Sean Corfield, all rights reserved ;; copyright (c) 2020-2024 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 [lazytest.core :refer [around]] (:require [clojure.test :refer [deftest is use-fixtures]]
[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 col-kw index]] :refer [with-test-db ds]]
[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
{:context [(around [f] (with-test-db f))]} (is (= {:id 1}
(is (= {(col-kw :id) 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 (= 1 (is (= 1
(plan/select-one! (ds) (col-kw :id) [(str "select * from fruit order by " (index))]))) (plan/select-one! (ds) :id ["select * from fruit order by id"])))
(is (= "Banana" (is (= "Banana"
(plan/select-one! (ds) :name [(str "select * from fruit where " (index) " = ?") 2]))) (plan/select-one! (ds) :name ["select * from fruit where id = ?" 2])))
(is (= [1 "Apple"] (is (= [1 "Apple"]
(plan/select-one! (ds) (juxt (col-kw :id) :name) (plan/select-one! (ds) (juxt :id :name)
[(str "select * from fruit order by " (index))]))) ["select * from fruit order by id"])))
(is (= {(col-kw :id) 1 :name "Apple"} (is (= {:id 1 :name "Apple"}
(plan/select-one! (ds) #(select-keys % [(col-kw :id) :name]) (plan/select-one! (ds) #(select-keys % [:id :name])
[(str "select * from fruit order by " (index))])))) ["select * from fruit order by id"]))))
(deftest select-vector-tests (deftest select-vector-tests
{:context [(around [f] (with-test-db f))]} (is (= [{:id 1} {:id 2} {:id 3} {:id 4}]
(is (= [{(col-kw :id) 1} {(col-kw :id) 2} {(col-kw :id) 3} {(col-kw :id) 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 (= [1 2 3 4] (is (= [1 2 3 4]
(plan/select! (ds) (col-kw :id) [(str "select * from fruit order by " (index))]))) (plan/select! (ds) :id ["select * from fruit order by id"])))
(is (= ["Banana"] (is (= ["Banana"]
(plan/select! (ds) :name [(str "select * from fruit where " (index) " = ?") 2]))) (plan/select! (ds) :name ["select * from fruit where id = ?" 2])))
(is (= [[2 "Banana"]] (is (= [[2 "Banana"]]
(plan/select! (ds) (juxt (col-kw :id) :name) (plan/select! (ds) (juxt :id :name)
[(str "select * from fruit where " (index) " = ?") 2]))) ["select * from fruit where id = ?" 2])))
(is (= [{(col-kw :id) 2 :name "Banana"}] (is (= [{:id 2 :name "Banana"}]
(plan/select! (ds) [(col-kw :id) :name] (plan/select! (ds) [:id :name]
[(str "select * from fruit where " (index) " = ?") 2])))) ["select * from fruit where id = ?" 2]))))
(deftest select-set-tests (deftest select-set-tests
{:context [(around [f] (with-test-db f))]} (is (= #{{:id 1} {:id 2} {:id 3} {:id 4}}
(is (= #{{(col-kw :id) 1} {(col-kw :id) 2} {(col-kw :id) 3} {(col-kw :id) 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 #{}})))
(is (= #{1 2 3 4} (is (= #{1 2 3 4}
(plan/select! (ds) (col-kw :id) [(str "select * from fruit order by " (index))] (plan/select! (ds) :id ["select * from fruit order by id"]
{: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 (col-kw :id) :name) [(str "select * from fruit order by " (index))] (plan/select! (ds) (juxt :id :name) ["select * from fruit order by id"]
{: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 [(str "select * from fruit where " (index) " = ?") 1] (plan/select! (ds) :name ["select * from fruit where id = ?" 1]
{:column-fn #(str/replace % "-" "_")}))) {:column-fn #(str/replace % "-" "_")})))
(is (= ["Apple"] (is (= ["Apple"]
(plan/select! (ds) :foo/name [(str "select * from fruit where " (index) " = ?") 1] (plan/select! (ds) :foo/name ["select * from fruit where id = ?" 1]
{:column-fn #(str/replace % "-" "_")}))) {:column-fn #(str/replace % "-" "_")})))
(is (= ["Apple"] (is (= ["Apple"]
(plan/select! (ds) #(get % "name") [(str "select * from fruit where " (index) " = ?") 1] (plan/select! (ds) #(get % "name") ["select * from fruit where id = ?" 1]
{:column-fn #(str/replace % "-" "_")}))) {:column-fn #(str/replace % "-" "_")})))
(is (= [["Apple"]] (is (= [["Apple"]]
(plan/select! (ds) (juxt :name) [(str "select * from fruit where " (index) " = ?") 1] (plan/select! (ds) (juxt :name) ["select * from fruit where id = ?" 1]
{:column-fn #(str/replace % "-" "_")})))) {:column-fn #(str/replace % "-" "_")}))))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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,40 +8,75 @@
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 [lazytest.core :refer [around set-ns-context!]] (:require [clojure.test :refer [deftest is testing use-fixtures]]
[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? xtdb?]] :refer [with-test-db ds jtds? mssql? sqlite?]]
[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)
(set-ns-context! [(around [f] (with-test-db f))]) (use-fixtures :once with-test-db)
(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}] (with-open [ps (jdbc/prepare t ["
(with-open [ps (jdbc/prepare t ["
INSERT INTO fruit (name, appearance) VALUES (?,?) INSERT INTO fruit (name, appearance) VALUES (?,?)
"])] "])]
(let [result (prep/execute-batch! ps [["fruit1" "one"] (let [result (prep/execute-batch! ps [["fruit1" "one"]
["fruit2" "two"] ["fruit2" "two"]
["fruit3" "three"] ["fruit3" "three"]
["fruit4" "four"] ["fruit4" "four"]
["fruit5" "five"] ["fruit5" "five"]
["fruit6" "six"] ["fruit6" "six"]
["fruit7" "seven"] ["fruit7" "seven"]
["fruit8" "eight"] ["fruit8" "eight"]
["fruit9" "nine"]])] ["fruit9" "nine"]])]
(conj result (count (jdbc/execute! t ["select * from fruit"])))))))) (conj result (count (jdbc/execute! t ["select * from fruit"]))))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) (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]
(jdbc/with-transaction [t (ds) {:rollback-only true}]
(with-open [ps (jdbc/prepare t ["
INSERT INTO fruit (name, appearance) VALUES (?,?)
"])]
(let [result (prep/execute-batch! ps [["fruit1" "one"]
["fruit2" "two"]
["fruit3" "three"]
["fruit4" "four"]
["fruit5" "five"]
["fruit6" "six"]
["fruit7" "seven"]
["fruit8" "eight"]
["fruit9" "nine"]]
{:batch-size 3})]
(conj result (count (jdbc/execute! t ["select * from fruit"]))))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "big batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13]
(jdbc/with-transaction [t (ds) {:rollback-only true}]
(with-open [ps (jdbc/prepare t ["
INSERT INTO fruit (name, appearance) VALUES (?,?)
"])]
(let [result (prep/execute-batch! ps [["fruit1" "one"]
["fruit2" "two"]
["fruit3" "three"]
["fruit4" "four"]
["fruit5" "five"]
["fruit6" "six"]
["fruit7" "seven"]
["fruit8" "eight"]
["fruit9" "nine"]]
{:batch-size 8})]
(conj result (count (jdbc/execute! t ["select * from fruit"]))))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "large batch insert"
(when-not (or (jtds?) (sqlite?))
(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}]
(with-open [ps (jdbc/prepare t [" (with-open [ps (jdbc/prepare t ["
@ -56,70 +91,33 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
["fruit7" "seven"] ["fruit7" "seven"]
["fruit8" "eight"] ["fruit8" "eight"]
["fruit9" "nine"]] ["fruit9" "nine"]]
{:batch-size 3})] {:batch-size 4
:large true})]
(conj result (count (jdbc/execute! t ["select * from fruit"])))))))) (conj result (count (jdbc/execute! t ["select * from fruit"]))))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))
(testing "big batch insert" (testing "return generated keys"
(is (= [1 1 1 1 1 1 1 1 1 13] (when-not (or (mssql?) (sqlite?))
(jdbc/with-transaction [t (ds) {:rollback-only true}] (let [results
(with-open [ps (jdbc/prepare t [" (jdbc/with-transaction [t (ds) {:rollback-only true}]
INSERT INTO fruit (name, appearance) VALUES (?,?) (with-open [ps (jdbc/prepare t ["
"])]
(let [result (prep/execute-batch! ps [["fruit1" "one"]
["fruit2" "two"]
["fruit3" "three"]
["fruit4" "four"]
["fruit5" "five"]
["fruit6" "six"]
["fruit7" "seven"]
["fruit8" "eight"]
["fruit9" "nine"]]
{:batch-size 8})]
(conj result (count (jdbc/execute! t ["select * from fruit"]))))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "large batch insert"
(when-not (or (jtds?) (sqlite?))
(is (= [1 1 1 1 1 1 1 1 1 13]
(jdbc/with-transaction [t (ds) {:rollback-only true}]
(with-open [ps (jdbc/prepare t ["
INSERT INTO fruit (name, appearance) VALUES (?,?)
"])]
(let [result (prep/execute-batch! ps [["fruit1" "one"]
["fruit2" "two"]
["fruit3" "three"]
["fruit4" "four"]
["fruit5" "five"]
["fruit6" "six"]
["fruit7" "seven"]
["fruit8" "eight"]
["fruit9" "nine"]]
{:batch-size 4
:large true})]
(conj result (count (jdbc/execute! t ["select * from fruit"]))))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))
(testing "return generated keys"
(when-not (or (mssql?) (sqlite?))
(let [results
(jdbc/with-transaction [t (ds) {:rollback-only true}]
(with-open [ps (jdbc/prepare t ["
INSERT INTO fruit (name, appearance) VALUES (?,?) INSERT INTO fruit (name, appearance) VALUES (?,?)
"] "]
{:return-keys true})] {:return-keys true})]
(let [result (prep/execute-batch! ps [["fruit1" "one"] (let [result (prep/execute-batch! ps [["fruit1" "one"]
["fruit2" "two"] ["fruit2" "two"]
["fruit3" "three"] ["fruit3" "three"]
["fruit4" "four"] ["fruit4" "four"]
["fruit5" "five"] ["fruit5" "five"]
["fruit6" "six"] ["fruit6" "six"]
["fruit7" "seven"] ["fruit7" "seven"]
["fruit8" "eight"] ["fruit8" "eight"]
["fruit9" "nine"]] ["fruit9" "nine"]]
{:batch-size 4 {:batch-size 4
:return-generated-keys true})] :return-generated-keys true})]
(conj result (count (jdbc/execute! t ["select * from fruit"]))))))] (conj result (count (jdbc/execute! t ["select * from fruit"]))))))]
(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"])))))))

View file

@ -1,8 +1,9 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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 [next.jdbc.protocols])) (:require [clojure.test :refer [deftest is testing]]
[next.jdbc.protocols :refer :all]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)

View file

@ -1,30 +1,34 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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 [lazytest.core :refer [defdescribe describe it expect]] (:require [clojure.test :refer [deftest are testing]]
[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)
(def ^:private quote-fns [ansi mysql sql-server oracle postgres]) (deftest basic-quoting
(are [quote-fn quoted] (= (quote-fn "x") quoted)
ansi "\"x\""
mysql "`x`"
sql-server "[x]"
oracle "\"x\""
postgres "\"x\""))
(defdescribe quoted-functionality (deftest schema-quoting
(describe "base quoting" (testing "verify non-schema behavior"
(it "should correctly quote simple names" (are [quote-fn quoted] (= (quote-fn "x.y") quoted)
(doseq [[f e] (map vector quote-fns ansi "\"x.y\""
["\"x\"" "`x`" "[x]" "\"x\"" "\"x\""])] mysql "`x.y`"
(expect (= (f "x") e))))) sql-server "[x.y]"
(describe "dotted name quoting" oracle "\"x.y\""
(describe "basic quoting" postgres "\"x.y\""))
(it "should quote dotted names 'as-is'" (testing "verify schema behavior"
(doseq [[f e] (map vector quote-fns (are [quote-fn quoted] (= ((schema quote-fn) "x.y") quoted)
["\"x.y\"" "`x.y`" "[x.y]" "\"x.y\"" "\"x.y\""])] ansi "\"x\".\"y\""
(expect (= (f "x.y") e))))) mysql "`x`.`y`"
(describe "schema quoting" sql-server "[x].[y]"
(it "should split and quote dotted names with schema" oracle "\"x\".\"y\""
(doseq [[f e] (map vector quote-fns postgres "\"x\".\"y\"")))
["\"x\".\"y\"" "`x`.`y`" "[x].[y]" "\"x\".\"y\"" "\"x\".\"y\""])]
(expect (= ((schema f) "x.y") e)))))))

View file

@ -1,4 +1,4 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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,19 +8,18 @@
(: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]
[lazytest.core :refer [around set-ns-context!]] [clojure.test :refer [deftest is testing use-fixtures]]
[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 index col-kw [next.jdbc.test-fixtures :refer [with-test-db ds column
default-options default-options
derby? mssql? mysql? postgres? xtdb?]]) derby? mssql? mysql? postgres?]])
(:import (java.sql ResultSet ResultSetMetaData))) (:import (java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(set-ns-context! [(around [f] (with-test-db f))]) (use-fixtures :once with-test-db)
(specs/instrument) (specs/instrument)
@ -28,9 +27,7 @@
(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
(cond-> (default-options) (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
@ -43,10 +40,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 :schema {:foo/bar :fruit/id}))
(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
@ -59,10 +53,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 :schema {:foo/bar [:fruit/id]}))
[(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
@ -76,7 +67,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 (col-kw :id)]})) :schema {:foo/bar [: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
@ -88,7 +79,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 (col-kw :id) :many]})) :schema {:foo/bar [:fruit :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
@ -102,7 +93,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)
[(str "select * from fruit where " (index) " = ?") 1] ["select * from fruit where id = ?" 1]
(default-options))] (default-options))]
(is (map? row)) (is (map? row))
(is (contains? row (column :FRUIT/GRADE))) (is (contains? row (column :FRUIT/GRADE)))
@ -110,7 +101,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)
[(str "select * from fruit order by " (index))] ["select * from fruit order by id"]
(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))))
@ -119,7 +110,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)
[(str "select * from fruit where " (index) " = ?") 2] ["select * from fruit where id = ?" 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)))
@ -128,35 +119,34 @@
(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)
[(str "select * from fruit where " (index) " = ?") 3] ["select * from fruit where id = ?" 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 (col-kw :fruit/appearance))) (is (contains? row :fruit/appearance))
(is (nil? ((col-kw :fruit/appearance) row))) (is (nil? (:fruit/appearance row)))
(is (= 3 ((col-kw :fruit/id) row))) (is (= 3 (:fruit/id row)))
(is (= "Peach" ((col-kw :fruit/name) row))))) (is (= "Peach" (: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)
[(str "select * from fruit where " (index) " = ?") 4] ["select * from fruit where id = ?" 4]
{:builder-fn rs/as-unqualified-lower-maps})] {:builder-fn rs/as-unqualified-lower-maps})]
(is (map? row)) (is (map? row))
(is (= 4 ((col-kw :id) row))) (is (= 4 (:id row)))
(is (= "Orange" ((col-kw :name) row))))) (is (= "Orange" (:name row)))))
(testing "kebab-case row builder" (testing "kebab-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
[(str "select " (index) ",name,appearance as looks_like from fruit where " (index) " = ?") 3] ["select id,name,appearance as looks_like from fruit where id = ?" 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 (col-kw :fruit/looks-like))) (is (contains? row :fruit/looks-like))
(is (nil? ((col-kw :fruit/looks-like) row))) (is (nil? (:fruit/looks-like row)))
;; kebab-case strips leading _ from _id (XTDB): (is (= 3 (:fruit/id row)))
(is (= 3 ((if (xtdb?) :id :fruit/id) row))) (is (= "Peach" (:fruit/name 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)
[(str "select " (index) ",name,appearance as looks_like from fruit where " (index) " = ?") 4] ["select id,name,appearance as looks_like from fruit where id = ?" 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))
@ -165,7 +155,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)
[(str "select fruit.*, " (index) " + 100 as newid from fruit where " (index) " = ?") 3] ["select fruit.*, id + 100 as newid from fruit where id = ?" 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
@ -178,7 +168,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)
[(str "select fruit.*, " (index) " + 100 as newid from fruit where " (index) " = ?") 3] ["select fruit.*, id + 100 as newid from fruit where id = ?" 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
@ -186,12 +176,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 ((if (xtdb?) :vegetable/_id :vegetable/id) row))) (is (= 3 (: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)
[(str "select * from fruit where " (index) " = ?") 3] ["select * from fruit where id = ?" 3]
(assoc (assoc
(default-options) (default-options)
:builder-fn (rs/as-maps-adapter :builder-fn (rs/as-maps-adapter
@ -217,7 +207,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)
[(str "select * from fruit where " (index) " = ?") 3] ["select * from fruit where id = ?" 3]
(assoc (assoc
(default-options) (default-options)
:builder-fn (rs/as-maps-adapter :builder-fn (rs/as-maps-adapter
@ -246,7 +236,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) [(str "select * from fruit where " (index) " < ?") 4] (p/-execute (ds) ["select * from fruit where id < ?" 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?)
@ -257,7 +247,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) [(str "select * from fruit where " (index) " < ?") 4] (p/-execute (ds) ["select * from fruit where id < ?" 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?)
@ -267,7 +257,7 @@
(deftest test-column-names (deftest test-column-names
(testing "column-names on bare abstraction" (testing "column-names on bare abstraction"
(is (= #{(index) "appearance" "grade" "cost" "name"} (is (= #{"id" "appearance" "grade" "cost" "name"}
(reduce (fn [_ row] (reduce (fn [_ row]
(-> row (-> row
(->> (rs/column-names) (->> (rs/column-names)
@ -275,11 +265,11 @@
(set) (set)
(reduced)))) (reduced))))
nil nil
(p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4] (p/-execute (ds) ["select * from fruit where id < ?" 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 (= #{(index) "appearance" "grade" "cost" "name"} (is (= #{"id" "appearance" "grade" "cost" "name"}
(reduce (fn [_ row] (reduce (fn [_ row]
(-> row (-> row
(rs/datafiable-row (ds) {}) (rs/datafiable-row (ds) {})
@ -288,7 +278,7 @@
(set) (set)
(reduced)))) (reduced))))
nil nil
(p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4] (p/-execute (ds) ["select * from fruit where id < ?" 4]
{:builder-fn rs/as-arrays})))))) {:builder-fn rs/as-arrays}))))))
(deftest test-over-partition-all (deftest test-over-partition-all
@ -309,31 +299,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) [(str "select * from fruit where " (index) " = ?") 1] (p/-execute (ds) ["select * from fruit where id = ?" 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) [(str "select * from fruit where " (index) " = ?") 1] (p/-execute (ds) ["select * from fruit where id = ?" 1]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [[2 [:name "Banana"]]] (is (= [[2 [:name "Banana"]]]
(into [] (map (juxt #(get % (index)) ; get by string key works (into [] (map (juxt #(get % "id") ; get by string key works
#(find % :name))) ; get MapEntry works #(find % :name))) ; get MapEntry works
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 2] (p/-execute (ds) ["select * from fruit where id = ?" 2]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [{(col-kw :id) 3 :name "Peach"}] (is (= [{:id 3 :name "Peach"}]
(into [] (map #(select-keys % [(col-kw :id) :name])) ; select-keys works (into [] (map #(select-keys % [:id :name])) ; select-keys works
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 3] (p/-execute (ds) ["select * from fruit where id = ?" 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 % (col-kw :id) 0))) ; get with not-found works (get % :id 0))) ; get with not-found works
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 4] (p/-execute (ds) ["select * from fruit where id = ?" 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) [(str "select * from fruit where " (index) " = ?") 1] (p/-execute (ds) ["select * from fruit where id = ?" 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]
@ -341,7 +331,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) [(str "select * from fruit where " (index) " = ?") 1] (p/-execute (ds) ["select * from fruit where id = ?" 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)))
@ -427,7 +417,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 ^String (index)) (->row [_] (->Fruit (.getObject rs "id")
(.getObject rs "name") (.getObject rs "name")
(.getObject rs "appearance") (.getObject rs "appearance")
(.getObject rs "cost") (.getObject rs "cost")
@ -444,7 +434,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 [(col-kw :id) :name :appearance :cost :grade] :cols [:id :name :appearance :cost :grade]
:rsmeta rsmeta :rsmeta rsmeta
not-found)))) not-found))))
@ -477,7 +467,7 @@
metadata)))) metadata))))
(deftest clob-reading (deftest clob-reading
(when-not (or (mssql?) (mysql?) (postgres?) (xtdb?)) ; no clob in these (when-not (or (mssql?) (mysql?) (postgres?)) ; 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"] {})
@ -507,10 +497,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) [(str "select " (index) " from fruit where " (index) " < ? order by " (index)) 4] (p/-execute (ds) ["select id from fruit where id < ?" 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) [(str "select " (index) " from fruit where " (index) " < ? order by " (index)) 4] (p/-execute (ds) ["select id from fruit where id < ?" 4]
{:builder-fn rs/as-arrays})))))) {:builder-fn rs/as-arrays}))))))

View file

@ -1,10 +1,11 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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 [next.jdbc.specs])) (:require [clojure.test :refer [deftest is testing]]
[next.jdbc.specs :refer :all]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)

View file

@ -1,8 +1,8 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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 [lazytest.experimental.interfaces.clojure-test :refer [deftest is testing thrown?]] (:require [clojure.test :refer [deftest is testing]]
[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]))

View file

@ -1,26 +1,25 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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 [lazytest.core :refer [around set-ns-context!]] (:require [clojure.test :refer [deftest is testing use-fixtures]]
[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 [col-kw column default-options derby? ds index jtds? :refer [with-test-db ds column default-options
maria? mssql? mysql? postgres? sqlite? with-test-db xtdb?]] derby? jtds? maria? mssql? mysql? postgres? sqlite?]]
[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)
(set-ns-context! [(around [f] (with-test-db f))]) (use-fixtures :once with-test-db)
(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 [(str "select * from fruit order by " (index))])] rs (sql/query ds-opts ["select * from fruit order by id"])]
(is (= 4 (count rs))) (is (= 4 (count rs)))
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
@ -35,10 +34,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 [(col-kw :ID) :columns [:ID
["CASE WHEN grade > 91 THEN 'ok ' ELSE 'bad' END" ["CASE WHEN grade > 91 THEN 'ok ' ELSE 'bad' END"
:QUALITY]] :QUALITY]]
:order-by [(col-kw :id)]))] :order-by [: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))
@ -68,18 +67,17 @@
(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 (str "max(" (index) ")") :all)] (let [max-id (sql/aggregate-by-keys ds-opts :fruit "max(id)" :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 (col-kw :id) {}))) (is (nil? (sql/get-by-id ds-opts :fruit -1)))
(let [row (sql/get-by-id ds-opts :fruit 3 (col-kw :id) {})] (let [row (sql/get-by-id ds-opts :fruit 3)]
(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 {})]
@ -90,28 +88,23 @@
(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 (= (update-count 1) (is (= {:next.jdbc/update-count 1}
(sql/update! ds-opts :fruit {:appearance "brown"} {(col-kw :id) 2}))) (sql/update! ds-opts :fruit {:appearance "brown"} {:id 2})))
(is (= "brown" ((column :FRUIT/APPEARANCE) (is (= "brown" ((column :FRUIT/APPEARANCE)
(sql/get-by-id ds-opts :fruit 2 (col-kw :id) {})))) (sql/get-by-id ds-opts :fruit 2))))
(finally (finally
(sql/update! ds-opts :fruit {:appearance "yellow"} {(col-kw :id) 2}))) (sql/update! ds-opts :fruit {:appearance "yellow"} {:id 2})))
(try (try
(is (= (update-count 1) (is (= {:next.jdbc/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 (col-kw :id) {})))) (sql/get-by-id ds-opts :fruit 2))))
(finally (finally
(sql/update! ds-opts :fruit {:appearance "yellow"} {(col-kw :id) 2}))))) (sql/update! ds-opts :fruit {:appearance "yellow"} {:id 2})))))
(deftest test-insert-delete (deftest test-insert-delete
(let [new-key (cond (derby?) :1 (let [new-key (cond (derby?) :1
@ -120,24 +113,18 @@
(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
(cond-> {:name (as-varchar "Kiwi") {: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 (= (update-count 1) (is (= {:next.jdbc/update-count 1}
(sql/delete! (ds) :fruit {(col-kw :id) 5}))) (sql/delete! (ds) :fruit {: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?)
@ -146,28 +133,23 @@
[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
(cond->> [:name :appearance :cost :grade] [:name :appearance :cost :grade]
(xtdb?) (cons :_id)) [["Kiwi" "green & fuzzy" 100 99.9]
(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 (= (update-count 1) (is (= {:next.jdbc/update-count 1}
(sql/delete! (ds) :fruit {(col-kw :id) 6}))) (sql/delete! (ds) :fruit {:id 6})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"])))) (is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= (update-count 2) (is (= {:next.jdbc/update-count 2}
(sql/delete! (ds) :fruit [(str (index) " > ?") 4]))) (sql/delete! (ds) :fruit ["id > ?" 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?)
@ -176,28 +158,23 @@
[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
(cond->> '(:name :appearance :cost :grade) '(:name :appearance :cost :grade)
(xtdb?) (cons :_id)) '(("Kiwi" "green & fuzzy" 100 99.9)
(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 (= (update-count 1) (is (= {:next.jdbc/update-count 1}
(sql/delete! (ds) :fruit {(col-kw :id) 9}))) (sql/delete! (ds) :fruit {:id 9})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"])))) (is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= (update-count 2) (is (= {:next.jdbc/update-count 2}
(sql/delete! (ds) :fruit [(str (index) " > ?") 4]))) (sql/delete! (ds) :fruit ["id > ?" 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?)
@ -206,35 +183,31 @@
[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
(cond->> [{:name "Kiwi" [{:name "Kiwi"
:appearance "green & fuzzy" :appearance "green & fuzzy"
:cost 100 :cost 100
:grade 99.9} :grade 99.9}
{:name "Grape" {:name "Grape"
:appearance "black" :appearance "black"
:cost 10 :cost 10
:grade 50} :grade 50}
{:name "Lemon" {:name "Lemon"
: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 (= (update-count 1) (is (= {:next.jdbc/update-count 1}
(sql/delete! (ds) :fruit {(col-kw :id) 12}))) (sql/delete! (ds) :fruit {:id 12})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"])))) (is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= (update-count 2) (is (= {:next.jdbc/update-count 2}
(sql/delete! (ds) :fruit [(str (index) " > ?") 10]))) (sql/delete! (ds) :fruit ["id > ?" 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
@ -277,12 +250,12 @@
(deftest no-empty-order-by (deftest no-empty-order-by
(is (thrown? clojure.lang.ExceptionInfo (is (thrown? clojure.lang.ExceptionInfo
(sql/find-by-keys (ds) :fruit (sql/find-by-keys (ds) :fruit
{:name "Apple"} {:name "Apple"}
{:order-by []})))) {:order-by []}))))
(deftest array-in (deftest array-in
(when (postgres?) (when (postgres?)
(let [data (sql/find-by-keys (ds) :fruit [(str (index) " = any(?)") (int-array [1 2 3 4])])] (let [data (sql/find-by-keys (ds) :fruit ["id = any(?)" (int-array [1 2 3 4])])]
(is (= 4 (count data)))))) (is (= 4 (count data))))))
(deftest enum-pg (deftest enum-pg

View file

@ -64,27 +64,16 @@
(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)))
@ -97,34 +86,19 @@
(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" "xtdb"} (defn stored-proc? [] (not (#{"derby" "h2" "h2:mem" "sqlite"} (:dbtype @test-db-spec))))
(: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}
@ -182,54 +156,29 @@
: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 (when (stored-proc?)
(do (try
(try (jdbc/execute-one! con ["DROP PROCEDURE FRUITP"])
(do-commands con ["ERASE FROM fruit WHERE true"]) (catch Throwable _)))
(catch Throwable _)) (try
(try (do-commands con [(str "DROP TABLE " fruit)])
(do-commands con ["ERASE FROM btest WHERE true"]) (catch Exception _))
(catch Throwable _)) (try
(sql/insert-multi! con :fruit (do-commands con [(str "DROP TABLE " btest)])
[:_id :name :appearance :cost] (catch Exception _))
[[1 "Apple" "red" 59]] (when (postgres?)
{:return-keys false}) (try
(sql/insert-multi! con :fruit (do-commands con ["DROP TABLE LANG_TEST"])
[:_id :name :appearance :grade] (catch Exception _))
[[2 "Banana" "yellow" 92.2]] (try
{:return-keys false}) (do-commands con ["DROP TYPE LANGUAGE"])
(sql/insert-multi! con :fruit (catch Exception _))
[:_id :name :cost :grade] (do-commands con ["CREATE TYPE LANGUAGE AS ENUM('en','fr','de')"])
[[3 "Peach" 139 90.0]] (do-commands con ["
{: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?)
(try
(jdbc/execute-one! con ["DROP PROCEDURE FRUITP"])
(catch Throwable _)))
(try
(do-commands con [(str "DROP TABLE " fruit)])
(catch Exception _))
(try
(do-commands con [(str "DROP TABLE " btest)])
(catch Exception _))
(when (postgres?)
(try
(do-commands con ["DROP TABLE LANG_TEST"])
(catch Exception _))
(try
(do-commands con ["DROP TYPE LANGUAGE"])
(catch Exception _))
(do-commands con ["CREATE TYPE LANGUAGE AS ENUM('en','fr','de')"])
(do-commands con ["
CREATE TABLE LANG_TEST ( CREATE TABLE LANG_TEST (
LANG LANGUAGE NOT NULL LANG LANGUAGE NOT NULL
)"])) )"]))
(do-commands con [(str " (do-commands con [(str "
CREATE TABLE " fruit " ( CREATE TABLE " fruit " (
ID INTEGER " auto-inc-pk ", ID INTEGER " auto-inc-pk ",
NAME VARCHAR(32), NAME VARCHAR(32),
@ -237,28 +186,28 @@ CREATE TABLE " fruit " (
COST INT DEFAULT NULL, COST INT DEFAULT NULL,
GRADE REAL DEFAULT NULL GRADE REAL DEFAULT NULL
)")]) )")])
(let [created (atom false)] (let [created (atom false)]
;; MS SQL Server does not support bool/boolean: ;; MS SQL Server does not support bool/boolean:
(doseq [btype ["BOOL" "BOOLEAN" "BIT"]] (doseq [btype ["BOOL" "BOOLEAN" "BIT"]]
;; Derby does not support bit: ;; Derby does not support bit:
(doseq [bitty ["BIT" "SMALLINT"]] (doseq [bitty ["BIT" "SMALLINT"]]
(try (try
(when-not @created (when-not @created
(do-commands con [(str " (do-commands con [(str "
CREATE TABLE " btest " ( CREATE TABLE " btest " (
NAME VARCHAR(32), NAME VARCHAR(32),
IS_IT " btype ", IS_IT " btype ",
TWIDDLE " bitty " TWIDDLE " bitty "
)")]) )")])
(reset! created true)) (reset! created true))
(catch Throwable _)))) (catch Throwable _))))
(when-not @created (when-not @created
(println (:dbtype db) "failed btest creation") (println (:dbtype db) "failed btest creation")
#_(throw (ex-info (str (:dbtype db) " has no boolean type?") {})))) #_(throw (ex-info (str (:dbtype db) " has no boolean type?") {}))))
(when (stored-proc?) (when (stored-proc?)
(let [[begin end] (if (postgres?) ["$$" "$$"] ["BEGIN" "END"])] (let [[begin end] (if (postgres?) ["$$" "$$"] ["BEGIN" "END"])]
(try (try
(do-commands con [(str " (do-commands con [(str "
CREATE PROCEDURE FRUITP" (cond (hsqldb?) "() READS SQL DATA DYNAMIC RESULT SETS 2 " CREATE PROCEDURE FRUITP" (cond (hsqldb?) "() READS SQL DATA DYNAMIC RESULT SETS 2 "
(mssql?) " AS " (mssql?) " AS "
(postgres?) "() LANGUAGE SQL AS " (postgres?) "() LANGUAGE SQL AS "
@ -274,15 +223,15 @@ CREATE PROCEDURE FRUITP" (cond (hsqldb?) "() READS SQL DATA DYNAMIC RESULT SETS
SELECT * FROM " fruit " WHERE GRADE >= 90.0;")) " SELECT * FROM " fruit " WHERE GRADE >= 90.0;")) "
" end " " end "
")]) ")])
(catch Throwable t (catch Throwable t
(println 'procedure (:dbtype db) (ex-message t)))))) (println 'procedure (:dbtype db) (ex-message t))))))
(sql/insert-multi! con :fruit (sql/insert-multi! con :fruit
[:name :appearance :cost :grade] [:name :appearance :cost :grade]
[["Apple" "red" 59 nil] [["Apple" "red" 59 nil]
["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)

View file

@ -1,10 +1,17 @@
;; copyright (c) 2019-2025 Sean Corfield, all rights reserved ;; copyright (c) 2019-2024 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 [next.jdbc.specs :as specs] (:require [clojure.test :refer [deftest is testing use-fixtures]]
[next.jdbc.transaction])) [next.jdbc :as jdbc]
[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)

View file

@ -1,19 +1,14 @@
;; copyright (c) 2020-2025 Sean Corfield, all rights reserved ;; copyright (c) 2020-2024 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 [lazytest.core :refer [defdescribe describe it expect]] (:require [clojure.test :refer [deftest is]]
[next.jdbc.types :refer [as-varchar]])) [next.jdbc.types :refer [as-varchar]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(defdescribe as-varchar-tests (deftest as-varchar-test
(let [v (as-varchar "Hello")] (let [v (as-varchar "Hello")]
(describe "produces a function" (is (= "Hello" (v)))
(it "yields the original value when invoked" (is (contains? (meta v) 'next.jdbc.prepare/set-parameter))
(expect (fn? v)) (is (fn? (get (meta v) 'next.jdbc.prepare/set-parameter)))))
(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)))))))

File diff suppressed because it is too large Load diff