From 44ffd340f56c896e4bcb2d9eedf8dbe1cc177532 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Sat, 26 Aug 2023 16:20:32 -0700 Subject: [PATCH] fix #501 by making insert/columns/values talk to each other --- CHANGELOG.md | 3 +- README.md | 47 ++++++------ doc/clause-reference.md | 11 ++- doc/postgresql.md | 48 ++++++------- src/honey/sql.cljc | 151 ++++++++++++++++++++++++--------------- test/honey/sql_test.cljc | 31 ++++---- 6 files changed, 163 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 457e405..d82256a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`) diff --git a/README.md b/README.md index 983ef94..1b75569 100644 --- a/README.md +++ b/README.md @@ -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"] @@ -606,8 +601,8 @@ regular function calls in a select: => ["SELECT MAX(id) FROM foo"] ``` -Custom columns using functions are built with the same vector format. -Be sure to properly nest the vectors so that the first element in the selection +Custom columns using functions are built with the same vector format. +Be sure to properly nest the vectors so that the first element in the selection is the custom function and the second is the column alias. ```clojure (sql/format @@ -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] ``` diff --git a/doc/clause-reference.md b/doc/clause-reference.md index f0a617c..376d2f9 100644 --- a/doc/clause-reference.md +++ b/doc/clause-reference.md @@ -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}]}) diff --git a/doc/postgresql.md b/doc/postgresql.md index dfcca9b..14afde0 100644 --- a/doc/postgresql.md +++ b/doc/postgresql.md @@ -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] diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 8a15db6..7f3d5e9 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -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 diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index c7591e4..3230a3d 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -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]})