wip xtdb testing

Signed-off-by: Sean Corfield <sean@corfield.org>
This commit is contained in:
Sean Corfield 2024-11-23 23:56:44 -08:00
parent ecd950d009
commit 0c50cf28b5
No known key found for this signature in database
10 changed files with 770 additions and 637 deletions

View file

@ -0,0 +1,8 @@
{:hooks
{:analyze-call
{next.jdbc/with-transaction
hooks.com.github.seancorfield.next-jdbc/with-transaction
next.jdbc/with-transaction+options
hooks.com.github.seancorfield.next-jdbc/with-transaction+options}}
:lint-as {next.jdbc/on-connection clojure.core/with-open
next.jdbc/on-connection+options clojure.core/with-open}}

View file

@ -0,0 +1,34 @@
(ns hooks.com.github.seancorfield.next-jdbc
(:require [clj-kondo.hooks-api :as api]))
(defn with-transaction
"Expands (with-transaction [tx expr opts] body)
to (let [tx expr] opts body) per clj-kondo examples."
[{:keys [:node]}]
(let [[binding-vec & body] (rest (:children node))
[sym val opts] (:children binding-vec)]
(when-not (and sym val)
(throw (ex-info "No sym and val provided" {})))
(let [new-node (api/list-node
(list*
(api/token-node 'let)
(api/vector-node [sym val])
opts
body))]
{:node new-node})))
(defn with-transaction+options
"Expands (with-transaction+options [tx expr opts] body)
to (let [tx expr] opts body) per clj-kondo examples."
[{:keys [:node]}]
(let [[binding-vec & body] (rest (:children node))
[sym val opts] (:children binding-vec)]
(when-not (and sym val)
(throw (ex-info "No sym and val provided" {})))
(let [new-node (api/list-node
(list*
(api/token-node 'let)
(api/vector-node [sym val])
opts
body))]
{:node new-node})))

View file

@ -1,4 +1,5 @@
{: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"]
:deps {org.clojure/clojure {:mvn/version "1.10.3"}
org.clojure/java.data {:mvn/version "1.2.107"}
@ -40,6 +41,8 @@
io.zonky.test.postgres/embedded-postgres-binaries-windows-amd64 {:mvn/version "17.0.0"}
org.xerial/sqlite-jdbc {:mvn/version "3.46.1.3"}
com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.8.1.jre11"}
;; prerelease XTDB JDBC module:
com.xtdb/xtdb-jdbc {:mvn/version "2.0.0-SNAPSHOT"}
;; use log4j2 to reduce log noise during testing:
org.apache.logging.log4j/log4j-api {:mvn/version "2.24.0"}
;; bridge everything into log4j:

View file

@ -14,3 +14,8 @@ services:
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
ports:
- "1433:1433"
xtdb:
image: ghcr.io/xtdb/xtdb
pull_policy: always
ports:
- "5432:5432"

View file

@ -10,7 +10,7 @@
[next.jdbc :as jdbc]
[next.jdbc.date-time] ; to extend SettableParameter to date/time
[next.jdbc.test-fixtures :refer [with-test-db db ds
mssql?]]
mssql? xtdb?]]
[next.jdbc.specs :as specs])
(:import (java.sql ResultSet)))
@ -21,6 +21,7 @@
(specs/instrument)
(deftest issue-73
(when-not (xtdb?)
(try
(jdbc/execute-one! (ds) ["drop table fruit_time"])
(catch Throwable _))
@ -46,4 +47,4 @@
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)])
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 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 (?,?)" 4 (java.time.LocalDateTime/now)]))
(jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 4 (java.time.LocalDateTime/now)])))

View file

@ -11,7 +11,7 @@
(:require [clojure.test :refer [deftest is testing use-fixtures]]
[next.jdbc :as jdbc]
[next.jdbc.test-fixtures
:refer [with-test-db ds jtds? mssql? sqlite?]]
:refer [with-test-db ds jtds? mssql? sqlite? xtdb?]]
[next.jdbc.prepare :as prep]
[next.jdbc.specs :as specs]))
@ -22,6 +22,7 @@
(specs/instrument)
(deftest execute-batch-tests
(when-not (xtdb?)
(testing "simple batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13]
(jdbc/with-transaction [t (ds) {:rollback-only true}]
@ -120,4 +121,4 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
;; Derby and SQLite only return one generated key per batch so there
;; are only three keys, plus the overall count here:
(is (< 3 (count results))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))))

View file

@ -12,9 +12,9 @@
[next.jdbc.protocols :as p]
[next.jdbc.result-set :as rs]
[next.jdbc.specs :as specs]
[next.jdbc.test-fixtures :refer [with-test-db ds column
[next.jdbc.test-fixtures :refer [with-test-db ds column index col-kw
default-options
derby? mssql? mysql? postgres?]])
derby? mssql? mysql? postgres? xtdb?]])
(:import (java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true)
@ -93,7 +93,7 @@
(deftest test-map-row-builder
(testing "default row builder"
(let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 1]
[(str "select * from fruit where " (index) " = ?") 1]
(default-options))]
(is (map? row))
(is (contains? row (column :FRUIT/GRADE)))
@ -101,7 +101,7 @@
(is (= 1 ((column :FRUIT/ID) row)))
(is (= "Apple" ((column :FRUIT/NAME) row))))
(let [rs (p/-execute-all (ds)
["select * from fruit order by id"]
[(str "select * from fruit order by " (index))]
(default-options))]
(is (every? map? rs))
(is (= 1 ((column :FRUIT/ID) (first rs))))
@ -110,7 +110,7 @@
(is (= "Orange" ((column :FRUIT/NAME) (last rs))))))
(testing "unqualified row builder"
(let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 2]
[(str "select * from fruit where " (index) " = ?") 2]
{:builder-fn rs/as-unqualified-maps})]
(is (map? row))
(is (contains? row (column :COST)))
@ -119,7 +119,7 @@
(is (= "Banana" ((column :NAME) row)))))
(testing "lower-case row builder"
(let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3]
[(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options)
:builder-fn rs/as-lower-maps))]
(is (map? row))
@ -129,33 +129,33 @@
(is (= "Peach" (:fruit/name row)))))
(testing "unqualified lower-case row builder"
(let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 4]
[(str "select * from fruit where " (index) " = ?") 4]
{:builder-fn rs/as-unqualified-lower-maps})]
(is (map? row))
(is (= 4 (:id row)))
(is (= "Orange" (:name row)))))
(testing "kebab-case row builder"
(let [row (p/-execute-one (ds)
["select id,name,appearance as looks_like from fruit where id = ?" 3]
[(str "select " (index) ",name,appearance as looks_like from fruit where " (index) " = ?") 3]
(assoc (default-options)
:builder-fn rs/as-kebab-maps))]
(is (map? row))
(is (contains? row :fruit/looks-like))
(is (nil? (:fruit/looks-like row)))
(is (= 3 (:fruit/id row)))
(is (= "Peach" (:fruit/name row)))))
(is (contains? row (col-kw :fruit/looks-like)))
(is (nil? ((col-kw :fruit/looks-like) row)))
(is (= 3 ((col-kw :fruit/id) row)))
(is (= "Peach" ((col-kw :fruit/name) row)))))
(testing "unqualified kebab-case row builder"
(let [row (p/-execute-one (ds)
["select id,name,appearance as looks_like from fruit where id = ?" 4]
[(str "select " (index) ",name,appearance as looks_like from fruit where " (index) " = ?") 4]
{:builder-fn rs/as-unqualified-kebab-maps})]
(is (map? row))
(is (contains? row :looks-like))
(is (= "juicy" (:looks-like row)))
(is (= 4 (:id row)))
(is (= 4 ((col-kw :id) row)))
(is (= "Orange" (:name row)))))
(testing "custom row builder 1"
(let [row (p/-execute-one (ds)
["select fruit.*, id + 100 as newid from fruit where id = ?" 3]
[(str "select fruit.*, " (index) " + 100 as newid from fruit where " (index) " = ?") 3]
(assoc (default-options)
:builder-fn rs/as-modified-maps
:label-fn str/lower-case
@ -181,7 +181,7 @@
(is (= "Peach" (:vegetable/name row)))))
(testing "adapted row builder"
(let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3]
[(str "select * from fruit where " (index) " = ?") 3]
(assoc
(default-options)
:builder-fn (rs/as-maps-adapter
@ -207,7 +207,7 @@
(fn [^ResultSet rs _ ^Integer i]
(.getObject rs i)))
row (p/-execute-one (ds)
["select * from fruit where id = ?" 3]
[(str "select * from fruit where " (index) " = ?") 3]
(assoc
(default-options)
:builder-fn (rs/as-maps-adapter
@ -299,31 +299,31 @@
(testing "no row builder is used"
(is (= [true]
(into [] (map map?) ; it looks like a real map now
(p/-execute (ds) ["select * from fruit where id = ?" 1]
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn (constantly nil)}))))
(is (= ["Apple"]
(into [] (map :name) ; keyword selection works
(p/-execute (ds) ["select * from fruit where id = ?" 1]
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn (constantly nil)}))))
(is (= [[2 [:name "Banana"]]]
(into [] (map (juxt #(get % "id") ; get by string key works
#(find % :name))) ; get MapEntry works
(p/-execute (ds) ["select * from fruit where id = ?" 2]
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 2]
{:builder-fn (constantly nil)}))))
(is (= [{:id 3 :name "Peach"}]
(into [] (map #(select-keys % [:id :name])) ; select-keys works
(p/-execute (ds) ["select * from fruit where id = ?" 3]
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 3]
{:builder-fn (constantly nil)}))))
(is (= [[:orange 4]]
(into [] (map #(vector (if (contains? % :name) ; contains works
(keyword (str/lower-case (:name %)))
:unnamed)
(get % :id 0))) ; get with not-found works
(p/-execute (ds) ["select * from fruit where id = ?" 4]
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 4]
{:builder-fn (constantly nil)}))))
(is (= [{}]
(into [] (map empty) ; return empty map without building
(p/-execute (ds) ["select * from fruit where id = ?" 1]
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn (constantly nil)})))))
(testing "count does not build a map"
(let [count-builder (fn [_1 _2]
@ -331,7 +331,7 @@
(column-count [_] 13)))]
(is (= [13]
(into [] (map count) ; count relies on columns, not row fields
(p/-execute (ds) ["select * from fruit where id = ?" 1]
(p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn count-builder}))))))
(testing "assoc, dissoc, cons, seq, and = build maps"
(is (map? (reduce (fn [_ row] (reduced (assoc row :x 1)))
@ -467,7 +467,7 @@
metadata))))
(deftest clob-reading
(when-not (or (mssql?) (mysql?) (postgres?)) ; no clob in these
(when-not (or (mssql?) (mysql?) (postgres?) (xtdb?)) ; no clob in these
(with-open [con (p/get-connection (ds) {})]
(try
(p/-execute-one con ["DROP TABLE CLOBBER"] {})

View file

@ -2,13 +2,14 @@
(ns next.jdbc.sql-test
"Tests for the syntactic sugar SQL functions."
(:require [clojure.test :refer [deftest is testing use-fixtures]]
(:require
[clojure.test :refer [deftest is testing use-fixtures]]
[next.jdbc :as jdbc]
[next.jdbc.specs :as specs]
[next.jdbc.sql :as sql]
[next.jdbc.test-fixtures
:refer [with-test-db ds column default-options
derby? jtds? maria? mssql? mysql? postgres? sqlite?]]
:refer [column col-kw default-options derby? ds index
jtds? maria? mssql? mysql? postgres? sqlite? with-test-db xtdb?]]
[next.jdbc.types :refer [as-other as-real as-varchar]]))
(set! *warn-on-reflection* true)
@ -76,8 +77,8 @@
(deftest test-get-by-id
(let [ds-opts (jdbc/with-options (ds) (default-options))]
(is (nil? (sql/get-by-id ds-opts :fruit -1)))
(let [row (sql/get-by-id ds-opts :fruit 3)]
(is (nil? (sql/get-by-id ds-opts :fruit -1 (col-kw :id) {})))
(let [row (sql/get-by-id ds-opts :fruit 3 (col-kw :id) {})]
(is (map? row))
(is (= "Peach" ((column :FRUIT/NAME) row))))
(let [row (sql/get-by-id ds-opts :fruit "juicy" :appearance {})]
@ -92,19 +93,19 @@
(let [ds-opts (jdbc/with-options (ds) (default-options))]
(try
(is (= {:next.jdbc/update-count 1}
(sql/update! ds-opts :fruit {:appearance "brown"} {:id 2})))
(sql/update! ds-opts :fruit {:appearance "brown"} {(col-kw :id) 2})))
(is (= "brown" ((column :FRUIT/APPEARANCE)
(sql/get-by-id ds-opts :fruit 2))))
(sql/get-by-id ds-opts :fruit 2 (col-kw :id) {}))))
(finally
(sql/update! ds-opts :fruit {:appearance "yellow"} {:id 2})))
(sql/update! ds-opts :fruit {:appearance "yellow"} {(col-kw :id) 2})))
(try
(is (= {:next.jdbc/update-count 1}
(sql/update! ds-opts :fruit {:appearance "green"}
["name = ?" "Banana"])))
(is (= "green" ((column :FRUIT/APPEARANCE)
(sql/get-by-id ds-opts :fruit 2))))
(sql/get-by-id ds-opts :fruit 2 (col-kw :id) {}))))
(finally
(sql/update! ds-opts :fruit {:appearance "yellow"} {:id 2})))))
(sql/update! ds-opts :fruit {:appearance "yellow"} {(col-kw :id) 2})))))
(deftest test-insert-delete
(let [new-key (cond (derby?) :1
@ -113,18 +114,23 @@
(mssql?) :GENERATED_KEYS
(mysql?) :GENERATED_KEY
(postgres?) :fruit/id
(xtdb?) :_id
:else :FRUIT/ID)]
(testing "single insert/delete"
(is (== 5 (new-key (sql/insert! (ds) :fruit
{:name (as-varchar "Kiwi")
(is (== 5 (new-key (doto
(sql/insert! (ds) :fruit
(cond-> {:name (as-varchar "Kiwi")
:appearance "green & fuzzy"
:cost 100 :grade (as-real 99.9)}
(xtdb?)
(assoc :_id 5))
{:suffix
(when (sqlite?)
"RETURNING *")}))))
"RETURNING *")})
(println (ds))))))
(is (= 5 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1}
(sql/delete! (ds) :fruit {:id 5})))
(sql/delete! (ds) :fruit {(col-kw :id) 5})))
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "multiple insert/delete"
(is (= (cond (derby?)
@ -137,19 +143,22 @@
[6 7 8])
(mapv new-key
(sql/insert-multi! (ds) :fruit
[:name :appearance :cost :grade]
[["Kiwi" "green & fuzzy" 100 99.9]
(cond->> [:name :appearance :cost :grade]
(xtdb?) (cons :_id))
(cond->> [["Kiwi" "green & fuzzy" 100 99.9]
["Grape" "black" 10 50]
["Lemon" "yellow" 20 9.9]]
(xtdb?)
(map cons [6 7 8]))
{:suffix
(when (sqlite?)
"RETURNING *")}))))
(is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1}
(sql/delete! (ds) :fruit {:id 6})))
(sql/delete! (ds) :fruit {(col-kw :id) 6})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 2}
(sql/delete! (ds) :fruit ["id > ?" 4])))
(sql/delete! (ds) :fruit [(str (index) " > ?") 4])))
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "multiple insert/delete with sequential cols/rows" ; per #43
(is (= (cond (derby?)
@ -162,19 +171,22 @@
[9 10 11])
(mapv new-key
(sql/insert-multi! (ds) :fruit
'(:name :appearance :cost :grade)
'(("Kiwi" "green & fuzzy" 100 99.9)
(cond->> '(:name :appearance :cost :grade)
(xtdb?) (cons :_id))
(cond->> '(("Kiwi" "green & fuzzy" 100 99.9)
("Grape" "black" 10 50)
("Lemon" "yellow" 20 9.9))
(xtdb?)
(map cons [9 10 11]))
{:suffix
(when (sqlite?)
"RETURNING *")}))))
(is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1}
(sql/delete! (ds) :fruit {:id 9})))
(sql/delete! (ds) :fruit {(col-kw :id) 9})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 2}
(sql/delete! (ds) :fruit ["id > ?" 4])))
(sql/delete! (ds) :fruit [(str (index) " > ?") 4])))
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "multiple insert/delete with maps"
(is (= (cond (derby?)
@ -187,7 +199,7 @@
[12 13 14])
(mapv new-key
(sql/insert-multi! (ds) :fruit
[{:name "Kiwi"
(cond->> [{:name "Kiwi"
:appearance "green & fuzzy"
:cost 100
:grade 99.9}
@ -199,15 +211,17 @@
:appearance "yellow"
:cost 20
:grade 9.9}]
(xtdb?)
(map #(assoc %2 :_id %1) [12 13 14]))
{:suffix
(when (sqlite?)
"RETURNING *")}))))
(is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1}
(sql/delete! (ds) :fruit {:id 12})))
(sql/delete! (ds) :fruit {(col-kw :id) 12})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 2}
(sql/delete! (ds) :fruit ["id > ?" 10])))
(sql/delete! (ds) :fruit [(str (index) " > ?") 10])))
(is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "empty insert-multi!" ; per #44 and #264
(is (= [] (sql/insert-multi! (ds) :fruit
@ -255,7 +269,7 @@
(deftest array-in
(when (postgres?)
(let [data (sql/find-by-keys (ds) :fruit ["id = any(?)" (int-array [1 2 3 4])])]
(let [data (sql/find-by-keys (ds) :fruit [(str (index) " = any(?)") (int-array [1 2 3 4])])]
(is (= 4 (count data))))))
(deftest enum-pg

View file

@ -64,11 +64,17 @@
(def ^:private test-jtds
(when (System/getenv "NEXT_JDBC_TEST_MSSQL") test-jtds-map))
(def ^:private test-xtdb-map {:dbtype "xtdb"})
(def ^:private test-xtdb
(when (System/getenv "NEXT_JDBC_TEST_XTDB") test-xtdb-map))
(def ^:private test-db-specs
(cond-> [test-derby test-h2-mem test-h2 test-hsql test-sqlite]
test-postgres (conj test-postgres)
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))
@ -86,19 +92,34 @@
(defn postgres? [] (= "embedded-postgres" (:dbtype @test-db-spec)))
(defn xtdb? [] (= "xtdb" (:dbtype @test-db-spec)))
(defn sqlite? [] (= "sqlite" (:dbtype @test-db-spec)))
(defn stored-proc? [] (not (#{"derby" "h2" "h2:mem" "sqlite"} (:dbtype @test-db-spec))))
(defn stored-proc? [] (not (#{"derby" "h2" "h2:mem" "sqlite" "xtdb"}
(:dbtype @test-db-spec))))
(defn column [k]
(let [n (namespace k)]
(keyword (when n (cond (postgres?) (str/lower-case n)
(mssql?) (str/lower-case n)
(mysql?) (str/lower-case n)
(xtdb?) nil
:else n))
(cond (postgres?) (str/lower-case (name k))
(xtdb?) (let [c (str/lower-case (name k))]
(if (= "id" c) "_id" c))
:else (name k)))))
(defn index []
(if (xtdb?) "_id" "id"))
(defn col-kw [k]
(if (xtdb?)
(let [n (name k)]
(if (= "id" n) :_id (keyword n)))
k))
(defn default-options []
(if (mssql?) ; so that we get table names back from queries
{:result-type :scroll-insensitive :concurrency :read-only}
@ -156,6 +177,28 @@
:else
"AUTO_INCREMENT PRIMARY KEY")]
(with-open [con (jdbc/get-connection (ds))]
(if (xtdb?) ; no DDL for creation
(do
(try
(do-commands con ["DELETE FROM fruit WHERE true"])
(catch Throwable _))
(sql/insert-multi! con :fruit
[:_id :name :appearance :cost]
[[1 "Apple" "red" 59]]
{:return-keys false})
(sql/insert-multi! con :fruit
[:_id :name :appearance :grade]
[[2 "Banana" "yellow" 92.2]]
{:return-keys false})
(sql/insert-multi! con :fruit
[:_id :name :cost :grade]
[[3 "Peach" 139 90.0]]
{:return-keys false})
(sql/insert-multi! con :fruit
[:_id :name :appearance :cost :grade]
[[4 "Orange" "juicy" 89 88.6]]
{:return-keys false}))
(do
(when (stored-proc?)
(try
(jdbc/execute-one! con ["DROP PROCEDURE FRUITP"])
@ -231,7 +274,7 @@ CREATE PROCEDURE FRUITP" (cond (hsqldb?) "() READS SQL DATA DYNAMIC RESULT SETS
["Banana" "yellow" nil 92.2]
["Peach" nil 139 90.0]
["Orange" "juicy" 89 88.6]]
{:return-keys false})
{:return-keys false})))
(t)))))
(create-clojure-test)

View file

@ -2,21 +2,23 @@
(ns next.jdbc-test
"Basic tests for the primary API of `next.jdbc`."
(:require [clojure.core.reducers :as r]
(:require
[clojure.core.reducers :as r]
[clojure.string :as str]
[clojure.test :refer [deftest is testing use-fixtures]]
[next.jdbc :as jdbc]
[next.jdbc.connection :as c]
[next.jdbc.test-fixtures
:refer [with-test-db db ds column
default-options stored-proc?
derby? hsqldb? jtds? mssql? mysql? postgres? sqlite?]]
[next.jdbc.prepare :as prep]
[next.jdbc.result-set :as rs]
[next.jdbc.specs :as specs]
[next.jdbc.test-fixtures
:refer [col-kw column db default-options derby? ds hsqldb? index
jtds? mssql? mysql? postgres? sqlite? stored-proc?
with-test-db xtdb?]]
[next.jdbc.types :as types])
(:import (com.zaxxer.hikari HikariDataSource)
(:import
(com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource)
(com.zaxxer.hikari HikariDataSource)
(java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true)
@ -60,27 +62,27 @@
(jdbc/execute-one!
ds-opts
["select * from fruit where appearance = ?" "red"]))))
(is (= "red" (:fruit/looks-like
(is (= "red" ((col-kw :fruit/looks-like)
(jdbc/execute-one!
ds-opts
["select appearance as looks_like from fruit where id = ?" 1]
[(str "select appearance as looks_like from fruit where " (index) " = ?") 1]
jdbc/snake-kebab-opts))))
(let [ds' (jdbc/with-options ds-opts jdbc/snake-kebab-opts)]
(is (= "red" (:fruit/looks-like
(is (= "red" ((col-kw :fruit/looks-like)
(jdbc/execute-one!
ds'
["select appearance as looks_like from fruit where id = ?" 1])))))
[(str "select appearance as looks_like from fruit where " (index) " = ?") 1])))))
(jdbc/with-transaction+options [ds' (jdbc/with-options ds-opts jdbc/snake-kebab-opts)]
(is (= (merge (default-options) jdbc/snake-kebab-opts)
(:options ds')))
(is (= "red" (:fruit/looks-like
(is (= "red" ((col-kw :fruit/looks-like)
(jdbc/execute-one!
ds'
["select appearance as looks_like from fruit where id = ?" 1])))))
[(str "select appearance as looks_like from fruit where " (index) " = ?") 1])))))
(is (= "red" (:looks-like
(jdbc/execute-one!
ds-opts
["select appearance as looks_like from fruit where id = ?" 1]
[(str "select appearance as looks_like from fruit where " (index) " = ?") 1]
jdbc/unqualified-snake-kebab-opts)))))
(testing "execute!"
(let [rs (jdbc/execute!
@ -95,7 +97,7 @@
(is (= 1 ((column :FRUIT/ID) (first rs)))))
(let [rs (jdbc/execute!
ds-opts
["select * from fruit order by id"]
[(str "select * from fruit order by " (index))]
{:builder-fn rs/as-maps})]
(is (every? map? rs))
(is (every? meta rs))
@ -104,22 +106,26 @@
(is (= 4 ((column :FRUIT/ID) (last rs)))))
(let [rs (jdbc/execute!
ds-opts
["select * from fruit order by id"]
[(str "select * from fruit order by " (index))]
{:builder-fn rs/as-arrays})]
(is (every? vector? rs))
(is (= 5 (count rs)))
(is (every? #(= 5 (count %)) rs))
;; columns come first
(is (every? qualified-keyword? (first rs)))
(is (every? (if (xtdb?) keyword? qualified-keyword?) (first rs)))
;; :FRUIT/ID should be first column
(is (= (column :FRUIT/ID) (ffirst rs)))
;; and all its corresponding values should be ints
(is (every? int? (map first (rest rs))))
(is (every? string? (map second (rest rs))))))
(when (xtdb?) (println (first rs)
(.indexOf ^java.util.List (first rs) :name)
(.indexOf (first rs) :name)))
(let [n (.indexOf ^java.util.List (first rs) :name)]
(is (every? string? (map #(nth % n) (rest rs)))))))
(testing "execute! with adapter"
(let [rs (jdbc/execute! ; test again, with adapter and lower columns
ds-opts
["select * from fruit order by id"]
[(str "select * from fruit order by " (index))]
{:builder-fn (rs/as-arrays-adapter
rs/as-lower-arrays
(fn [^ResultSet rs _ ^Integer i]
@ -128,16 +134,20 @@
(is (= 5 (count rs)))
(is (every? #(= 5 (count %)) rs))
;; columns come first
(is (every? qualified-keyword? (first rs)))
(is (every? (if (xtdb?) keyword? qualified-keyword?) (first rs)))
;; :fruit/id should be first column
(is (= :fruit/id (ffirst rs)))
(is (= (col-kw :fruit/id) (ffirst rs)))
;; and all its corresponding values should be ints
(is (every? int? (map first (rest rs))))
(is (every? string? (map second (rest rs))))))
(when (xtdb?) (println (first rs)
(.indexOf ^java.util.List (first rs) :name)
(.indexOf (first rs) :name)))
(let [n (.indexOf ^java.util.List (first rs) :name)]
(is (every? string? (map #(nth % n) (rest rs)))))))
(testing "execute! with unqualified"
(let [rs (jdbc/execute!
(ds)
["select * from fruit order by id"]
[(str "select * from fruit order by " (index))]
{:builder-fn rs/as-unqualified-maps})]
(is (every? map? rs))
(is (every? meta rs))
@ -146,7 +156,7 @@
(is (= 4 ((column :ID) (last rs)))))
(let [rs (jdbc/execute!
ds-opts
["select * from fruit order by id"]
[(str "select * from fruit order by " (index))]
{:builder-fn rs/as-unqualified-arrays})]
(is (every? vector? rs))
(is (= 5 (count rs)))
@ -157,11 +167,15 @@
(is (= (column :ID) (ffirst rs)))
;; and all its corresponding values should be ints
(is (every? int? (map first (rest rs))))
(is (every? string? (map second (rest rs))))))
(when (xtdb?) (println (first rs)
(.indexOf ^java.util.List (first rs) :name)
(.indexOf (first rs) :name)))
(let [n (.indexOf ^java.util.List (first rs) :name)]
(is (every? string? (map #(nth % n) (rest rs)))))))
(testing "execute! with :max-rows / :maxRows"
(let [rs (jdbc/execute!
ds-opts
["select * from fruit order by id"]
[(str "select * from fruit order by " (index))]
{:max-rows 2})]
(is (every? map? rs))
(is (every? meta rs))
@ -170,7 +184,7 @@
(is (= 2 ((column :FRUIT/ID) (last rs)))))
(let [rs (jdbc/execute!
ds-opts
["select * from fruit order by id"]
[(str "select * from fruit order by " (index))]
{:statement {:maxRows 2}})]
(is (every? map? rs))
(is (every? meta rs))
@ -182,7 +196,7 @@
(let [rs (with-open [con (jdbc/get-connection (ds))
ps (jdbc/prepare
con
["select * from fruit order by id"]
[(str "select * from fruit order by " (index))]
(default-options))]
(jdbc/execute! ps))]
(is (every? map? rs))
@ -194,7 +208,7 @@
(let [rs (with-open [con (jdbc/get-connection (ds))
ps (jdbc/prepare
con
["select * from fruit where id = ?"]
[(str "select * from fruit where " (index) " = ?")]
(default-options))]
(jdbc/execute! (prep/set-parameters ps [4]) nil {}))]
(is (every? map? rs))
@ -205,7 +219,8 @@
;; default options do not flow over get-connection
(let [rs (with-open [con (jdbc/get-connection (ds))]
(jdbc/execute! (prep/statement con (default-options))
["select * from fruit order by id"]))]
[(str "select * from fruit order by " (index))]))]
(when (xtdb?) (println rs))
(is (every? map? rs))
(is (every? meta rs))
(is (= 4 (count rs)))
@ -214,11 +229,13 @@
;; default options do not flow over get-connection
(let [rs (with-open [con (jdbc/get-connection (ds))]
(jdbc/execute! (prep/statement con (default-options))
["select * from fruit where id = 4"]))]
[(str "select * from fruit where " (index) " = 4")]))]
(when (xtdb?) (println rs))
(is (every? map? rs))
(is (every? meta rs))
(is (= 1 (count rs)))
(is (= 4 ((column :FRUIT/ID) (first rs))))))
(when-not (xtdb?)
(testing "transact"
(is (= [{:next.jdbc/update-count 1}]
(jdbc/transact (ds)
@ -354,11 +371,11 @@ VALUES ('Pear', 'green', 49, 47)
(.rollback t save-point)
result))))
(is (= 4 (count (jdbc/execute! con ["select * from fruit"]))))
(is (= ac (.getAutoCommit con)))))))
(is (= ac (.getAutoCommit con))))))))
(deftest issue-146
;; since we use an embedded PostgreSQL data source, we skip this:
(when-not (or (postgres?)
(when-not (or (postgres?) (xtdb?)
;; and now we skip MS SQL because we can't use the db-spec
;; we'd need to build the jdbcUrl with encryption turned off:
(and (mssql?) (not (jtds?))))
@ -495,6 +512,7 @@ VALUES ('Pear', 'green', 49, 47)
"\n\t" (ex-message t)))))
(deftest bool-tests
(when-not (xtdb?)
(doseq [[n b] [["zero" 0] ["one" 1] ["false" false] ["true" true]]
:let [v-bit (if (number? b) b (if b 1 0))
v-bool (if (number? b) (pos? b) b)]]
@ -548,9 +566,10 @@ VALUES ('Pear', 'green', 49, 47)
[]
(jdbc/plan (ds) ["select * from btest"]))]
(is (every? boolean? (map :is_it data)))
(is (every? boolean? (map :twiddle data)))))
(is (every? boolean? (map :twiddle data))))))
(deftest execute-batch-tests
(when-not (xtdb?)
(testing "simple batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13]
(jdbc/with-transaction [t (ds) {:rollback-only true}]
@ -649,9 +668,10 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
;; Derby and SQLite only return one generated key per batch so there
;; are only three keys, plus the overall count here:
(is (< 3 (count results))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))))
(deftest execute-batch-connectable-tests
(when-not (xtdb?)
(testing "simple batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13]
(try
@ -669,7 +689,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "batch with-options"
(is (= [1 1 1 1 1 1 1 1 1 13]
@ -688,7 +708,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "batch with-logging"
(is (= [1 1 1 1 1 1 1 1 1 13]
@ -707,7 +727,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "small batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13]
@ -726,7 +746,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{:batch-size 3})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "big batch insert"
(is (= [1 1 1 1 1 1 1 1 1 13]
@ -745,7 +765,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
{:batch-size 8})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "large batch insert"
(when-not (or (jtds?) (sqlite?))
@ -766,7 +786,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
:large true})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))))
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))
(testing "return generated keys"
(when-not (or (mssql?) (sqlite?))
@ -790,26 +810,30 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
:return-generated-keys true})]
(conj result (count (jdbc/execute! (ds) ["select * from fruit"]))))
(finally
(jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))]
(jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")])))]
(is (= 13 (last results)))
(is (every? map? (butlast results)))
;; Derby and SQLite only return one generated key per batch so there
;; are only three keys, plus the overall count here:
(is (< 3 (count results))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))))))
(deftest folding-test
(jdbc/execute-one! (ds) ["delete from fruit"])
(if (xtdb?)
(with-open [con (jdbc/get-connection (ds))
ps (jdbc/prepare con ["insert into fruit(_id,name) values (?,?)"])]
(jdbc/execute-batch! ps (mapv #(vector % (str "Fruit-" %)) (range 1 1001))))
(with-open [con (jdbc/get-connection (ds))
ps (jdbc/prepare con ["insert into fruit(name) values (?)"])]
(jdbc/execute-batch! ps (mapv #(vector (str "Fruit-" %)) (range 1 1001))))
(jdbc/execute-batch! ps (mapv #(vector (str "Fruit-" %)) (range 1 1001)))))
(testing "foldable result set"
(testing "from a Connection"
(let [result
(with-open [con (jdbc/get-connection (ds))]
(r/foldcat
(r/map (column :FRUIT/NAME)
(jdbc/plan con ["select * from fruit order by id"]
(jdbc/plan con [(str "select * from fruit order by " (index))]
(default-options)))))]
(is (= 1000 (count result)))
(is (= "Fruit-1" (first result)))
@ -821,7 +845,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(try
(r/fold n r/cat r/append!
(r/map (column :FRUIT/NAME)
(jdbc/plan (ds) ["select * from fruit order by id"]
(jdbc/plan (ds) [(str "select * from fruit order by " (index))]
(default-options))))
(catch java.util.concurrent.RejectedExecutionException _
[]))]
@ -832,7 +856,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(let [result
(with-open [con (jdbc/get-connection (ds))
stmt (jdbc/prepare con
["select * from fruit order by id"]
[(str "select * from fruit order by " (index))]
(default-options))]
(r/foldcat
(r/map (column :FRUIT/NAME)
@ -846,7 +870,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
stmt (prep/statement con (default-options))]
(r/foldcat
(r/map (column :FRUIT/NAME)
(jdbc/plan stmt ["select * from fruit order by id"]
(jdbc/plan stmt [(str "select * from fruit order by " (index))]
(default-options)))))]
(is (= 1000 (count result)))
(is (= "Fruit-1" (first result)))
@ -854,7 +878,7 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(deftest connection-tests
(testing "datasource via jdbcUrl"
(when-not (postgres?)
(when-not (or (postgres?) (xtdb?))
(let [[url etc] (#'c/spec->url+etc (db))
ds (jdbc/get-datasource (assoc etc :jdbcUrl url))]
(cond (derby?) (is (= {:create true} etc))
@ -937,11 +961,11 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
(let [s (pr-str (into [] (take 3) (jdbc/plan (ds) ["select * from fruit"]
(default-options))))]
(is (or (re-find #"missing `map` or `reduce`" s)
(re-find #"(?i)^\[#:fruit\{.*:id.*\}\]$" s))))
(is (every? #(re-find #"(?i)^#:fruit\{.*:id.*\}$" %)
(re-find #"(?i)^\[(#:fruit)?\{.*:_?id.*\}\]$" s))))
(is (every? #(re-find #"(?i)^(#:fruit)?\{.*:_?id.*\}$" %)
(into [] (map str) (jdbc/plan (ds) ["select * from fruit"]
(default-options)))))
(is (every? #(re-find #"(?i)^#:fruit\{.*:id.*\}$" %)
(is (every? #(re-find #"(?i)^(#:fruit)?\{.*:_?id.*\}$" %)
(into [] (map pr-str) (jdbc/plan (ds) ["select * from fruit"]
(default-options)))))
(is (thrown? IllegalArgumentException