From d70e89ae3bf77829efc27c161358f7f62d3a0cf3 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Wed, 12 Mar 2025 13:36:49 -0700 Subject: [PATCH 1/3] part of #570 -- colon path selection Signed-off-by: Sean Corfield --- CHANGELOG.md | 3 +++ doc/special-syntax.md | 12 ++++++++-- src/honey/sql.cljc | 15 +++++++----- test/honey/sql_test.cljc | 49 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21e0631..b57415a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changes +* 2.7.next in progress + * Address #570 by adding `:.:.` as special syntax for Snowflake's JSON path syntax. + * 2.6.1281 -- 2025-03-06 * Address [#568](https://github.com/seancorfield/honeysql/issues/568) by adding `honey.sql/semicolon` to merge multiple SQL+params vectors into one (with semicolons separating the SQL statements). * Address [#567](https://github.com/seancorfield/honeysql/issues/567) by adding support for `ASSERT` clause. diff --git a/doc/special-syntax.md b/doc/special-syntax.md index 97d872c..5f109e0 100644 --- a/doc/special-syntax.md +++ b/doc/special-syntax.md @@ -211,15 +211,23 @@ Accepts a single expression and prefixes it with `DISTINCT `: ;;=> ["SELECT COUNT(DISTINCT status) AS n FROM table"] ``` -## dot . +## dot . .:. -Accepts an expression and a field (or column) selection: +Accepts an expression and one or more fields (or columns). Plain dot produces +plain dotted selection: ```clojure (sql/format {:select [ [[:. :t :c]] [[:. :s :t :c]] ]}) ;;=> ["SELECT t.c, s.t.c"] ``` +Dot colon dot produces Snowflake-style dotted selection: + +```clojure +(sql/format {:select [ [[:.:. :t :c]] [[:.:. :s :t :c]] ]}) +;;=> ["SELECT t:c, s:t.c"] +``` + Can be used with `:nest` for field selection from composites: ```clojure diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 4bcf9f8..9668deb 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -1953,6 +1953,12 @@ (let [[sql & params] (format-expr x)] (into [(str sql " " (sql-kw k))] params))) +(defn dot-navigation [sep [expr col & subcols]] + (let [[sql & params] (format-expr expr)] + (into [(str sql sep (format-entity col) + (when (seq subcols) + (str "." (join "." (map format-entity subcols)))))] + params))) (def ^:private special-syntax (atom {;; these "functions" are mostly used in column @@ -1973,12 +1979,9 @@ :references #'function-1 :unique #'function-1-opt ;; dynamic dotted name creation: - :. (fn [_ [expr col subcol]] - (let [[sql & params] (format-expr expr)] - (into [(str sql "." (format-entity col) - (when subcol - (str "." (format-entity subcol))))] - params))) + :. (fn [_ data] (dot-navigation "." data)) + ;; snowflake variant #570: + :.:. (fn [_ data] (dot-navigation ":" data)) ;; used in DDL to force rendering as a SQL entity instead ;; of a SQL keyword: :entity (fn [_ [e]] [(format-entity e)]) diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index ba6bce4..f5c4301 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -1178,9 +1178,10 @@ ORDER BY id = ? DESC (deftest issue-474-dot-selection (testing "basic dot selection" - (is (= ["SELECT a.b, c.d, a.d.x"] + (is (= ["SELECT a.b, c.d, a.d.x, a.d.x.y"] (let [t :a c :d] - (sut/format {:select [[[:. t :b]] [[:. :c c]] [[:. t c :x]]]})))) + (sut/format {:select [[[:. t :b]] [[:. :c c]] + [[:. t c :x]] [[:. t c :x :y]]]})))) (is (= ["SELECT [a].[b], [c].[d], [a].[d].[x]"] (let [t :a c :d] (sut/format {:select [[[:. t :b]] [[:. :c c]] [[:. t c :x]]]} @@ -1194,6 +1195,45 @@ ORDER BY id = ? DESC (sut/format '{select (((. (nest v) *)) ((. (nest w) x)) ((. (nest (y z)) *)))} + {:dialect :mysql}))) + (is (= ["SELECT (v).*, (w).x, (Y(z)).*"] + (sut/format '{select (((get-in v *)) + ((get-in w x)) + ((get-in (y z) *)))}))) + (is (= ["SELECT (`v`).*, (`w`).`x`, (Y(`z`)).*"] + (sut/format '{select (((get-in v *)) + ((get-in w x)) + ((get-in (y z) *)))} + {:dialect :mysql}))))) + +(deftest issue-570-snowflake-dot-selection + (testing "basic colon selection" + (is (= ["SELECT a:b, c:d, a:d.x, a:d.x.y"] + (let [t :a c :d] + (sut/format {:select [[[:.:. t :b]] [[:.:. :c c]] + [[:.:. t c :x]] [[:.:. t c :x :y]]]})))) + (is (= ["SELECT [a]:[b], [c]:[d], [a]:[d].[x]"] + (let [t :a c :d] + (sut/format {:select [[[:.:. t :b]] [[:.:. :c c]] [[:.:. t c :x]]]} + {:dialect :sqlserver}))))) + (testing "basic field selection from composite" + (is (= ["SELECT (v):*, (w):x, (Y(z)):*"] + (sut/format '{select (((.:. (nest v) *)) + ((.:. (nest w) x)) + ((.:. (nest (y z)) *)))}))) + (is (= ["SELECT (`v`):*, (`w`):`x`, (Y(`z`)):*"] + (sut/format '{select (((.:. (nest v) *)) + ((.:. (nest w) x)) + ((.:. (nest (y z)) *)))} + {:dialect :mysql}))) + (is (= ["SELECT (v):*, (w):x, (Y(z)):*"] + (sut/format '{select (((get-in v *)) + ((get-in w x)) + ((get-in (y z) *)))}))) + (is (= ["SELECT (`v`):*, (`w`):`x`, (Y(`z`)):*"] + (sut/format '{select (((get-in v *)) + ((get-in w x)) + ((get-in (y z) *)))} {:dialect :mysql}))))) (deftest issue-476-raw @@ -1451,4 +1491,9 @@ ORDER BY id = ? DESC :select [:*] :from [:a-b.b-c.c-d]} (sut/format {:dialect :nrql})) + (sut/format {:select :a:b.c}) ; quotes a:b + (sut/format [:. :a :b :c]) ; a.b.c + (sut/format [:. :a :b :c :d]) ; drops d ; a.b.c + (sut/format [:.:. :a :b :c]) ; .(a, b, c) + (sut/format '(.:. a b c)) ; .(a, b, c) ) From 44494e61c0df7446cc6257b5af6055246ebc67a0 Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Wed, 12 Mar 2025 13:43:02 -0700 Subject: [PATCH 2/3] remove failing tests for #570 Signed-off-by: Sean Corfield --- test/honey/sql_test.cljc | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index f5c4301..a8dbf37 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -1,4 +1,4 @@ -;; copyright (c) 2021-2024 sean corfield, all rights reserved +;; copyright (c) 2021-2025 sean corfield, all rights reserved (ns honey.sql-test (:refer-clojure :exclude [format]) @@ -1225,15 +1225,6 @@ ORDER BY id = ? DESC (sut/format '{select (((.:. (nest v) *)) ((.:. (nest w) x)) ((.:. (nest (y z)) *)))} - {:dialect :mysql}))) - (is (= ["SELECT (v):*, (w):x, (Y(z)):*"] - (sut/format '{select (((get-in v *)) - ((get-in w x)) - ((get-in (y z) *)))}))) - (is (= ["SELECT (`v`):*, (`w`):`x`, (Y(`z`)):*"] - (sut/format '{select (((get-in v *)) - ((get-in w x)) - ((get-in (y z) *)))} {:dialect :mysql}))))) (deftest issue-476-raw From d74046c658e065d43cd4dc30dbb90421d6a6f91b Mon Sep 17 00:00:00 2001 From: Sean Corfield Date: Wed, 12 Mar 2025 14:48:09 -0700 Subject: [PATCH 3/3] fix #570 by adding :at there is also :.:. Signed-off-by: Sean Corfield --- CHANGELOG.md | 2 +- doc/special-syntax.md | 26 ++++++++++++++++++++++++++ src/honey/sql.cljc | 21 ++++++++++++++------- test/honey/sql_test.cljc | 18 +++++++++++++++++- 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b57415a..77ebb0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changes * 2.7.next in progress - * Address #570 by adding `:.:.` as special syntax for Snowflake's JSON path syntax. + * Address #570 by adding `:.:.` as special syntax for Snowflake's JSON path syntax, and `:at` as special syntax for general `[`..`]` path syntax. * 2.6.1281 -- 2025-03-06 * Address [#568](https://github.com/seancorfield/honeysql/issues/568) by adding `honey.sql/semicolon` to merge multiple SQL+params vectors into one (with semicolons separating the SQL statements). diff --git a/doc/special-syntax.md b/doc/special-syntax.md index 5f109e0..dccd7e9 100644 --- a/doc/special-syntax.md +++ b/doc/special-syntax.md @@ -88,6 +88,29 @@ In the subquery case, produces `ARRAY(subquery)`: ;;=> ["SELECT ARRAY(SELECT * FROM table) AS arr"] ``` +## at + +If addition to dot navigation (for JSON) -- see the `.` and `.:.` syntax below -- +HoneySQL also supports bracket notation for JSON navigation. + +The first argument to `:at` is treated as an expression that identifies +the column, and subsequent arguments are treated as field names or array +indices to navigate into that document. + +```clojure +user=> (sql/format {:select [[[:at :col :field1 :field2]]]}) +["SELECT col.field1.field2"] +user=> (sql/format {:select [[[:at :table.col 0 :field]]]}) +["SELECT table.col[0].field"] +``` + +If you want an array index to be a parameter, use `:lift`: + +```clojure +user=> (sql/format {:select [[[:at :col [:lift 0] :field]]]}) +["SELECT col[?].field" 0] +``` + ## at time zone Accepts two arguments: an expression (assumed to be a date/time of some sort) @@ -235,6 +258,9 @@ Can be used with `:nest` for field selection from composites: ;;=> ["SELECT (v).*, (MYFUNC(x)).y"] ``` +See also [`get-in`](xtdb.md#object-navigation-expressions) +and [`at`](#at) for additional path navigation functions. + ## entity Accepts a single keyword or symbol argument and produces a diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 9668deb..6026763 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -1936,28 +1936,34 @@ (defn- get-in-navigation "[:get-in expr key-or-index1 key-or-index2 ...]" - [_ [expr & kix]] + [wrap [expr & kix]] (let [[sql & params] (format-expr expr) [sqls params'] (reduce-sql (map #(cond (number? %) [(str "[" % "]")] + (string? %) + [(str "[" (sqlize-value %) "]")] (ident? %) [(str "." (format-entity %))] :else (let [[sql' & params'] (format-expr %)] (cons (str "[" sql' "]") params'))) kix))] - (into* [(str "(" sql ")" (join "" sqls))] params params'))) + (into* [(str (if wrap (str "(" sql ")") sql) + (join "" sqls))] + params + params'))) -(defn ignore-respect-nulls [k [x]] +(defn- ignore-respect-nulls [k [x]] (let [[sql & params] (format-expr x)] (into [(str sql " " (sql-kw k))] params))) -(defn dot-navigation [sep [expr col & subcols]] +(defn- dot-navigation [sep [expr col & subcols]] (let [[sql & params] (format-expr expr)] - (into [(str sql sep (format-entity col) + (into [(str sql sep (format-simple-expr col "dot navigation") (when (seq subcols) - (str "." (join "." (map format-entity subcols)))))] + (str "." (join "." (map #(format-simple-expr % "dot navigation") + subcols)))))] params))) (def ^:private special-syntax (atom @@ -2006,6 +2012,7 @@ (let [[sqls params] (format-expr-list arr) type-str (when type (str "::" (sql-kw type) "[]"))] (into [(str "ARRAY[" (join ", " sqls) "]" type-str)] params)))) + :at (fn [_ data] (get-in-navigation false data)) :at-time-zone (fn [_ [expr tz]] (let [[sql & params] (format-expr expr {:nested true}) @@ -2038,7 +2045,7 @@ [sql-e & params-e] (format-expr escape-chars)] (into* [(str sql-p " " (sql-kw :escape) " " sql-e)] params-p params-e))) :filter expr-clause-pairs - :get-in #'get-in-navigation + :get-in (fn [_ data] (get-in-navigation true data)) :ignore-nulls ignore-respect-nulls :inline (fn [_ xs] diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index a8dbf37..3bd63bc 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -1225,7 +1225,23 @@ ORDER BY id = ? DESC (sut/format '{select (((.:. (nest v) *)) ((.:. (nest w) x)) ((.:. (nest (y z)) *)))} - {:dialect :mysql}))))) + {:dialect :mysql})))) + (testing "bracket selection" + (is (= ["SELECT a['b'], c['b'], a['d'].x, a:e[0].name"] + (sut/format {:select [[[:at :a [:inline "b"]]] + [[:at :c "b"]] + [[:at :a [:inline "d"] :x]] + [[:.:. :a [:at :e [:inline 0]] :name]]]}))) + (is (= ["SELECT a[?].name" 0] + (sut/format '{select (((at a (lift 0) name)))}))) + ;; sanity check, compare with get-in: + (is (= ["SELECT (a)[?].name" 0] + (sut/format '{select (((get-in a (lift 0) name)))}))) + (is (= ["SELECT (a)['b'], (c)['b'], (a)['d'].x, a:(e)[0].name"] + (sut/format {:select [[[:get-in :a [:inline "b"]]] + [[:get-in :c "b"]] + [[:get-in :a [:inline "d"] :x]] + [[:.:. :a [:get-in :e [:inline 0]] :name]]]}))))) (deftest issue-476-raw (testing "single argument :raw"