diff --git a/deps.edn b/deps.edn index 2ebbb6c7..24e636ae 100644 --- a/deps.edn +++ b/deps.edn @@ -71,6 +71,7 @@ aero/aero {:mvn/version "1.1.6"} org.clojure/data.generators {:mvn/version "1.0.0"} honeysql/honeysql {:mvn/version "1.0.444"} + com.github.seancorfield/honeysql {:mvn/version "2.0.0-rc2"} minimallist/minimallist {:mvn/version "0.0.6"} circleci/bond {:mvn/version "0.4.0"} version-clj/version-clj {:mvn/version "2.0.1"} diff --git a/test-resources/lib_tests/babashka/run_all_libtests.clj b/test-resources/lib_tests/babashka/run_all_libtests.clj index 5068837c..cc4e3c70 100644 --- a/test-resources/lib_tests/babashka/run_all_libtests.clj +++ b/test-resources/lib_tests/babashka/run_all_libtests.clj @@ -208,6 +208,10 @@ 'jasentaa.parser.basic-test 'jasentaa.parser.combinators-test) +(test-namespaces 'honey.sql-test + 'honey.sql.helpers-test + 'honey.sql.postgres-test) + ;;;; final exit code (let [{:keys [:test :fail :error] :as m} @status] diff --git a/test-resources/lib_tests/honey/sql/helpers_test.cljc b/test-resources/lib_tests/honey/sql/helpers_test.cljc new file mode 100644 index 00000000..33ea68c5 --- /dev/null +++ b/test-resources/lib_tests/honey/sql/helpers_test.cljc @@ -0,0 +1,868 @@ +;; copyright (c) 2020-2021 sean corfield, all rights reserved + +(ns honey.sql.helpers-test + (:refer-clojure :exclude [filter for group-by partition-by set update]) + (:require #?(:clj [clojure.test :refer [deftest is testing]] + :cljs [cljs.test :refer-macros [deftest is testing]]) + [honey.sql :as sql] + [honey.sql.helpers :as h + :refer [add-column add-index alter-table columns create-table create-table-as create-view + create-materialized-view drop-view drop-materialized-view + bulk-collect-into + cross-join do-update-set drop-column drop-index drop-table + filter from full-join + group-by having insert-into + join-by join lateral left-join limit offset on-conflict + on-duplicate-key-update + order-by over partition-by refresh-materialized-view + rename-column rename-table returning right-join + select select-distinct select-top select-distinct-top + values where window with with-columns + with-data within-group]])) + +(deftest test-select + (testing "large helper expression" + (let [m1 (-> (with [:cte (-> (select :*) + (from :example) + (where [:= :example-column 0]))]) + (select-distinct :f.* :b.baz :c.quux [:b.bla "bla-bla"] + :%now [[:raw "@x := 10"]]) + (from [:foo :f] [:baz :b]) + (join :draq [:= :f.b :draq.x]) + (left-join [:clod :c] [:= :f.a :c.d]) + (right-join :bock [:= :bock.z :c.e]) + (full-join :beck [:= :beck.x :c.y]) + (where [:or + [:and [:= :f.a "bort"] [:not= :b.baz :?param1]] + [:and [:< 1 2] [:< 2 3]] + [:in :f.e [1 [:param :param2] 3]] + [:between :f.e 10 20]]) + (group-by :f.a) + (having [:< 0 :f.e]) + (order-by [:b.baz :desc] :c.quux [:f.a :nulls-first]) + (limit 50) + (offset 10)) + m2 {:with [[:cte {:select [:*] + :from [:example] + :where [:= :example-column 0]}]] + :select-distinct [:f.* :b.baz :c.quux [:b.bla "bla-bla"] + :%now [[:raw "@x := 10"]]] + :from [[:foo :f] [:baz :b]] + :join [:draq [:= :f.b :draq.x]] + :left-join [[:clod :c] [:= :f.a :c.d]] + :right-join [:bock [:= :bock.z :c.e]] + :full-join [:beck [:= :beck.x :c.y]] + :where [:or + [:and [:= :f.a "bort"] [:not= :b.baz :?param1]] + [:and [:< 1 2] [:< 2 3]] + [:in :f.e [1 [:param :param2] 3]] + [:between :f.e 10 20]] + :group-by [:f.a] + :having [:< 0 :f.e] + :order-by [[:b.baz :desc] :c.quux [:f.a :nulls-first]] + :limit 50 + :offset 10}] + (testing "Various construction methods are consistent" + (is (= m1 m2))) + (testing "SQL data formats correctly" + (is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ?" + 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10] + (sql/format m1 {:params {:param1 "gabba" :param2 2}})))) + #?(:clj (testing "SQL data prints and reads correctly" + (is (= m1 (read-string (pr-str m1)))))) + #_(testing "SQL data formats correctly with alternate param naming" + (is (= (sql/format m1 {:params {:param1 "gabba" :param2 2}}) + ["WITH cte AS (SELECT * FROM example WHERE example_column = $1) SELECT DISTINCT f.*, b.baz, c.quux, b.bla \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = $2) AND (b.baz <> $3)) OR (($4 < $5) AND ($6 < $7)) OR (f.e IN ($8, $9, $10)) OR f.e BETWEEN $11 AND $12 GROUP BY f.a HAVING $13 < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT $14 OFFSET $15" + 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]))) + (testing "Locking" + (is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS `bla-bla`, NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ? LOCK IN SHARE MODE" + 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10] + (sql/format (assoc m1 :lock [:in-share-mode]) + {:params {:param1 "gabba" :param2 2} + ;; to enable :lock + :dialect :mysql :quoted false})))))) + (testing "large helper expression with simplified where" + (let [m1 (-> (with [:cte (-> (select :*) + (from :example) + (where := :example-column 0))]) + (select-distinct :f.* :b.baz :c.quux [:b.bla "bla-bla"] + :%now [[:raw "@x := 10"]]) + (from [:foo :f] [:baz :b]) + (join :draq [:= :f.b :draq.x]) + (left-join [:clod :c] [:= :f.a :c.d]) + (right-join :bock [:= :bock.z :c.e]) + (full-join :beck [:= :beck.x :c.y]) + (where :or + [:and [:= :f.a "bort"] [:not= :b.baz :?param1]] + [:and [:< 1 2] [:< 2 3]] + [:in :f.e [1 [:param :param2] 3]] + [:between :f.e 10 20]) + (group-by :f.a) + (having :< 0 :f.e) + (order-by [:b.baz :desc] :c.quux [:f.a :nulls-first]) + (limit 50) + (offset 10)) + m2 {:with [[:cte {:select [:*] + :from [:example] + :where [:= :example-column 0]}]] + :select-distinct [:f.* :b.baz :c.quux [:b.bla "bla-bla"] + :%now [[:raw "@x := 10"]]] + :from [[:foo :f] [:baz :b]] + :join [:draq [:= :f.b :draq.x]] + :left-join [[:clod :c] [:= :f.a :c.d]] + :right-join [:bock [:= :bock.z :c.e]] + :full-join [:beck [:= :beck.x :c.y]] + :where [:or + [:and [:= :f.a "bort"] [:not= :b.baz :?param1]] + [:and [:< 1 2] [:< 2 3]] + [:in :f.e [1 [:param :param2] 3]] + [:between :f.e 10 20]] + :group-by [:f.a] + :having [:< 0 :f.e] + :order-by [[:b.baz :desc] :c.quux [:f.a :nulls-first]] + :limit 50 + :offset 10}] + (testing "Various construction methods are consistent" + (is (= m1 m2))) + (testing "SQL data formats correctly" + (is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ?" + 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10] + (sql/format m1 {:params {:param1 "gabba" :param2 2}})))) + #?(:clj (testing "SQL data prints and reads correctly" + (is (= m1 (read-string (pr-str m1)))))) + #_(testing "SQL data formats correctly with alternate param naming" + (is (= (sql/format m1 {:params {:param1 "gabba" :param2 2}}) + ["WITH cte AS (SELECT * FROM example WHERE example_column = $1) SELECT DISTINCT f.*, b.baz, c.quux, b.bla \"bla-bla\", NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = $2) AND (b.baz <> $3)) OR (($4 < $5) AND ($6 < $7)) OR (f.e IN ($8, $9, $10)) OR f.e BETWEEN $11 AND $12 GROUP BY f.a HAVING $13 < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT $14 OFFSET $15" + 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10]))) + (testing "Locking" + (is (= ["WITH cte AS (SELECT * FROM example WHERE example_column = ?) SELECT DISTINCT f.*, b.baz, c.quux, b.bla AS `bla-bla`, NOW(), @x := 10 FROM foo AS f, baz AS b INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y WHERE ((f.a = ?) AND (b.baz <> ?)) OR ((? < ?) AND (? < ?)) OR (f.e IN (?, ?, ?)) OR f.e BETWEEN ? AND ? GROUP BY f.a HAVING ? < f.e ORDER BY b.baz DESC, c.quux ASC, f.a NULLS FIRST LIMIT ? OFFSET ? LOCK IN SHARE MODE" + 0 "bort" "gabba" 1 2 2 3 1 2 3 10 20 0 50 10] + (sql/format (assoc m1 :lock [:in-share-mode]) + {:params {:param1 "gabba" :param2 2} + ;; to enable :lock + :dialect :mysql :quoted false}))))))) + +(deftest select-top-tests + (testing "Basic TOP syntax" + (is (= ["SELECT TOP(?) foo FROM bar ORDER BY quux ASC" 10] + (sql/format {:select-top [10 :foo] :from :bar :order-by [:quux]}))) + (is (= ["SELECT TOP(?) foo FROM bar ORDER BY quux ASC" 10] + (sql/format (-> (select-top 10 :foo) + (from :bar) + (order-by :quux)))))) + (testing "Expanded TOP syntax" + (is (= ["SELECT TOP(?) PERCENT WITH TIES foo, baz FROM bar ORDER BY quux ASC" 10] + (sql/format {:select-top [[10 :percent :with-ties] :foo :baz] :from :bar :order-by [:quux]}))) + (is (= ["SELECT TOP(?) PERCENT WITH TIES foo, baz FROM bar ORDER BY quux ASC" 10] + (sql/format (-> (select-top [10 :percent :with-ties] :foo :baz) + (from :bar) + (order-by :quux))))))) + +(deftest select-into-tests + (testing "SELECT INTO" + (is (= ["SELECT * INTO foo FROM bar"] + (sql/format {:select :* :into :foo :from :bar}))) + (is (= ["SELECT * INTO foo IN otherdb FROM bar"] + (sql/format {:select :* :into [:foo :otherdb] :from :bar}))) + (is (= ["SELECT * INTO foo FROM bar"] + (sql/format (-> (select '*) (h/into 'foo) (from 'bar))))) + (is (= ["SELECT * INTO foo IN otherdb FROM bar"] + (sql/format (-> (select :*) (h/into :foo :otherdb) (from :bar)))))) + (testing "SELECT BULK COLLECT INTO" + (is (= ["SELECT * BULK COLLECT INTO foo FROM bar"] + (sql/format {:select :* :bulk-collect-into :foo :from :bar}))) + (is (= ["SELECT * BULK COLLECT INTO foo LIMIT ? FROM bar" 100] + (sql/format {:select :* :bulk-collect-into [:foo 100] :from :bar}))) + (is (= ["SELECT * BULK COLLECT INTO foo FROM bar"] + (sql/format (-> (select :*) (bulk-collect-into :foo) (from :bar))))) + (is (= ["SELECT * BULK COLLECT INTO foo LIMIT ? FROM bar" 100] + (sql/format (-> (select :*) (bulk-collect-into :foo 100) (from :bar))))))) + +(deftest from-expression-tests + (testing "FROM can be a function invocation" + (is (= ["SELECT foo, bar FROM F(?) AS x" 1] + (sql/format {:select [:foo :bar] :from [[[:f 1] :x]]})))) + ;; these two examples are from https://www.postgresql.org/docs/9.3/queries-table-expressions.html#QUERIES-LATERAL + (testing "FROM can be a LATERAL select" + (is (= ["SELECT * FROM foo, LATERAL (SELECT * FROM bar WHERE bar.id = foo.bar_id) AS ss"] + (sql/format {:select :* + :from [:foo + [[:lateral {:select :* + :from :bar + :where [:= :bar.id :foo.bar_id]}] :ss]]})))) + (testing "FROM can be a LATERAL expression" + (is (= [(str "SELECT p1.id, p2.id, v1, v2" + " FROM polygons AS p1, polygons AS p2," + " LATERAL VERTICES(p1.poly) AS v1," + " LATERAL VERTICES(p2.poly) AS v2" + " WHERE ((v1 <-> v2) < ?) AND (p1.id <> p2.id)") 10] + (sql/format {:select [:p1.id :p2.id :v1 :v2] + :from [[:polygons :p1] [:polygons :p2] + [[:lateral [:vertices :p1.poly]] :v1] + [[:lateral [:vertices :p2.poly]] :v2]] + :where [:and [:< [:<-> :v1 :v2] 10] [:!= :p1.id :p2.id]]}))) + (is (= [(str "SELECT m.name" + " FROM manufacturers AS m" + " LEFT JOIN LATERAL GET_PRODUCT_NAMES(m.id) AS pname ON TRUE" + " WHERE pname IS NULL")] + (sql/format {:select :m.name + :from [[:manufacturers :m]] + :left-join [[[:lateral [:get_product_names :m.id]] :pname] true] + :where [:= :pname nil]}))))) + +(deftest join-by-test + (testing "Natural JOIN orders" + (is (= ["SELECT * FROM foo INNER JOIN draq ON f.b = draq.x LEFT JOIN clod AS c ON f.a = c.d RIGHT JOIN bock ON bock.z = c.e FULL JOIN beck ON beck.x = c.y"] + (sql/format {:select [:*] :from [:foo] + :full-join [:beck [:= :beck.x :c.y]] + :right-join [:bock [:= :bock.z :c.e]] + :left-join [[:clod :c] [:= :f.a :c.d]] + :join [:draq [:= :f.b :draq.x]]})))) + (testing "Specific JOIN orders" + (is (= ["SELECT * FROM foo FULL JOIN beck ON beck.x = c.y RIGHT JOIN bock ON bock.z = c.e LEFT JOIN clod AS c ON f.a = c.d INNER JOIN draq ON f.b = draq.x"] + (sql/format {:select [:*] :from [:foo] + :join-by [:full [:beck [:= :beck.x :c.y]] + :right [:bock [:= :bock.z :c.e]] + :left [[:clod :c] [:= :f.a :c.d]] + :join [:draq [:= :f.b :draq.x]]]}))) + (is (= ["SELECT * FROM foo FULL JOIN beck ON beck.x = c.y RIGHT JOIN bock ON bock.z = c.e LEFT JOIN clod AS c ON f.a = c.d INNER JOIN draq ON f.b = draq.x"] + (-> (select :*) + (from :foo) + (join-by :full-join [:beck [:= :beck.x :c.y]] + :right-join [:bock [:= :bock.z :c.e]] + :left-join [[:clod :c] [:= :f.a :c.d]] + :inner-join [:draq [:= :f.b :draq.x]]) + (sql/format))))) + (testing "Specific JOIN orders with join clauses" + (is (= ["SELECT * FROM foo FULL JOIN beck ON beck.x = c.y RIGHT JOIN bock ON bock.z = c.e LEFT JOIN clod AS c ON f.a = c.d INNER JOIN draq ON f.b = draq.x"] + (sql/format {:select [:*] :from [:foo] + :join-by [{:full-join [:beck [:= :beck.x :c.y]]} + {:right-join [:bock [:= :bock.z :c.e]]} + {:left-join [[:clod :c] [:= :f.a :c.d]]} + {:join [:draq [:= :f.b :draq.x]]}]}))) + (is (= ["SELECT * FROM foo FULL JOIN beck ON beck.x = c.y RIGHT JOIN bock ON bock.z = c.e LEFT JOIN clod AS c ON f.a = c.d INNER JOIN draq ON f.b = draq.x"] + (-> (select :*) + (from :foo) + (join-by (full-join :beck [:= :beck.x :c.y]) + (right-join :bock [:= :bock.z :c.e]) + (left-join [:clod :c] [:= :f.a :c.d]) + (join :draq [:= :f.b :draq.x])) + (sql/format)))))) + +(deftest test-cast + (is (= ["SELECT foo, CAST(bar AS integer)"] + (sql/format {:select [:foo [[:cast :bar :integer]]]}))) + (is (= ["SELECT foo, CAST(bar AS integer)"] + (sql/format {:select [:foo [[:cast :bar 'integer]]]})))) + +(deftest test-value + (is (= ["INSERT INTO foo (bar) VALUES (?)" {:baz "my-val"}] + (-> + (insert-into :foo) + (columns :bar) + (values [[[:lift {:baz "my-val"}]]]) + sql/format))) + (is (= ["INSERT INTO foo (a, b, c) VALUES (?, ?, ?), (?, ?, ?)" + "a" "b" "c" "a" "b" "c"] + (-> (insert-into :foo) + (values [(array-map :a "a" :b "b" :c "c") + (hash-map :a "a" :b "b" :c "c")]) + sql/format)))) + +(deftest test-operators + (testing "=" + (testing "with nil" + (is (= ["SELECT * FROM customers WHERE name IS NULL"] + (sql/format {:select [:*] + :from [:customers] + :where [:= :name nil]}))) + (is (= ["SELECT * FROM customers WHERE name = ?" nil] + (sql/format {:select [:*] + :from [:customers] + :where [:= :name :?name]} + {:params {:name nil}}))))) + (testing "in" + (doseq [[cname coll] [[:vector []] [:set #{}] [:list '()]]] + (testing (str "with values from a " (name cname)) + (let [values (conj coll 1)] + (is (= ["SELECT * FROM customers WHERE id IN (?)" 1] + (sql/format {:select [:*] + :from [:customers] + :where [:in :id values]}))) + (is (= ["SELECT * FROM customers WHERE id IN (?)" 1] + (sql/format {:select [:*] + :from [:customers] + :where [:in :id :?ids]} + {:params {:ids values}})))))) + (testing "with more than one integer" + (let [values [1 2]] + (is (= ["SELECT * FROM customers WHERE id IN (?, ?)" 1 2] + (sql/format {:select [:*] + :from [:customers] + :where [:in :id values]}))) + (is (= ["SELECT * FROM customers WHERE id IN (?, ?)" 1 2] + (sql/format {:select [:*] + :from [:customers] + :where [:in :id :?ids]} + {:params {:ids values}}))))) + (testing "with more than one string" + (let [values ["1" "2"]] + (is (= ["SELECT * FROM customers WHERE id IN (?, ?)" "1" "2"] + (sql/format {:select [:*] + :from [:customers] + :where [:in :id values]}) + (sql/format {:select [:*] + :from [:customers] + :where [:in :id :?ids]} + {:params {:ids values}}))))))) + +(deftest test-case + (is (= ["SELECT CASE WHEN foo < ? THEN ? WHEN (foo > ?) AND ((foo MOD ?) = ?) THEN foo / ? ELSE ? END FROM bar" + 0 -1 0 2 0 2 0] + (sql/format + {:select [[[:case + [:< :foo 0] -1 + [:and [:> :foo 0] [:= [:mod :foo 2] 0]] [:/ :foo 2] + :else 0]]] + :from [:bar]}))) + (let [param1 1 + param2 2 + param3 "three"] + (is (= ["SELECT CASE WHEN foo = ? THEN ? WHEN foo = bar THEN ? WHEN bar = ? THEN bar * ? ELSE ? END FROM baz" + param1 0 param2 0 param3 "param4"] + (sql/format + {:select [[[:case + [:= :foo :?param1] 0 + [:= :foo :bar] [:param :param2] + [:= :bar 0] [:* :bar :?param3] + :else "param4"]]] + :from [:baz]} + {:params + {:param1 param1 + :param2 param2 + :param3 param3}}))))) + +(deftest test-raw + (is (= ["SELECT 1 + 1 FROM foo"] + (-> (select [[:raw "1 + 1"]]) + (from :foo) + sql/format)))) + +(deftest test-call + (is (= ["SELECT MIN(?) FROM ?" "time" "table"] + (-> (select [[:min "time"]]) + (from "table") + sql/format)))) + +(deftest join-test + (testing "nil join" + (is (= ["SELECT * FROM foo INNER JOIN x ON foo.id = x.id INNER JOIN y"] + (-> (select :*) + (from :foo) + (join :x [:= :foo.id :x.id] :y nil) + sql/format))))) + +(deftest join-using-test + (testing "nil join" + (is (= ["SELECT * FROM foo INNER JOIN x USING (id) INNER JOIN y USING (foo, bar)"] + (-> (select :*) + (from :foo) + (join :x [:using :id] :y [:using :foo :bar]) + sql/format))))) + +(deftest inline-test + (is (= ["SELECT * FROM foo WHERE id = 5"] + (-> (select :*) + (from :foo) + (where [:= :id [:inline 5]]) + sql/format))) + ;; testing for = NULL always fails in SQL -- this test is just to show + ;; that an #inline nil should render as NULL (so make sure you only use + ;; it in contexts where a literal NULL is acceptable!) + (is (= ["SELECT * FROM foo WHERE id = NULL"] + (-> (select :*) + (from :foo) + (where [:= :id [:inline nil]]) + sql/format)))) + +(deftest where-no-params-test + (testing "where called with just the map as parameter - see #228" + (let [sqlmap (-> (select :*) + (from :table) + (where [:= :foo :bar]))] + (is (= ["SELECT * FROM table WHERE foo = bar"] + (sql/format (apply merge sqlmap []))))))) + +(deftest where-test + (is (= ["SELECT * FROM table WHERE (foo = bar) AND (quuz = xyzzy)"] + (-> (select :*) + (from :table) + (where [:= :foo :bar] [:= :quuz :xyzzy]) + sql/format))) + (is (= ["SELECT * FROM table WHERE (foo = bar) AND (quuz = xyzzy)"] + (-> (select :*) + (from :table) + (where [:= :foo :bar]) + (where [:= :quuz :xyzzy]) + sql/format)))) + +(deftest where-nil-params-test + (testing "where called with nil parameters - see #246" + (is (= ["SELECT * FROM table WHERE (foo = bar) AND (quuz = xyzzy)"] + (-> (select :*) + (from :table) + (where nil [:= :foo :bar] nil [:= :quuz :xyzzy] nil) + sql/format))) + (is (= ["SELECT * FROM table"] + (-> (select :*) + (from :table) + (where) + sql/format))) + (is (= ["SELECT * FROM table"] + (-> (select :*) + (from :table) + (where nil nil nil nil) + sql/format))))) + +(deftest cross-join-test + (is (= ["SELECT * FROM foo CROSS JOIN bar"] + (-> (select :*) + (from :foo) + (cross-join :bar) + sql/format))) + (is (= ["SELECT * FROM foo AS f CROSS JOIN bar b"] + (-> (select :*) + (from [:foo :f]) + (cross-join [:bar :b]) + sql/format)))) + +(defn- stack-overflow-282 [num-ids] + (let [ids (range num-ids)] + (sql/format (reduce + where + {:select [[:id :id]] + :from [:collection] + :where [:= :personal_owner_id nil]} + (clojure.core/for [id ids] + [:not-like :location [:raw (str "'/" id "/%'")]]))))) + +(deftest issue-282 + (is (= [(str "SELECT id AS id FROM collection" + " WHERE (personal_owner_id IS NULL)" + " AND (location NOT LIKE '/0/%')" + " AND (location NOT LIKE '/1/%')")] + (stack-overflow-282 2)))) + +(deftest issue-293-sql + ;; these tests are based on the README at https://github.com/nilenso/honeysql-postgres + (is (= (-> (insert-into :distributors) + (values [{:did 5 :dname "Gizmo Transglobal"} + {:did 6 :dname "Associated Computing, Inc"}]) + (-> (on-conflict :did) + (do-update-set :dname)) + (returning :*) + sql/format) + [(str "INSERT INTO distributors (did, dname)" + " VALUES (?, ?), (?, ?)" + " ON CONFLICT (did)" + " DO UPDATE SET dname = EXCLUDED.dname" + " RETURNING *") + 5 "Gizmo Transglobal" + 6 "Associated Computing, Inc"])) + (is (= (-> (insert-into :distributors) + (values [{:did 23 :dname "Foo Distributors"}]) + (on-conflict :did) + ;; instead of do-update-set! + (do-update-set {:dname [:|| :EXCLUDED.dname " (formerly " :distributors.dname ")"] + :downer :EXCLUDED.downer}) + sql/format) + [(str "INSERT INTO distributors (did, dname)" + " VALUES (?, ?)" + " ON CONFLICT (did)" + " DO UPDATE SET dname = EXCLUDED.dname || ? || distributors.dname || ?," + " downer = EXCLUDED.downer") + 23 "Foo Distributors" " (formerly " ")"])) + ;; insert into / insert into as tests are below + (is (= (-> (select :id + (over [[:avg :salary] (-> (partition-by :department) (order-by :designation)) :Average] + [[:max :salary] :w :MaxSalary])) + (from :employee) + (window :w (partition-by :department)) + sql/format) + [(str "SELECT id," + " AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average," + " MAX(salary) OVER w AS MaxSalary" + " FROM employee" + " WINDOW w AS (PARTITION BY department)")])) + ;; test nil / empty window function clause: + (is (= (-> (select :id + (over [[:avg :salary] {} :Average] + [[:max :salary] nil :MaxSalary])) + (from :employee) + sql/format) + [(str "SELECT id," + " AVG(salary) OVER () AS Average," + " MAX(salary) OVER () AS MaxSalary" + " FROM employee")]))) + +(deftest issue-293-basic-ddl + (is (= (sql/format {:create-view :metro :select [:*] :from [:cities] :where [:= :metroflag "y"]}) + ["CREATE VIEW metro AS SELECT * FROM cities WHERE metroflag = ?" "y"])) + (is (= (sql/format {:create-table :films + :with-columns [[:id :int :unsigned :auto-increment] + [:name [:varchar 50] [:not nil]]]}) + ["CREATE TABLE films (id INT UNSIGNED AUTO_INCREMENT, name VARCHAR(50) NOT NULL)"])) + (is (= (sql/format (-> (create-view :metro) + (select :*) + (from :cities) + (where [:= :metroflag "y"]))) + ["CREATE VIEW metro AS SELECT * FROM cities WHERE metroflag = ?" "y"])) + (is (= (sql/format (-> (create-table-as :metro :if-not-exists) + (select :*) + (from :cities) + (where [:= :metroflag "y"]) + (with-data false))) + ["CREATE TABLE IF NOT EXISTS metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"])) + (is (= (sql/format (-> (create-materialized-view :metro :if-not-exists) + (select :*) + (from :cities) + (where [:= :metroflag "y"]) + (with-data false))) + ["CREATE MATERIALIZED VIEW IF NOT EXISTS metro AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA" "y"])) + (is (= (sql/format (-> (create-table-as :metro :if-not-exists + (columns :foo :bar :baz) + [:tablespace [:entity :quux]]) + (select :*) + (from :cities) + (where [:= :metroflag "y"]) + (with-data false))) + [(str "CREATE TABLE IF NOT EXISTS metro" + " (foo, bar, baz) TABLESPACE quux" + " AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA") "y"])) + (is (= (sql/format (-> (create-materialized-view :metro :if-not-exists + (columns :foo :bar :baz) + [:tablespace [:entity :quux]]) + (select :*) + (from :cities) + (where [:= :metroflag "y"]) + (with-data false))) + [(str "CREATE MATERIALIZED VIEW IF NOT EXISTS metro" + " (foo, bar, baz) TABLESPACE quux" + " AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA") "y"])) + (is (= (sql/format {:create-materialized-view [:metro :if-not-exists] + :select [:*] + :from :cities + :where [:= :metroflag "y"] + :with-data true}) + ["CREATE MATERIALIZED VIEW IF NOT EXISTS metro AS SELECT * FROM cities WHERE metroflag = ? WITH DATA" "y"])) + (is (= (sql/format {:create-materialized-view [:metro :if-not-exists + (columns :foo :bar :baz) + [:tablespace [:entity :quux]]] + :select [:*] + :from :cities + :where [:= :metroflag "y"] + :with-data false}) + [(str "CREATE MATERIALIZED VIEW IF NOT EXISTS metro" + " (foo, bar, baz) TABLESPACE quux" + " AS SELECT * FROM cities WHERE metroflag = ? WITH NO DATA") "y"])) + (is (= (sql/format (-> (create-table :films) + (with-columns + [:id :int :unsigned :auto-increment] + [:name [:varchar 50] [:not nil]]))) + ["CREATE TABLE films (id INT UNSIGNED AUTO_INCREMENT, name VARCHAR(50) NOT NULL)"])) + (is (= (sql/format (-> (create-table :films :if-not-exists) + (with-columns + [:id :int :unsigned :auto-increment] + [:name [:varchar 50] [:not nil]]))) + ["CREATE TABLE IF NOT EXISTS films (id INT UNSIGNED AUTO_INCREMENT, name VARCHAR(50) NOT NULL)"])) + (is (= (sql/format (-> {:create-table :films + :with-columns + [[:id :int :unsigned :auto-increment] + [:name [:varchar 50] [:not nil]]]})) + ["CREATE TABLE films (id INT UNSIGNED AUTO_INCREMENT, name VARCHAR(50) NOT NULL)"])) + (is (= (sql/format (-> {:create-table [:films :if-not-exists] + :with-columns + [[:id :int :unsigned :auto-increment] + [:name [:varchar 50] [:not nil]]]})) + ["CREATE TABLE IF NOT EXISTS films (id INT UNSIGNED AUTO_INCREMENT, name VARCHAR(50) NOT NULL)"])) + (is (= (sql/format {:drop-table :foo}) + ["DROP TABLE foo"])) + (is (= (sql/format {:drop-table [:if-exists :foo]}) + ["DROP TABLE IF EXISTS foo"])) + (is (= (sql/format {:drop-view [:if-exists :foo]}) + ["DROP VIEW IF EXISTS foo"])) + (is (= (sql/format {:drop-materialized-view [:if-exists :foo]}) + ["DROP MATERIALIZED VIEW IF EXISTS foo"])) + (is (= (sql/format {:refresh-materialized-view [:concurrently :foo] + :with-data true}) + ["REFRESH MATERIALIZED VIEW CONCURRENTLY foo WITH DATA"])) + (is (= (sql/format '{drop-table (if-exists foo)}) + ["DROP TABLE IF EXISTS foo"])) + (is (= (sql/format {:drop-table [:foo :bar]}) + ["DROP TABLE foo, bar"])) + (is (= (sql/format {:drop-table [:if-exists :foo :bar]}) + ["DROP TABLE IF EXISTS foo, bar"])) + (is (= (sql/format {:drop-table [:if-exists :foo :bar [:cascade]]}) + ["DROP TABLE IF EXISTS foo, bar CASCADE"])) + (is (= (sql/format (drop-table :foo)) + ["DROP TABLE foo"])) + (is (= (sql/format (drop-table :if-exists :foo)) + ["DROP TABLE IF EXISTS foo"])) + (is (= (sql/format (-> (refresh-materialized-view :concurrently :foo) + (with-data true))) + ["REFRESH MATERIALIZED VIEW CONCURRENTLY foo WITH DATA"])) + (is (= (sql/format (drop-table :foo :bar)) + ["DROP TABLE foo, bar"])) + (is (= (sql/format (drop-table :if-exists :foo :bar [:cascade])) + ["DROP TABLE IF EXISTS foo, bar CASCADE"]))) + +(deftest issue-293-alter-table + (is (= (sql/format (-> (alter-table :fruit) + (add-column :id :int [:not nil]))) + ["ALTER TABLE fruit ADD COLUMN id INT NOT NULL"])) + (is (= (sql/format (alter-table :fruit + (add-column :id :int [:not nil]) + (drop-column :ident))) + ["ALTER TABLE fruit ADD COLUMN id INT NOT NULL, DROP COLUMN ident"]))) + +(deftest issue-293-insert-into-data + ;; insert into as (and other tests) based on :insert-into + ;; examples in the clause reference docs: + ;; first case -- table specifier: + (is (= (sql/format {:insert-into :transport + :values [[1 "Car"] [2 "Boat"] [3 "Bike"]]}) + ["INSERT INTO transport VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + (is (= (sql/format {:insert-into :transport + :columns [:id :name] + :values [[1 "Car"] [2 "Boat"] [3 "Bike"]]}) + ["INSERT INTO transport (id, name) VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + ;; with an alias: + (is (= (sql/format {:insert-into [:transport :t] + :values [[1 "Car"] [2 "Boat"] [3 "Bike"]]}) + ["INSERT INTO transport AS t VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + (is (= (sql/format {:insert-into [:transport :t] + :columns [:id :name] + :values [[1 "Car"] [2 "Boat"] [3 "Bike"]]}) + ["INSERT INTO transport AS t (id, name) VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + ;; second case -- table specifier and columns: + (is (= (sql/format {:insert-into [:transport [:id :name]] + :values [[1 "Car"] [2 "Boat"] [3 "Bike"]]}) + ["INSERT INTO transport (id, name) VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + ;; with an alias: + (is (= (sql/format {:insert-into [[:transport :t] [:id :name]] + :values [[1 "Car"] [2 "Boat"] [3 "Bike"]]}) + ["INSERT INTO transport AS t (id, name) VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + ;; third case -- table/column specifier and query: + (is (= (sql/format '{insert-into (transport {select (id, name) from (cars)})}) + ["INSERT INTO transport SELECT id, name FROM cars"])) + ;; with columns: + (is (= (sql/format '{insert-into ((transport (id, name)) {select (*) from (cars)})}) + ["INSERT INTO transport (id, name) SELECT * FROM cars"])) + ;; with an alias: + (is (= (sql/format '{insert-into ((transport t) {select (id, name) from (cars)})}) + ["INSERT INTO transport AS t SELECT id, name FROM cars"])) + ;; with columns: + (is (= (sql/format '{insert-into ((transport (id, name)) {select (*) from (cars)})}) + ["INSERT INTO transport (id, name) SELECT * FROM cars"])) + ;; with an alias and columns: + (is (= (sql/format '{insert-into (((transport t) (id, name)) {select (*) from (cars)})}) + ["INSERT INTO transport AS t (id, name) SELECT * FROM cars"]))) + +(deftest issue-293-insert-into-helpers + ;; and the same set of tests using the helper functions instead: + (is (= (sql/format (-> (insert-into :transport) + (values [[1 "Car"] [2 "Boat"] [3 "Bike"]]))) + ["INSERT INTO transport VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + (is (= (sql/format (-> (insert-into :transport) + (columns :id :name) + (values [[1 "Car"] [2 "Boat"] [3 "Bike"]]))) + ["INSERT INTO transport (id, name) VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + ;; with an alias: + (is (= (sql/format (-> (insert-into :transport :t) + (values [[1 "Car"] [2 "Boat"] [3 "Bike"]]))) + ["INSERT INTO transport AS t VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + (is (= (sql/format (-> (insert-into :transport :t) + (columns :id :name) + (values [[1 "Car"] [2 "Boat"] [3 "Bike"]]))) + ["INSERT INTO transport AS t (id, name) VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + ;; second case -- table specifier and columns: + (is (= (sql/format (-> (insert-into :transport [:id :name]) + (values [[1 "Car"] [2 "Boat"] [3 "Bike"]]))) + ["INSERT INTO transport (id, name) VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + ;; with an alias: + (is (= (sql/format (-> (insert-into [:transport :t] [:id :name]) + (values [[1 "Car"] [2 "Boat"] [3 "Bike"]]))) + ["INSERT INTO transport AS t (id, name) VALUES (?, ?), (?, ?), (?, ?)" 1 "Car" 2 "Boat" 3 "Bike"])) + ;; third case -- table/column specifier and query: + (is (= (sql/format (insert-into :transport '{select (id, name) from (cars)})) + ["INSERT INTO transport SELECT id, name FROM cars"])) + ;; with columns: + (is (= (sql/format (insert-into [:transport [:id :name]] '{select (*) from (cars)})) + ["INSERT INTO transport (id, name) SELECT * FROM cars"])) + ;; with an alias: + (is (= (sql/format (insert-into '(transport t) '{select (id, name) from (cars)})) + ["INSERT INTO transport AS t SELECT id, name FROM cars"])) + ;; with columns: + (is (= (sql/format (insert-into '(transport (id, name)) '{select (*) from (cars)})) + ["INSERT INTO transport (id, name) SELECT * FROM cars"])) + ;; with an alias and columns: + (is (= (sql/format (insert-into ['(transport t) '(id, name)] '{select (*) from (cars)})) + ["INSERT INTO transport AS t (id, name) SELECT * FROM cars"])) + ;; three arguments with columns: + (is (= (sql/format (insert-into :transport [:id :name] '{select (*) from (cars)})) + ["INSERT INTO transport (id, name) SELECT * FROM cars"])) + ;; three arguments with an alias and columns: + (is (= (sql/format (insert-into '(transport t) '(id, name) '{select (*) from (cars)})) + ["INSERT INTO transport AS t (id, name) SELECT * FROM cars"]))) + +;; these tests are adapted from Cam Saul's PR #283 + +(deftest merge-where-no-params-test + (doseq [[k [f merge-f]] {"WHERE" [where where] + "HAVING" [having having]}] + (testing "merge-where called with just the map as parameter - see #228" + (let [sqlmap (-> (select :*) + (from :table) + (f [:= :foo :bar]))] + (is (= [(str "SELECT * FROM table " k " foo = bar")] + (sql/format (apply merge-f sqlmap [])))))))) + +(deftest merge-where-test + (doseq [[k sql-keyword f merge-f] [[:where "WHERE" where where] + [:having "HAVING" having having]]] + (is (= [(str "SELECT * FROM table " sql-keyword " (foo = bar) AND (quuz = xyzzy)")] + (-> (select :*) + (from :table) + (f [:= :foo :bar] [:= :quuz :xyzzy]) + sql/format))) + (is (= [(str "SELECT * FROM table " sql-keyword " (foo = bar) AND (quuz = xyzzy)")] + (-> (select :*) + (from :table) + (f [:= :foo :bar]) + (merge-f [:= :quuz :xyzzy]) + sql/format))) + (testing "Should work when first arg isn't a map" + (is (= {k [:and [:x] [:y]]} + (merge-f [:x] [:y])))) + (testing "Shouldn't use conjunction if there is only one clause in the result" + (is (= {k [:x]} + (merge-f {} [:x])))) + (testing "Should be able to specify the conjunction type" + (is (= {k [:or [:x] [:y]]} + (merge-f {} + :or + [:x] [:y])))) + (testing "Should ignore nil clauses" + (is (= {k [:or [:x] [:y]]} + (merge-f {} + :or + [:x] nil [:y])))))) + +(deftest merge-where-combine-clauses-test + (doseq [[k f] {:where where + :having having}] + (testing (str "Combine new " k " clauses into the existing clause when appropriate. (#282)") + (testing "No existing clause" + (is (= {k [:and [:x] [:y]]} + (f {} + [:x] [:y])))) + (testing "Existing clause is not a conjunction." + (is (= {k [:and [:a] [:x] [:y]]} + (f {k [:a]} + [:x] [:y])))) + (testing "Existing clause IS a conjunction." + (testing "New clause(s) are not conjunctions" + (is (= {k [:and [:a] [:b] [:x] [:y]]} + (f {k [:and [:a] [:b]]} + [:x] [:y])))) + (testing "New clauses(s) ARE conjunction(s)" + (is (= {k [:and [:a] [:b] [:x] [:y]]} + (f {k [:and [:a] [:b]]} + [:and [:x] [:y]]))) + (is (= {k [:and [:a] [:b] [:x] [:y]]} + (f {k [:and [:a] [:b]]} + [:and [:x]] + [:y]))) + (is (= {k [:and [:a] [:b] [:x] [:y]]} + (f {k [:and [:a] [:b]]} + [:and [:x]] + [:and [:y]]))))) + (testing "if existing clause isn't the same conjunction, don't merge into it" + (testing "existing conjunction is `:or`" + (is (= {k [:and [:or [:a] [:b]] [:x] [:y]]} + (f {k [:or [:a] [:b]]} + [:x] [:y])))) + (testing "pass conjunction type as a param (override default of :and)" + (is (= {k [:or [:and [:a] [:b]] [:x] [:y]]} + (f {k [:and [:a] [:b]]} + :or + [:x] [:y])))))))) + +(deftest mysql-on-duplicate-key-update + (testing "From https://www.mysqltutorial.org/mysql-insert-or-update-on-duplicate-key-update" + (is (= (sql/format (-> (insert-into :device) + (columns :name) + (values [["Printer"]]) + (on-duplicate-key-update {:name "Printer"}))) + ["INSERT INTO device (name) VALUES (?) ON DUPLICATE KEY UPDATE name = ?" + "Printer" "Printer"])) + (is (= (sql/format (-> (insert-into :device) + (columns :id :name) + (values [[4 "Printer"]]) + (on-duplicate-key-update {:name "Central Printer"}))) + ["INSERT INTO device (id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = ?" + 4 "Printer" "Central Printer"])) + (is (= (sql/format (-> (insert-into :table) + (columns :c1) + (values [[42]]) + (on-duplicate-key-update {:c1 [:+ [:values :c1] 1]}))) + ["INSERT INTO table (c1) VALUES (?) ON DUPLICATE KEY UPDATE c1 = VALUES(c1) + ?" + 42 1])))) + +(deftest filter-within-order-by-test + (testing "PostgreSQL filter, within group, order-by as special syntax" + (is (= (sql/format {:select [[[:filter :%count.* {:where [:> :i 5]}] :a] + [[:filter ; two pairs -- alias is on last pair + [:avg :x [:order-by :y [:a :desc]]] {:where [:< :i 10]} + [:sum :q] {:where [:= :x nil]}] :b] + [[:within-group [:foo :y] {:order-by [:x]}]]]}) + [(str "SELECT COUNT(*) FILTER (WHERE i > ?) AS a," + " AVG(x, y ORDER BY a DESC) FILTER (WHERE i < ?)," + " SUM(q) FILTER (WHERE x IS NULL) AS b," + " FOO(y) WITHIN GROUP (ORDER BY x ASC)") + 5 10]))) + (testing "PostgreSQL filter, within group, order-by as helpers" + (is (= (sql/format (select [(filter :%count.* (where :> :i 5)) :a] + [(filter ; two pairs -- alias is on last pair + ;; order by must remain special syntax here: + [:avg :x [:order-by :y [:a :desc]]] (where :< :i 10) + [:sum :q] (where := :x nil)) :b] + [(within-group [:foo :y] (order-by :x))])) + [(str "SELECT COUNT(*) FILTER (WHERE i > ?) AS a," + " AVG(x, y ORDER BY a DESC) FILTER (WHERE i < ?)," + " SUM(q) FILTER (WHERE x IS NULL) AS b," + " FOO(y) WITHIN GROUP (ORDER BY x ASC)") + 5 10])))) + +(deftest issue-322 + (testing "Combining WHERE clauses with conditions" + (is (= {:where [:and [:= :a 1] [:or [:= :b 2] [:= :c 3]]]} + (where [:= :a 1] [:or [:= :b 2] [:= :c 3]]))) + (is (= (-> (where :or [:= :b 2] [:= :c 3]) ; or first + (where := :a 1)) ; then implicit and + (-> (where := :b 2) ; implicit and + (where :or [:= :c 3]) ; then explicit or + (where := :a 1)))) ; then implicit and + (is (= {:where [:and [:or [:= :b 2] [:= :c 3]] [:= :a 1]]} + (where [:or [:= :b 2] [:= :c 3]] [:= :a 1]) + (-> (where :or [:= :b 2] [:= :c 3]) ; explicit or + (where := :a 1)))))) ; then implicit and + +(deftest issue-324 + (testing "insert-into accepts statement" + (is (= (-> (with [:a]) + (insert-into [:quux [:x :y]] + {:select [:id] :from [:table]})) + {:with [[:a]], + :insert-into [[:quux [:x :y]] + {:select [:id], :from [:table]}]})))) diff --git a/test-resources/lib_tests/honey/sql/postgres_test.cljc b/test-resources/lib_tests/honey/sql/postgres_test.cljc new file mode 100644 index 00000000..ad173596 --- /dev/null +++ b/test-resources/lib_tests/honey/sql/postgres_test.cljc @@ -0,0 +1,382 @@ +;; copied from https://github.com/nilenso/honeysql-postgres +;; on 2021-02-13 to verify the completeness of support for +;; those features within HoneySQL 2.x + +;; where there are differences, the original code is kept +;; with #_ and the modified code follows it (aside from +;; the ns form which has numerous changes to both match +;; the structure of HoneySQL 2.x and to work with cljs) + +(ns honey.sql.postgres-test + (:refer-clojure :exclude [update partition-by set]) + (:require #?(:clj [clojure.test :refer [deftest is testing]] + :cljs [cljs.test :refer-macros [deftest is testing]]) + ;; pull in all the PostgreSQL helpers that the nilenso + ;; library provided (as well as the regular HoneySQL ones): + [honey.sql.helpers :as sqlh :refer + [upsert on-conflict do-nothing on-constraint + returning do-update-set + ;; not needed because do-update-set can do this directly + #_do-update-set! + alter-table rename-column drop-column + add-column partition-by + ;; not needed because insert-into can do this directly + #_insert-into-as + create-table rename-table drop-table + window create-view over with-columns + create-extension drop-extension + select-distinct-on + ;; already part of HoneySQL + insert-into values where select + from order-by update set]] + [honey.sql :as sql])) + +(deftest upsert-test + (testing "upsert sql generation for postgresql" + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname RETURNING *" 5 "Gizmo Transglobal" 6 "Associated Computing, Inc"] + ;; preferred in honeysql: + (-> (insert-into :distributors) + (values [{:did 5 :dname "Gizmo Transglobal"} + {:did 6 :dname "Associated Computing, Inc"}]) + (on-conflict :did) + (do-update-set :dname) + (returning :*) + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname RETURNING *" 5 "Gizmo Transglobal" 6 "Associated Computing, Inc"] + ;; identical to nilenso version: + (-> (insert-into :distributors) + (values [{:did 5 :dname "Gizmo Transglobal"} + {:did 6 :dname "Associated Computing, Inc"}]) + (upsert (-> (on-conflict :did) + (do-update-set :dname))) + (returning :*) + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT (did) DO NOTHING" 7 "Redline GmbH"] + ;; preferred in honeysql: + (-> (insert-into :distributors) + (values [{:did 7 :dname "Redline GmbH"}]) + (on-conflict :did) + do-nothing + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT (did) DO NOTHING" 7 "Redline GmbH"] + ;; identical to nilenso version: + (-> (insert-into :distributors) + (values [{:did 7 :dname "Redline GmbH"}]) + (upsert (-> (on-conflict :did) + do-nothing)) + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"] + ;; preferred in honeysql: + (-> (insert-into :distributors) + (values [{:did 9 :dname "Antwerp Design"}]) + (on-conflict (on-constraint :distributors_pkey)) + do-nothing + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT (did) ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"] + ;; with both name and clause: + (-> (insert-into :distributors) + (values [{:did 9 :dname "Antwerp Design"}]) + (on-conflict :did (on-constraint :distributors_pkey)) + do-nothing + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT (did, dname) ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"] + ;; with multiple names and a clause: + (-> (insert-into :distributors) + (values [{:did 9 :dname "Antwerp Design"}]) + (on-conflict :did :dname (on-constraint :distributors_pkey)) + do-nothing + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING" 9 "Antwerp Design"] + ;; almost identical to nilenso version: + (-> (insert-into :distributors) + (values [{:did 9 :dname "Antwerp Design"}]) + ;; in nilenso, this was (on-conflict-constraint :distributors_pkey) + (upsert (-> (on-conflict (on-constraint :distributors_pkey)) + do-nothing)) + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?), (?, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname" 10 "Pinp Design" 11 "Foo Bar Works"] + (sql/format {:insert-into :distributors + :values [{:did 10 :dname "Pinp Design"} + {:did 11 :dname "Foo Bar Works"}] + ;; in nilenso, these two were a submap under :upsert + :on-conflict :did + :do-update-set :dname}))) + (is (= ["INSERT INTO distributors (did, dname) VALUES (?, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname || ? || d.dname || ?" 23 "Foo Distributors" " (formerly " ")"] + (-> (insert-into :distributors) + (values [{:did 23 :dname "Foo Distributors"}]) + (on-conflict :did) + ;; nilenso: + #_(do-update-set! [:dname "EXCLUDED.dname || ' (formerly ' || d.dname || ')'"]) + ;; honeysql + (do-update-set {:dname [:|| :EXCLUDED.dname " (formerly " :d.dname ")"]}) + sql/format))) + (is (= ["INSERT INTO distributors (did, dname) SELECT ?, ? ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING" 1 "whatever"] + ;; honeysql version: + (-> (insert-into :distributors + [:did :dname] + (select 1 "whatever")) + (on-conflict (on-constraint :distributors_pkey)) + do-nothing + sql/format) + ;; nilenso version: + #_(-> (insert-into :distributors) + (columns :did :dname) + (query-values (select 1 "whatever")) + (upsert (-> (on-conflict-constraint :distributors_pkey) + do-nothing)) + sql/format))))) + +(deftest upsert-where-test + (is (= ["INSERT INTO user (phone, name) VALUES (?, ?) ON CONFLICT (phone) WHERE phone IS NOT NULL DO UPDATE SET phone = EXCLUDED.phone, name = EXCLUDED.name WHERE user.active = FALSE" "5555555" "John"] + (sql/format + {:insert-into :user + :values [{:phone "5555555" :name "John"}] + :on-conflict [:phone + {:where [:<> :phone nil]}] + :do-update-set {:fields [:phone :name] + :where [:= :user.active false]}}) + ;; nilenso version + #_(sql/format + {:insert-into :user + :values [{:phone "5555555" :name "John"}] + ;; nested under :upsert + :upsert {:on-conflict [:phone] + ;; but :where is at the same level as :on-conflict + :where [:<> :phone nil] + ;; this is the same as in honeysql: + :do-update-set {:fields [:phone :name] + :where [:= :user.active false]}}})))) + +(deftest returning-test + (testing "returning clause in sql generation for postgresql" + (is (= ["DELETE FROM distributors WHERE did > 10 RETURNING *"] + (sql/format {:delete-from :distributors + :where [:> :did :10] + :returning [:*]}))) + (is (= ["UPDATE distributors SET dname = ? WHERE did = 2 RETURNING did dname" "Foo Bar Designs"] + (-> (update :distributors) + (set {:dname "Foo Bar Designs"}) + (where [:= :did :2]) + (returning [:did :dname]) + sql/format))))) + +(deftest create-view-test + (testing "creating a view from a table" + (is (= ["CREATE VIEW metro AS SELECT * FROM cities WHERE metroflag = ?" "Y"] + (-> (create-view :metro) + (select :*) + (from :cities) + (where [:= :metroflag "Y"]) + sql/format))))) + +(deftest drop-table-test + (testing "drop table sql generation for a single table" + (is (= ["DROP TABLE cities"] + (sql/format (drop-table :cities))))) + (testing "drop table sql generation for multiple tables" + (is (= ["DROP TABLE cities, towns, vilages"] + (sql/format (drop-table :cities :towns :vilages)))))) + +(deftest create-table-test + ;; the nilenso versions of these tests required sql/call for function-like syntax + (testing "create table with two columns" + (is (= ["CREATE TABLE cities (city VARCHAR(80) PRIMARY KEY, location POINT)"] + (-> (create-table :cities) + (with-columns [[:city [:varchar 80] [:primary-key]] + [:location :point]]) + sql/format)))) + (testing "create table with foreign key reference" + (is (= ["CREATE TABLE weather (city VARCHAR(80) REFERENCES CITIES(CITY), temp_lo INT, temp_hi INT, prcp REAL, date DATE)"] + (-> (create-table :weather) + (with-columns [[:city [:varchar :80] [:references :cities :city]] + [:temp_lo :int] + [:temp_hi :int] + [:prcp :real] + [:date :date]]) + sql/format)))) + (testing "creating table with table level constraint" + (is (= ["CREATE TABLE films (code CHAR(5), title VARCHAR(40), did INTEGER, date_prod DATE, kind VARCHAR(10), CONSTRAINT code_title PRIMARY KEY(CODE, TITLE))"] + (-> (create-table :films) + (with-columns [[:code [:char 5]] + [:title [:varchar 40]] + [:did :integer] + [:date_prod :date] + [:kind [:varchar 10]] + [[:constraint :code_title] [:primary-key :code :title]]]) + sql/format)))) + (testing "creating table with column level constraint" + (is (= ["CREATE TABLE films (code CHAR(5) CONSTRAINT FIRSTKEY PRIMARY KEY, title VARCHAR(40) NOT NULL, did INTEGER NOT NULL, date_prod DATE, kind VARCHAR(10))"] + (-> (create-table :films) + (with-columns [[:code [:char 5] [:constraint :firstkey] [:primary-key]] + [:title [:varchar 40] [:not nil]] + [:did :integer [:not nil]] + [:date_prod :date] + [:kind [:varchar 10]]]) + sql/format)))) + (testing "creating table with columns with default values" + (is (= ["CREATE TABLE distributors (did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'), name VARCHAR(40) NOT NULL)"] + (-> (create-table :distributors) + (with-columns [[:did :integer [:primary-key] [:default [:nextval "serial"]]] + [:name [:varchar 40] [:not nil]]]) + sql/format)))) + (testing "creating table with column checks" + (is (= ["CREATE TABLE products (product_no INTEGER, name TEXT, price NUMERIC CHECK(PRICE > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))"] + (-> (create-table :products) + (with-columns [[:product_no :integer] + [:name :text] + [:price :numeric [:check [:> :price 0]]] + [:discounted_price :numeric] + [[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]]) + sql/format))))) + +(deftest over-test + (testing "window function over on select statemt" + (is (= ["SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, MAX(salary) OVER w AS MaxSalary FROM employee WINDOW w AS (PARTITION BY department)"] + ;; honeysql treats over as a function: + (-> (select :id + (over + [[:avg :salary] (-> (partition-by :department) (order-by [:designation])) :Average] + [[:max :salary] :w :MaxSalary])) + (from :employee) + (window :w (partition-by :department)) + sql/format) + ;; nilenso treated over as a clause + #_(-> (select :id) + (over + [[:avg :salary] (-> (partition-by :department) (order-by [:designation])) :Average] + [[:max :salary] :w :MaxSalary]) + (from :employee) + (window :w (partition-by :department)) + sql/format))))) + +(deftest alter-table-test + (testing "alter table add column generates the required sql" + (is (= ["ALTER TABLE employees ADD COLUMN address TEXT"] + (-> (alter-table :employees) + (add-column :address :text) + sql/format)))) + (testing "alter table drop column generates the required sql" + (is (= ["ALTER TABLE employees DROP COLUMN address"] + (-> (alter-table :employees) + (drop-column :address) + sql/format)))) + (testing "alter table rename column generates the requred sql" + (is (= ["ALTER TABLE employees RENAME COLUMN address TO homeaddress"] + (-> (alter-table :employees) + (rename-column :address :homeaddress) + sql/format)))) + (testing "alter table rename table generates the required sql" + (is (= ["ALTER TABLE employees RENAME TO managers"] + (-> (alter-table :employees) + (rename-table :managers) + sql/format))))) + +(deftest insert-into-with-alias + (testing "insert into with alias" + (is (= ["INSERT INTO distributors AS d (did, dname) VALUES (?, ?), (?, ?) ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname WHERE d.zipcode <> ? RETURNING d.*" 5 "Gizmo Transglobal" 6 "Associated Computing, Inc" "21201"] + ;; honeysql supports alias in insert-into: + (-> (insert-into :distributors :d) + ;; nilensor required insert-into-as: + #_(insert-into-as :distributors :d) + (values [{:did 5 :dname "Gizmo Transglobal"} + {:did 6 :dname "Associated Computing, Inc"}]) + (on-conflict :did) + ;; honeysql supports names and a where clause: + (do-update-set :dname (where [:<> :d.zipcode "21201"])) + ;; nilenso nested those under upsert: + #_(upsert (-> (on-conflict :did) + (do-update-set :dname) + (where [:<> :d.zipcode "21201"]))) + (returning :d.*) + sql/format))))) + +(deftest create-table-if-not-exists + (testing "create a table if not exists" + (is (= ["CREATE TABLE IF NOT EXISTS tablename"] + (-> (create-table :tablename :if-not-exists) + sql/format))))) + +(deftest drop-table-if-exists + (testing "drop a table if it exists" + (is (= ["DROP TABLE IF EXISTS t1, t2, t3"] + (-> (drop-table :if-exists :t1 :t2 :t3) + sql/format))))) + +(deftest select-where-ilike + (testing "select from table with ILIKE operator" + (is (= ["SELECT * FROM products WHERE name ILIKE ?" "%name%"] + (-> (select :*) + (from :products) + (where [:ilike :name "%name%"]) + sql/format))))) + +(deftest select-where-not-ilike + (testing "select from table with NOT ILIKE operator" + (is (= ["SELECT * FROM products WHERE name NOT ILIKE ?" "%name%"] + (-> (select :*) + (from :products) + (where [:not-ilike :name "%name%"]) + sql/format))))) + +(deftest values-except-select + (testing "select which values are not not present in a table" + (is (= ["(VALUES (?), (?), (?)) EXCEPT (SELECT id FROM images)" 4 5 6] + (sql/format + {:except + [{:values [[4] [5] [6]]} + {:select [:id] :from [:images]}]}))))) + +(deftest select-except-select + (testing "select which rows are not present in another table" + (is (= ["(SELECT ip) EXCEPT (SELECT ip FROM ip_location)"] + (sql/format + {:except + [{:select [:ip]} + {:select [:ip] :from [:ip_location]}]}))))) + +(deftest values-except-all-select + (testing "select which values are not not present in a table" + (is (= ["(VALUES (?), (?), (?)) EXCEPT ALL (SELECT id FROM images)" 4 5 6] + (sql/format + {:except-all + [{:values [[4] [5] [6]]} + {:select [:id] :from [:images]}]}))))) + +(deftest select-except-all-select + (testing "select which rows are not present in another table" + (is (= ["(SELECT ip) EXCEPT ALL (SELECT ip FROM ip_location)"] + (sql/format + {:except-all + [{:select [:ip]} + {:select [:ip] :from [:ip_location]}]}))))) + +(deftest select-distinct-on-test + (testing "select distinct on" + (is (= ["SELECT DISTINCT ON(\"a\", \"b\") \"c\" FROM \"products\""] + ;; honeysql has select-distinct-on: + (-> (select-distinct-on [:a :b] :c) + (from :products) + (sql/format {:quoted true})) + ;; nilenso handled that via modifiers: + #_(-> (select :c) + (from :products) + (modifiers :distinct-on :a :b) + (sql/format :quoting :ansi)))))) + +(deftest create-extension-test + ;; previously, honeysql required :allow-dashed-names? true + (testing "create extension" + (is (= ["CREATE EXTENSION \"uuid-ossp\""] + (-> (create-extension :uuid-ossp) + (sql/format {:quoted true}))))) + (testing "create extension if not exists" + (is (= ["CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\""] + (-> (create-extension :uuid-ossp :if-not-exists) + (sql/format {:quoted true})))))) + +(deftest drop-extension-test + ;; previously, honeysql required :allow-dashed-names? true + (testing "create extension" + (is (= ["DROP EXTENSION \"uuid-ossp\""] + (-> (drop-extension :uuid-ossp) + (sql/format {:quoted true})))))) diff --git a/test-resources/lib_tests/honey/sql_test.cljc b/test-resources/lib_tests/honey/sql_test.cljc new file mode 100644 index 00000000..d1043ed7 --- /dev/null +++ b/test-resources/lib_tests/honey/sql_test.cljc @@ -0,0 +1,786 @@ +;; copyright (c) 2021 sean corfield, all rights reserved + +(ns honey.sql-test + (:refer-clojure :exclude [format]) + (:require [clojure.string :as str] + #?(:clj [clojure.test :refer [deftest is testing]] + :cljs [cljs.test :refer-macros [deftest is testing]]) + [honey.sql :as sut :refer [format]] + [honey.sql.helpers :as h]) + #?(:clj (:import (clojure.lang ExceptionInfo)))) + +(deftest mysql-tests + (is (= ["SELECT * FROM `table` WHERE `id` = ?" 1] + (sut/format {:select [:*] :from [:table] :where [:= :id 1]} + {:dialect :mysql})))) + +(deftest expr-tests + (is (= ["id IS NULL"] + (sut/format-expr [:= :id nil]))) + (is (= ["id IS NULL"] + (sut/format-expr [:is :id nil]))) + (is (= ["id IS NOT NULL"] + (sut/format-expr [:<> :id nil]))) + (is (= ["id IS NOT NULL"] + (sut/format-expr [:!= :id nil]))) + (is (= ["id IS NOT NULL"] + (sut/format-expr [:is-not :id nil]))) + ;; degenerate cases: + (is (= ["NULL IS NULL"] + (sut/format-expr [:= nil nil]))) + (is (= ["NULL IS NOT NULL"] + (sut/format-expr [:<> nil nil]))) + (is (= ["id = ?" 1] + (sut/format-expr [:= :id 1]))) + (is (= ["id + ?" 1] + (sut/format-expr [:+ :id 1]))) + (is (= ["? + (? + quux)" 1 1] + (sut/format-expr [:+ 1 [:+ 1 :quux]]))) + (is (= ["? + ? + quux" 1 1] + (sut/format-expr [:+ 1 1 :quux]))) + (is (= ["FOO(BAR(? + G(abc)), F(?, quux))" 2 1] + (sut/format-expr [:foo [:bar [:+ 2 [:g :abc]]] [:f 1 :quux]]))) + (is (= ["id"] + (sut/format-expr :id))) + (is (= ["?" 1] + (sut/format-expr 1))) + (is (= ["INTERVAL ? DAYS" 30] + (sut/format-expr [:interval 30 :days])))) + +(deftest where-test + (is (= ["WHERE id = ?" 1] + (#'sut/format-on-expr :where [:= :id 1])))) + +(deftest general-tests + (is (= ["SELECT * FROM \"table\" WHERE \"id\" = ?" 1] + (sut/format {:select [:*] :from [:table] :where [:= :id 1]} {:quoted true}))) + (is (= ["SELECT \"t\".* FROM \"table\" AS \"t\" WHERE \"id\" = ?" 1] + (sut/format {:select [:t.*] :from [[:table :t]] :where [:= :id 1]} {:quoted true}))) + (is (= ["SELECT * FROM \"table\" GROUP BY \"foo\", \"bar\""] + (sut/format {:select [:*] :from [:table] :group-by [:foo :bar]} {:quoted true}))) + (is (= ["SELECT * FROM \"table\" GROUP BY DATE(\"bar\")"] + (sut/format {:select [:*] :from [:table] :group-by [[:date :bar]]} {:quoted true}))) + (is (= ["SELECT * FROM \"table\" ORDER BY \"foo\" DESC, \"bar\" ASC"] + (sut/format {:select [:*] :from [:table] :order-by [[:foo :desc] :bar]} {:quoted true}))) + (is (= ["SELECT * FROM \"table\" ORDER BY DATE(\"expiry\") DESC, \"bar\" ASC"] + (sut/format {:select [:*] :from [:table] :order-by [[[:date :expiry] :desc] :bar]} {:quoted true}))) + (is (= ["SELECT * FROM \"table\" WHERE DATE_ADD(\"expiry\", INTERVAL ? DAYS) < NOW()" 30] + (sut/format {:select [:*] :from [:table] :where [:< [:date_add :expiry [:interval 30 :days]] [:now]]} {:quoted true}))) + (is (= ["SELECT * FROM `table` WHERE `id` = ?" 1] + (sut/format {:select [:*] :from [:table] :where [:= :id 1]} {:dialect :mysql}))) + (is (= ["SELECT * FROM \"table\" WHERE \"id\" IN (?, ?, ?, ?)" 1 2 3 4] + (sut/format {:select [:*] :from [:table] :where [:in :id [1 2 3 4]]} {:quoted true})))) + +;; issue-based tests + +(deftest subquery-alias-263 + (is (= ["SELECT type FROM (SELECT address AS field_alias FROM Candidate) AS sub_q_alias"] + (sut/format {:select [:type] + :from [[{:select [[:address :field-alias]] + :from [:Candidate]} :sub_q_alias]]}))) + (is (= ["SELECT type FROM (SELECT address field_alias FROM Candidate) sub_q_alias"] + (sut/format {:select [:type] + :from [[{:select [[:address :field-alias]] + :from [:Candidate]} :sub-q-alias]]} + {:dialect :oracle :quoted false})))) + +;; tests lifted from HoneySQL 1.x to check for compatibility + +(deftest alias-splitting + (is (= ["SELECT `aa`.`c` AS `a.c`, `bb`.`c` AS `b.c`, `cc`.`c` AS `c.c`"] + (format {:select [[:aa.c "a.c"] + [:bb.c :b.c] + [:cc.c 'c.c]]} + {:dialect :mysql})) + "aliases containing \".\" are quoted as necessary but not split")) + +(deftest values-alias + (is (= ["SELECT vals.a FROM (VALUES (?, ?, ?)) AS vals (a, b, c)" 1 2 3] + (format {:select [:vals.a] + :from [[{:values [[1 2 3]]} [:vals {:columns [:a :b :c]}]]]})))) +(deftest test-cte + (is (= (format {:with [[:query {:select [:foo] :from [:bar]}]]}) + ["WITH query AS (SELECT foo FROM bar)"])) + (is (= (format {:with [[:query1 {:select [:foo] :from [:bar]}] + [:query2 {:select [:bar] :from [:quux]}]] + :select [:query1.id :query2.name] + :from [:query1 :query2]}) + ["WITH query1 AS (SELECT foo FROM bar), query2 AS (SELECT bar FROM quux) SELECT query1.id, query2.name FROM query1, query2"])) + (is (= (format {:with-recursive [[:query {:select [:foo] :from [:bar]}]]}) + ["WITH RECURSIVE query AS (SELECT foo FROM bar)"])) + (is (= (format {:with [[[:static {:columns [:a :b :c]}] {:values [[1 2 3] [4 5]]}]]}) + ["WITH static (a, b, c) AS (VALUES (?, ?, ?), (?, ?, NULL))" 1 2 3 4 5])) + (is (= (format + {:with [[[:static {:columns [:a :b :c]}] + {:values [[1 2] [4 5 6]]}]] + :select [:*] + :from [:static]}) + ["WITH static (a, b, c) AS (VALUES (?, ?, NULL), (?, ?, ?)) SELECT * FROM static" 1 2 4 5 6]))) + +(deftest insert-into + (is (= (format {:insert-into :foo}) + ["INSERT INTO foo"])) + (is (= (format {:insert-into [:foo {:select [:bar] :from [:baz]}]}) + ["INSERT INTO foo SELECT bar FROM baz"])) + (is (= (format {:insert-into [[:foo [:a :b :c]] {:select [:d :e :f] :from [:baz]}]}) + ["INSERT INTO foo (a, b, c) SELECT d, e, f FROM baz"])) + (is (= (format {:insert-into [[:foo [:a :b :c]] {:select [:d :e :f] :from [:baz]}]}) + ["INSERT INTO foo (a, b, c) SELECT d, e, f FROM baz"]))) + +(deftest insert-into-namespaced + ;; un-namespaced: works as expected: + (is (= (format {:insert-into :foo :values [{:foo/id 1}]}) + ["INSERT INTO foo (id) VALUES (?)" 1])) + (is (= (format {:insert-into :foo :columns [:foo/id] :values [[2]]}) + ["INSERT INTO foo (id) VALUES (?)" 2])) + (is (= (format {:insert-into :foo :values [{:foo/id 1}]} + {:namespace-as-table? true}) + ["INSERT INTO foo (id) VALUES (?)" 1])) + (is (= (format {:insert-into :foo :columns [:foo/id] :values [[2]]} + {:namespace-as-table? true}) + ["INSERT INTO foo (id) VALUES (?)" 2]))) + +(deftest insert-into-uneven-maps + ;; we can't rely on ordering when the set of keys differs between maps: + (let [res (format {:insert-into :foo :values [{:id 1} {:id 2, :bar "quux"}]})] + (is (or (= res ["INSERT INTO foo (id, bar) VALUES (?, NULL), (?, ?)" 1 2 "quux"]) + (= res ["INSERT INTO foo (bar, id) VALUES (NULL, ?), (?, ?)" 1 "quux" 2])))) + (let [res (format {:insert-into :foo :values [{:id 1, :bar "quux"} {:id 2}]})] + (is (or (= res ["INSERT INTO foo (id, bar) VALUES (?, ?), (?, NULL)" 1 "quux" 2]) + (= res ["INSERT INTO foo (bar, id) VALUES (?, ?), (NULL, ?)" "quux" 1 2]))))) + +(deftest exists-test + ;; EXISTS should never have been implemented as SQL syntax: it's an operator! + #_(is (= (format {:exists {:select [:a] :from [:foo]}}) + ["EXISTS (SELECT a FROM foo)"])) + ;; select function call with an alias: + (is (= (format {:select [[[:exists {:select [:a] :from [:foo]}] :x]]}) + ["SELECT EXISTS (SELECT a FROM foo) AS x"])) + ;; select function call with no alias required: + (is (= (format {:select [[[:exists {:select [:a] :from [:foo]}]]]}) + ["SELECT EXISTS (SELECT a FROM foo)"])) + (is (= (format {:select [:id] + :from [:foo] + :where [:exists {:select [1] + :from [:bar] + :where :deleted}]}) + ["SELECT id FROM foo WHERE EXISTS (SELECT ? FROM bar WHERE deleted)" 1]))) + +(deftest array-test + (is (= (format {:insert-into :foo + :columns [:baz] + :values [[[:array [1 2 3 4]]]]}) + ["INSERT INTO foo (baz) VALUES (ARRAY[?, ?, ?, ?])" 1 2 3 4])) + (is (= (format {:insert-into :foo + :columns [:baz] + :values [[[:array ["one" "two" "three"]]]]}) + ["INSERT INTO foo (baz) VALUES (ARRAY[?, ?, ?])" "one" "two" "three"]))) + +(deftest union-test + ;; UNION and INTERSECT subexpressions should not be parenthesized. + ;; If you need to add more complex expressions, use a subquery like this: + ;; SELECT foo FROM bar1 + ;; UNION + ;; SELECT foo FROM (SELECT foo FROM bar2 ORDER BY baz LIMIT 2) + ;; ORDER BY foo ASC + (is (= (format {:union [{:select [:foo] :from [:bar1]} + {:select [:foo] :from [:bar2]}]}) + ["(SELECT foo FROM bar1) UNION (SELECT foo FROM bar2)"])) + + (testing "union complex values" + (is (= (format {:union [{:select [:foo] :from [:bar1]} + {:select [:foo] :from [:bar2]}] + :with [[[:bar {:columns [:spam :eggs]}] + {:values [[1 2] [3 4] [5 6]]}]]}) + ["WITH bar (spam, eggs) AS (VALUES (?, ?), (?, ?), (?, ?)) (SELECT foo FROM bar1) UNION (SELECT foo FROM bar2)" + 1 2 3 4 5 6])))) + +(deftest union-all-test + (is (= (format {:union-all [{:select [:foo] :from [:bar1]} + {:select [:foo] :from [:bar2]}]}) + ["(SELECT foo FROM bar1) UNION ALL (SELECT foo FROM bar2)"]))) + +(deftest intersect-test + (is (= (format {:intersect [{:select [:foo] :from [:bar1]} + {:select [:foo] :from [:bar2]}]}) + ["(SELECT foo FROM bar1) INTERSECT (SELECT foo FROM bar2)"]))) + +(deftest except-test + (is (= (format {:except [{:select [:foo] :from [:bar1]} + {:select [:foo] :from [:bar2]}]}) + ["(SELECT foo FROM bar1) EXCEPT (SELECT foo FROM bar2)"]))) + +(deftest inner-parts-test + (testing "The correct way to apply ORDER BY to various parts of a UNION" + (is (= (format + {:union + [{:select [:amount :id :created_on] + :from [:transactions]} + {:select [:amount :id :created_on] + :from [{:select [:amount :id :created_on] + :from [:other_transactions] + :order-by [[:amount :desc]] + :limit 5}]}] + :order-by [[:amount :asc]]}) + ["(SELECT amount, id, created_on FROM transactions) UNION (SELECT amount, id, created_on FROM (SELECT amount, id, created_on FROM other_transactions ORDER BY amount DESC LIMIT ?)) ORDER BY amount ASC" 5])))) + +(deftest compare-expressions-test + (testing "Sequences should be fns when in value/comparison spots" + (is (= ["SELECT foo FROM bar WHERE (col1 MOD ?) = (col2 + ?)" 4 4] + (format {:select [:foo] + :from [:bar] + :where [:= [:mod :col1 4] [:+ :col2 4]]})))) + + (testing "Example from dharrigan" + (is (= ["SELECT PG_TRY_ADVISORY_LOCK(1)"] + (format {:select [:%pg_try_advisory_lock.1]})))) + + (testing "Value context only applies to sequences in value/comparison spots" + (let [sub {:select [:%sum.amount] + :from [:bar] + :where [:in :id ["id-1" "id-2"]]}] + (is (= ["SELECT total FROM foo WHERE (SELECT SUM(amount) FROM bar WHERE id IN (?, ?)) = total" "id-1" "id-2"] + (format {:select [:total] + :from [:foo] + :where [:= sub :total]}))) + (is (= ["WITH t AS (SELECT SUM(amount) FROM bar WHERE id IN (?, ?)) SELECT total FROM foo WHERE total = t" "id-1" "id-2"] + (format {:with [[:t sub]] + :select [:total] + :from [:foo] + :where [:= :total :t]})))))) + +(deftest union-with-cte + (is (= (format {:union [{:select [:foo] :from [:bar1]} + {:select [:foo] :from [:bar2]}] + :with [[[:bar {:columns [:spam :eggs]}] + {:values [[1 2] [3 4] [5 6]]}]]}) + ["WITH bar (spam, eggs) AS (VALUES (?, ?), (?, ?), (?, ?)) (SELECT foo FROM bar1) UNION (SELECT foo FROM bar2)" 1 2 3 4 5 6]))) + +(deftest union-all-with-cte + (is (= (format {:union-all [{:select [:foo] :from [:bar1]} + {:select [:foo] :from [:bar2]}] + :with [[[:bar {:columns [:spam :eggs]}] + {:values [[1 2] [3 4] [5 6]]}]]}) + ["WITH bar (spam, eggs) AS (VALUES (?, ?), (?, ?), (?, ?)) (SELECT foo FROM bar1) UNION ALL (SELECT foo FROM bar2)" 1 2 3 4 5 6]))) + +(deftest parameterizer-none + (testing "array parameter" + (is (= (format {:insert-into :foo + :columns [:baz] + :values [[[:array [1 2 3 4]]]]} + {:inline true}) + ["INSERT INTO foo (baz) VALUES (ARRAY[1, 2, 3, 4])"]))) + + (testing "union complex values -- fail: parameterizer" + (is (= (format {:union [{:select [:foo] :from [:bar1]} + {:select [:foo] :from [:bar2]}] + :with [[[:bar {:columns [:spam :eggs]}] + {:values [[1 2] [3 4] [5 6]]}]]} + {:inline true}) + ["WITH bar (spam, eggs) AS (VALUES (1, 2), (3, 4), (5, 6)) (SELECT foo FROM bar1) UNION (SELECT foo FROM bar2)"])))) + +(deftest inline-was-parameterizer-none + (testing "array parameter" + (is (= (format {:insert-into :foo + :columns [:baz] + :values [[[:array (mapv vector + (repeat :inline) + [1 2 3 4])]]]}) + ["INSERT INTO foo (baz) VALUES (ARRAY[1, 2, 3, 4])"]))) + + (testing "union complex values" + (is (= (format {:union [{:select [:foo] :from [:bar1]} + {:select [:foo] :from [:bar2]}] + :with [[[:bar {:columns [:spam :eggs]}] + {:values (mapv #(mapv vector (repeat :inline) %) + [[1 2] [3 4] [5 6]])}]]}) + ["WITH bar (spam, eggs) AS (VALUES (1, 2), (3, 4), (5, 6)) (SELECT foo FROM bar1) UNION (SELECT foo FROM bar2)"])))) + +(deftest similar-regex-tests + (testing "basic similar to" + (is (= (format {:select :* :from :foo + :where [:similar-to :foo [:escape "bar" [:inline "*"]]]}) + ["SELECT * FROM foo WHERE foo SIMILAR TO ? ESCAPE '*'" "bar"])))) + +(deftest former-parameterizer-tests-where-and + ;; I have no plans for positional parameters -- I just don't see the point + #_(testing "should ignore a nil predicate -- fail: postgresql parameterizer" + (is (= (format {:where [:and + [:= :foo "foo"] + [:= :bar "bar"] + nil + [:= :quux "quux"]]} + {:parameterizer :postgresql}) + ["WHERE (foo = ?) AND (bar = $2) AND (quux = $3)" "foo" "bar" "quux"]))) + ;; new :inline option is similar to :parameterizer :none in 1.x + (testing "should fill param with single quote" + (is (= (format {:where [:and + [:= :foo "foo"] + [:= :bar "bar"] + nil + [:= :quux "quux"]]} + {:inline true}) + ["WHERE (foo = 'foo') AND (bar = 'bar') AND (quux = 'quux')"]))) + (testing "should inline params with single quote" + (is (= (format {:where [:and + [:= :foo [:inline "foo"]] + [:= :bar [:inline "bar"]] + nil + [:= :quux [:inline "quux"]]]}) + ["WHERE (foo = 'foo') AND (bar = 'bar') AND (quux = 'quux')"]))) + ;; this is the normal behavior -- not a custom parameterizer! + (testing "should fill param with ?" + (is (= (format {:where [:and + [:= :foo "foo"] + [:= :bar "bar"] + nil + [:= :quux "quux"]]} + ;; this never did anything useful: + #_{:parameterizer :mysql-fill}) + ["WHERE (foo = ?) AND (bar = ?) AND (quux = ?)" "foo" "bar" "quux"])))) + +(deftest set-before-from + ;; issue 235 + (is (= + ["UPDATE \"films\" \"f\" SET \"kind\" = \"c\".\"test\" FROM (SELECT \"b\".\"test\" FROM \"bar\" AS \"b\" WHERE \"b\".\"id\" = ?) AS \"c\" WHERE \"f\".\"kind\" = ?" 1 "drama"] + (-> + {:update [:films :f] + :set {:kind :c.test} + :from [[{:select [:b.test] + :from [[:bar :b]] + :where [:= :b.id 1]} :c]] + :where [:= :f.kind "drama"]} + (format {:quoted true})))) + ;; issue 317 + (is (= + ["UPDATE \"films\" \"f\" SET \"kind\" = \"c\".\"test\" FROM (SELECT \"b\".\"test\" FROM \"bar\" AS \"b\" WHERE \"b\".\"id\" = ?) AS \"c\" WHERE \"f\".\"kind\" = ?" 1 "drama"] + (-> + {:update [:films :f] + ;; drop ns in set clause... + :set {:f/kind :c.test} + :from [[{:select [:b.test] + :from [[:bar :b]] + :where [:= :b.id 1]} :c]] + :where [:= :f.kind "drama"]} + (format {:quoted true})))) + (is (= + ["UPDATE \"films\" \"f\" SET \"f\".\"kind\" = \"c\".\"test\" FROM (SELECT \"b\".\"test\" FROM \"bar\" AS \"b\" WHERE \"b\".\"id\" = ?) AS \"c\" WHERE \"f\".\"kind\" = ?" 1 "drama"] + (-> + {:update [:films :f] + ;; ...but keep literal dotted name + :set {:f.kind :c.test} + :from [[{:select [:b.test] + :from [[:bar :b]] + :where [:= :b.id 1]} :c]] + :where [:= :f.kind "drama"]} + (format {:quoted true}))))) + +(deftest set-after-join + (is (= + ["UPDATE `foo` INNER JOIN `bar` ON `bar`.`id` = `foo`.`bar_id` SET `a` = ? WHERE `bar`.`b` = ?" 1 42] + (-> + {:update :foo + :join [:bar [:= :bar.id :foo.bar_id]] + :set {:a 1} + :where [:= :bar.b 42]} + (format {:dialect :mysql}))))) + +(deftest format-arity-test + (testing "format can be called with no options" + (is (= ["DELETE FROM foo WHERE foo.id = ?" 42] + (-> {:delete-from :foo + :where [:= :foo.id 42]} + (format))))) + (testing "format can be called with an options hash map" + (is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42] + (-> {:delete-from :foo + :where [:= :foo.id 42]} + (format {:dialect :mysql :pretty true}))))) + (testing "format can be called with named arguments" + (is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42] + (-> {:delete-from :foo + :where [:= :foo.id 42]} + (format :dialect :mysql :pretty true))))) + (when (str/starts-with? #?(:bb "1.11" + :clj (clojure-version) + :cljs *clojurescript-version*) "1.11") + (testing "format can be called with mixed arguments" + (is (= ["\nDELETE FROM `foo`\nWHERE `foo`.`id` = ?\n" 42] + (-> {:delete-from :foo + :where [:= :foo.id 42]} + (format :dialect :mysql {:pretty true}))))))) + +(deftest delete-from-test + (is (= ["DELETE FROM `foo` WHERE `foo`.`id` = ?" 42] + (-> {:delete-from :foo + :where [:= :foo.id 42]} + (format {:dialect :mysql}))))) + +(deftest delete-test + (is (= ["DELETE `t1`, `t2` FROM `table1` AS `t1` INNER JOIN `table2` AS `t2` ON `t1`.`fk` = `t2`.`id` WHERE `t1`.`bar` = ?" 42] + (-> {:delete [:t1 :t2] + :from [[:table1 :t1]] + :join [[:table2 :t2] [:= :t1.fk :t2.id]] + :where [:= :t1.bar 42]} + (format {:dialect :mysql}))))) + +(deftest delete-using + (is (= ["DELETE FROM films USING producers WHERE (producer_id = producers.id) AND (producers.name = ?)" "foo"] + (-> {:delete-from :films + :using [:producers] + :where [:and + [:= :producer_id :producers.id] + [:= :producers.name "foo"]]} + (format))))) + +(deftest truncate-test + (is (= ["TRUNCATE `foo`"] + (-> {:truncate :foo} + (format {:dialect :mysql}))))) + +(deftest inlined-values-are-stringified-correctly + (is (= ["SELECT 'foo', 'It''s a quote!', BAR, NULL"] + (format {:select [[[:inline "foo"]] + [[:inline "It's a quote!"]] + [[:inline :bar]] + [[:inline nil]]]})))) + +;; Make sure if Locale is Turkish we're not generating queries like İNNER JOIN (dot over the I) because +;; `string/upper-case` is converting things to upper-case using the default Locale. Generated query should be the same +;; regardless of system Locale. See #236 +#?(:clj + (deftest statements-generated-correctly-with-turkish-locale + (let [format-with-locale (fn [^String language-tag] + (let [original-locale (java.util.Locale/getDefault)] + (try + (java.util.Locale/setDefault (java.util.Locale/forLanguageTag language-tag)) + (format {:select [:t2.name] + :from [[:table1 :t1]] + :join [[:table2 :t2] [:= :t1.fk :t2.id]] + :where [:= :t1.id 1]}) + (finally + (java.util.Locale/setDefault original-locale)))))] + (is (= (format-with-locale "en") + (format-with-locale "tr")))))) + +(deftest join-on-true-253 + ;; used to work on honeysql 0.9.2; broke in 0.9.3 + (is (= ["SELECT foo FROM bar INNER JOIN table AS t ON TRUE"] + (format {:select [:foo] + :from [:bar] + :join [[:table :t] true]})))) + +(deftest cross-join-test + (is (= ["SELECT * FROM foo CROSS JOIN bar"] + (format {:select [:*] + :from [:foo] + :cross-join [:bar]}))) + (is (= ["SELECT * FROM foo AS f CROSS JOIN bar b"] + (format {:select [:*] + :from [[:foo :f]] + :cross-join [[:bar :b]]})))) + +(deftest locking-select-tests + (testing "PostgreSQL/ANSI FOR" + (is (= ["SELECT * FROM foo FOR UPDATE"] + (format {:select [:*] :from :foo :for :update}))) + (is (= ["SELECT * FROM foo FOR NO KEY UPDATE"] + (format {:select [:*] :from :foo :for :no-key-update}))) + (is (= ["SELECT * FROM foo FOR SHARE"] + (format {:select [:*] :from :foo :for :share}))) + (is (= ["SELECT * FROM foo FOR KEY SHARE"] + (format {:select [:*] :from :foo :for :key-share}))) + (is (= ["SELECT * FROM foo FOR UPDATE"] + (format {:select [:*] :from :foo :for [:update]}))) + (is (= ["SELECT * FROM foo FOR NO KEY UPDATE"] + (format {:select [:*] :from :foo :for [:no-key-update]}))) + (is (= ["SELECT * FROM foo FOR SHARE"] + (format {:select [:*] :from :foo :for [:share]}))) + (is (= ["SELECT * FROM foo FOR KEY SHARE"] + (format {:select [:*] :from :foo :for [:key-share]}))) + (is (= ["SELECT * FROM foo FOR UPDATE NOWAIT"] + (format {:select [:*] :from :foo :for [:update :nowait]}))) + (is (= ["SELECT * FROM foo FOR UPDATE OF bar NOWAIT"] + (format {:select [:*] :from :foo :for [:update :bar :nowait]}))) + (is (= ["SELECT * FROM foo FOR UPDATE WAIT"] + (format {:select [:*] :from :foo :for [:update :wait]}))) + (is (= ["SELECT * FROM foo FOR UPDATE OF bar WAIT"] + (format {:select [:*] :from :foo :for [:update :bar :wait]}))) + (is (= ["SELECT * FROM foo FOR UPDATE SKIP LOCKED"] + (format {:select [:*] :from :foo :for [:update :skip-locked]}))) + (is (= ["SELECT * FROM foo FOR UPDATE OF bar SKIP LOCKED"] + (format {:select [:*] :from :foo :for [:update :bar :skip-locked]}))) + (is (= ["SELECT * FROM foo FOR UPDATE OF bar, quux"] + (format {:select [:*] :from :foo :for [:update [:bar :quux]]})))) + (testing "MySQL for/lock" + ;; these examples come from: + (is (= ["SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2) FOR UPDATE"] ; portable + (format {:select [:*] :from :t1 + :where [:= :c1 {:select [:c1] :from :t2}] + :for [:update]}))) + (is (= ["SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2 FOR UPDATE) FOR UPDATE"] + (format {:select [:*] :from :t1 + :where [:= :c1 {:select [:c1] :from :t2 :for [:update]}] + :for [:update]}))) + (is (= ["SELECT * FROM foo WHERE name = 'Jones' LOCK IN SHARE MODE"] ; MySQL-specific + (format {:select [:*] :from :foo + :where [:= :name [:inline "Jones"]] + :lock [:in-share-mode]} + {:dialect :mysql :quoted false}))))) + +(deftest insert-example-tests + ;; these examples are taken from https://www.postgresql.org/docs/13/sql-insert.html + (is (= [" +INSERT INTO films +VALUES ('UA502', 'Bananas', 105, '1971-07-13', 'Comedy', '82 minutes') +"] + (format {:insert-into :films + :values [[[:inline "UA502"] [:inline "Bananas"] [:inline 105] + [:inline "1971-07-13"] [:inline "Comedy"] + [:inline "82 minutes"]]]} + {:pretty true}))) + (is (= [" +INSERT INTO films +VALUES (?, ?, ?, ?, ?, ?) +" "UA502", "Bananas", 105, "1971-07-13", "Comedy", "82 minutes"] + (format {:insert-into :films + :values [["UA502" "Bananas" 105 "1971-07-13" "Comedy" "82 minutes"]]} + {:pretty true}))) + (is (= [" +INSERT INTO films +(code, title, did, date_prod, kind) +VALUES (?, ?, ?, ?, ?) +" "T_601", "Yojimo", 106, "1961-06-16", "Drama"] + (format {:insert-into :films + :columns [:code :title :did :date_prod :kind] + :values [["T_601", "Yojimo", 106, "1961-06-16", "Drama"]]} + {:pretty true}))) + (is (= [" +INSERT INTO films +VALUES (?, ?, ?, DEFAULT, ?, ?) +" "UA502", "Bananas", 105, "Comedy", "82 minutes"] + (format {:insert-into :films + :values [["UA502" "Bananas" 105 [:default] "Comedy" "82 minutes"]]} + {:pretty true}))) + (is (= [" +INSERT INTO films +(code, title, did, date_prod, kind) +VALUES (?, ?, ?, DEFAULT, ?) +" "T_601", "Yojimo", 106, "Drama"] + (format {:insert-into :films + :columns [:code :title :did :date_prod :kind] + :values [["T_601", "Yojimo", 106, [:default], "Drama"]]} + {:pretty true})))) + +(deftest on-conflict-tests + ;; these examples are taken from https://www.postgresqltutorial.com/postgresql-upsert/ + (is (= [" +INSERT INTO customers +(name, email) +VALUES ('Microsoft', 'hotline@microsoft.com') +ON CONFLICT ON CONSTRAINT customers_name_key +DO NOTHING +"] + (format {:insert-into :customers + :columns [:name :email] + :values [[[:inline "Microsoft"], [:inline "hotline@microsoft.com"]]] + :on-conflict {:on-constraint :customers_name_key} + :do-nothing true} + {:pretty true}))) + (is (= [" +INSERT INTO customers +(name, email) +VALUES ('Microsoft', 'hotline@microsoft.com') +ON CONFLICT +ON CONSTRAINT customers_name_key +DO NOTHING +"] + (format {:insert-into :customers + :columns [:name :email] + :values [[[:inline "Microsoft"], [:inline "hotline@microsoft.com"]]] + :on-conflict [] + :on-constraint :customers_name_key + :do-nothing true} + {:pretty true}))) + (is (= [" +INSERT INTO customers +(name, email) +VALUES ('Microsoft', 'hotline@microsoft.com') +ON CONFLICT (name) +DO NOTHING +"] + (format {:insert-into :customers + :columns [:name :email] + :values [[[:inline "Microsoft"], [:inline "hotline@microsoft.com"]]] + :on-conflict :name + :do-nothing true} + {:pretty true}))) + (is (= [" +INSERT INTO customers +(name, email) +VALUES ('Microsoft', 'hotline@microsoft.com') +ON CONFLICT (name) +DO NOTHING +"] + (format {:insert-into :customers + :columns [:name :email] + :values [[[:inline "Microsoft"], [:inline "hotline@microsoft.com"]]] + :on-conflict [:name] + :do-nothing true} + {:pretty true}))) + (is (= [" +INSERT INTO customers +(name, email) +VALUES ('Microsoft', 'hotline@microsoft.com') +ON CONFLICT (name, email) +DO NOTHING +"] + (format {:insert-into :customers + :columns [:name :email] + :values [[[:inline "Microsoft"], [:inline "hotline@microsoft.com"]]] + :on-conflict [:name :email] + :do-nothing true} + {:pretty true}))) + (is (= [" +INSERT INTO customers +(name, email) +VALUES ('Microsoft', 'hotline@microsoft.com') +ON CONFLICT (name) +DO UPDATE SET email = EXCLUDED.email || ';' || customers.email +"] + (format {:insert-into :customers + :columns [:name :email] + :values [[[:inline "Microsoft"], [:inline "hotline@microsoft.com"]]] + :on-conflict :name + :do-update-set {:email [:|| :EXCLUDED.email [:inline ";"] :customers.email]}} + {:pretty true})))) + +(deftest issue-285 + (is (= [" +SELECT * +FROM processes +WHERE state = ? +ORDER BY id = ? DESC +" 42 123] + (format (-> (h/select :*) + (h/from :processes) + (h/where [:= :state 42]) + (h/order-by [[:= :id 123] :desc])) + {:pretty true})))) + +(deftest issue-299-test + (let [name "test field" + ;; this was a bug in 1.x -- adding here to prevent regression: + enabled [true, "); SELECT case when (SELECT current_setting('is_superuser'))='off' then pg_sleep(0.2) end; -- "]] + (is (= ["INSERT INTO table (name, enabled) VALUES (?, (TRUE, ?))" name (second enabled)] + (format {:insert-into :table + :values [{:name name + :enabled enabled}]}))))) + +(deftest issue-316-test + (testing "SQL injection via keyword is detected" + (let [sort-column "foo; select * from users"] + (try + (-> {:select [:foo :bar] + :from [:mytable] + :order-by [(keyword sort-column)]} + (format)) + (is false "; not detected in entity!") + (catch #?(:clj Throwable :cljs :default) e + (is (:disallowed (ex-data e)))))))) + ;; should not produce: ["SELECT foo, bar FROM mytable ORDER BY foo; select * from users"] + + +(deftest issue-319-test + (testing "that registering a clause is idempotent" + (is (= ["FOO"] + (do + (sut/register-clause! :foo (constantly ["FOO"]) nil) + (sut/register-clause! :foo (constantly ["FOO"]) nil) + (format {:foo []})))))) + +(deftest issue-321-linting + (testing "empty IN is ignored by default" + (is (= ["WHERE x IN ()"] + (format {:where [:in :x []]}))) + (is (= ["WHERE x IN ()"] + (format {:where [:in :x :?y]} + {:params {:y []}})))) + (testing "empty IN is flagged in basic mode" + (is (thrown-with-msg? ExceptionInfo #"empty collection" + (format {:where [:in :x []]} + {:checking :basic}))) + (is (thrown-with-msg? ExceptionInfo #"empty collection" + (format {:where [:in :x :?y]} + {:params {:y []} :checking :basic})))) + (testing "IN NULL is ignored by default and basic" + (is (= ["WHERE x IN (NULL)"] + (format {:where [:in :x [nil]]}))) + (is (= ["WHERE x IN (NULL)"] + (format {:where [:in :x [nil]]} + {:checking :basic}))) + (is (= ["WHERE x IN (?)" nil] + (format {:where [:in :x :?y]} + {:params {:y [nil]}}))) + (is (= ["WHERE x IN (?)" nil] + (format {:where [:in :x :?y]} + {:params {:y [nil]} :checking :basic})))) + (testing "IN NULL is flagged in strict mode" + (is (thrown-with-msg? ExceptionInfo #"does not match" + (format {:where [:in :x [nil]]} + {:checking :strict}))) + (is (thrown-with-msg? ExceptionInfo #"does not match" + (format {:where [:in :x :?y]} + {:params {:y [nil]} :checking :strict}))))) + +(deftest quoting-:%-syntax + (testing "quoting of expressions in functions shouldn't depend on syntax" + (is (= ["SELECT SYSDATE()"] + (format {:select [[[:sysdate]]]}) + (format {:select :%sysdate}))) + (is (= ["SELECT COUNT(*)"] + (format {:select [[[:count :*]]]}) + (format {:select :%count.*}))) + (is (= ["SELECT AVERAGE(`foo-foo`)"] + (format {:select [[[:average :foo-foo]]]} :dialect :mysql) + (format {:select :%average.foo-foo} :dialect :mysql))) + (is (= ["SELECT GREATER(`foo-foo`, `bar-bar`)"] + (format {:select [[[:greater :foo-foo :bar-bar]]]} :dialect :mysql) + (format {:select :%greater.foo-foo.bar-bar} :dialect :mysql))) + (is (= ["SELECT MIXED_KEBAB(`yum-yum`)"] + (format {:select :%mixed-kebab.yum-yum} :dialect :mysql))) + (is (= ["SELECT MIXED_KEBAB(`yum_yum`)"] + (format {:select :%mixed-kebab.yum-yum} :dialect :mysql :quoted-snake true))) + ;; qualifier is always - -> _ converted: + (is (= ["SELECT MIXED_KEBAB(`yum_yum`.`bar-bar`, `a_b`.`c-d`)"] + (format {:select (keyword "%mixed-kebab.yum-yum/bar-bar.a-b/c-d")} :dialect :mysql))) + ;; name is only - -> _ converted when snake_case requested: + (is (= ["SELECT MIXED_KEBAB(`yum_yum`.`bar_bar`, `a_b`.`c_d`)"] + (format {:select (keyword "%mixed-kebab.yum-yum/bar-bar.a-b/c-d")} :dialect :mysql :quoted-snake true))) + (is (= ["SELECT RANSOM(`NoTe`)"] + (format {:select [[[:ransom :NoTe]]]} :dialect :mysql) + (format {:select :%ransom.NoTe} :dialect :mysql))))) + +(deftest join-without-on-using + ;; essentially issue 326 + (testing "join does not need on or using" + (is (= ["SELECT foo FROM bar INNER JOIN quux"] + (format {:select :foo + :from :bar + :join [:quux]})))) + (testing "join on select with parameters" + (is (= ["SELECT foo FROM bar INNER JOIN (SELECT a FROM b WHERE id = ?) WHERE id = ?" 123 456] + (format {:select :foo + :from :bar + :join [{:select :a :from :b :where [:= :id 123]}] + :where [:= :id 456]}))) + (is (= ["SELECT foo FROM bar INNER JOIN (SELECT a FROM b WHERE id = ?) AS x WHERE id = ?" 123 456] + (format {:select :foo + :from :bar + :join [[{:select :a :from :b :where [:= :id 123]} :x]] + :where [:= :id 456]}))) + (is (= ["SELECT foo FROM bar INNER JOIN (SELECT a FROM b WHERE id = ?) AS x ON y WHERE id = ?" 123 456] + (format {:select :foo + :from :bar + :join [[{:select :a :from :b :where [:= :id 123]} :x] :y] + :where [:= :id 456]})))))