Honey SQL
repository·develop·Indexed 23 days ago
https://github.com/seancorfield/honeysqlA library for building SQL queries using Clojure data structures instead of string manipulation. It allows for programmatic query construction at runtime and supports multiple SQL dialects (including MySQL, Oracle, SQL Server, and NRQL) and runtimes such as Clojure, ClojureScript, Babashka, Jolt, and let-go. Features include a helper DSL via honey.sql.helpers, support for parameterized SQL strings via sql/format, and extensibility through register-op!, register-fn!, and register-clause!.
What's inside Honey SQL
- Honey SQL allows you to represent SQL as Clojure data structures. This enables you to build queries programmatically—even at runtime—without the need to manually concatenate strings, which reduces errors and improves maintainability.
Supported Runtimes and Dialects
developHoneySQL is compatible with:
- Clojure
- ClojureScript
- Babashka
It also provides basic support for:
- let-go: Note that
honey.sql/formatvis omitted becauseclojure.templateis not provided inlet-go. - Jolt: Requires a dependency on
io.github.jolt-lang/timeand Jolt version 0.5.1 or higher.
How SQL entity generation works with symbols and keywords
developHoneySQL treats keywords and symbols as SQL entities. Their formatting depends on whether quoting is enabled (via
:dialector:quoted true) and how they are structured:- Unqualified symbols/keywords: If quoting is active, they are quoted as-is. If quoting is inactive, dashes (
-) are converted to underscores (_). - Dot notation (
.): A symbol containing a dot is split into a table/alias name and a column name. Dots are respected during quoting. - Slash notation (
/): A symbol containing a slash is treated as a qualified name (namespace/name). In the namespace portion, dashes (-) are converted to underscores (_) even when quoting is enabled. - String literals: In contexts where only a SQL entity is accepted (rather than an expression), strings are treated as entities. For strings, quoting is always applied, dashes are NOT converted to underscores, and slashes are NOT treated as qualifiers.
(require '[honey.sql :as sql]) ;; Quoting enabled (sql/format {:select :foo-bar} {:quoted true}) ;;=> ["SELECT \"foo-bar\""] ;; Quoting disabled (dashes become underscores) (sql/format {:select :foo-bar}) ;;=> ["SELECT foo_bar"] ;; Dot notation (table.column) (sql/format {:select :foo-bar.baz-quux} {:quoted true}) ;;=> ["SELECT \"foo-bar\".\"baz-quux\""] ;; Slash notation (namespace/name) (sql/format {:select :foo-bar/baz-quux} {:quoted true}) ;;=> ["SELECT \"foo_bar\".\"baz-quux\""] ;; Strings as entities (no dash conversion, no slash splitting) (sql/format {:update :table :set {"foo-bar" 1 "baz/quux" 2}}) ;;=> ["UPDATE table SET \"foo-bar\" = ?, \"baz/quux\" = ?" 1 2]- Unqualified symbols/keywords: If quoting is active, they are quoted as-is. If quoting is inactive, dashes (
Handle database precedence issues using :nest
developHoneySQL follows ANSI SQL precedence by default. Because different databases (like MySQL or SQLite) may have different precedence rules for
SETclauses or set operations (UNION,EXCEPT,INTERSECT), you may need to force specific grouping.You can use the
:nestpseudo-clause in the DSL to wrap the generated SQL in parentheses( ... )to ensure the intended precedence is respected by the target database.{:nest DSL} ;; will produce DSL wrapped in ( .. )Use the NRQL dialect for New Relic queries
developHoneySQL supports New Relic's NRQL query language by using the
:nrqldialect. When this dialect is selected, HoneySQL automatically sets:inline truefor formatting, ensuring the generated SQL string is compatible with NRQL requirements.Additionally, identifier stropping (quoting) uses backticks (similar to MySQL), but unlike MySQL, it does not split entities at
/or.characters. For example,:foo/bar.bazwill be rendered as`foo/bar.baz`.(sql/format {:select [:mulog/timestamp :mulog/event-name] :from :Log :where [:= :mulog/data.account "foo-account-id"] :since [2 :days :ago] :limit 2000} {:dialect :nrql :pretty true})Migrating from HoneySQL 1.x to 2.x
developHoneySQL 2.x features a streamlined codebase, a simpler DSL extension method, and out-of-the-box support for SQL dialects.
Because 1.x and 2.x use different group IDs and namespaces, you can use them side-by-side in the same project to facilitate a piecemeal migration. For a detailed comparison, refer to the
doc/differences-from-1-x.mdfile in the repository.Cache generated SQL with :cache
developProviding an atom containing a
clojure.core.cachestructure to the:cacheoption allowsformatto cache generated SQL strings based on the DSL data structure. This improves performance for repeated queries.Important: When using
:cache, you should use named parameters (starting with?) instead of regular values to ensure the cache lookup works correctly.Limitation: You cannot use named parameters with the
:inclause when using:cachebecause:inunrolls the parameter, breaking cache lookup rules.How to construct SQL tuples and composite values
developIn HoneySQL 2.x, vectors and sequences are treated as function calls. To represent SQL composite values (tuples like
(col1, col2)), use the:compositespecial syntax.Alternatively, you can use the
compositehelper function.(sql/format-expr [:composite :col1 :col2]) ;;=> ["(col1, col2)"] (sql/format-expr [:composite 13 42 "foo"]) ;;=> ["(?, ?, ?)" 13 42 "foo"] ;; Using symbols (sql/format-expr '(composite col1 col2)) ;;=> ["(col1, col2)"]Use FILTER, WITHIN GROUP, and Window functions
developHoneySQL supports advanced SQL features for aggregations and window functions:
- FILTER and WITHIN GROUP: These are available as "functions" in Special Syntax. There are also specific helpers for
filterandwithin-group. - Window Functions: Support for
:window,:partition-by, and:overis built-in. - ORDER BY in expressions: Supported as part of the special syntax integration.
- FILTER and WITHIN GROUP: These are available as "functions" in Special Syntax. There are also specific helpers for
Use :inline to embed values directly in SQL
developThe
:inlineoption (Boolean) suppresses the generation of?placeholders and instead embeds values directly into the SQL string.Warning: Using
:inline truecan introduce security vulnerabilities (SQL injection) because it bypasses parameterized statements. Use it only for trusted data or specific non-parameterized needs.Inlining behavior:
nil$\rightarrow$NULL- Clojure strings $\rightarrow$ single-quoted SQL strings (
'foo') - Keywords/Symbols $\rightarrow$ Uppercase SQL keywords (with
-replaced by space) - Others $\rightarrow$
(str value)
Handle Nested Sub-Queries with NEST_ONE and NEST_MANY
developXTDB uses
NEST_ONEandNEST_MANYto produce structured results from sub-queries. In HoneySQL, these are implemented as regular function calls within the:selectclause.Note: Function calls in
:selectclauses require three levels of nesting (parentheses/brackets) to ensure correct formatting::select [:col-a [:col-b :alias-b] [[:fn-call :col-c] :alias-c]].(sql/format '{select (a.* ((nest_many {select * from bar where (= foo_id a._id)}) b)) from ((foo a))}) ;; => ["SELECT a.*, NEST_MANY (SELECT * FROM bar WHERE foo_id = a._id) AS b FROM foo AS a"]Use boolean operators `and` and `or` with conditional expressions
developHoneySQL supports
andandoras boolean operators. They can take any number of arguments.Key Feature: Nil-safety
nilexpressions are ignored withinand/orblocks. This allows you to programmatically build queries by having conditional logic evaluate tonilwhen a clause should be omitted, rather than having to manually restructure the vector.Operators can be specified as keywords or symbols. Use
-in the operator name where the formatted SQL would have a space (e.g.,:not-likebecomesNOT LIKE).