diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ed1b94..eaafc4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changes * 2.2.next in progress - * Address [#352](https://github.com/seancorfield/honeysql/issues/352) by treating `:'` as introducing a function name that should be transcribed exactly as-is into the generated SQL. + * Address [#352](https://github.com/seancorfield/honeysql/issues/352) by treating `:'` as introducing a function name that should be formatted as a SQL entity (respects quoting, dot-splitting, etc). * 2.2.861 -- 2022-01-30 * Address [#382](https://github.com/seancorfield/honeysql/issues/382) by adding `:case-expr` for BigQuery support. diff --git a/README.md b/README.md index 18f995d..fcfcfd0 100644 --- a/README.md +++ b/README.md @@ -574,15 +574,24 @@ regular function calls in a select: => ["SELECT MAX(id) FROM foo"] ``` -If a keyword begins with `'`, the function name is treated literally -without being converted to uppercase (and without hyphens `-` becoming -spaces): +If a keyword begins with `'`, the function name is formatted as a SQL +entity rather than being converted to uppercase and having hyphens `-` +converted to spaces). That means that hyphens `-` will become underscores `_` +unless you have quoting enabled: ```clojure (-> (select :*) (from :foo) (where [:'my-schema.SomeFunction :bar 0]) (sql/format)) -=> ["SELECT * FROM foo WHERE my-schema.SomeFunction(bar, ?)" 0] +=> ["SELECT * FROM foo WHERE my_schema.SomeFunction(bar, ?)" 0] +(-> (select :*) (from :foo) + (where [:'my-schema.SomeFunction :bar 0]) + (sql/format :quoted true)) +=> ["SELECT * FROM \"foo\" WHERE \"my-schema\".\"SomeFunction\"(\"bar\", ?)" 0] +(-> (select :*) (from :foo) + (where [:'my-schema.SomeFunction :bar 0]) + (sql/format :dialect :mysql)) +=> ["SELECT * FROM `foo` WHERE `my-schema`.`SomeFunction`(`bar`, ?)" 0] ``` ### Bindable parameters diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 2ca8b55..d5bbfcd 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -164,31 +164,6 @@ s (recur (str/replace s #"(\w)-(\w)" "$1 $2") s)))) -(defn sql-kw - "Given a keyword, return a SQL representation of it as a string. - - A keyword whose name begins with a single quote is left exactly as-is - (with the `:` and `'` removed), otherwise a `:kebab-case` keyword - becomes a `KEBAB CASE` (uppercase) string with hyphens replaced - by spaces, e.g., `:insert-into` => `INSERT INTO`. - - Any namespace qualifier is ignored." - [k] - (let [n (name k)] - (if (= \' (first n)) - (subs n 1 (count n)) - (-> n (dehyphen) (upper-case))))) - -(defn- sym->kw - "Given a symbol, produce a keyword, retaining the namespace - qualifier, if any." - [s] - (if (symbol? s) - (if-let [n (namespace s)] - (keyword n (name s)) - (keyword (name s))) - s)) - (defn- namespace-_ "Return the namespace portion of a symbol, with dashes converted." [x] @@ -211,14 +186,6 @@ {:symbol x :failure (str t)}))))) -(defn- sqlize-value [x] - (cond - (nil? x) "NULL" - (string? x) (str \' (str/replace x "'" "''") \') - (ident? x) (sql-kw x) - (vector? x) (str "[" (str/join ", " (map #'sqlize-value x)) "]") - :else (str x))) - (defn format-entity "Given a simple SQL entity (a keyword or symbol -- or string), return the equivalent SQL fragment (as a string -- no parameters). @@ -255,6 +222,39 @@ [v a d (format-entity v {:aliased a :drop-ns d})]))) .) +(defn sql-kw + "Given a keyword, return a SQL representation of it as a string. + + A keyword whose name begins with a single quote is left exactly as-is + (with the `:` and `'` removed), otherwise a `:kebab-case` keyword + becomes a `KEBAB CASE` (uppercase) string with hyphens replaced + by spaces, e.g., `:insert-into` => `INSERT INTO`. + + Any namespace qualifier is ignored." + [k] + (let [n (name k)] + (if (= \' (first n)) + (format-entity (keyword (subs n 1 (count n)))) + (-> n (dehyphen) (upper-case))))) + +(defn- sym->kw + "Given a symbol, produce a keyword, retaining the namespace + qualifier, if any." + [s] + (if (symbol? s) + (if-let [n (namespace s)] + (keyword n (name s)) + (keyword (name s))) + s)) + +(defn- sqlize-value [x] + (cond + (nil? x) "NULL" + (string? x) (str \' (str/replace x "'" "''") \') + (ident? x) (sql-kw x) + (vector? x) (str "[" (str/join ", " (map #'sqlize-value x)) "]") + :else (str x))) + (defn- param-value [k] (if (contains? *params* k) (get *params* k) diff --git a/test/honey/sql_test.cljc b/test/honey/sql_test.cljc index 012fbc8..00b7b57 100644 --- a/test/honey/sql_test.cljc +++ b/test/honey/sql_test.cljc @@ -800,10 +800,14 @@ ORDER BY id = ? DESC (format {:select [[[:'sysdate]]]}))) (is (= ["SELECT count(*)"] (format {:select [[[:'count :*]]]}))) - (is (= ["SELECT Mixed-Kebab(`yum-yum`)"] + (is (= ["SELECT Mixed_Kebab(yum_yum)"] + (format {:select [[[:'Mixed-Kebab :yum-yum]]]}))) + (is (= ["SELECT `Mixed-Kebab`(`yum-yum`)"] (format {:select [[[:'Mixed-Kebab :yum-yum]]]} :dialect :mysql))) - (is (= ["SELECT other-project.other_dataset.other_function(?, ?)" 1 2] - (format {:select [[[:'other-project.other_dataset.other_function 1 2]]]}))))) + (is (= ["SELECT other_project.other_dataset.other_function(?, ?)" 1 2] + (format {:select [[[:'other-project.other_dataset.other_function 1 2]]]}))) + (is (= ["SELECT \"other-project\".\"other_dataset\".\"other_function\"(?, ?)" 1 2] + (format {:select [[[:'other-project.other_dataset.other_function 1 2]]]} :dialect :ansi))))) (deftest join-without-on-using ;; essentially issue 326