fix #501 by making insert/columns/values talk to each other

This commit is contained in:
Sean Corfield 2023-08-26 16:20:32 -07:00
parent aa4ebf5f47
commit 44ffd340f5
6 changed files with 163 additions and 128 deletions

View file

@ -1,11 +1,12 @@
# Changes
* 2.4.next in progress
* Add `:create-or-replace-view` to support PostgreSQL's lack of `IF NOT EXISTS` for `CREATE VIEW`.
* Add `:select` with function call and alias example to README (PR [#502](https://github.com/seancorfield/honeysql/pull/502) [@markbastian](https://github.com/markbastian)).
* Address [#501](https://github.com/seancorfield/honeysql/issues/501) by making `INSERT INTO` (and `REPLACE INTO`) use the `:columns` or `:values` clauses to produce column names (which are then omitted from those other clauses).
* Address [#497](https://github.com/seancorfield/honeysql/issues/497) by adding `:alias` special syntax.
* Address [#407](https://github.com/seancorfield/honeysql/issues/407) by adding support for temporal queries (see `FROM` in [SQL Clause Reference](https://cljdoc.org/d/com.github.seancorfield/honeysql/CURRENT/doc/getting-started/sql-clause-reference#from)).
* Address [#389](https://github.com/seancorfield/honeysql/issues/389) by adding examples of `[:only :table]` producing `ONLY(table)`.
* Add `:create-or-replace-view` to support PostgreSQL's lack of `IF NOT EXISTS` for `CREATE VIEW`.
* Attempt to clarify the formatting behavior of the `:values` clause when used to produce column names.
* Update `tools.build` to 0.9.5 (and remove `:java-opts` setting from `build/run-task`)

View file

@ -241,8 +241,7 @@ then provide a collection of rows, each a collection of column values:
["Jane" "Daniels" 56]])
(sql/format {:pretty true}))
=> ["
INSERT INTO properties
(name, surname, age)
INSERT INTO properties (name, surname, age)
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
"
"Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56]
@ -254,8 +253,7 @@ VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
["Jane" "Daniels" 56]]}
(sql/format {:pretty true}))
=> ["
INSERT INTO properties
(name, surname, age)
INSERT INTO properties (name, surname, age)
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
"
"Jon" "Smith" 34 "Andrew" "Cooper" 12 "Jane" "Daniels" 56]
@ -272,8 +270,8 @@ Alternately, you can simply specify the values as maps:
{:name "Jane" :surname "Daniels" :age 56}])
(sql/format {:pretty true}))
=> ["
INSERT INTO properties
(name, surname, age) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
INSERT INTO properties (name, surname, age)
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
"
"John" "Smith" 34
"Andrew" "Cooper" 12
@ -285,8 +283,8 @@ INSERT INTO properties
{:name "Jane", :surname "Daniels", :age 56}]}
(sql/format {:pretty true}))
=> ["
INSERT INTO properties
(name, surname, age) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
INSERT INTO properties (name, surname, age)
VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
"
"John" "Smith" 34
"Andrew" "Cooper" 12
@ -306,8 +304,8 @@ a set of column names that should get the value `DEFAULT` instead of `NULL`:
{:name "Jane" :surname "Daniels"}])
(sql/format {:pretty true}))
=> ["
INSERT INTO properties
(name, surname, age) VALUES (?, ?, ?), (?, NULL, ?), (?, ?, NULL)
INSERT INTO properties (name, surname, age)
VALUES (?, ?, ?), (?, NULL, ?), (?, ?, NULL)
"
"John" "Smith" 34
"Andrew" 12
@ -318,8 +316,8 @@ INSERT INTO properties
{:name "Jane" :surname "Daniels"}])
(sql/format {:pretty true :values-default-columns #{:age}}))
=> ["
INSERT INTO properties
(name, surname, age) VALUES (?, ?, ?), (?, NULL, ?), (?, ?, DEFAULT)
INSERT INTO properties (name, surname, age)
VALUES (?, ?, ?), (?, NULL, ?), (?, ?, DEFAULT)
"
"John" "Smith" 34
"Andrew" 12
@ -341,8 +339,8 @@ The column values do not have to be literals, they can be nested queries:
(sql/format {:pretty true})))
=> ["
INSERT INTO user_profile_to_role
(user_profile_id, role_id) VALUES (?, (SELECT id FROM role WHERE name = ?))
INSERT INTO user_profile_to_role (user_profile_id, role_id)
VALUES (?, (SELECT id FROM role WHERE name = ?))
"
12345
"user"]
@ -356,8 +354,8 @@ INSERT INTO user_profile_to_role
:where [:= :name "user"]}}]}
(sql/format {:pretty true})))
=> ["
INSERT INTO user_profile_to_role
(user_profile_id, role_id) VALUES (?, (SELECT id FROM role WHERE name = ?))
INSERT INTO user_profile_to_role (user_profile_id, role_id)
VALUES (?, (SELECT id FROM role WHERE name = ?))
"
12345
"user"]
@ -398,8 +396,7 @@ Composite types are supported:
["large" (composite 10 "feet")]])
(sql/format {:pretty true}))
=> ["
INSERT INTO comp_table
(name, comp_column)
INSERT INTO comp_table (name, comp_column)
VALUES (?, (?, ?)), (?, (?, ?))
"
"small" 1 "inch" "large" 10 "feet"]
@ -411,8 +408,7 @@ VALUES (?, (?, ?)), (?, (?, ?))
["large" (composite 10 "feet")]])
(sql/format {:pretty true :numbered true}))
=> ["
INSERT INTO comp_table
(name, comp_column)
INSERT INTO comp_table (name, comp_column)
VALUES ($1, ($2, $3)), ($4, ($5, $6))
"
"small" 1 "inch" "large" 10 "feet"]
@ -423,8 +419,7 @@ VALUES ($1, ($2, $3)), ($4, ($5, $6))
["large" [:composite 10 "feet"]]]}
(sql/format {:pretty true}))
=> ["
INSERT INTO comp_table
(name, comp_column)
INSERT INTO comp_table (name, comp_column)
VALUES (?, (?, ?)), (?, (?, ?))
"
"small" 1 "inch" "large" 10 "feet"]
@ -751,8 +746,8 @@ have a lot of function calls needed in code:
[:cast 4325 :integer]]}])
(sql/format {:pretty true}))
=> ["
INSERT INTO sample
(location) VALUES (ST_SETSRID(ST_MAKEPOINT(?, ?), CAST(? AS INTEGER)))
INSERT INTO sample (location)
VALUES (ST_SETSRID(ST_MAKEPOINT(?, ?), CAST(? AS INTEGER)))
"
0.291 32.621 4325]
```

View file

@ -585,7 +585,8 @@ There are three use cases with `:insert-into`.
The first case takes just a table specifier (either a
table name or a table/alias pair),
and then you can optionally specify the columns (via a `:columns` clause).
and then you can optionally specify the columns (via a `:columns` clause,
or via a `:values` clause using hash maps).
The second case takes a pair of a table specifier (either a
table name or table/alias pair) and a sequence of column
@ -1103,8 +1104,12 @@ values.
### values with hash maps
If you provide a sequence of hash maps, the `:values` clause
will generate a `VALUES` clause with the column names preceding
and the row values following.
will generate a `VALUES` clause, and will also generate the column names
as part of the `INSERT INTO` (or `REPLACE INTO`) statement.
If there is no `INSERT INTO` (or `REPLACE INTO`) statement in the context
of the `:values` clause, the column names will be generated as a part of
the `VALUES` clause itself.
```clojure
user=> (sql/format {:values [{:col-a 1 :col-b 2}]})

View file

@ -102,8 +102,8 @@ user=> (-> (insert-into :distributors)
(returning :*)
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?), (?, ?)
INSERT INTO distributors (did, dname)
VALUES (?, ?), (?, ?)
ON CONFLICT (did)
DO UPDATE SET dname = EXCLUDED.dname
RETURNING *
@ -124,8 +124,8 @@ user=> (-> (insert-into :distributors)
(returning :*)
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?), (?, ?)
INSERT INTO distributors (did, dname)
VALUES (?, ?), (?, ?)
ON CONFLICT (did)
DO UPDATE SET dname = EXCLUDED.dname
RETURNING *
@ -144,8 +144,8 @@ user=> (-> (insert-into :distributors)
do-nothing))
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?)
INSERT INTO distributors (did, dname)
VALUES (?, ?)
ON CONFLICT (did)
DO NOTHING
"
@ -161,8 +161,8 @@ user=> (-> (insert-into :distributors)
do-nothing
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?)
INSERT INTO distributors (did, dname)
VALUES (?, ?)
ON CONFLICT (did)
DO NOTHING
"
@ -180,8 +180,8 @@ user=> (-> (insert-into :distributors)
do-nothing
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?)
INSERT INTO distributors (did, dname)
VALUES (?, ?)
ON CONFLICT ON CONSTRAINT distributors_pkey
DO NOTHING
"
@ -194,8 +194,8 @@ user=> (-> (insert-into :distributors)
do-nothing
(sql/format {:pretty true}))
["
INSERT INTO distributors
(did, dname) VALUES (?, ?)
INSERT INTO distributors (did, dname)
VALUES (?, ?)
ON CONFLICT
ON CONSTRAINT distributors_pkey
DO NOTHING
@ -215,8 +215,8 @@ user=> (-> (insert-into :user)
(do-update-set :phone :name (where [:= :user.active false]))
(sql/format {:pretty true}))
["
INSERT INTO user
(phone, name) VALUES (?, ?)
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
"
@ -231,8 +231,8 @@ user=> (sql/format
:where [:= :user.active false]}}
{:pretty true})
["
INSERT INTO user
(phone, name) VALUES (?, ?)
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
"
@ -268,8 +268,8 @@ user=> (-> (insert-into :table)
(do-update-set {:counter [:+ :table.counter 1]})
(sql/format {:pretty true}))
["
INSERT INTO table
(id, counter) VALUES (?, ?)
INSERT INTO table (id, counter)
VALUES (?, ?)
ON CONFLICT (id)
DO UPDATE SET counter = table.counter + ?
" "id" 1 1]
@ -280,8 +280,8 @@ user=> (-> {:insert-into :table
:do-update-set {:counter [:+ :table.counter 1]}}
(sql/format {:pretty true}))
["
INSERT INTO table
(id, counter) VALUES (?, ?)
INSERT INTO table (id, counter)
VALUES (?, ?)
ON CONFLICT (id)
DO UPDATE SET counter = table.counter + ?
" "id" 1 1]
@ -300,8 +300,8 @@ user=> (-> (insert-into :table)
:where [:> :table.counter 1]})
(sql/format {:pretty true}))
["
INSERT INTO table
(id, counter) VALUES (?, ?)
INSERT INTO table (id, counter)
VALUES (?, ?)
ON CONFLICT (id)
DO UPDATE SET counter = table.counter + ? WHERE table.counter > ?
" "id" 1 1 1]
@ -313,8 +313,8 @@ user=> (-> {:insert-into :table
:where [:> :table.counter 1]}}
(sql/format {:pretty true}))
["
INSERT INTO table
(id, counter) VALUES (?, ?)
INSERT INTO table (id, counter)
VALUES (?, ?)
ON CONFLICT (id)
DO UPDATE SET counter = table.counter + ? WHERE table.counter > ?
" "id" 1 1 1]

View file

@ -132,7 +132,7 @@
(def ^:private ^:dynamic *allow-suspicious-entities* false)
;; "linting" mode (:none, :basic, :strict):
(def ^:private ^:dynamic *checking* @default-checking)
;; the current DSL hash map being formatted (for contains-clause?):
;; the current DSL hash map being formatted (for clause-body / contains-clause?):
(def ^:private ^:dynamic *dsl* nil)
;; caching data to detect expressions that cannot be cached:
(def ^:private ^:dynamic *caching* nil)
@ -140,15 +140,21 @@
;; clause helpers
(defn clause-body
"If the current DSL expression being formatted contains the specified clause
(as a keyword or symbol), returns that clause's value."
[clause]
(or (get *dsl* clause)
(get *dsl*
(if (keyword? clause)
(symbol (name clause))
(keyword (name clause))))))
(defn contains-clause?
"Returns true if the current DSL expression being formatted
contains the specified clause (as a keyword or symbol)."
[clause]
(or (contains? *dsl* clause)
(contains? *dsl*
(if (keyword? clause)
(symbol (name clause))
(keyword (name clause))))))
(some? (clause-body clause)))
(defn- mysql?
"Helper to detect if MySQL is the current dialect."
@ -628,8 +634,12 @@
)
(defn- format-columns [k xs]
(let [[sqls params] (format-expr-list xs {:drop-ns (= :columns k)})]
(into [(str "(" (str/join ", " sqls) ")")] params)))
(if (and (= :columns k)
(or (contains-clause? :insert-into)
(contains-clause? :replace-into)))
[]
(let [[sqls params] (format-expr-list xs {:drop-ns true})]
(into [(str "(" (str/join ", " sqls) ")")] params))))
(defn- format-selects-common [prefix as xs]
(if (sequential? xs)
@ -732,42 +742,60 @@
(defn- format-selector [k xs]
(format-selects k [xs]))
(declare columns-from-values)
(defn- format-insert [k table]
(if (sequential? table)
(cond (map? (second table))
(let [[table statement] table
[table cols]
(if (and (sequential? table) (sequential? (second table)))
table
[table])
[sql & params] (format-dsl statement)
[t-sql & t-params] (format-entity-alias table)
[c-sqls c-params] (reduce-sql (map #'format-entity-alias cols))]
(-> [(str (sql-kw k) " " t-sql
" "
(when (seq cols)
(str "("
(str/join ", " c-sqls)
") "))
sql)]
(into t-params)
(into c-params)
(into params)))
(sequential? (second table))
(let [[table cols] table
[t-sql & t-params] (format-entity-alias table)
[c-sqls c-params] (reduce-sql (map #'format-entity-alias cols))]
(-> [(str (sql-kw k) " " t-sql
" ("
(str/join ", " c-sqls)
")")]
(into t-params)
(into c-params)))
:else
(let [[sql & params] (format-entity-alias table)]
(into [(str (sql-kw k) " " sql)] params)))
(let [[sql & params] (format-entity-alias table)]
(into [(str (sql-kw k) " " sql)] params))))
(let [[cols' cols-sql' cols-params']
(if-let [columns (clause-body :columns)]
(cons columns (format-columns :force-columns columns))
(when-let [values (clause-body :values)]
(columns-from-values values false)))]
(if (sequential? table)
(cond (map? (second table))
(let [[table statement] table
[table cols]
(if (and (sequential? table) (sequential? (second table)))
table
[table])
[sql & params] (format-dsl statement)
[t-sql & t-params] (format-entity-alias table)
[c-sqls c-params] (reduce-sql (map #'format-entity-alias cols))]
(-> [(str (sql-kw k) " " t-sql
" "
(cond (seq cols)
(str "("
(str/join ", " c-sqls)
") ")
(seq cols')
(str cols-sql' " "))
sql)]
(into t-params)
(into c-params)
(into cols-params')
(into params)))
(sequential? (second table))
(let [[table cols] table
[t-sql & t-params] (format-entity-alias table)
[c-sqls c-params] (reduce-sql (map #'format-entity-alias cols))]
(-> [(str (sql-kw k) " " t-sql
" ("
(str/join ", " c-sqls)
")")]
(into t-params)
(into c-params)))
:else
(let [[sql & params] (format-entity-alias table)]
(-> [(str (sql-kw k) " " sql
(when (seq cols')
(str " " cols-sql')))]
(into cols-params')
(into params))))
(let [[sql & params] (format-entity-alias table)]
(-> [(str (sql-kw k) " " sql
(when (seq cols')
(str " " cols-sql')))]
(into cols-params')
(into params))))))
(comment
(format-insert :insert-into [[[:raw ":foo"]] {:select :bar}])
@ -879,6 +907,22 @@
(when nowait
(str " " (sql-kw nowait))))))]))
(defn- columns-from-values [xs skip-cols-sql]
(let [first-xs (when (sequential? xs) (first (drop-while ident? xs)))]
(when (map? first-xs)
(let [cols-1 (keys (first xs))
;; issue #291: check for all keys in all maps but still
;; use the keys from the first map if they match so that
;; users can rely on the key ordering if they want to,
;; e.g., see test that uses array-map for the first row
cols-n (into #{} (mapcat keys) (filter map? xs))
cols (if (= (set cols-1) cols-n) cols-1 cols-n)]
[cols (when-not skip-cols-sql
(str "("
(str/join ", "
(map #(format-entity % {:drop-ns true}) cols))
")"))]))))
(defn- format-values [k xs]
(let [first-xs (when (sequential? xs) (first (drop-while ident? xs)))]
(cond (contains? #{:default 'default} xs)
@ -913,13 +957,10 @@
(map? first-xs)
;; [{:a 1 :b 2 :c 3}]
(let [cols-1 (keys (first xs))
;; issue #291: check for all keys in all maps but still
;; use the keys from the first map if they match so that
;; users can rely on the key ordering if they want to,
;; e.g., see test that uses array-map for the first row
cols-n (into #{} (mapcat keys) (filter map? xs))
cols (if (= (set cols-1) cols-n) cols-1 cols-n)
(let [[cols cols-sql]
(columns-from-values xs (or (contains-clause? :insert-into)
(contains-clause? :replace-into)
(contains-clause? :columns)))
[sqls params]
(reduce (fn [[sql params] [sqls' params']]
[(conj sql
@ -941,10 +982,8 @@
cols))
[(sql-kw m)]))
xs))]
(into [(str "("
(str/join ", "
(map #(format-entity % {:drop-ns true}) cols))
") "
(into [(str (when cols-sql
(str cols-sql " "))
(sql-kw k)
" "
(str/join ", " sqls))]
@ -2187,10 +2226,6 @@
:where [:= :u.id :u2.id]}
{:inline true})
(sql/register-clause! :output :select :values)
(sql/format {:insert-into :foo :output [:inserted.*] :values [{:bar 1}]})
(sql/format {:insert-into :foo :columns [:bar] :output [:inserted.*] :values [[1]]})
(sql/format {:select [[:a.b :c.d]]} {:dialect :mysql})
(sql/format {:select [[:column-name :'some-alias]]
:from :b

View file

@ -687,8 +687,7 @@ VALUES (?, ?, ?, ?, ?, ?)
:values [["UA502" "Bananas" 105 "1971-07-13" "Comedy" "82 minutes"]]}
{:pretty true})))
(is (= ["
INSERT INTO films
(code, title, did, date_prod, kind)
INSERT INTO films (code, title, did, date_prod, kind)
VALUES (?, ?, ?, ?, ?)
" "T_601", "Yojimo", 106, "1961-06-16", "Drama"]
(format {:insert-into :films
@ -703,8 +702,7 @@ VALUES (?, ?, ?, DEFAULT, ?, ?)
:values [["UA502" "Bananas" 105 [:default] "Comedy" "82 minutes"]]}
{:pretty true})))
(is (= ["
INSERT INTO films
(code, title, did, date_prod, kind)
INSERT INTO films (code, title, did, date_prod, kind)
VALUES (?, ?, ?, DEFAULT, ?)
" "T_601", "Yojimo", 106, "Drama"]
(format {:insert-into :films
@ -715,8 +713,7 @@ VALUES (?, ?, ?, DEFAULT, ?)
(deftest on-conflict-tests
;; these examples are taken from https://www.postgresqltutorial.com/postgresql-upsert/
(is (= ["
INSERT INTO customers
(name, email)
INSERT INTO customers (name, email)
VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT ON CONSTRAINT customers_name_key
DO NOTHING
@ -728,8 +725,7 @@ DO NOTHING
:do-nothing true}
{:pretty true})))
(is (= ["
INSERT INTO customers
(name, email)
INSERT INTO customers (name, email)
VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT
ON CONSTRAINT customers_name_key
@ -743,8 +739,7 @@ DO NOTHING
:do-nothing true}
{:pretty true})))
(is (= ["
INSERT INTO customers
(name, email)
INSERT INTO customers (name, email)
VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT (name)
DO NOTHING
@ -756,8 +751,7 @@ DO NOTHING
:do-nothing true}
{:pretty true})))
(is (= ["
INSERT INTO customers
(name, email)
INSERT INTO customers (name, email)
VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT (name)
DO NOTHING
@ -769,8 +763,7 @@ DO NOTHING
:do-nothing true}
{:pretty true})))
(is (= ["
INSERT INTO customers
(name, email)
INSERT INTO customers (name, email)
VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT ((foo + ?), name, (TRIM(email)))
DO NOTHING
@ -782,8 +775,7 @@ DO NOTHING
:do-nothing true}
{:pretty true})))
(is (= ["
INSERT INTO customers
(name, email)
INSERT INTO customers (name, email)
VALUES ('Microsoft', 'hotline@microsoft.com')
ON CONFLICT (name)
DO UPDATE SET email = EXCLUDED.email || ';' || customers.email
@ -1250,6 +1242,13 @@ ORDER BY id = ? DESC
:order-by [[[:alias "some-alias"]]]}
{:dialect :mysql}))))
(deftest output-clause-post-501
(sut/register-clause! :output :select :values)
(is (= ["INSERT INTO foo (bar) OUTPUT inserted.* VALUES (?)" 1]
(sut/format {:insert-into :foo :output [:inserted.*] :values [{:bar 1}]})))
(is (= ["INSERT INTO foo (bar) OUTPUT inserted.* VALUES (?)" 1]
(sut/format {:insert-into :foo :columns [:bar] :output [:inserted.*] :values [[1]]}))))
(comment
;; partial workaround for #407:
(sut/format {:select :f.* :from [[:foo [:f :for :system-time]]] :where [:= :f.id 1]})