Merge pull request #290 from seancorfield/xtdb-testing

enable xtdb testing automatically
This commit is contained in:
Sean Corfield 2024-12-11 09:54:07 -08:00 committed by GitHub
commit 87f4224c22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1481 additions and 770 deletions

View file

@ -0,0 +1,8 @@
{:linters {:xtql/redundant-pipeline {:level :warning}
:xtql/redundant-unify {:level :warning}
:xtql/unrecognized-operation {:level :error}
:xtql/unrecognized-parameter {:level :warning}
:xtql/missing-parameter {:level :error}
:xtql/type-mismatch {:level :error}
:xtql/invalid-arity {:level :error}}
:hooks {:analyze-call {xtdb.api/q hooks.xtql/q}}}

View file

@ -0,0 +1,567 @@
(ns ^:no-doc hooks.xtql
(:require [clj-kondo.hooks-api :as api]))
(def source-op?
#{'from 'rel 'unify})
(def tail-op?
#{'aggregate
'limit 'offset
'where
'order-by
'with 'without 'return
'unnest})
(def unify-clause?
#{'from 'rel
'join 'left-join
'unnest
'where
'with})
(defn node-map? [node]
(contains? #{:map :namespaced-map}
(:tag node)))
(defn node-namespaced-map? [node]
(= :namespaced-map (:tag node)))
(defn map-children [node]
(->> (if (node-namespaced-map? node)
(-> node :children first)
node)
:children
(partition-all 2)))
(defn node-vector? [node]
(= :vector (:tag node)))
(defn node-list? [node]
(= :list (:tag node)))
(defn node-symbol? [node]
(symbol? (:value node)))
(defn node-symbol [node]
(:value node))
(defn node-keyword? [node]
(keyword? (:k node)))
(defn node-keyword [node]
(:k node))
(defn node-quote? [node]
(= :quote (:tag node)))
(defn node-op [node]
(-> node :children first))
(declare lint-query)
(defmulti lint-unify-clause #(-> % node-op node-symbol))
(defmulti lint-source-op #(-> % node-op node-symbol))
(defmulti lint-tail-op #(-> % node-op node-symbol))
(defn lint-not-arg-symbol [node]
(when (= \$ (-> node node-symbol str first))
(api/reg-finding!
(assoc (meta node)
:message "unexpected parameter in binding"
:type :xtql/unrecognized-parameter))))
(defn lint-bind [node]
(cond
(node-symbol? node)
;; TODO: Make own type, should really be a warning
(lint-not-arg-symbol node)
(node-map? node)
(doseq [[k _v] (map-children node)]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "all keys in binding maps must be keywords"
:type :xtql/type-mismatch))))
:else
(api/reg-finding!
(assoc (meta node)
:message "expected a symbol or map"
:type :xtql/type-mismatch))))
;; TODO: Lint more unify clauses
(defmethod lint-unify-clause :default [node]
(when-not (unify-clause? (-> node node-op node-symbol))
(api/reg-finding!
(assoc (some-> node :children first meta)
:message "unrecognized unify clause"
:type :xtql/unrecognized-operation))))
(defmethod lint-unify-clause 'from [node]
(lint-source-op node))
(defmethod lint-unify-clause 'rel [node]
(lint-source-op node))
(defmethod lint-unify-clause 'with [node]
(let [opts (-> node :children rest)]
(when-not (>= (count opts) 1)
(api/reg-finding!
(assoc (meta node)
:message "expected at least one argument"
:type :xtql/invalid-arity)))
(doseq [opt opts]
(if (node-map? opt)
(let [ks (->> opt
map-children
(map first)
(remove node-symbol?))]
(doseq [k ks]
(api/reg-finding!
(assoc (meta k)
:message "expected all keys to be symbols in a unify"
:type :xtql/type-mismatch))))
(api/reg-finding!
(assoc (meta opt)
:message "opts must be a map"
:type :xtql/type-mismatch))))))
(defn lint-join-clause [node]
(let [args (-> node :children rest)]
(if-not (= (count args) 2)
(api/reg-finding!
(assoc (meta node)
:message "expected at exactly two arguments"
:type :xtql/invalid-arity))
(let [[query opts] args]
(lint-query query)
(cond
(node-vector? opts)
(->> opts :children (run! lint-bind))
(node-map? opts)
(let [kvs (map-children opts)
ks (->> kvs
(map first)
(map node-keyword)
(remove nil?)
(into #{}))]
(when-not (contains? ks :bind)
(api/reg-finding!
(assoc (meta opts)
:message "Missing :bind parameter"
:type :xtql/missing-parameter)))
(doseq [[k v] kvs]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "All keys in 'opts' must be keywords"
:type :xtql/type-mismatch)))
(case (node-keyword k)
:bind (if (node-vector? v)
(->> v :children (run! lint-bind))
(api/reg-finding!
(assoc (meta v)
:message "expected :bind value to be a vector"
:type :xtql/type-mismatch)))
:args (if (node-vector? v)
;; TODO: Make args specific
(->> v :children (run! lint-bind))
(api/reg-finding!
(assoc (meta v)
:message "expected :args value to be a vector"
:type :xtql/type-mismatch)))
; else
(api/reg-finding!
(assoc (meta k)
:message "unrecognized parameter"
:type :xtql/unrecognized-parameter)))))
:else
(api/reg-finding!
(assoc (meta node)
:message "opts must be a map or vector"
:type :xtql/type-mismatch)))))))
(defmethod lint-unify-clause 'join [node]
(lint-join-clause node))
(defmethod lint-unify-clause 'inner-join [node]
(lint-join-clause node))
(defmethod lint-unify-clause 'unnest [node]
(let [opts (-> node :children rest)]
(when-not (= 1 (count opts))
(api/reg-finding!
(assoc (meta node)
:message "expected at exactly one argument"
:type :xtql/invalid-arity)))
(let [opt (first opts)]
(if (node-map? opt)
(doseq [[k _v] (map-children opt)]
(when-not (node-symbol? k)
(api/reg-finding!
(assoc (meta k)
:message "expected all columns to be symbols"
:type :xtql/type-mismatch))))
(api/reg-finding!
(assoc (meta opt)
:message "expected opt to be a map"
:type :xtql/type-mismatch))))))
(defmethod lint-source-op :default [node]
(let [op (-> node node-op node-symbol)]
(if (tail-op? op)
(api/reg-finding!
(assoc (some-> node :children first meta)
:message "tail op in source position"
:type :xtql/unrecognized-operation))
(when-not (source-op? op)
(api/reg-finding!
(assoc (some-> node :children first meta)
:message "unrecognized source operation"
:type :xtql/unrecognized-operation))))))
(defmethod lint-source-op 'from [node]
(let [[_ table opts] (some-> node :children)]
(when-not (node-keyword? table)
(api/reg-finding!
(assoc (meta table)
:message "expected 'table' to be a keyword"
:type :xtql/type-mismatch)))
(case (:tag opts)
:vector (->> opts :children (run! lint-bind))
:map
(let [kvs (map-children opts)
ks (->> kvs
(map first)
(map node-keyword)
(remove nil?)
(into #{}))]
(when-not (contains? ks :bind)
(api/reg-finding!
(assoc (meta opts)
:message "Missing :bind parameter"
:type :xtql/missing-parameter)))
(doseq [[k v] kvs]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "All keys in 'opts' must be keywords"
:type :xtql/type-mismatch)))
(case (node-keyword k)
:bind (if (node-vector? v)
(->> v :children (run! lint-bind))
(api/reg-finding!
(assoc (meta opts)
:message "expected :bind value to be a vector"
:type :xtql/type-mismatch)))
;; TODO
:for-valid-time nil
;; TODO
:for-system-time nil
; else
(api/reg-finding!
(assoc (meta k)
:message "unrecognized parameter"
:type :xtql/unrecognized-parameter)))))
(api/reg-finding!
(assoc (meta opts)
:message "expected 'opts' to be either a map or vector"
:type :xtql/type-mismatch)))))
(defmethod lint-source-op 'unify [node]
(let [[_ & clauses] (some-> node :children)]
(doseq [bad-op (remove node-list? clauses)]
(api/reg-finding!
(assoc (meta bad-op)
:message "all operations in a unify must be lists"
:type :xtql/type-mismatch)))
(when (= (count clauses) 1)
(let [clause (first clauses)
clause-op (-> clause node-op node-symbol)
unify-node (some-> node :children first)]
(case clause-op
from (api/reg-finding!
(assoc (meta unify-node)
:message "redundant unify"
:type :xtql/redundant-unify))
rel (api/reg-finding!
(assoc (meta unify-node)
:message "redundant unify"
:type :xtql/redundant-unify))
;; TODO: Cover other operators
nil)))
(->> clauses
(filter node-list?)
(run! lint-unify-clause))))
(defmethod lint-source-op 'rel [node]
(let [[_ _expr binds] (some-> node :children)]
(if (node-vector? binds)
(->> binds :children (run! lint-bind))
(api/reg-finding!
(assoc (meta binds)
:message "expected rel binding to be a vector"
:type :xtql/type-mismatch)))))
;; TODO: Lint more tail ops
(defmethod lint-tail-op :default [node]
(let [op (-> node node-op node-symbol)]
(if (source-op? op)
(api/reg-finding!
(assoc (some-> node :children first meta)
:message "source op in tail position"
:type :xtql/unrecognized-operation))
(when-not (tail-op? op)
(api/reg-finding!
(assoc (some-> node :children first meta)
:message "unrecognized tail operation"
:type :xtql/unrecognized-operation))))))
(defn lint-keyword [node name]
(when-not (node-keyword? node)
(api/reg-finding!
(assoc (meta node)
:message (str "expected '" name "' to be a keyword")
:type :xtql/type-mismatch))))
(defn lint-enum [node name values]
;; TODO: Expand to more than just keywords?
;; Maybe a `node-value` function?
(when-not (contains? values (node-keyword node))
(api/reg-finding!
(assoc (meta node)
:message (str "expected '" name "' to be one of " values)
;; TODO: change to different type?
:type :xtql/type-mismatch))))
(defmethod lint-tail-op 'limit [node]
(let [opts (-> node :children rest)]
(when-not (= 1 (count opts))
(api/reg-finding!
(assoc (meta node)
:message "expected exactly one argument"
:type :xtql/invalid-arity)))
(when-let [opt (first opts)]
(when-not (some-> opt :value int?)
(api/reg-finding!
(assoc (meta opt)
:message "expected limit to be an integer"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'offset [node]
(let [opts (-> node :children rest)]
(when-not (= 1 (count opts))
(api/reg-finding!
(assoc (meta node)
:message "expected exactly one argument"
:type :xtql/invalid-arity)))
(when-let [opt (first opts)]
(when-not (some-> opt :value int?)
(api/reg-finding!
(assoc (meta opt)
:message "expected offset to be an integer"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'with [node]
(let [opts (-> node :children rest)]
(when-not (>= (count opts) 1)
(api/reg-finding!
(assoc (meta node)
:message "expected at least one argument"
:type :xtql/invalid-arity)))
(doseq [opt opts]
(cond
(node-symbol? opt)
(lint-not-arg-symbol opt)
(node-map? opt)
(let [ks (->> opt
map-children
(map first)
(remove node-keyword?))]
(doseq [k ks]
(api/reg-finding!
(assoc (meta k)
:message "expected all keys to be keywords"
:type :xtql/type-mismatch))))
:else
(api/reg-finding!
(assoc (meta opt)
:message "opts must be a symbol or map"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'return [node]
(let [opts (-> node :children rest)]
(when-not (>= (count opts) 1)
(api/reg-finding!
(assoc (meta node)
:message "expected at least one argument"
:type :xtql/invalid-arity)))
(doseq [opt opts]
(cond
(node-symbol? opt)
(lint-not-arg-symbol opt)
(node-map? opt)
(let [ks (->> opt
map-children
(map first)
(remove node-keyword?))]
(doseq [k ks]
(api/reg-finding!
(assoc (meta k)
:message "expected all keys to be keywords"
:type :xtql/type-mismatch))))
:else
(api/reg-finding!
(assoc (meta opt)
:message "opts must be a symbol or map"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'order-by [node]
(doseq [opt (-> node :children rest)]
(cond
(node-symbol? opt)
(lint-not-arg-symbol opt)
(node-map? opt)
(let [kvs (map-children opt)
ks (->> kvs
(map first)
(map node-keyword)
(remove nil?)
(into #{}))]
(when-not (contains? ks :val)
(api/reg-finding!
(assoc (meta opt)
:message "Missing :val parameter"
:type :xtql/missing-parameter)))
(doseq [[k v] kvs]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "All keys in 'opts' must be keywords"
:type :xtql/type-mismatch)))
(case (node-keyword k)
:val
(cond
(node-symbol? v)
(lint-not-arg-symbol v)
(node-keyword? v)
(api/reg-finding!
(assoc (meta v)
:message "expected :val value to be a symbol or an expression"
:type :xtql/type-mismatch)))
; else do nothing
:dir
(if (node-keyword? v)
(lint-enum v :dir #{:asc :desc})
(lint-keyword v ":dir value"))
:nulls
(if (node-keyword? v)
(lint-enum v :nulls #{:first :last})
(lint-keyword v ":nulls value"))
; else
(api/reg-finding!
(assoc (meta k)
:message "unrecognized parameter"
:type :xtql/unrecognized-parameter)))))
:else
(api/reg-finding!
(assoc (meta opt)
:message "opts must be a symbol or map"
:type :xtql/type-mismatch)))))
(defmethod lint-tail-op 'without [node]
(let [columns (-> node :children rest)]
(when-not (>= (count columns) 1)
;; TODO: Should be a warning really
(api/reg-finding!
(assoc (meta node)
:message "expected at least one column"
:type :xtql/invalid-arity)))
(doseq [column columns]
(when-not (node-keyword? column)
(api/reg-finding!
(assoc (meta column)
:message "expected column to be a keyword"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'aggregate [node]
(let [opts (-> node :children rest)]
(when-not (>= (count opts) 1)
(api/reg-finding!
(assoc (meta node)
:message "expected at least one argument"
:type :xtql/invalid-arity)))
(doseq [opt opts]
(cond
(node-symbol? opt)
(lint-not-arg-symbol opt)
(node-map? opt)
(doseq [[k _v] (map-children opt)]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "expected all keys to be keywords"
:type :xtql/type-mismatch))))
:else
(api/reg-finding!
(assoc (meta opt)
:message "expected opts to be a symbol or map"
:type :xtql/type-mismatch))))))
(defmethod lint-tail-op 'unnest [node]
(let [opts (-> node :children rest)]
(when-not (= 1 (count opts))
(api/reg-finding!
(assoc (meta node)
:message "expected at exactly one argument"
:type :xtql/invalid-arity)))
(let [opt (first opts)]
(if (node-map? opt)
(doseq [[k _v] (map-children opt)]
(when-not (node-keyword? k)
(api/reg-finding!
(assoc (meta k)
:message "expected all columns to be keywords"
:type :xtql/type-mismatch))))
(api/reg-finding!
(assoc (meta opt)
:message "expected opt to be a map"
:type :xtql/type-mismatch))))))
(defn lint-pipeline [node]
(let [[_ & ops] (some-> node :children)]
(doseq [bad-op (remove node-list? ops)]
(api/reg-finding!
(assoc (meta bad-op)
:message "all operations in a pipeline must be lists"
:type :xtql/type-mismatch)))
(when (= 1 (count ops))
(api/reg-finding!
(assoc (-> node :children first meta)
:message "redundant pipeline"
:type :xtql/redundant-pipeline)))
(let [first-op (first ops)]
(when (node-list? first-op)
(lint-source-op (first ops))))
(->> ops
(drop 1)
(filter node-list?)
(run! lint-tail-op))))
(defn lint-query [node]
(if (= '-> (node-symbol (-> node :children first)))
(lint-pipeline node)
(lint-source-op node)))
;; TODO: Lint other functions that take queries
(defn q [{:keys [node]}]
(let [[_ _node quoted-query] (some-> node :children)]
(when (node-quote? quoted-query)
(let [query (-> quoted-query :children first)]
(lint-query query)))))

View file

@ -44,6 +44,7 @@ jobs:
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
NEXT_JDBC_TEST_MYSQL: yes NEXT_JDBC_TEST_MYSQL: yes
NEXT_JDBC_TEST_XTDB: yes
NEXT_JDBC_TEST_MSSQL: yes NEXT_JDBC_TEST_MSSQL: yes
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
- name: Deploy Release - name: Deploy Release

View file

@ -42,6 +42,7 @@ jobs:
env: env:
MYSQL_ROOT_PASSWORD: testing MYSQL_ROOT_PASSWORD: testing
NEXT_JDBC_TEST_MYSQL: yes NEXT_JDBC_TEST_MYSQL: yes
NEXT_JDBC_TEST_XTDB: yes
NEXT_JDBC_TEST_MSSQL: yes NEXT_JDBC_TEST_MSSQL: yes
MSSQL_SA_PASSWORD: Str0ngP4ssw0rd MSSQL_SA_PASSWORD: Str0ngP4ssw0rd
- name: Deploy Snapshot - name: Deploy Snapshot

View file

@ -42,5 +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

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"] :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.2.107"} 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.2.0"} io.zonky.test.postgres/embedded-postgres-binaries-windows-amd64 {:mvn/version "17.2.0"}
org.xerial/sqlite-jdbc {:mvn/version "3.47.1.0"} org.xerial/sqlite-jdbc {:mvn/version "3.47.1.0"}
com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.8.1.jre11"} 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: ;; use log4j2 to reduce log noise during testing:
org.apache.logging.log4j/log4j-api {:mvn/version "2.24.2"} org.apache.logging.log4j/log4j-api {:mvn/version "2.24.2"}
;; bridge everything into log4j: ;; bridge everything into log4j:

View file

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

View file

@ -12,12 +12,15 @@
(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

@ -278,6 +278,10 @@
:qualifier-fn ->kebab-case :qualifier-fn ->kebab-case
:label-fn ->kebab-case))) :label-fn ->kebab-case)))
(comment
(->kebab-case "_id") ;;=> "id"!!
)
(defn as-unqualified-kebab-maps (defn as-unqualified-kebab-maps
"Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder` "Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder`
that produces bare vectors of hash map rows, with simple, kebab-case keys." that produces bare vectors of hash map rows, with simple, kebab-case keys."

View file

@ -10,8 +10,8 @@
[next.jdbc.result-set :as rs] [next.jdbc.result-set :as rs]
[next.jdbc.specs :as specs] [next.jdbc.specs :as specs]
[next.jdbc.test-fixtures [next.jdbc.test-fixtures
:refer [with-test-db db ds :refer [db derby? ds jtds? mysql? postgres? sqlite? with-test-db
derby? jtds? mysql? postgres? sqlite?]])) xtdb?]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
@ -83,6 +83,26 @@
:rowIdLifetime/exception)) :rowIdLifetime/exception))
(postgres?) (-> (disj :rowIdLifetime) (postgres?) (-> (disj :rowIdLifetime)
(conj :rowIdLifetime/exception)) (conj :rowIdLifetime/exception))
(xtdb?) (-> (disj :clientInfoProperties
:defaultTransactionIsolation
:maxCatalogNameLength
:maxColumnNameLength
:maxCursorNameLength
:maxProcedureNameLength
:maxSchemaNameLength
:maxTableNameLength
:maxUserNameLength
:rowIdLifetime)
(conj :clientInfoProperties/exception
:defaultTransactionIsolation/exception
:maxCatalogNameLength/exception
:maxColumnNameLength/exception
:maxCursorNameLength/exception
:maxProcedureNameLength/exception
:maxSchemaNameLength/exception
:maxTableNameLength/exception
:maxUserNameLength/exception
:rowIdLifetime/exception))
(sqlite?) (-> (disj :clientInfoProperties :rowIdLifetime) (sqlite?) (-> (disj :clientInfoProperties :rowIdLifetime)
(conj :clientInfoProperties/exception (conj :clientInfoProperties/exception
:rowIdLifetime/exception))) :rowIdLifetime/exception)))
@ -97,7 +117,8 @@
(let [data (d/datafy (.getMetaData con))] (let [data (d/datafy (.getMetaData con))]
(doseq [k (cond-> #{:catalogs :clientInfoProperties :schemas :tableTypes :typeInfo} (doseq [k (cond-> #{:catalogs :clientInfoProperties :schemas :tableTypes :typeInfo}
(jtds?) (disj :clientInfoProperties) (jtds?) (disj :clientInfoProperties)
(sqlite?) (disj :clientInfoProperties))] (sqlite?) (disj :clientInfoProperties)
(xtdb?) (disj :clientInfoProperties))]
(let [rs (d/nav data k nil)] (let [rs (d/nav data k nil)]
(is (vector? rs)) (is (vector? rs))
(is (every? map? rs)))))))) (is (every? map? rs))))))))
@ -122,4 +143,5 @@
(.execute ps) (.execute ps)
(.getResultSet ps) (.getResultSet ps)
(.close ps) (.close ps)
(.close con)) (.close con)
)

View file

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

View file

@ -15,39 +15,40 @@
[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]])) :refer [ds with-test-db xtdb?]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(use-fixtures :once with-test-db) (use-fixtures :once with-test-db)
(deftest basic-test (deftest basic-test
(testing "data structures" (when-not (xtdb?)
(is (= [{:sql-p ["INSERT INTO foo (name) VALUES (?)" "Sean"] (testing "data structures"
:key-fn :GENERATED_KEY (is (= [{:sql-p ["INSERT INTO foo (name) VALUES (?)" "Sean"]
:key :id :key-fn :GENERATED_KEY
:opts {:key-fn :GENERATED_KEY :key :id}}] :key :id
@(sut/defer-ops :opts {:key-fn :GENERATED_KEY :key :id}}]
#(sut/insert! :foo {:name "Sean"} {:key-fn :GENERATED_KEY :key :id}))))) @(sut/defer-ops
(testing "execution" #(sut/insert! :foo {:name "Sean"} {:key-fn :GENERATED_KEY :key :id})))))
(let [effects (sut/with-deferred (ds) (testing "execution"
(sut/insert! :fruit {:name "Mango"} {:key :test}))] (let [effects (sut/with-deferred (ds)
(is (= {:test 1} @effects)) (sut/insert! :fruit {:name "Mango"} {:key :test}))]
(is (= 1 (count (jdbc/execute! (ds) (is (= {:test 1} @effects))
["select * from fruit where name = ?" (is (= 1 (count (jdbc/execute! (ds)
"Mango"]))))) ["select * from fruit where name = ?"
(let [effects (sut/with-deferred (ds) "Mango"])))))
(sut/insert! :fruit {:name "Dragonfruit"} {:key :test}) (let [effects (sut/with-deferred (ds)
(sut/update! :fruit {:cost 123} {:name "Dragonfruit"}) (sut/insert! :fruit {:name "Dragonfruit"} {:key :test})
(sut/delete! :fruit {:name "Dragonfruit"}))] (sut/update! :fruit {:cost 123} {:name "Dragonfruit"})
(is (= {:test 1} @effects)) (sut/delete! :fruit {:name "Dragonfruit"}))]
(is (= 0 (count (jdbc/execute! (ds) (is (= {:test 1} @effects))
["select * from fruit where name = ?" (is (= 0 (count (jdbc/execute! (ds)
"Dragonfruit"]))))) ["select * from fruit where name = ?"
(let [effects (sut/with-deferred (ds) "Dragonfruit"])))))
(sut/insert! :fruit {:name "Grapefruit" :bad_column 0} {:key :test}))] (let [effects (sut/with-deferred (ds)
(is (= :failed (try @effects (sut/insert! :fruit {:name "Grapefruit" :bad_column 0} {:key :test}))]
(catch Exception _ :failed)))) (is (= :failed (try @effects
(is (= 0 (count (jdbc/execute! (ds) (catch Exception _ :failed))))
["select * from fruit where name = ?" (is (= 0 (count (jdbc/execute! (ds)
"Grapefruit"]))))))) ["select * from fruit where name = ?"
"Grapefruit"]))))))))

View file

@ -6,9 +6,10 @@
[clojure.test :refer [deftest is testing use-fixtures]] [clojure.test :refer [deftest is testing use-fixtures]]
[next.jdbc.optional :as opt] [next.jdbc.optional :as opt]
[next.jdbc.protocols :as p] [next.jdbc.protocols :as p]
[next.jdbc.test-fixtures :refer [with-test-db ds column [next.jdbc.test-fixtures :refer [col-kw column default-options ds index
default-options]]) with-test-db]])
(:import (java.sql ResultSet ResultSetMetaData))) (:import
(java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
@ -17,7 +18,7 @@
(deftest test-map-row-builder (deftest test-map-row-builder
(testing "default row builder" (testing "default row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 1] [(str "select * from fruit where " (index) " = ?") 1]
(assoc (default-options) (assoc (default-options)
:builder-fn opt/as-maps))] :builder-fn opt/as-maps))]
(is (map? row)) (is (map? row))
@ -26,7 +27,7 @@
(is (= "Apple" ((column :FRUIT/NAME) row))))) (is (= "Apple" ((column :FRUIT/NAME) row)))))
(testing "unqualified row builder" (testing "unqualified row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 2] [(str "select * from fruit where " (index) " = ?") 2]
{:builder-fn opt/as-unqualified-maps})] {:builder-fn opt/as-unqualified-maps})]
(is (map? row)) (is (map? row))
(is (not (contains? row (column :COST)))) (is (not (contains? row (column :COST))))
@ -34,23 +35,23 @@
(is (= "Banana" ((column :NAME) row))))) (is (= "Banana" ((column :NAME) row)))))
(testing "lower-case row builder" (testing "lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn opt/as-lower-maps))] :builder-fn opt/as-lower-maps))]
(is (map? row)) (is (map? row))
(is (not (contains? row :fruit/appearance))) (is (not (contains? row (col-kw :fruit/appearance))))
(is (= 3 (:fruit/id row))) (is (= 3 ((col-kw :fruit/id) row)))
(is (= "Peach" (:fruit/name row))))) (is (= "Peach" ((col-kw :fruit/name) row)))))
(testing "unqualified lower-case row builder" (testing "unqualified lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 4] [(str "select * from fruit where " (index) " = ?") 4]
{:builder-fn opt/as-unqualified-lower-maps})] {:builder-fn opt/as-unqualified-lower-maps})]
(is (map? row)) (is (map? row))
(is (= 4 (:id row))) (is (= 4 ((col-kw :id) row)))
(is (= "Orange" (:name row))))) (is (= "Orange" ((col-kw :name) row)))))
(testing "custom row builder" (testing "custom row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn opt/as-modified-maps :builder-fn opt/as-modified-maps
:label-fn str/lower-case :label-fn str/lower-case
@ -67,7 +68,7 @@
(deftest test-map-row-adapter (deftest test-map-row-adapter
(testing "default row builder" (testing "default row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 1] [(str "select * from fruit where " (index) " = ?") 1]
(assoc (default-options) (assoc (default-options)
:builder-fn (opt/as-maps-adapter :builder-fn (opt/as-maps-adapter
opt/as-maps opt/as-maps
@ -78,7 +79,7 @@
(is (= "Apple" ((column :FRUIT/NAME) row))))) (is (= "Apple" ((column :FRUIT/NAME) row)))))
(testing "unqualified row builder" (testing "unqualified row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 2] [(str "select * from fruit where " (index) " = ?") 2]
{:builder-fn (opt/as-maps-adapter {:builder-fn (opt/as-maps-adapter
opt/as-unqualified-maps opt/as-unqualified-maps
default-column-reader)})] default-column-reader)})]
@ -88,27 +89,27 @@
(is (= "Banana" ((column :NAME) row))))) (is (= "Banana" ((column :NAME) row)))))
(testing "lower-case row builder" (testing "lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn (opt/as-maps-adapter :builder-fn (opt/as-maps-adapter
opt/as-lower-maps opt/as-lower-maps
default-column-reader)))] default-column-reader)))]
(is (map? row)) (is (map? row))
(is (not (contains? row :fruit/appearance))) (is (not (contains? row (col-kw :fruit/appearance))))
(is (= 3 (:fruit/id row))) (is (= 3 ((col-kw :fruit/id) row)))
(is (= "Peach" (:fruit/name row))))) (is (= "Peach" ((col-kw :fruit/name) row)))))
(testing "unqualified lower-case row builder" (testing "unqualified lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 4] [(str "select * from fruit where " (index) " = ?") 4]
{:builder-fn (opt/as-maps-adapter {:builder-fn (opt/as-maps-adapter
opt/as-unqualified-lower-maps opt/as-unqualified-lower-maps
default-column-reader)})] default-column-reader)})]
(is (map? row)) (is (map? row))
(is (= 4 (:id row))) (is (= 4 ((col-kw :id) row)))
(is (= "Orange" (:name row))))) (is (= "Orange" ((col-kw :name) row)))))
(testing "custom row builder" (testing "custom row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn (opt/as-maps-adapter :builder-fn (opt/as-maps-adapter
opt/as-modified-maps opt/as-modified-maps

View file

@ -6,7 +6,7 @@
[next.jdbc.plan :as plan] [next.jdbc.plan :as plan]
[next.jdbc.specs :as specs] [next.jdbc.specs :as specs]
[next.jdbc.test-fixtures [next.jdbc.test-fixtures
:refer [with-test-db ds]] :refer [with-test-db ds col-kw index]]
[clojure.string :as str])) [clojure.string :as str]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
@ -17,56 +17,56 @@
(specs/instrument) (specs/instrument)
(deftest select-one!-tests (deftest select-one!-tests
(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) :id ["select * from fruit order by id"]))) (plan/select-one! (ds) (col-kw :id) [(str "select * from fruit order by " (index))])))
(is (= "Banana" (is (= "Banana"
(plan/select-one! (ds) :name ["select * from fruit where id = ?" 2]))) (plan/select-one! (ds) :name [(str "select * from fruit where " (index) " = ?") 2])))
(is (= [1 "Apple"] (is (= [1 "Apple"]
(plan/select-one! (ds) (juxt :id :name) (plan/select-one! (ds) (juxt (col-kw :id) :name)
["select * from fruit order by id"]))) [(str "select * from fruit order by " (index))])))
(is (= {:id 1 :name "Apple"} (is (= {(col-kw :id) 1 :name "Apple"}
(plan/select-one! (ds) #(select-keys % [:id :name]) (plan/select-one! (ds) #(select-keys % [(col-kw :id) :name])
["select * from fruit order by id"])))) [(str "select * from fruit order by " (index))]))))
(deftest select-vector-tests (deftest select-vector-tests
(is (= [{:id 1} {:id 2} {:id 3} {:id 4}] (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) :id ["select * from fruit order by id"]))) (plan/select! (ds) (col-kw :id) [(str "select * from fruit order by " (index))])))
(is (= ["Banana"] (is (= ["Banana"]
(plan/select! (ds) :name ["select * from fruit where id = ?" 2]))) (plan/select! (ds) :name [(str "select * from fruit where " (index) " = ?") 2])))
(is (= [[2 "Banana"]] (is (= [[2 "Banana"]]
(plan/select! (ds) (juxt :id :name) (plan/select! (ds) (juxt (col-kw :id) :name)
["select * from fruit where id = ?" 2]))) [(str "select * from fruit where " (index) " = ?") 2])))
(is (= [{:id 2 :name "Banana"}] (is (= [{(col-kw :id) 2 :name "Banana"}]
(plan/select! (ds) [:id :name] (plan/select! (ds) [(col-kw :id) :name]
["select * from fruit where id = ?" 2])))) [(str "select * from fruit where " (index) " = ?") 2]))))
(deftest select-set-tests (deftest select-set-tests
(is (= #{{:id 1} {:id 2} {:id 3} {:id 4}} (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) :id ["select * from fruit order by id"] (plan/select! (ds) (col-kw :id) [(str "select * from fruit order by " (index))]
{:into #{}})))) {:into #{}}))))
(deftest select-map-tests (deftest select-map-tests
(is (= {1 "Apple", 2 "Banana", 3 "Peach", 4 "Orange"} (is (= {1 "Apple", 2 "Banana", 3 "Peach", 4 "Orange"}
(plan/select! (ds) (juxt :id :name) ["select * from fruit order by id"] (plan/select! (ds) (juxt (col-kw :id) :name) [(str "select * from fruit order by " (index))]
{:into {}})))) {:into {}}))))
(deftest select-issue-227 (deftest select-issue-227
(is (= ["Apple"] (is (= ["Apple"]
(plan/select! (ds) :name ["select * from fruit where id = ?" 1] (plan/select! (ds) :name [(str "select * from fruit where " (index) " = ?") 1]
{:column-fn #(str/replace % "-" "_")}))) {:column-fn #(str/replace % "-" "_")})))
(is (= ["Apple"] (is (= ["Apple"]
(plan/select! (ds) :foo/name ["select * from fruit where id = ?" 1] (plan/select! (ds) :foo/name [(str "select * from fruit where " (index) " = ?") 1]
{:column-fn #(str/replace % "-" "_")}))) {:column-fn #(str/replace % "-" "_")})))
(is (= ["Apple"] (is (= ["Apple"]
(plan/select! (ds) #(get % "name") ["select * from fruit where id = ?" 1] (plan/select! (ds) #(get % "name") [(str "select * from fruit where " (index) " = ?") 1]
{:column-fn #(str/replace % "-" "_")}))) {:column-fn #(str/replace % "-" "_")})))
(is (= [["Apple"]] (is (= [["Apple"]]
(plan/select! (ds) (juxt :name) ["select * from fruit where id = ?" 1] (plan/select! (ds) (juxt :name) [(str "select * from fruit where " (index) " = ?") 1]
{:column-fn #(str/replace % "-" "_")})))) {:column-fn #(str/replace % "-" "_")}))))

View file

@ -11,7 +11,7 @@
(:require [clojure.test :refer [deftest is testing use-fixtures]] (:require [clojure.test :refer [deftest is testing use-fixtures]]
[next.jdbc :as jdbc] [next.jdbc :as jdbc]
[next.jdbc.test-fixtures [next.jdbc.test-fixtures
:refer [with-test-db ds jtds? mssql? sqlite?]] :refer [with-test-db ds jtds? mssql? sqlite? xtdb?]]
[next.jdbc.prepare :as prep] [next.jdbc.prepare :as prep]
[next.jdbc.specs :as specs])) [next.jdbc.specs :as specs]))
@ -22,61 +22,25 @@
(specs/instrument) (specs/instrument)
(deftest execute-batch-tests (deftest execute-batch-tests
(testing "simple batch insert" (when-not (xtdb?)
(is (= [1 1 1 1 1 1 1 1 1 13] (testing "simple batch insert"
(jdbc/with-transaction [t (ds) {:rollback-only true}] (is (= [1 1 1 1 1 1 1 1 1 13]
(with-open [ps (jdbc/prepare t [" (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 (?,?)
"])] "])]
(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 ["
@ -91,33 +55,70 @@ INSERT INTO fruit (name, appearance) VALUES (?,?)
["fruit7" "seven"] ["fruit7" "seven"]
["fruit8" "eight"] ["fruit8" "eight"]
["fruit9" "nine"]] ["fruit9" "nine"]]
{:batch-size 4 {:batch-size 3})]
: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 "return generated keys" (testing "big batch insert"
(when-not (or (mssql?) (sqlite?)) (is (= [1 1 1 1 1 1 1 1 1 13]
(let [results (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 (?,?)
"])]
(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

@ -12,9 +12,9 @@
[next.jdbc.protocols :as p] [next.jdbc.protocols :as p]
[next.jdbc.result-set :as rs] [next.jdbc.result-set :as rs]
[next.jdbc.specs :as specs] [next.jdbc.specs :as specs]
[next.jdbc.test-fixtures :refer [with-test-db ds column [next.jdbc.test-fixtures :refer [with-test-db ds column index col-kw
default-options default-options
derby? mssql? mysql? postgres?]]) derby? mssql? mysql? postgres? xtdb?]])
(:import (java.sql ResultSet ResultSetMetaData))) (:import (java.sql ResultSet ResultSetMetaData)))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
@ -27,7 +27,9 @@
(testing "default schema" (testing "default schema"
(let [connectable (ds) (let [connectable (ds)
test-row (rs/datafiable-row {:TABLE/FRUIT_ID 1} connectable test-row (rs/datafiable-row {:TABLE/FRUIT_ID 1} connectable
(default-options)) (cond-> (default-options)
(xtdb?)
(assoc :schema-opts {:pk "_id"})))
data (d/datafy test-row) data (d/datafy test-row)
v (get data :TABLE/FRUIT_ID)] v (get data :TABLE/FRUIT_ID)]
;; check datafication is sane ;; check datafication is sane
@ -40,7 +42,10 @@
(let [connectable (ds) (let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 2} connectable test-row (rs/datafiable-row {:foo/bar 2} connectable
(assoc (default-options) (assoc (default-options)
:schema {:foo/bar :fruit/id})) :schema {:foo/bar
(if (xtdb?)
:fruit/_id
:fruit/id)}))
data (d/datafy test-row) data (d/datafy test-row)
v (get data :foo/bar)] v (get data :foo/bar)]
;; check datafication is sane ;; check datafication is sane
@ -53,7 +58,10 @@
(let [connectable (ds) (let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 3} connectable test-row (rs/datafiable-row {:foo/bar 3} connectable
(assoc (default-options) (assoc (default-options)
:schema {:foo/bar [:fruit/id]})) :schema {:foo/bar
[(if (xtdb?)
:fruit/_id
:fruit/id)]}))
data (d/datafy test-row) data (d/datafy test-row)
v (get data :foo/bar)] v (get data :foo/bar)]
;; check datafication is sane ;; check datafication is sane
@ -67,7 +75,7 @@
(let [connectable (ds) (let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 2} connectable test-row (rs/datafiable-row {:foo/bar 2} connectable
(assoc (default-options) (assoc (default-options)
:schema {:foo/bar [:fruit :id]})) :schema {:foo/bar [:fruit (col-kw :id)]}))
data (d/datafy test-row) data (d/datafy test-row)
v (get data :foo/bar)] v (get data :foo/bar)]
;; check datafication is sane ;; check datafication is sane
@ -79,7 +87,7 @@
(let [connectable (ds) (let [connectable (ds)
test-row (rs/datafiable-row {:foo/bar 3} connectable test-row (rs/datafiable-row {:foo/bar 3} connectable
(assoc (default-options) (assoc (default-options)
:schema {:foo/bar [:fruit :id :many]})) :schema {:foo/bar [:fruit (col-kw :id) :many]}))
data (d/datafy test-row) data (d/datafy test-row)
v (get data :foo/bar)] v (get data :foo/bar)]
;; check datafication is sane ;; check datafication is sane
@ -93,7 +101,7 @@
(deftest test-map-row-builder (deftest test-map-row-builder
(testing "default row builder" (testing "default row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 1] [(str "select * from fruit where " (index) " = ?") 1]
(default-options))] (default-options))]
(is (map? row)) (is (map? row))
(is (contains? row (column :FRUIT/GRADE))) (is (contains? row (column :FRUIT/GRADE)))
@ -101,7 +109,7 @@
(is (= 1 ((column :FRUIT/ID) row))) (is (= 1 ((column :FRUIT/ID) row)))
(is (= "Apple" ((column :FRUIT/NAME) row)))) (is (= "Apple" ((column :FRUIT/NAME) row))))
(let [rs (p/-execute-all (ds) (let [rs (p/-execute-all (ds)
["select * from fruit order by id"] [(str "select * from fruit order by " (index))]
(default-options))] (default-options))]
(is (every? map? rs)) (is (every? map? rs))
(is (= 1 ((column :FRUIT/ID) (first rs)))) (is (= 1 ((column :FRUIT/ID) (first rs))))
@ -110,7 +118,7 @@
(is (= "Orange" ((column :FRUIT/NAME) (last rs)))))) (is (= "Orange" ((column :FRUIT/NAME) (last rs))))))
(testing "unqualified row builder" (testing "unqualified row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 2] [(str "select * from fruit where " (index) " = ?") 2]
{:builder-fn rs/as-unqualified-maps})] {:builder-fn rs/as-unqualified-maps})]
(is (map? row)) (is (map? row))
(is (contains? row (column :COST))) (is (contains? row (column :COST)))
@ -119,34 +127,35 @@
(is (= "Banana" ((column :NAME) row))))) (is (= "Banana" ((column :NAME) row)))))
(testing "lower-case row builder" (testing "lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn rs/as-lower-maps))] :builder-fn rs/as-lower-maps))]
(is (map? row)) (is (map? row))
(is (contains? row :fruit/appearance)) (is (contains? row (col-kw :fruit/appearance)))
(is (nil? (:fruit/appearance row))) (is (nil? ((col-kw :fruit/appearance) row)))
(is (= 3 (:fruit/id row))) (is (= 3 ((col-kw :fruit/id) row)))
(is (= "Peach" (:fruit/name row))))) (is (= "Peach" ((col-kw :fruit/name) row)))))
(testing "unqualified lower-case row builder" (testing "unqualified lower-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 4] [(str "select * from fruit where " (index) " = ?") 4]
{:builder-fn rs/as-unqualified-lower-maps})] {:builder-fn rs/as-unqualified-lower-maps})]
(is (map? row)) (is (map? row))
(is (= 4 (:id row))) (is (= 4 ((col-kw :id) row)))
(is (= "Orange" (:name row))))) (is (= "Orange" ((col-kw :name) row)))))
(testing "kebab-case row builder" (testing "kebab-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select id,name,appearance as looks_like from fruit where id = ?" 3] [(str "select " (index) ",name,appearance as looks_like from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn rs/as-kebab-maps))] :builder-fn rs/as-kebab-maps))]
(is (map? row)) (is (map? row))
(is (contains? row :fruit/looks-like)) (is (contains? row (col-kw :fruit/looks-like)))
(is (nil? (:fruit/looks-like row))) (is (nil? ((col-kw :fruit/looks-like) row)))
(is (= 3 (:fruit/id row))) ;; kebab-case strips leading _ from _id (XTDB):
(is (= "Peach" (:fruit/name row))))) (is (= 3 ((if (xtdb?) :id :fruit/id) row)))
(is (= "Peach" ((col-kw :fruit/name) row)))))
(testing "unqualified kebab-case row builder" (testing "unqualified kebab-case row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select id,name,appearance as looks_like from fruit where id = ?" 4] [(str "select " (index) ",name,appearance as looks_like from fruit where " (index) " = ?") 4]
{:builder-fn rs/as-unqualified-kebab-maps})] {:builder-fn rs/as-unqualified-kebab-maps})]
(is (map? row)) (is (map? row))
(is (contains? row :looks-like)) (is (contains? row :looks-like))
@ -155,7 +164,7 @@
(is (= "Orange" (:name row))))) (is (= "Orange" (:name row)))))
(testing "custom row builder 1" (testing "custom row builder 1"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select fruit.*, id + 100 as newid from fruit where id = ?" 3] [(str "select fruit.*, " (index) " + 100 as newid from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn rs/as-modified-maps :builder-fn rs/as-modified-maps
:label-fn str/lower-case :label-fn str/lower-case
@ -168,7 +177,7 @@
(is (= "Peach" ((column :FRUIT/name) row))))) (is (= "Peach" ((column :FRUIT/name) row)))))
(testing "custom row builder 2" (testing "custom row builder 2"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select fruit.*, id + 100 as newid from fruit where id = ?" 3] [(str "select fruit.*, " (index) " + 100 as newid from fruit where " (index) " = ?") 3]
(assoc (default-options) (assoc (default-options)
:builder-fn rs/as-modified-maps :builder-fn rs/as-modified-maps
:label-fn str/lower-case :label-fn str/lower-case
@ -176,12 +185,12 @@
(is (map? row)) (is (map? row))
(is (contains? row :vegetable/appearance)) (is (contains? row :vegetable/appearance))
(is (nil? (:vegetable/appearance row))) (is (nil? (:vegetable/appearance row)))
(is (= 3 (:vegetable/id row))) (is (= 3 ((if (xtdb?) :vegetable/_id :vegetable/id) row)))
(is (= 103 (:vegetable/newid row))) ; constant qualifier here (is (= 103 (:vegetable/newid row))) ; constant qualifier here
(is (= "Peach" (:vegetable/name row))))) (is (= "Peach" (:vegetable/name row)))))
(testing "adapted row builder" (testing "adapted row builder"
(let [row (p/-execute-one (ds) (let [row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (assoc
(default-options) (default-options)
:builder-fn (rs/as-maps-adapter :builder-fn (rs/as-maps-adapter
@ -207,7 +216,7 @@
(fn [^ResultSet rs _ ^Integer i] (fn [^ResultSet rs _ ^Integer i]
(.getObject rs i))) (.getObject rs i)))
row (p/-execute-one (ds) row (p/-execute-one (ds)
["select * from fruit where id = ?" 3] [(str "select * from fruit where " (index) " = ?") 3]
(assoc (assoc
(default-options) (default-options)
:builder-fn (rs/as-maps-adapter :builder-fn (rs/as-maps-adapter
@ -236,7 +245,7 @@
(testing "row-numbers on bare abstraction" (testing "row-numbers on bare abstraction"
(is (= [1 2 3] (is (= [1 2 3]
(into [] (map rs/row-number) (into [] (map rs/row-number)
(p/-execute (ds) ["select * from fruit where id < ?" 4] (p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
;; we do not need a real builder here... ;; we do not need a real builder here...
(cond-> {:builder-fn (constantly nil)} (cond-> {:builder-fn (constantly nil)}
(derby?) (derby?)
@ -247,7 +256,7 @@
(is (= [1 2 3] (is (= [1 2 3]
(into [] (comp (map #(rs/datafiable-row % (ds) {})) (into [] (comp (map #(rs/datafiable-row % (ds) {}))
(map rs/row-number)) (map rs/row-number))
(p/-execute (ds) ["select * from fruit where id < ?" 4] (p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
;; ...but datafiable-row requires a real builder ;; ...but datafiable-row requires a real builder
(cond-> {:builder-fn rs/as-arrays} (cond-> {:builder-fn rs/as-arrays}
(derby?) (derby?)
@ -257,7 +266,7 @@
(deftest test-column-names (deftest test-column-names
(testing "column-names on bare abstraction" (testing "column-names on bare abstraction"
(is (= #{"id" "appearance" "grade" "cost" "name"} (is (= #{(index) "appearance" "grade" "cost" "name"}
(reduce (fn [_ row] (reduce (fn [_ row]
(-> row (-> row
(->> (rs/column-names) (->> (rs/column-names)
@ -265,11 +274,11 @@
(set) (set)
(reduced)))) (reduced))))
nil nil
(p/-execute (ds) ["select * from fruit where id < ?" 4] (p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
;; column-names require a real builder ;; column-names require a real builder
{:builder-fn rs/as-arrays}))))) {:builder-fn rs/as-arrays})))))
(testing "column-names on realized row" (testing "column-names on realized row"
(is (= #{"id" "appearance" "grade" "cost" "name"} (is (= #{(index) "appearance" "grade" "cost" "name"}
(reduce (fn [_ row] (reduce (fn [_ row]
(-> row (-> row
(rs/datafiable-row (ds) {}) (rs/datafiable-row (ds) {})
@ -278,7 +287,7 @@
(set) (set)
(reduced)))) (reduced))))
nil nil
(p/-execute (ds) ["select * from fruit where id < ?" 4] (p/-execute (ds) [(str "select * from fruit where " (index) " < ?") 4]
{:builder-fn rs/as-arrays})))))) {:builder-fn rs/as-arrays}))))))
(deftest test-over-partition-all (deftest test-over-partition-all
@ -299,31 +308,31 @@
(testing "no row builder is used" (testing "no row builder is used"
(is (= [true] (is (= [true]
(into [] (map map?) ; it looks like a real map now (into [] (map map?) ; it looks like a real map now
(p/-execute (ds) ["select * from fruit where id = ?" 1] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= ["Apple"] (is (= ["Apple"]
(into [] (map :name) ; keyword selection works (into [] (map :name) ; keyword selection works
(p/-execute (ds) ["select * from fruit where id = ?" 1] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [[2 [:name "Banana"]]] (is (= [[2 [:name "Banana"]]]
(into [] (map (juxt #(get % "id") ; get by string key works (into [] (map (juxt #(get % (index)) ; get by string key works
#(find % :name))) ; get MapEntry works #(find % :name))) ; get MapEntry works
(p/-execute (ds) ["select * from fruit where id = ?" 2] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 2]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [{:id 3 :name "Peach"}] (is (= [{(col-kw :id) 3 :name "Peach"}]
(into [] (map #(select-keys % [:id :name])) ; select-keys works (into [] (map #(select-keys % [(col-kw :id) :name])) ; select-keys works
(p/-execute (ds) ["select * from fruit where id = ?" 3] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 3]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [[:orange 4]] (is (= [[:orange 4]]
(into [] (map #(vector (if (contains? % :name) ; contains works (into [] (map #(vector (if (contains? % :name) ; contains works
(keyword (str/lower-case (:name %))) (keyword (str/lower-case (:name %)))
:unnamed) :unnamed)
(get % :id 0))) ; get with not-found works (get % (col-kw :id) 0))) ; get with not-found works
(p/-execute (ds) ["select * from fruit where id = ?" 4] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 4]
{:builder-fn (constantly nil)})))) {:builder-fn (constantly nil)}))))
(is (= [{}] (is (= [{}]
(into [] (map empty) ; return empty map without building (into [] (map empty) ; return empty map without building
(p/-execute (ds) ["select * from fruit where id = ?" 1] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn (constantly nil)}))))) {:builder-fn (constantly nil)})))))
(testing "count does not build a map" (testing "count does not build a map"
(let [count-builder (fn [_1 _2] (let [count-builder (fn [_1 _2]
@ -331,7 +340,7 @@
(column-count [_] 13)))] (column-count [_] 13)))]
(is (= [13] (is (= [13]
(into [] (map count) ; count relies on columns, not row fields (into [] (map count) ; count relies on columns, not row fields
(p/-execute (ds) ["select * from fruit where id = ?" 1] (p/-execute (ds) [(str "select * from fruit where " (index) " = ?") 1]
{:builder-fn count-builder})))))) {:builder-fn count-builder}))))))
(testing "assoc, dissoc, cons, seq, and = build maps" (testing "assoc, dissoc, cons, seq, and = build maps"
(is (map? (reduce (fn [_ row] (reduced (assoc row :x 1))) (is (map? (reduce (fn [_ row] (reduced (assoc row :x 1)))
@ -417,7 +426,7 @@
(defn fruit-builder [^ResultSet rs ^ResultSetMetaData rsmeta] (defn fruit-builder [^ResultSet rs ^ResultSetMetaData rsmeta]
(reify (reify
rs/RowBuilder rs/RowBuilder
(->row [_] (->Fruit (.getObject rs "id") (->row [_] (->Fruit (.getObject rs ^String (index))
(.getObject rs "name") (.getObject rs "name")
(.getObject rs "appearance") (.getObject rs "appearance")
(.getObject rs "cost") (.getObject rs "cost")
@ -434,7 +443,7 @@
(valAt [this k] (get this k nil)) (valAt [this k] (get this k nil))
(valAt [this k not-found] (valAt [this k not-found]
(case k (case k
:cols [:id :name :appearance :cost :grade] :cols [(col-kw :id) :name :appearance :cost :grade]
:rsmeta rsmeta :rsmeta rsmeta
not-found)))) not-found))))
@ -467,7 +476,7 @@
metadata)))) metadata))))
(deftest clob-reading (deftest clob-reading
(when-not (or (mssql?) (mysql?) (postgres?)) ; no clob in these (when-not (or (mssql?) (mysql?) (postgres?) (xtdb?)) ; no clob in these
(with-open [con (p/get-connection (ds) {})] (with-open [con (p/get-connection (ds) {})]
(try (try
(p/-execute-one con ["DROP TABLE CLOBBER"] {}) (p/-execute-one con ["DROP TABLE CLOBBER"] {})
@ -497,10 +506,10 @@ CREATE TABLE CLOBBER (
(testing "get n on bare abstraction over arrays" (testing "get n on bare abstraction over arrays"
(is (= [1 2 3] (is (= [1 2 3]
(into [] (map #(get % 0)) (into [] (map #(get % 0))
(p/-execute (ds) ["select id from fruit where id < ?" 4] (p/-execute (ds) [(str "select " (index) " from fruit where " (index) " < ? order by " (index)) 4]
{:builder-fn rs/as-arrays}))))) {:builder-fn rs/as-arrays})))))
(testing "nth on bare abstraction over arrays" (testing "nth on bare abstraction over arrays"
(is (= [1 2 3] (is (= [1 2 3]
(into [] (map #(nth % 0)) (into [] (map #(nth % 0))
(p/-execute (ds) ["select id from fruit where id < ?" 4] (p/-execute (ds) [(str "select " (index) " from fruit where " (index) " < ? order by " (index)) 4]
{:builder-fn rs/as-arrays})))))) {:builder-fn rs/as-arrays}))))))

View file

@ -7,8 +7,8 @@
[next.jdbc.specs :as specs] [next.jdbc.specs :as specs]
[next.jdbc.sql :as sql] [next.jdbc.sql :as sql]
[next.jdbc.test-fixtures [next.jdbc.test-fixtures
:refer [with-test-db ds column default-options :refer [column col-kw default-options derby? ds index
derby? jtds? maria? mssql? mysql? postgres? sqlite?]] jtds? maria? mssql? mysql? postgres? sqlite? with-test-db xtdb?]]
[next.jdbc.types :refer [as-other as-real as-varchar]])) [next.jdbc.types :refer [as-other as-real as-varchar]]))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
@ -19,7 +19,7 @@
(deftest test-query (deftest test-query
(let [ds-opts (jdbc/with-options (ds) (default-options)) (let [ds-opts (jdbc/with-options (ds) (default-options))
rs (sql/query ds-opts ["select * from fruit order by id"])] rs (sql/query ds-opts [(str "select * from fruit order by " (index))])]
(is (= 4 (count rs))) (is (= 4 (count rs)))
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
@ -34,10 +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 [:ID :columns [(col-kw :ID)
["CASE WHEN grade > 91 THEN 'ok ' ELSE 'bad' END" ["CASE WHEN grade > 91 THEN 'ok ' ELSE 'bad' END"
:QUALITY]] :QUALITY]]
:order-by [:id]))] :order-by [(col-kw :id)]))]
(is (= 2 (count rs))) (is (= 2 (count rs)))
(is (every? map? rs)) (is (every? map? rs))
(is (every? meta rs)) (is (every? meta rs))
@ -67,17 +67,18 @@
(is (= 1 count-v))) (is (= 1 count-v)))
(let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" :all)] (let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" :all)]
(is (= 4 count-v))) (is (= 4 count-v)))
(let [max-id (sql/aggregate-by-keys ds-opts :fruit "max(id)" :all)] (let [max-id (sql/aggregate-by-keys ds-opts :fruit (str "max(" (index) ")") :all)]
(is (= 4 max-id))) (is (= 4 max-id)))
(let [min-name (sql/aggregate-by-keys ds-opts :fruit "min(name)" :all)] (when-not (xtdb?) ; XTDB does not support min/max on strings?
(is (= "Apple" min-name))) (let [min-name (sql/aggregate-by-keys ds-opts :fruit "min(name)" :all)]
(is (= "Apple" min-name))))
(is (thrown? IllegalArgumentException (is (thrown? IllegalArgumentException
(sql/aggregate-by-keys ds-opts :fruit "count(*)" :all {:columns []}))))) (sql/aggregate-by-keys ds-opts :fruit "count(*)" :all {:columns []})))))
(deftest test-get-by-id (deftest test-get-by-id
(let [ds-opts (jdbc/with-options (ds) (default-options))] (let [ds-opts (jdbc/with-options (ds) (default-options))]
(is (nil? (sql/get-by-id ds-opts :fruit -1))) (is (nil? (sql/get-by-id ds-opts :fruit -1 (col-kw :id) {})))
(let [row (sql/get-by-id ds-opts :fruit 3)] (let [row (sql/get-by-id ds-opts :fruit 3 (col-kw :id) {})]
(is (map? row)) (is (map? row))
(is (= "Peach" ((column :FRUIT/NAME) row)))) (is (= "Peach" ((column :FRUIT/NAME) row))))
(let [row (sql/get-by-id ds-opts :fruit "juicy" :appearance {})] (let [row (sql/get-by-id ds-opts :fruit "juicy" :appearance {})]
@ -88,23 +89,28 @@
(is (map? row)) (is (map? row))
(is (= 2 ((column :FRUIT/ID) row)))))) (is (= 2 ((column :FRUIT/ID) row))))))
(defn- update-count [n]
(if (xtdb?)
{:next.jdbc/update-count 0}
{:next.jdbc/update-count n}))
(deftest test-update! (deftest test-update!
(let [ds-opts (jdbc/with-options (ds) (default-options))] (let [ds-opts (jdbc/with-options (ds) (default-options))]
(try (try
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/update! ds-opts :fruit {:appearance "brown"} {:id 2}))) (sql/update! ds-opts :fruit {:appearance "brown"} {(col-kw :id) 2})))
(is (= "brown" ((column :FRUIT/APPEARANCE) (is (= "brown" ((column :FRUIT/APPEARANCE)
(sql/get-by-id ds-opts :fruit 2)))) (sql/get-by-id ds-opts :fruit 2 (col-kw :id) {}))))
(finally (finally
(sql/update! ds-opts :fruit {:appearance "yellow"} {:id 2}))) (sql/update! ds-opts :fruit {:appearance "yellow"} {(col-kw :id) 2})))
(try (try
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/update! ds-opts :fruit {:appearance "green"} (sql/update! ds-opts :fruit {:appearance "green"}
["name = ?" "Banana"]))) ["name = ?" "Banana"])))
(is (= "green" ((column :FRUIT/APPEARANCE) (is (= "green" ((column :FRUIT/APPEARANCE)
(sql/get-by-id ds-opts :fruit 2)))) (sql/get-by-id ds-opts :fruit 2 (col-kw :id) {}))))
(finally (finally
(sql/update! ds-opts :fruit {:appearance "yellow"} {:id 2}))))) (sql/update! ds-opts :fruit {:appearance "yellow"} {(col-kw :id) 2})))))
(deftest test-insert-delete (deftest test-insert-delete
(let [new-key (cond (derby?) :1 (let [new-key (cond (derby?) :1
@ -113,18 +119,24 @@
(mssql?) :GENERATED_KEYS (mssql?) :GENERATED_KEYS
(mysql?) :GENERATED_KEY (mysql?) :GENERATED_KEY
(postgres?) :fruit/id (postgres?) :fruit/id
;; XTDB does not return the generated key so we fix it
;; to be the one we insert here, and then fake it in all
;; the other tests.
(xtdb?) (constantly 5)
:else :FRUIT/ID)] :else :FRUIT/ID)]
(testing "single insert/delete" (testing "single insert/delete"
(is (== 5 (new-key (sql/insert! (ds) :fruit (is (== 5 (new-key (sql/insert! (ds) :fruit
{:name (as-varchar "Kiwi") (cond-> {:name (as-varchar "Kiwi")
:appearance "green & fuzzy" :appearance "green & fuzzy"
:cost 100 :grade (as-real 99.9)} :cost 100 :grade (as-real 99.9)}
(xtdb?)
(assoc :_id 5))
{:suffix {:suffix
(when (sqlite?) (when (sqlite?)
"RETURNING *")})))) "RETURNING *")}))))
(is (= 5 (count (sql/query (ds) ["select * from fruit"])))) (is (= 5 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/delete! (ds) :fruit {:id 5}))) (sql/delete! (ds) :fruit {(col-kw :id) 5})))
(is (= 4 (count (sql/query (ds) ["select * from fruit"]))))) (is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "multiple insert/delete" (testing "multiple insert/delete"
(is (= (cond (derby?) (is (= (cond (derby?)
@ -133,23 +145,28 @@
[8M] [8M]
(maria?) (maria?)
[6] [6]
(xtdb?)
[]
:else :else
[6 7 8]) [6 7 8])
(mapv new-key (mapv new-key
(sql/insert-multi! (ds) :fruit (sql/insert-multi! (ds) :fruit
[:name :appearance :cost :grade] (cond->> [:name :appearance :cost :grade]
[["Kiwi" "green & fuzzy" 100 99.9] (xtdb?) (cons :_id))
["Grape" "black" 10 50] (cond->> [["Kiwi" "green & fuzzy" 100 99.9]
["Lemon" "yellow" 20 9.9]] ["Grape" "black" 10 50]
["Lemon" "yellow" 20 9.9]]
(xtdb?)
(map cons [6 7 8]))
{:suffix {:suffix
(when (sqlite?) (when (sqlite?)
"RETURNING *")})))) "RETURNING *")}))))
(is (= 7 (count (sql/query (ds) ["select * from fruit"])))) (is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/delete! (ds) :fruit {:id 6}))) (sql/delete! (ds) :fruit {(col-kw :id) 6})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"])))) (is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 2} (is (= (update-count 2)
(sql/delete! (ds) :fruit ["id > ?" 4]))) (sql/delete! (ds) :fruit [(str (index) " > ?") 4])))
(is (= 4 (count (sql/query (ds) ["select * from fruit"]))))) (is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "multiple insert/delete with sequential cols/rows" ; per #43 (testing "multiple insert/delete with sequential cols/rows" ; per #43
(is (= (cond (derby?) (is (= (cond (derby?)
@ -158,23 +175,28 @@
[11M] [11M]
(maria?) (maria?)
[9] [9]
(xtdb?)
[]
:else :else
[9 10 11]) [9 10 11])
(mapv new-key (mapv new-key
(sql/insert-multi! (ds) :fruit (sql/insert-multi! (ds) :fruit
'(:name :appearance :cost :grade) (cond->> '(:name :appearance :cost :grade)
'(("Kiwi" "green & fuzzy" 100 99.9) (xtdb?) (cons :_id))
("Grape" "black" 10 50) (cond->> '(("Kiwi" "green & fuzzy" 100 99.9)
("Lemon" "yellow" 20 9.9)) ("Grape" "black" 10 50)
("Lemon" "yellow" 20 9.9))
(xtdb?)
(map cons [9 10 11]))
{:suffix {:suffix
(when (sqlite?) (when (sqlite?)
"RETURNING *")})))) "RETURNING *")}))))
(is (= 7 (count (sql/query (ds) ["select * from fruit"])))) (is (= 7 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/delete! (ds) :fruit {:id 9}))) (sql/delete! (ds) :fruit {(col-kw :id) 9})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"])))) (is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 2} (is (= (update-count 2)
(sql/delete! (ds) :fruit ["id > ?" 4]))) (sql/delete! (ds) :fruit [(str (index) " > ?") 4])))
(is (= 4 (count (sql/query (ds) ["select * from fruit"]))))) (is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "multiple insert/delete with maps" (testing "multiple insert/delete with maps"
(is (= (cond (derby?) (is (= (cond (derby?)
@ -183,31 +205,35 @@
[14M] [14M]
(maria?) (maria?)
[12] [12]
(xtdb?)
[]
:else :else
[12 13 14]) [12 13 14])
(mapv new-key (mapv new-key
(sql/insert-multi! (ds) :fruit (sql/insert-multi! (ds) :fruit
[{:name "Kiwi" (cond->> [{:name "Kiwi"
:appearance "green & fuzzy" :appearance "green & fuzzy"
:cost 100 :cost 100
:grade 99.9} :grade 99.9}
{: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 (= {:next.jdbc/update-count 1} (is (= (update-count 1)
(sql/delete! (ds) :fruit {:id 12}))) (sql/delete! (ds) :fruit {(col-kw :id) 12})))
(is (= 6 (count (sql/query (ds) ["select * from fruit"])))) (is (= 6 (count (sql/query (ds) ["select * from fruit"]))))
(is (= {:next.jdbc/update-count 2} (is (= (update-count 2)
(sql/delete! (ds) :fruit ["id > ?" 10]))) (sql/delete! (ds) :fruit [(str (index) " > ?") 10])))
(is (= 4 (count (sql/query (ds) ["select * from fruit"]))))) (is (= 4 (count (sql/query (ds) ["select * from fruit"])))))
(testing "empty insert-multi!" ; per #44 and #264 (testing "empty insert-multi!" ; per #44 and #264
(is (= [] (sql/insert-multi! (ds) :fruit (is (= [] (sql/insert-multi! (ds) :fruit
@ -255,7 +281,7 @@
(deftest array-in (deftest array-in
(when (postgres?) (when (postgres?)
(let [data (sql/find-by-keys (ds) :fruit ["id = any(?)" (int-array [1 2 3 4])])] (let [data (sql/find-by-keys (ds) :fruit [(str (index) " = any(?)") (int-array [1 2 3 4])])]
(is (= 4 (count data)))))) (is (= 4 (count data))))))
(deftest enum-pg (deftest enum-pg

View file

@ -64,11 +64,17 @@
(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 (System/getenv "NEXT_JDBC_TEST_XTDB") 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))
@ -86,19 +92,34 @@
(defn postgres? [] (= "embedded-postgres" (:dbtype @test-db-spec))) (defn postgres? [] (= "embedded-postgres" (:dbtype @test-db-spec)))
(defn xtdb? [] (= "xtdb" (:dbtype @test-db-spec)))
(defn sqlite? [] (= "sqlite" (:dbtype @test-db-spec))) (defn sqlite? [] (= "sqlite" (:dbtype @test-db-spec)))
(defn stored-proc? [] (not (#{"derby" "h2" "h2:mem" "sqlite"} (:dbtype @test-db-spec)))) (defn stored-proc? [] (not (#{"derby" "h2" "h2:mem" "sqlite" "xtdb"}
(:dbtype @test-db-spec))))
(defn column [k] (defn column [k]
(let [n (namespace k)] (let [n (namespace k)]
(keyword (when n (cond (postgres?) (str/lower-case n) (keyword (when n (cond (postgres?) (str/lower-case n)
(mssql?) (str/lower-case n) (mssql?) (str/lower-case n)
(mysql?) (str/lower-case n) (mysql?) (str/lower-case n)
(xtdb?) nil
:else n)) :else n))
(cond (postgres?) (str/lower-case (name k)) (cond (postgres?) (str/lower-case (name k))
(xtdb?) (let [c (str/lower-case (name k))]
(if (= "id" c) "_id" c))
:else (name k))))) :else (name k)))))
(defn index []
(if (xtdb?) "_id" "id"))
(defn col-kw [k]
(if (xtdb?)
(let [n (name k)]
(if (= "id" (str/lower-case n)) :_id (keyword n)))
k))
(defn default-options [] (defn default-options []
(if (mssql?) ; so that we get table names back from queries (if (mssql?) ; so that we get table names back from queries
{:result-type :scroll-insensitive :concurrency :read-only} {:result-type :scroll-insensitive :concurrency :read-only}
@ -156,29 +177,51 @@
:else :else
"AUTO_INCREMENT PRIMARY KEY")] "AUTO_INCREMENT PRIMARY KEY")]
(with-open [con (jdbc/get-connection (ds))] (with-open [con (jdbc/get-connection (ds))]
(when (stored-proc?) (if (xtdb?) ; no DDL for creation
(try (do
(jdbc/execute-one! con ["DROP PROCEDURE FRUITP"]) (try
(catch Throwable _))) (do-commands con ["DELETE FROM fruit WHERE true"])
(try (catch Throwable _))
(do-commands con [(str "DROP TABLE " fruit)]) (sql/insert-multi! con :fruit
(catch Exception _)) [:_id :name :appearance :cost]
(try [[1 "Apple" "red" 59]]
(do-commands con [(str "DROP TABLE " btest)]) {:return-keys false})
(catch Exception _)) (sql/insert-multi! con :fruit
(when (postgres?) [:_id :name :appearance :grade]
(try [[2 "Banana" "yellow" 92.2]]
(do-commands con ["DROP TABLE LANG_TEST"]) {:return-keys false})
(catch Exception _)) (sql/insert-multi! con :fruit
(try [:_id :name :cost :grade]
(do-commands con ["DROP TYPE LANGUAGE"]) [[3 "Peach" 139 90.0]]
(catch Exception _)) {:return-keys false})
(do-commands con ["CREATE TYPE LANGUAGE AS ENUM('en','fr','de')"]) (sql/insert-multi! con :fruit
(do-commands con [" [:_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),
@ -186,28 +229,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 "
@ -223,15 +266,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)

File diff suppressed because it is too large Load diff