diff --git a/.clj-kondo/imports/com.xtdb/xtdb-api/config.edn b/.clj-kondo/imports/com.xtdb/xtdb-api/config.edn new file mode 100644 index 0000000..3b0abdb --- /dev/null +++ b/.clj-kondo/imports/com.xtdb/xtdb-api/config.edn @@ -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}}} diff --git a/.clj-kondo/imports/com.xtdb/xtdb-api/hooks/xtql.clj b/.clj-kondo/imports/com.xtdb/xtdb-api/hooks/xtql.clj new file mode 100644 index 0000000..17e7998 --- /dev/null +++ b/.clj-kondo/imports/com.xtdb/xtdb-api/hooks/xtql.clj @@ -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))))) diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index ab4c541..429c2b0 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -44,6 +44,7 @@ jobs: env: MYSQL_ROOT_PASSWORD: testing NEXT_JDBC_TEST_MYSQL: yes + NEXT_JDBC_TEST_XTDB: yes NEXT_JDBC_TEST_MSSQL: yes MSSQL_SA_PASSWORD: Str0ngP4ssw0rd - name: Deploy Release diff --git a/.github/workflows/test-and-snapshot.yml b/.github/workflows/test-and-snapshot.yml index a932628..88a0194 100644 --- a/.github/workflows/test-and-snapshot.yml +++ b/.github/workflows/test-and-snapshot.yml @@ -42,6 +42,7 @@ jobs: env: MYSQL_ROOT_PASSWORD: testing NEXT_JDBC_TEST_MYSQL: yes + NEXT_JDBC_TEST_XTDB: yes NEXT_JDBC_TEST_MSSQL: yes MSSQL_SA_PASSWORD: Str0ngP4ssw0rd - name: Deploy Snapshot diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87b2fc4..551cf4e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,5 +42,6 @@ jobs: env: MYSQL_ROOT_PASSWORD: testing NEXT_JDBC_TEST_MYSQL: yes + NEXT_JDBC_TEST_XTDB: yes NEXT_JDBC_TEST_MSSQL: yes MSSQL_SA_PASSWORD: Str0ngP4ssw0rd diff --git a/deps.edn b/deps.edn index 94de157..32e32d2 100644 --- a/deps.edn +++ b/deps.edn @@ -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.2.0"} org.xerial/sqlite-jdbc {:mvn/version "3.47.1.0"} 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.2"} ;; bridge everything into log4j: diff --git a/docker-compose.yml b/docker-compose.yml index 5981a9f..dc6492a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,3 +14,8 @@ services: MSSQL_SA_PASSWORD: Str0ngP4ssw0rd ports: - "1433:1433" + xtdb: + image: ghcr.io/xtdb/xtdb + pull_policy: always + ports: + - "5432:5432" diff --git a/run-tests.clj b/run-tests.clj index d2f9040..2e84aa3 100755 --- a/run-tests.clj +++ b/run-tests.clj @@ -12,12 +12,15 @@ (System/exit exit)))) (let [maria? (some #(= "maria" %) *command-line-args*) + xtdb? (some #(= "xtdb" %) *command-line-args*) all? (some #(= "all" %) *command-line-args*) env (cond-> {"NEXT_JDBC_TEST_MSSQL" "yes" "NEXT_JDBC_TEST_MYSQL" "yes" "MSSQL_SA_PASSWORD" "Str0ngP4ssw0rd"} 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])] (run-tests env v))) diff --git a/src/next/jdbc/result_set.clj b/src/next/jdbc/result_set.clj index 42dcae0..cb30397 100644 --- a/src/next/jdbc/result_set.clj +++ b/src/next/jdbc/result_set.clj @@ -278,6 +278,10 @@ :qualifier-fn ->kebab-case :label-fn ->kebab-case))) +(comment + (->kebab-case "_id") ;;=> "id"!! + ) + (defn as-unqualified-kebab-maps "Given a `ResultSet` and options, return a `RowBuilder` / `ResultSetBuilder` that produces bare vectors of hash map rows, with simple, kebab-case keys." diff --git a/test/next/jdbc/datafy_test.clj b/test/next/jdbc/datafy_test.clj index 35701b1..4aaf89c 100644 --- a/test/next/jdbc/datafy_test.clj +++ b/test/next/jdbc/datafy_test.clj @@ -10,8 +10,8 @@ [next.jdbc.result-set :as rs] [next.jdbc.specs :as specs] [next.jdbc.test-fixtures - :refer [with-test-db db ds - derby? jtds? mysql? postgres? sqlite?]])) + :refer [db derby? ds jtds? mysql? postgres? sqlite? with-test-db + xtdb?]])) (set! *warn-on-reflection* true) @@ -83,6 +83,26 @@ :rowIdLifetime/exception)) (postgres?) (-> (disj :rowIdLifetime) (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) (conj :clientInfoProperties/exception :rowIdLifetime/exception))) @@ -97,7 +117,8 @@ (let [data (d/datafy (.getMetaData con))] (doseq [k (cond-> #{:catalogs :clientInfoProperties :schemas :tableTypes :typeInfo} (jtds?) (disj :clientInfoProperties) - (sqlite?) (disj :clientInfoProperties))] + (sqlite?) (disj :clientInfoProperties) + (xtdb?) (disj :clientInfoProperties))] (let [rs (d/nav data k nil)] (is (vector? rs)) (is (every? map? rs)))))))) @@ -122,4 +143,5 @@ (.execute ps) (.getResultSet ps) (.close ps) - (.close con)) + (.close con) + ) diff --git a/test/next/jdbc/date_time_test.clj b/test/next/jdbc/date_time_test.clj index 7040561..4bab04e 100644 --- a/test/next/jdbc/date_time_test.clj +++ b/test/next/jdbc/date_time_test.clj @@ -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,29 +21,30 @@ (specs/instrument) (deftest issue-73 - (try - (jdbc/execute-one! (ds) ["drop table fruit_time"]) - (catch Throwable _)) - (jdbc/execute-one! (ds) [(str "create table fruit_time (id int not null, deadline " - (if (mssql?) "datetime" "timestamp") - " not null)")]) - (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)]) - (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 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)]) - (try - (jdbc/execute-one! (ds) ["drop table fruit_time"]) - (catch Throwable _)) - (jdbc/execute-one! (ds) ["create table fruit_time (id int not null, deadline time not null)"]) - (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)]) - (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 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)]) - (try - (jdbc/execute-one! (ds) ["drop table fruit_time"]) - (catch Throwable _)) - (jdbc/execute-one! (ds) ["create table fruit_time (id int not null, deadline date not null)"]) - (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)]) - (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 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)])) + (when-not (xtdb?) + (try + (jdbc/execute-one! (ds) ["drop table fruit_time"]) + (catch Throwable _)) + (jdbc/execute-one! (ds) [(str "create table fruit_time (id int not null, deadline " + (if (mssql?) "datetime" "timestamp") + " not null)")]) + (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)]) + (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 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)]) + (try + (jdbc/execute-one! (ds) ["drop table fruit_time"]) + (catch Throwable _)) + (jdbc/execute-one! (ds) ["create table fruit_time (id int not null, deadline time not null)"]) + (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)]) + (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 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)]) + (try + (jdbc/execute-one! (ds) ["drop table fruit_time"]) + (catch Throwable _)) + (jdbc/execute-one! (ds) ["create table fruit_time (id int not null, deadline date not null)"]) + (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 1 (java.util.Date.)]) + (jdbc/execute-one! (ds) ["insert into fruit_time (id, deadline) values (?,?)" 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)]))) diff --git a/test/next/jdbc/defer_test.clj b/test/next/jdbc/defer_test.clj index 5ed9b1a..d4285de 100644 --- a/test/next/jdbc/defer_test.clj +++ b/test/next/jdbc/defer_test.clj @@ -15,39 +15,40 @@ [next.jdbc :as jdbc] [next.jdbc.defer :as sut] [next.jdbc.test-fixtures - :refer [ds with-test-db]])) + :refer [ds with-test-db xtdb?]])) (set! *warn-on-reflection* true) (use-fixtures :once with-test-db) (deftest basic-test - (testing "data structures" - (is (= [{:sql-p ["INSERT INTO foo (name) VALUES (?)" "Sean"] - :key-fn :GENERATED_KEY - :key :id - :opts {:key-fn :GENERATED_KEY :key :id}}] - @(sut/defer-ops - #(sut/insert! :foo {:name "Sean"} {:key-fn :GENERATED_KEY :key :id}))))) - (testing "execution" - (let [effects (sut/with-deferred (ds) - (sut/insert! :fruit {:name "Mango"} {:key :test}))] - (is (= {:test 1} @effects)) - (is (= 1 (count (jdbc/execute! (ds) - ["select * from fruit where name = ?" - "Mango"]))))) - (let [effects (sut/with-deferred (ds) - (sut/insert! :fruit {:name "Dragonfruit"} {:key :test}) - (sut/update! :fruit {:cost 123} {:name "Dragonfruit"}) - (sut/delete! :fruit {:name "Dragonfruit"}))] - (is (= {:test 1} @effects)) - (is (= 0 (count (jdbc/execute! (ds) - ["select * from fruit where name = ?" - "Dragonfruit"]))))) - (let [effects (sut/with-deferred (ds) - (sut/insert! :fruit {:name "Grapefruit" :bad_column 0} {:key :test}))] - (is (= :failed (try @effects - (catch Exception _ :failed)))) - (is (= 0 (count (jdbc/execute! (ds) - ["select * from fruit where name = ?" - "Grapefruit"]))))))) + (when-not (xtdb?) + (testing "data structures" + (is (= [{:sql-p ["INSERT INTO foo (name) VALUES (?)" "Sean"] + :key-fn :GENERATED_KEY + :key :id + :opts {:key-fn :GENERATED_KEY :key :id}}] + @(sut/defer-ops + #(sut/insert! :foo {:name "Sean"} {:key-fn :GENERATED_KEY :key :id}))))) + (testing "execution" + (let [effects (sut/with-deferred (ds) + (sut/insert! :fruit {:name "Mango"} {:key :test}))] + (is (= {:test 1} @effects)) + (is (= 1 (count (jdbc/execute! (ds) + ["select * from fruit where name = ?" + "Mango"]))))) + (let [effects (sut/with-deferred (ds) + (sut/insert! :fruit {:name "Dragonfruit"} {:key :test}) + (sut/update! :fruit {:cost 123} {:name "Dragonfruit"}) + (sut/delete! :fruit {:name "Dragonfruit"}))] + (is (= {:test 1} @effects)) + (is (= 0 (count (jdbc/execute! (ds) + ["select * from fruit where name = ?" + "Dragonfruit"]))))) + (let [effects (sut/with-deferred (ds) + (sut/insert! :fruit {:name "Grapefruit" :bad_column 0} {:key :test}))] + (is (= :failed (try @effects + (catch Exception _ :failed)))) + (is (= 0 (count (jdbc/execute! (ds) + ["select * from fruit where name = ?" + "Grapefruit"])))))))) diff --git a/test/next/jdbc/optional_test.clj b/test/next/jdbc/optional_test.clj index 50444cf..1a71f43 100644 --- a/test/next/jdbc/optional_test.clj +++ b/test/next/jdbc/optional_test.clj @@ -6,9 +6,10 @@ [clojure.test :refer [deftest is testing use-fixtures]] [next.jdbc.optional :as opt] [next.jdbc.protocols :as p] - [next.jdbc.test-fixtures :refer [with-test-db ds column - default-options]]) - (:import (java.sql ResultSet ResultSetMetaData))) + [next.jdbc.test-fixtures :refer [col-kw column default-options ds index + with-test-db]]) + (:import + (java.sql ResultSet ResultSetMetaData))) (set! *warn-on-reflection* true) @@ -17,7 +18,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] (assoc (default-options) :builder-fn opt/as-maps))] (is (map? row)) @@ -26,7 +27,7 @@ (is (= "Apple" ((column :FRUIT/NAME) row))))) (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 opt/as-unqualified-maps})] (is (map? row)) (is (not (contains? row (column :COST)))) @@ -34,23 +35,23 @@ (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 opt/as-lower-maps))] (is (map? row)) - (is (not (contains? row :fruit/appearance))) - (is (= 3 (:fruit/id row))) - (is (= "Peach" (:fruit/name row))))) + (is (not (contains? row (col-kw :fruit/appearance)))) + (is (= 3 ((col-kw :fruit/id) row))) + (is (= "Peach" ((col-kw :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 opt/as-unqualified-lower-maps})] (is (map? row)) - (is (= 4 (:id row))) - (is (= "Orange" (:name row))))) + (is (= 4 ((col-kw :id) row))) + (is (= "Orange" ((col-kw :name) row))))) (testing "custom 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 opt/as-modified-maps :label-fn str/lower-case @@ -67,7 +68,7 @@ (deftest test-map-row-adapter (testing "default row builder" (let [row (p/-execute-one (ds) - ["select * from fruit where id = ?" 1] + [(str "select * from fruit where " (index) " = ?") 1] (assoc (default-options) :builder-fn (opt/as-maps-adapter opt/as-maps @@ -78,7 +79,7 @@ (is (= "Apple" ((column :FRUIT/NAME) row))))) (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 (opt/as-maps-adapter opt/as-unqualified-maps default-column-reader)})] @@ -88,27 +89,27 @@ (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 (opt/as-maps-adapter opt/as-lower-maps default-column-reader)))] (is (map? row)) - (is (not (contains? row :fruit/appearance))) - (is (= 3 (:fruit/id row))) - (is (= "Peach" (:fruit/name row))))) + (is (not (contains? row (col-kw :fruit/appearance)))) + (is (= 3 ((col-kw :fruit/id) row))) + (is (= "Peach" ((col-kw :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 (opt/as-maps-adapter opt/as-unqualified-lower-maps default-column-reader)})] (is (map? row)) - (is (= 4 (:id row))) - (is (= "Orange" (:name row))))) + (is (= 4 ((col-kw :id) row))) + (is (= "Orange" ((col-kw :name) row))))) (testing "custom 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 (opt/as-maps-adapter opt/as-modified-maps diff --git a/test/next/jdbc/plan_test.clj b/test/next/jdbc/plan_test.clj index 51012f0..c477a8f 100644 --- a/test/next/jdbc/plan_test.clj +++ b/test/next/jdbc/plan_test.clj @@ -6,7 +6,7 @@ [next.jdbc.plan :as plan] [next.jdbc.specs :as specs] [next.jdbc.test-fixtures - :refer [with-test-db ds]] + :refer [with-test-db ds col-kw index]] [clojure.string :as str])) (set! *warn-on-reflection* true) @@ -17,56 +17,56 @@ (specs/instrument) (deftest select-one!-tests - (is (= {:id 1} - (plan/select-one! (ds) [:id] ["select * from fruit order by id"]))) + (is (= {(col-kw :id) 1} + (plan/select-one! (ds) [(col-kw :id)] [(str "select * from fruit order by " (index))]))) (is (= 1 - (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" - (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"] - (plan/select-one! (ds) (juxt :id :name) - ["select * from fruit order by id"]))) - (is (= {:id 1 :name "Apple"} - (plan/select-one! (ds) #(select-keys % [:id :name]) - ["select * from fruit order by id"])))) + (plan/select-one! (ds) (juxt (col-kw :id) :name) + [(str "select * from fruit order by " (index))]))) + (is (= {(col-kw :id) 1 :name "Apple"} + (plan/select-one! (ds) #(select-keys % [(col-kw :id) :name]) + [(str "select * from fruit order by " (index))])))) (deftest select-vector-tests - (is (= [{:id 1} {:id 2} {:id 3} {:id 4}] - (plan/select! (ds) [:id] ["select * from fruit order by id"]))) + (is (= [{(col-kw :id) 1} {(col-kw :id) 2} {(col-kw :id) 3} {(col-kw :id) 4}] + (plan/select! (ds) [(col-kw :id)] [(str "select * from fruit order by " (index))]))) (is (= [1 2 3 4] - (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"] - (plan/select! (ds) :name ["select * from fruit where id = ?" 2]))) + (plan/select! (ds) :name [(str "select * from fruit where " (index) " = ?") 2]))) (is (= [[2 "Banana"]] - (plan/select! (ds) (juxt :id :name) - ["select * from fruit where id = ?" 2]))) - (is (= [{:id 2 :name "Banana"}] - (plan/select! (ds) [:id :name] - ["select * from fruit where id = ?" 2])))) + (plan/select! (ds) (juxt (col-kw :id) :name) + [(str "select * from fruit where " (index) " = ?") 2]))) + (is (= [{(col-kw :id) 2 :name "Banana"}] + (plan/select! (ds) [(col-kw :id) :name] + [(str "select * from fruit where " (index) " = ?") 2])))) (deftest select-set-tests - (is (= #{{:id 1} {:id 2} {:id 3} {:id 4}} - (plan/select! (ds) [:id] ["select * from fruit order by id"] + (is (= #{{(col-kw :id) 1} {(col-kw :id) 2} {(col-kw :id) 3} {(col-kw :id) 4}} + (plan/select! (ds) [(col-kw :id)] [(str "select * from fruit order by " (index))] {:into #{}}))) (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 #{}})))) (deftest select-map-tests (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 {}})))) (deftest select-issue-227 (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 % "-" "_")}))) (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 % "-" "_")}))) (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 % "-" "_")}))) (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 % "-" "_")})))) diff --git a/test/next/jdbc/prepare_test.clj b/test/next/jdbc/prepare_test.clj index d8e3fad..675f5ae 100644 --- a/test/next/jdbc/prepare_test.clj +++ b/test/next/jdbc/prepare_test.clj @@ -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,61 +22,43 @@ (specs/instrument) (deftest execute-batch-tests - (testing "simple 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"]])] - (conj result (count (jdbc/execute! t ["select * from fruit"])))))))) - (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) - (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 [" + (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}] + (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 [" + (let [result (prep/execute-batch! ps [["fruit1" "one"] + ["fruit2" "two"] + ["fruit3" "three"] + ["fruit4" "four"] + ["fruit5" "five"] + ["fruit6" "six"] + ["fruit7" "seven"] + ["fruit8" "eight"] + ["fruit9" "nine"]])] + (conj result (count (jdbc/execute! t ["select * from fruit"])))))))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) + (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 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?)) + (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 [" @@ -91,33 +73,52 @@ INSERT INTO fruit (name, appearance) VALUES (?,?) ["fruit7" "seven"] ["fruit8" "eight"] ["fruit9" "nine"]] - {:batch-size 4 - :large true})] + {:batch-size 8})] (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 [" + (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 (?,?) "] - {:return-keys true})] - (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 - :return-generated-keys true})] - (conj result (count (jdbc/execute! t ["select * from fruit"]))))))] - (is (= 13 (last results))) - (is (every? map? (butlast results))) + {:return-keys true})] + (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 + :return-generated-keys true})] + (conj result (count (jdbc/execute! t ["select * from fruit"]))))))] + (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 (< 3 (count results)))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))))) diff --git a/test/next/jdbc/result_set_test.clj b/test/next/jdbc/result_set_test.clj index 033c910..8d893ac 100644 --- a/test/next/jdbc/result_set_test.clj +++ b/test/next/jdbc/result_set_test.clj @@ -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 - default-options - derby? mssql? mysql? postgres?]]) + [next.jdbc.test-fixtures :refer [with-test-db ds column index col-kw + default-options + derby? mssql? mysql? postgres? xtdb?]]) (:import (java.sql ResultSet ResultSetMetaData))) (set! *warn-on-reflection* true) @@ -27,7 +27,9 @@ (testing "default schema" (let [connectable (ds) 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) v (get data :TABLE/FRUIT_ID)] ;; check datafication is sane @@ -40,7 +42,10 @@ (let [connectable (ds) test-row (rs/datafiable-row {:foo/bar 2} connectable (assoc (default-options) - :schema {:foo/bar :fruit/id})) + :schema {:foo/bar + (if (xtdb?) + :fruit/_id + :fruit/id)})) data (d/datafy test-row) v (get data :foo/bar)] ;; check datafication is sane @@ -53,7 +58,10 @@ (let [connectable (ds) test-row (rs/datafiable-row {:foo/bar 3} connectable (assoc (default-options) - :schema {:foo/bar [:fruit/id]})) + :schema {:foo/bar + [(if (xtdb?) + :fruit/_id + :fruit/id)]})) data (d/datafy test-row) v (get data :foo/bar)] ;; check datafication is sane @@ -67,7 +75,7 @@ (let [connectable (ds) test-row (rs/datafiable-row {:foo/bar 2} connectable (assoc (default-options) - :schema {:foo/bar [:fruit :id]})) + :schema {:foo/bar [:fruit (col-kw :id)]})) data (d/datafy test-row) v (get data :foo/bar)] ;; check datafication is sane @@ -79,7 +87,7 @@ (let [connectable (ds) test-row (rs/datafiable-row {:foo/bar 3} connectable (assoc (default-options) - :schema {:foo/bar [:fruit :id :many]})) + :schema {:foo/bar [:fruit (col-kw :id) :many]})) data (d/datafy test-row) v (get data :foo/bar)] ;; check datafication is sane @@ -93,7 +101,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 +109,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 +118,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,34 +127,35 @@ (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)) - (is (contains? row :fruit/appearance)) - (is (nil? (:fruit/appearance row))) - (is (= 3 (:fruit/id row))) - (is (= "Peach" (:fruit/name row))))) + (is (contains? row (col-kw :fruit/appearance))) + (is (nil? ((col-kw :fruit/appearance) row))) + (is (= 3 ((col-kw :fruit/id) row))) + (is (= "Peach" ((col-kw :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))))) + (is (= 4 ((col-kw :id) row))) + (is (= "Orange" ((col-kw :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))) + ;; kebab-case strips leading _ from _id (XTDB): + (is (= 3 ((if (xtdb?) :id :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)) @@ -155,7 +164,7 @@ (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 @@ -168,7 +177,7 @@ (is (= "Peach" ((column :FRUIT/name) row))))) (testing "custom row builder 2" (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 @@ -176,12 +185,12 @@ (is (map? row)) (is (contains? row :vegetable/appearance)) (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 (= "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 +216,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 @@ -236,7 +245,7 @@ (testing "row-numbers on bare abstraction" (is (= [1 2 3] (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... (cond-> {:builder-fn (constantly nil)} (derby?) @@ -247,7 +256,7 @@ (is (= [1 2 3] (into [] (comp (map #(rs/datafiable-row % (ds) {})) (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 (cond-> {:builder-fn rs/as-arrays} (derby?) @@ -257,7 +266,7 @@ (deftest test-column-names (testing "column-names on bare abstraction" - (is (= #{"id" "appearance" "grade" "cost" "name"} + (is (= #{(index) "appearance" "grade" "cost" "name"} (reduce (fn [_ row] (-> row (->> (rs/column-names) @@ -265,11 +274,11 @@ (set) (reduced)))) 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 {:builder-fn rs/as-arrays}))))) (testing "column-names on realized row" - (is (= #{"id" "appearance" "grade" "cost" "name"} + (is (= #{(index) "appearance" "grade" "cost" "name"} (reduce (fn [_ row] (-> row (rs/datafiable-row (ds) {}) @@ -278,7 +287,7 @@ (set) (reduced)))) 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})))))) (deftest test-over-partition-all @@ -299,31 +308,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 + (into [] (map (juxt #(get % (index)) ; 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] + (is (= [{(col-kw :id) 3 :name "Peach"}] + (into [] (map #(select-keys % [(col-kw :id) :name])) ; select-keys works + (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] + (get % (col-kw :id) 0))) ; get with not-found works + (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 +340,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))) @@ -417,7 +426,7 @@ (defn fruit-builder [^ResultSet rs ^ResultSetMetaData rsmeta] (reify rs/RowBuilder - (->row [_] (->Fruit (.getObject rs "id") + (->row [_] (->Fruit (.getObject rs ^String (index)) (.getObject rs "name") (.getObject rs "appearance") (.getObject rs "cost") @@ -434,7 +443,7 @@ (valAt [this k] (get this k nil)) (valAt [this k not-found] (case k - :cols [:id :name :appearance :cost :grade] + :cols [(col-kw :id) :name :appearance :cost :grade] :rsmeta rsmeta not-found)))) @@ -467,7 +476,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"] {}) @@ -497,10 +506,10 @@ CREATE TABLE CLOBBER ( (testing "get n on bare abstraction over arrays" (is (= [1 2 3] (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}))))) (testing "nth on bare abstraction over arrays" (is (= [1 2 3] (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})))))) diff --git a/test/next/jdbc/sql_test.clj b/test/next/jdbc/sql_test.clj index 7a569aa..c5259f6 100644 --- a/test/next/jdbc/sql_test.clj +++ b/test/next/jdbc/sql_test.clj @@ -7,8 +7,8 @@ [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) @@ -19,7 +19,7 @@ (deftest test-query (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 (every? map? rs)) (is (every? meta rs)) @@ -34,10 +34,10 @@ (if (or (mysql?) (sqlite?)) {:limit 2 :offset 1} {:offset 1 :fetch 2}) - :columns [:ID + :columns [(col-kw :ID) ["CASE WHEN grade > 91 THEN 'ok ' ELSE 'bad' END" :QUALITY]] - :order-by [:id]))] + :order-by [(col-kw :id)]))] (is (= 2 (count rs))) (is (every? map? rs)) (is (every? meta rs)) @@ -67,17 +67,18 @@ (is (= 1 count-v))) (let [count-v (sql/aggregate-by-keys ds-opts :fruit "count(*)" :all)] (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))) - (let [min-name (sql/aggregate-by-keys ds-opts :fruit "min(name)" :all)] - (is (= "Apple" min-name))) + (when-not (xtdb?) ; XTDB does not support min/max on strings? + (let [min-name (sql/aggregate-by-keys ds-opts :fruit "min(name)" :all)] + (is (= "Apple" min-name)))) (is (thrown? IllegalArgumentException (sql/aggregate-by-keys ds-opts :fruit "count(*)" :all {:columns []}))))) (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 {})] @@ -88,23 +89,28 @@ (is (map? 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! (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}))) + (is (= (update-count 1) + (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} + (is (= (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 +119,24 @@ (mssql?) :GENERATED_KEYS (mysql?) :GENERATED_KEY (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)] (testing "single insert/delete" (is (== 5 (new-key (sql/insert! (ds) :fruit - {:name (as-varchar "Kiwi") - :appearance "green & fuzzy" - :cost 100 :grade (as-real 99.9)} + (cond-> {:name (as-varchar "Kiwi") + :appearance "green & fuzzy" + :cost 100 :grade (as-real 99.9)} + (xtdb?) + (assoc :_id 5)) {:suffix (when (sqlite?) "RETURNING *")})))) (is (= 5 (count (sql/query (ds) ["select * from fruit"])))) - (is (= {:next.jdbc/update-count 1} - (sql/delete! (ds) :fruit {:id 5}))) + (is (= (update-count 1) + (sql/delete! (ds) :fruit {(col-kw :id) 5}))) (is (= 4 (count (sql/query (ds) ["select * from fruit"]))))) (testing "multiple insert/delete" (is (= (cond (derby?) @@ -133,23 +145,28 @@ [8M] (maria?) [6] + (xtdb?) + [] :else [6 7 8]) (mapv new-key (sql/insert-multi! (ds) :fruit - [:name :appearance :cost :grade] - [["Kiwi" "green & fuzzy" 100 99.9] - ["Grape" "black" 10 50] - ["Lemon" "yellow" 20 9.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}))) + (is (= (update-count 1) + (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]))) + (is (= (update-count 2) + (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?) @@ -158,23 +175,28 @@ [11M] (maria?) [9] + (xtdb?) + [] :else [9 10 11]) (mapv new-key (sql/insert-multi! (ds) :fruit - '(:name :appearance :cost :grade) - '(("Kiwi" "green & fuzzy" 100 99.9) - ("Grape" "black" 10 50) - ("Lemon" "yellow" 20 9.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}))) + (is (= (update-count 1) + (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]))) + (is (= (update-count 2) + (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?) @@ -183,31 +205,35 @@ [14M] (maria?) [12] + (xtdb?) + [] :else [12 13 14]) (mapv new-key (sql/insert-multi! (ds) :fruit - [{:name "Kiwi" - :appearance "green & fuzzy" - :cost 100 - :grade 99.9} - {:name "Grape" - :appearance "black" - :cost 10 - :grade 50} - {:name "Lemon" - :appearance "yellow" - :cost 20 - :grade 9.9}] + (cond->> [{:name "Kiwi" + :appearance "green & fuzzy" + :cost 100 + :grade 99.9} + {:name "Grape" + :appearance "black" + :cost 10 + :grade 50} + {:name "Lemon" + :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}))) + (is (= (update-count 1) + (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]))) + (is (= (update-count 2) + (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 +281,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 diff --git a/test/next/jdbc/test_fixtures.clj b/test/next/jdbc/test_fixtures.clj index b5fde10..4cedac2 100644 --- a/test/next/jdbc/test_fixtures.clj +++ b/test/next/jdbc/test_fixtures.clj @@ -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" :dbname "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" (str/lower-case 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,29 +177,51 @@ :else "AUTO_INCREMENT PRIMARY KEY")] (with-open [con (jdbc/get-connection (ds))] - (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 [" + (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"]) + (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 ( LANG LANGUAGE NOT NULL )"])) - (do-commands con [(str " + (do-commands con [(str " CREATE TABLE " fruit " ( ID INTEGER " auto-inc-pk ", NAME VARCHAR(32), @@ -186,28 +229,28 @@ CREATE TABLE " fruit " ( COST INT DEFAULT NULL, GRADE REAL DEFAULT NULL )")]) - (let [created (atom false)] + (let [created (atom false)] ;; MS SQL Server does not support bool/boolean: - (doseq [btype ["BOOL" "BOOLEAN" "BIT"]] + (doseq [btype ["BOOL" "BOOLEAN" "BIT"]] ;; Derby does not support bit: - (doseq [bitty ["BIT" "SMALLINT"]] - (try - (when-not @created - (do-commands con [(str " + (doseq [bitty ["BIT" "SMALLINT"]] + (try + (when-not @created + (do-commands con [(str " CREATE TABLE " btest " ( NAME VARCHAR(32), IS_IT " btype ", TWIDDLE " bitty " )")]) - (reset! created true)) - (catch Throwable _)))) - (when-not @created - (println (:dbtype db) "failed btest creation") - #_(throw (ex-info (str (:dbtype db) " has no boolean type?") {})))) - (when (stored-proc?) - (let [[begin end] (if (postgres?) ["$$" "$$"] ["BEGIN" "END"])] - (try - (do-commands con [(str " + (reset! created true)) + (catch Throwable _)))) + (when-not @created + (println (:dbtype db) "failed btest creation") + #_(throw (ex-info (str (:dbtype db) " has no boolean type?") {})))) + (when (stored-proc?) + (let [[begin end] (if (postgres?) ["$$" "$$"] ["BEGIN" "END"])] + (try + (do-commands con [(str " CREATE PROCEDURE FRUITP" (cond (hsqldb?) "() READS SQL DATA DYNAMIC RESULT SETS 2 " (mssql?) " 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;")) " " end " ")]) - (catch Throwable t - (println 'procedure (:dbtype db) (ex-message t)))))) - (sql/insert-multi! con :fruit - [:name :appearance :cost :grade] - [["Apple" "red" 59 nil] - ["Banana" "yellow" nil 92.2] - ["Peach" nil 139 90.0] - ["Orange" "juicy" 89 88.6]] - {:return-keys false}) + (catch Throwable t + (println 'procedure (:dbtype db) (ex-message t)))))) + (sql/insert-multi! con :fruit + [:name :appearance :cost :grade] + [["Apple" "red" 59 nil] + ["Banana" "yellow" nil 92.2] + ["Peach" nil 139 90.0] + ["Orange" "juicy" 89 88.6]] + {:return-keys false}))) (t))))) (create-clojure-test) diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index 7843c5f..6a6cb64 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -2,22 +2,24 @@ (ns next.jdbc-test "Basic tests for the primary API of `next.jdbc`." - (: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.types :as types]) - (:import (com.zaxxer.hikari HikariDataSource) - (com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource) - (java.sql ResultSet ResultSetMetaData))) + (: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.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.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,23 @@ (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)))))) + (let [n (max (.indexOf ^java.util.List (first rs) :name) 1)] + (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 +131,17 @@ (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)))))) + (let [n (max (.indexOf ^java.util.List (first rs) :name) 1)] + (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 +150,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,32 +161,34 @@ (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)))))) - (testing "execute! with :max-rows / :maxRows" - (let [rs (jdbc/execute! - ds-opts - ["select * from fruit order by id"] - {:max-rows 2})] - (is (every? map? rs)) - (is (every? meta rs)) - (is (= 2 (count rs))) - (is (= 1 ((column :FRUIT/ID) (first rs)))) - (is (= 2 ((column :FRUIT/ID) (last rs))))) - (let [rs (jdbc/execute! - ds-opts - ["select * from fruit order by id"] - {:statement {:maxRows 2}})] - (is (every? map? rs)) - (is (every? meta rs)) - (is (= 2 (count rs))) - (is (= 1 ((column :FRUIT/ID) (first rs)))) - (is (= 2 ((column :FRUIT/ID) (last rs))))))) + (let [n (max (.indexOf ^java.util.List (first rs) :name) 1)] + (is (every? string? (map #(nth % n) (rest rs))))))) + (when-not (xtdb?) ; XTDB does not support this yet + (testing "execute! with :max-rows / :maxRows" + (let [rs (jdbc/execute! + ds-opts + [(str "select * from fruit order by " (index))] + {:max-rows 2})] + (is (every? map? rs)) + (is (every? meta rs)) + (is (= 2 (count rs))) + (is (= 1 ((column :FRUIT/ID) (first rs)))) + (is (= 2 ((column :FRUIT/ID) (last rs))))) + (let [rs (jdbc/execute! + ds-opts + [(str "select * from fruit order by " (index))] + {:statement {:maxRows 2}})] + (is (every? map? rs)) + (is (every? meta rs)) + (is (= 2 (count rs))) + (is (= 1 ((column :FRUIT/ID) (first rs)))) + (is (= 2 ((column :FRUIT/ID) (last rs)))))))) (testing "prepare" ;; default options do not flow over get-connection (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 +200,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 +211,7 @@ ;; 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))]))] (is (every? map? rs)) (is (every? meta rs)) (is (= 4 (count rs))) @@ -214,151 +220,152 @@ ;; 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")]))] (is (every? map? rs)) (is (every? meta rs)) (is (= 1 (count rs))) (is (= 4 ((column :FRUIT/ID) (first rs)))))) - (testing "transact" - (is (= [{:next.jdbc/update-count 1}] - (jdbc/transact (ds) - (fn [t] (jdbc/execute! t [" + (when-not (xtdb?) + (testing "transact" + (is (= [{:next.jdbc/update-count 1}] + (jdbc/transact (ds) + (fn [t] (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "])) - {:rollback-only true}))) - (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) - (testing "with-transaction rollback-only" - (is (not (jdbc/active-tx?)) "should not be in a transaction") - (is (= [{:next.jdbc/update-count 1}] - (jdbc/with-transaction [t (ds) {:rollback-only true}] - (is (jdbc/active-tx?) "should be in a transaction") - (is (jdbc/active-tx? t) "connection should be in a transaction") - (jdbc/execute! t [" + {:rollback-only true}))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) + (testing "with-transaction rollback-only" + (is (not (jdbc/active-tx?)) "should not be in a transaction") + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t (ds) {:rollback-only true}] + (is (jdbc/active-tx?) "should be in a transaction") + (is (jdbc/active-tx? t) "connection should be in a transaction") + (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "])))) - (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) - (is (not (jdbc/active-tx?)) "should not be in a transaction") - (with-open [con (jdbc/get-connection (ds))] - (let [ac (.getAutoCommit con)] - (is (= [{:next.jdbc/update-count 1}] - (jdbc/with-transaction [t con {:rollback-only true}] - (is (jdbc/active-tx?) "should be in a transaction") - (is (jdbc/active-tx? t) "connection should be in a transaction") - (jdbc/execute! t [" + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) + (is (not (jdbc/active-tx?)) "should not be in a transaction") + (with-open [con (jdbc/get-connection (ds))] + (let [ac (.getAutoCommit con)] + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t con {:rollback-only true}] + (is (jdbc/active-tx?) "should be in a transaction") + (is (jdbc/active-tx? t) "connection should be in a transaction") + (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "])))) - (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) - (is (= ac (.getAutoCommit con)))))) - (testing "with-transaction exception" - (is (thrown? Throwable - (jdbc/with-transaction [t (ds)] - (jdbc/execute! t [" + (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) + (is (= ac (.getAutoCommit con)))))) + (testing "with-transaction exception" + (is (thrown? Throwable + (jdbc/with-transaction [t (ds)] + (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "]) - (is (jdbc/active-tx?) "should be in a transaction") - (is (jdbc/active-tx? t) "connection should be in a transaction") - (throw (ex-info "abort" {}))))) - (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) - (is (not (jdbc/active-tx?)) "should not be in a transaction") - (with-open [con (jdbc/get-connection (ds))] - (let [ac (.getAutoCommit con)] - (is (thrown? Throwable - (jdbc/with-transaction [t con] - (jdbc/execute! t [" + (is (jdbc/active-tx?) "should be in a transaction") + (is (jdbc/active-tx? t) "connection should be in a transaction") + (throw (ex-info "abort" {}))))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) + (is (not (jdbc/active-tx?)) "should not be in a transaction") + (with-open [con (jdbc/get-connection (ds))] + (let [ac (.getAutoCommit con)] + (is (thrown? Throwable + (jdbc/with-transaction [t con] + (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "]) - (is (jdbc/active-tx?) "should be in a transaction") - (is (jdbc/active-tx? t) "connection should be in a transaction") - (throw (ex-info "abort" {}))))) - (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) - (is (= ac (.getAutoCommit con)))))) - (testing "with-transaction call rollback" - (is (= [{:next.jdbc/update-count 1}] - (jdbc/with-transaction [t (ds)] - (let [result (jdbc/execute! t [" + (is (jdbc/active-tx?) "should be in a transaction") + (is (jdbc/active-tx? t) "connection should be in a transaction") + (throw (ex-info "abort" {}))))) + (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) + (is (= ac (.getAutoCommit con)))))) + (testing "with-transaction call rollback" + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t (ds)] + (let [result (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "])] - (.rollback t) + (.rollback t) ;; still in a next.jdbc TX even tho' we rolled back! - (is (jdbc/active-tx?) "should be in a transaction") - (is (jdbc/active-tx? t) "connection should be in a transaction") - result)))) - (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) - (is (not (jdbc/active-tx?)) "should not be in a transaction") - (with-open [con (jdbc/get-connection (ds))] - (let [ac (.getAutoCommit con)] - (is (= [{:next.jdbc/update-count 1}] - (jdbc/with-transaction [t con] - (let [result (jdbc/execute! t [" + (is (jdbc/active-tx?) "should be in a transaction") + (is (jdbc/active-tx? t) "connection should be in a transaction") + result)))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) + (is (not (jdbc/active-tx?)) "should not be in a transaction") + (with-open [con (jdbc/get-connection (ds))] + (let [ac (.getAutoCommit con)] + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t con] + (let [result (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "])] - (.rollback t) - result)))) - (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) - (is (= ac (.getAutoCommit con)))))) - (testing "with-transaction with unnamed save point" - (is (= [{:next.jdbc/update-count 1}] - (jdbc/with-transaction [t (ds)] - (let [save-point (.setSavepoint t) - result (jdbc/execute! t [" + (.rollback t) + result)))) + (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) + (is (= ac (.getAutoCommit con)))))) + (testing "with-transaction with unnamed save point" + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t (ds)] + (let [save-point (.setSavepoint t) + result (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "])] - (.rollback t save-point) + (.rollback t save-point) ;; still in a next.jdbc TX even tho' we rolled back to a save point! - (is (jdbc/active-tx?) "should be in a transaction") - (is (jdbc/active-tx? t) "connection should be in a transaction") - result)))) - (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) - (is (not (jdbc/active-tx?)) "should not be in a transaction") - (with-open [con (jdbc/get-connection (ds))] - (let [ac (.getAutoCommit con)] - (is (= [{:next.jdbc/update-count 1}] - (jdbc/with-transaction [t con] - (let [save-point (.setSavepoint t) - result (jdbc/execute! t [" + (is (jdbc/active-tx?) "should be in a transaction") + (is (jdbc/active-tx? t) "connection should be in a transaction") + result)))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) + (is (not (jdbc/active-tx?)) "should not be in a transaction") + (with-open [con (jdbc/get-connection (ds))] + (let [ac (.getAutoCommit con)] + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t con] + (let [save-point (.setSavepoint t) + result (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "])] - (.rollback t save-point) - result)))) - (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) - (is (= ac (.getAutoCommit con)))))) - (testing "with-transaction with named save point" - (is (= [{:next.jdbc/update-count 1}] - (jdbc/with-transaction [t (ds)] - (let [save-point (.setSavepoint t (name (gensym))) - result (jdbc/execute! t [" + (.rollback t save-point) + result)))) + (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) + (is (= ac (.getAutoCommit con)))))) + (testing "with-transaction with named save point" + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t (ds)] + (let [save-point (.setSavepoint t (name (gensym))) + result (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "])] - (.rollback t save-point) - result)))) - (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) - (with-open [con (jdbc/get-connection (ds))] - (let [ac (.getAutoCommit con)] - (is (= [{:next.jdbc/update-count 1}] - (jdbc/with-transaction [t con] - (let [save-point (.setSavepoint t (name (gensym))) - result (jdbc/execute! t [" + (.rollback t save-point) + result)))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))) + (with-open [con (jdbc/get-connection (ds))] + (let [ac (.getAutoCommit con)] + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t con] + (let [save-point (.setSavepoint t (name (gensym))) + result (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "])] - (.rollback t save-point) - result)))) - (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) - (is (= ac (.getAutoCommit con))))))) + (.rollback t save-point) + result)))) + (is (= 4 (count (jdbc/execute! con ["select * from fruit"])))) + (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,117 +502,100 @@ VALUES ('Pear', 'green', 49, 47) "\n\t" (ex-message t))))) (deftest bool-tests - (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)]] - (jdbc/execute-one! - (ds) - ["insert into btest (name,is_it,twiddle) values (?,?,?)" - n - (if (postgres?) - (types/as-boolean b) - b) ; 0, 1, false, true are all acceptable - (cond (hsqldb?) - v-bool ; hsqldb requires a boolean here - (postgres?) - (types/as-other v-bit) ; really postgres?? - :else - v-bit)])) - (let [data (jdbc/execute! (ds) ["select * from btest"] - (default-options))] - (if (sqlite?) - (is (every? number? (map (column :BTEST/IS_IT) data))) - (is (every? boolean? (map (column :BTEST/IS_IT) data)))) - (if (or (sqlite?) (derby?)) - (is (every? number? (map (column :BTEST/TWIDDLE) data))) - (is (every? boolean? (map (column :BTEST/TWIDDLE) data))))) - (let [data (jdbc/execute! (ds) ["select * from btest"] - (cond-> (default-options) - (sqlite?) - (assoc :builder-fn - (rs/builder-adapter - rs/as-maps - (fn [builder ^ResultSet rs ^Integer i] - (let [rsm ^ResultSetMetaData (:rsmeta builder)] - (rs/read-column-by-index + (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)]] + (jdbc/execute-one! + (ds) + ["insert into btest (name,is_it,twiddle) values (?,?,?)" + n + (if (postgres?) + (types/as-boolean b) + b) ; 0, 1, false, true are all acceptable + (cond (hsqldb?) + v-bool ; hsqldb requires a boolean here + (postgres?) + (types/as-other v-bit) ; really postgres?? + :else + v-bit)])) + (let [data (jdbc/execute! (ds) ["select * from btest"] + (default-options))] + (if (sqlite?) + (is (every? number? (map (column :BTEST/IS_IT) data))) + (is (every? boolean? (map (column :BTEST/IS_IT) data)))) + (if (or (sqlite?) (derby?)) + (is (every? number? (map (column :BTEST/TWIDDLE) data))) + (is (every? boolean? (map (column :BTEST/TWIDDLE) data))))) + (let [data (jdbc/execute! (ds) ["select * from btest"] + (cond-> (default-options) + (sqlite?) + (assoc :builder-fn + (rs/builder-adapter + rs/as-maps + (fn [builder ^ResultSet rs ^Integer i] + (let [rsm ^ResultSetMetaData (:rsmeta builder)] + (rs/read-column-by-index ;; we only use bit and bool for ;; sqlite (not boolean) - (if (#{"BIT" "BOOL"} (.getColumnTypeName rsm i)) - (.getBoolean rs i) - (.getObject rs i)) - rsm - i)))))))] - (is (every? boolean? (map (column :BTEST/IS_IT) data))) - (if (derby?) - (is (every? number? (map (column :BTEST/TWIDDLE) data))) - (is (every? boolean? (map (column :BTEST/TWIDDLE) data))))) - (let [data (reduce (fn [acc row] - (conj acc (cond-> (select-keys row [:is_it :twiddle]) - (sqlite?) - (update :is_it pos?) - (or (sqlite?) (derby?)) - (update :twiddle pos?)))) - [] - (jdbc/plan (ds) ["select * from btest"]))] - (is (every? boolean? (map :is_it data))) - (is (every? boolean? (map :twiddle data))))) + (if (#{"BIT" "BOOL"} (.getColumnTypeName rsm i)) + (.getBoolean rs i) + (.getObject rs i)) + rsm + i)))))))] + (is (every? boolean? (map (column :BTEST/IS_IT) data))) + (if (derby?) + (is (every? number? (map (column :BTEST/TWIDDLE) data))) + (is (every? boolean? (map (column :BTEST/TWIDDLE) data))))) + (let [data (reduce (fn [acc row] + (conj acc (cond-> (select-keys row [:is_it :twiddle]) + (sqlite?) + (update :is_it pos?) + (or (sqlite?) (derby?)) + (update :twiddle pos?)))) + [] + (jdbc/plan (ds) ["select * from btest"]))] + (is (every? boolean? (map :is_it data))) + (is (every? boolean? (map :twiddle data)))))) (deftest execute-batch-tests - (testing "simple 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 (jdbc/execute-batch! ps [["fruit1" "one"] - ["fruit2" "two"] - ["fruit3" "three"] - ["fruit4" "four"] - ["fruit5" "five"] - ["fruit6" "six"] - ["fruit7" "seven"] - ["fruit8" "eight"] - ["fruit9" "nine"]])] - (conj result (count (jdbc/execute! t ["select * from fruit"])))))))) - (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) - (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 [" + (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}] + (with-open [ps (jdbc/prepare t [" INSERT INTO fruit (name, appearance) VALUES (?,?) "])] - (let [result (jdbc/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 [" + (let [result (jdbc/execute-batch! ps [["fruit1" "one"] + ["fruit2" "two"] + ["fruit3" "three"] + ["fruit4" "four"] + ["fruit5" "five"] + ["fruit6" "six"] + ["fruit7" "seven"] + ["fruit8" "eight"] + ["fruit9" "nine"]])] + (conj result (count (jdbc/execute! t ["select * from fruit"])))))))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) + (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 (jdbc/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?)) + (let [result (jdbc/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 [" @@ -620,135 +610,59 @@ INSERT INTO fruit (name, appearance) VALUES (?,?) ["fruit7" "seven"] ["fruit8" "eight"] ["fruit9" "nine"]] - {:batch-size 4 - :large true})] + {:batch-size 8})] (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 [" + (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 (jdbc/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 (?,?) "] - {:return-keys true})] - (let [result (jdbc/execute-batch! ps [["fruit1" "one"] - ["fruit2" "two"] - ["fruit3" "three"] - ["fruit4" "four"] - ["fruit5" "five"] - ["fruit6" "six"] - ["fruit7" "seven"] - ["fruit8" "eight"] - ["fruit9" "nine"]] - {:batch-size 4 - :return-generated-keys true})] - (conj result (count (jdbc/execute! t ["select * from fruit"]))))))] - (is (= 13 (last results))) - (is (every? map? (butlast results))) + {:return-keys true})] + (let [result (jdbc/execute-batch! ps [["fruit1" "one"] + ["fruit2" "two"] + ["fruit3" "three"] + ["fruit4" "four"] + ["fruit5" "five"] + ["fruit6" "six"] + ["fruit7" "seven"] + ["fruit8" "eight"] + ["fruit9" "nine"]] + {:batch-size 4 + :return-generated-keys true})] + (conj result (count (jdbc/execute! t ["select * from fruit"]))))))] + (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 (< 3 (count results)))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))))) (deftest execute-batch-connectable-tests - (testing "simple batch insert" - (is (= [1 1 1 1 1 1 1 1 1 13] - (try - (let [result (jdbc/execute-batch! (ds) - "INSERT INTO fruit (name, appearance) VALUES (?,?)" - [["fruit1" "one"] - ["fruit2" "two"] - ["fruit3" "three"] - ["fruit4" "four"] - ["fruit5" "five"] - ["fruit6" "six"] - ["fruit7" "seven"] - ["fruit8" "eight"] - ["fruit9" "nine"]] - {})] - (conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) - (finally - (jdbc/execute-one! (ds) ["delete from fruit where id > 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] - (try - (let [result (jdbc/execute-batch! (jdbc/with-options (ds) {}) - "INSERT INTO fruit (name, appearance) VALUES (?,?)" - [["fruit1" "one"] - ["fruit2" "two"] - ["fruit3" "three"] - ["fruit4" "four"] - ["fruit5" "five"] - ["fruit6" "six"] - ["fruit7" "seven"] - ["fruit8" "eight"] - ["fruit9" "nine"]] - {})] - (conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) - (finally - (jdbc/execute-one! (ds) ["delete from fruit where id > 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] - (try - (let [result (jdbc/execute-batch! (jdbc/with-logging (ds) println println) - "INSERT INTO fruit (name, appearance) VALUES (?,?)" - [["fruit1" "one"] - ["fruit2" "two"] - ["fruit3" "three"] - ["fruit4" "four"] - ["fruit5" "five"] - ["fruit6" "six"] - ["fruit7" "seven"] - ["fruit8" "eight"] - ["fruit9" "nine"]] - {})] - (conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) - (finally - (jdbc/execute-one! (ds) ["delete from fruit where id > 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] - (try - (let [result (jdbc/execute-batch! (ds) - "INSERT INTO fruit (name, appearance) VALUES (?,?)" - [["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! (ds) ["select * from fruit"])))) - (finally - (jdbc/execute-one! (ds) ["delete from fruit where id > 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] - (try - (let [result (jdbc/execute-batch! (ds) - "INSERT INTO fruit (name, appearance) VALUES (?,?)" - [["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! (ds) ["select * from fruit"])))) - (finally - (jdbc/execute-one! (ds) ["delete from fruit where id > 4"]))))) - (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) - (testing "large batch insert" - (when-not (or (jtds?) (sqlite?)) + (when-not (xtdb?) + (testing "simple batch insert" (is (= [1 1 1 1 1 1 1 1 1 13] (try (let [result (jdbc/execute-batch! (ds) @@ -762,54 +676,154 @@ INSERT INTO fruit (name, appearance) VALUES (?,?) ["fruit7" "seven"] ["fruit8" "eight"] ["fruit9" "nine"]] - {:batch-size 4 - :large true})] + {})] (conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) (finally - (jdbc/execute-one! (ds) ["delete from fruit where id > 4"]))))) - (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))) - (testing "return generated keys" - (when-not (or (mssql?) (sqlite?)) - (let [results - (try - (let [result (jdbc/execute-batch! (ds) - "INSERT INTO fruit (name, appearance) VALUES (?,?)" - [["fruit1" "one"] - ["fruit2" "two"] - ["fruit3" "three"] - ["fruit4" "four"] - ["fruit5" "five"] - ["fruit6" "six"] - ["fruit7" "seven"] - ["fruit8" "eight"] - ["fruit9" "nine"]] + (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] + (try + (let [result (jdbc/execute-batch! (jdbc/with-options (ds) {}) + "INSERT INTO fruit (name, appearance) VALUES (?,?)" + [["fruit1" "one"] + ["fruit2" "two"] + ["fruit3" "three"] + ["fruit4" "four"] + ["fruit5" "five"] + ["fruit6" "six"] + ["fruit7" "seven"] + ["fruit8" "eight"] + ["fruit9" "nine"]] + {})] + (conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) + (finally + (jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")]))))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) + (testing "batch with-logging" + (is (= [1 1 1 1 1 1 1 1 1 13] + (try + (let [result (jdbc/execute-batch! (jdbc/with-logging (ds) println println) + "INSERT INTO fruit (name, appearance) VALUES (?,?)" + [["fruit1" "one"] + ["fruit2" "two"] + ["fruit3" "three"] + ["fruit4" "four"] + ["fruit5" "five"] + ["fruit6" "six"] + ["fruit7" "seven"] + ["fruit8" "eight"] + ["fruit9" "nine"]] + {})] + (conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) + (finally + (jdbc/execute-one! (ds) [(str "delete from fruit where " (index) " > 4")]))))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) + (testing "small batch insert" + (is (= [1 1 1 1 1 1 1 1 1 13] + (try + (let [result (jdbc/execute-batch! (ds) + "INSERT INTO fruit (name, appearance) VALUES (?,?)" + [["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! (ds) ["select * from fruit"])))) + (finally + (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] + (try + (let [result (jdbc/execute-batch! (ds) + "INSERT INTO fruit (name, appearance) VALUES (?,?)" + [["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! (ds) ["select * from fruit"])))) + (finally + (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?)) + (is (= [1 1 1 1 1 1 1 1 1 13] + (try + (let [result (jdbc/execute-batch! (ds) + "INSERT INTO fruit (name, appearance) VALUES (?,?)" + [["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! (ds) ["select * from fruit"])))) + (finally + (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?)) + (let [results + (try + (let [result (jdbc/execute-batch! (ds) + "INSERT INTO fruit (name, appearance) VALUES (?,?)" + [["fruit1" "one"] + ["fruit2" "two"] + ["fruit3" "three"] + ["fruit4" "four"] + ["fruit5" "five"] + ["fruit6" "six"] + ["fruit7" "seven"] + ["fruit8" "eight"] + ["fruit9" "nine"]] ;; note: we need both :return-keys true for creating ;; the PreparedStatement and :return-generated-keys ;; true to control the way batch execution happens: - {:batch-size 4 :return-keys true - :return-generated-keys true})] - (conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) - (finally - (jdbc/execute-one! (ds) ["delete from fruit where id > 4"])))] - (is (= 13 (last results))) - (is (every? map? (butlast results))) + {:batch-size 4 :return-keys true + :return-generated-keys true})] + (conj result (count (jdbc/execute! (ds) ["select * from fruit"])))) + (finally + (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 (< 3 (count results)))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))))) (deftest folding-test (jdbc/execute-one! (ds) ["delete from fruit"]) - (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)))) + (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))))) (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 +835,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 +846,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 +860,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 +868,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 +951,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