[ai.z7/honeysql-h2 "0.0.6"]
H2 database dialect extension for the HoneySQL library.
Add to your project.clj:
[ai.z7/honeysql-h2 "0.0.6"]
Or deps.edn:
ai.z7/honeysql-h2 {:mvn/version "0.0.6"}
UPSERT support via H2’s MERGE INTO statement
LISTAGG aggregate function with full H2 syntax support
SEQUENCE support (CREATE, ALTER, DROP, NEXT/CURRENT VALUE FOR)
Automatic JDBC date/time conversion to Java 8 types
Boolean coercion utilities
(require '[honeysql-h2.core :as h2])
(require '[honey.sql :as sql])
The upsert function generates H2’s MERGE INTO statement for insert-or-update operations.
;; Basic upsert without key
(h2/upsert [{:name "Alice" :age 30}] :users)
;; => ["MERGE INTO users (name, age) VALUES (?, ?)" "Alice" 30]
;; With single primary key
(h2/upsert [{:id 1 :name "Alice" :age 30}] :users :id)
;; => ["MERGE INTO users (id, name, age) KEY(id) VALUES (?, ?, ?)" 1 "Alice" 30]
;; With composite primary key
(h2/upsert [{:tenant "acme" :id 1 :name "Alice"}] :users [:tenant :id])
;; => ["MERGE INTO users (tenant, id, name) KEY(tenant,id) VALUES (?, ?, ?)" "acme" 1 "Alice"]
;; Multiple records
(h2/upsert [{:id 1 :name "Alice"} {:id 2 :name "Bob"}] :users :id)
;; => ["MERGE INTO users (id, name) KEY(id) VALUES (?, ?), (?, ?)" 1 "Alice" 2 "Bob"]
Full support for H2’s LISTAGG function with all optional clauses.
LISTAGG ( [ DISTINCT | ALL ] expression [, separator]
[ ON OVERFLOW { ERROR | TRUNCATE [filler] { WITH | WITHOUT } COUNT } ] )
WITHIN GROUP (ORDER BY ...)
[ FILTER (WHERE ...) ]
[ OVER (...) ]
Use HoneySQL expressions directly:
;; Basic LISTAGG
(sql/format {:select [[[:listagg :name ", "] :names]]
:from [:users]})
;; => ["SELECT LISTAGG(name, ', ') AS names FROM users"]
;; With DISTINCT
(sql/format {:select [[[:listagg-distinct :name ", "] :names]]
:from [:users]})
;; => ["SELECT LISTAGG(DISTINCT name, ', ') AS names FROM users"]
;; With WITHIN GROUP (ordering)
(sql/format {:select [[[:within-group
[:listagg :name ", "]
{:order-by [:name]}]
:names]]
:from [:users]})
;; => ["SELECT LISTAGG(name, ', ') WITHIN GROUP (ORDER BY name ASC) AS names FROM users"]
;; With FILTER clause
(sql/format {:select [[[:filter
[:within-group [:listagg :name ", "] {:order-by [:name]}]
{:where [:= :active true]}]
:names]]
:from [:users]})
;; => ["SELECT LISTAGG(name, ', ') WITHIN GROUP (ORDER BY name ASC)
;; FILTER (WHERE active = TRUE) AS names FROM users"]
Handle overflow when the result exceeds the maximum length:
;; ON OVERFLOW ERROR (default H2 behavior)
[:listagg :name ", " :error]
;; ON OVERFLOW TRUNCATE
[:listagg :name ", " [:truncate]]
;; ON OVERFLOW TRUNCATE with custom filler
[:listagg :name ", " [:truncate "..."]]
;; ON OVERFLOW TRUNCATE with filler and count indicator
[:listagg :name ", " [:truncate "..." :with-count]]
;; => LISTAGG(name, ', ' ON OVERFLOW TRUNCATE '...' WITH COUNT)
[:listagg :name ", " [:truncate "..." :without-count]]
;; => LISTAGG(name, ', ' ON OVERFLOW TRUNCATE '...' WITHOUT COUNT)
The listagg helper provides a convenient keyword-argument interface:
(require '[honeysql-h2.core :refer [listagg]])
;; Simple usage
(listagg :name)
;; => [:listagg :name]
;; With separator
(listagg :name :separator ", ")
;; => [:listagg :name ", "]
;; With DISTINCT
(listagg :name :separator ", " :distinct true)
;; => [:listagg-distinct :name ", "]
;; With ordering
(listagg :name :separator ", " :within-group {:order-by [:name]})
;; => [:within-group [:listagg :name ", "] {:order-by [:name]}]
;; Full example with all options
(listagg :name
:separator ", "
:distinct true
:on-overflow [:truncate "..." :with-count]
:within-group {:order-by [:name]}
:filter [:= :active true])
;; Use in a query
(sql/format {:select [[(listagg :name
:separator ", "
:within-group {:order-by [:name]}
:filter [:> :age 18])
:adult_names]]
:from [:users]
:group-by [:department]})
LISTAGG can also be used as a window function with the :over clause:
;; Using the helper
(listagg :name
:separator ", "
:within-group {:order-by [:name]}
:over {:partition-by [:department]})
;; Direct HoneySQL syntax (note the nested vector structure)
(sql/format {:select [[[:over
[[[:within-group [:listagg :name ", "] {:order-by [:name]}]
{:partition-by [:department]}]]]
:dept_names]]
:from [:employees]})
Full support for H2’s SEQUENCE operations including CREATE, ALTER, DROP, and value expressions.
-- Create
CREATE SEQUENCE [IF NOT EXISTS] name [AS type] [START WITH n] [INCREMENT BY n]
[MINVALUE n | NO MINVALUE] [MAXVALUE n | NO MAXVALUE] [CYCLE | NO CYCLE] [CACHE n | NO CACHE]
-- Alter
ALTER SEQUENCE [IF EXISTS] name [RESTART [WITH n]] [INCREMENT BY n] ...
-- Drop
DROP SEQUENCE [IF EXISTS] name
-- Value expressions
NEXT VALUE FOR sequence_name
CURRENT VALUE FOR sequence_name
;; Basic sequence
(sql/format {:create-sequence :my_seq})
;; => ["CREATE SEQUENCE my_seq"]
;; With IF NOT EXISTS
(sql/format {:create-sequence [:my_seq {:if-not-exists true}]})
;; => ["CREATE SEQUENCE IF NOT EXISTS my_seq"]
;; With all options
(sql/format {:create-sequence [:my_seq {:if-not-exists true
:as :bigint
:start-with 100
:increment-by 10
:minvalue 1
:maxvalue 1000
:cycle true
:cache 32}]})
;; => ["CREATE SEQUENCE IF NOT EXISTS my_seq AS BIGINT START WITH 100
;; INCREMENT BY 10 MINVALUE 1 MAXVALUE 1000 CYCLE CACHE 32"]
;; Restart with value
(sql/format {:alter-sequence [:my_seq {:restart-with 500}]})
;; => ["ALTER SEQUENCE my_seq RESTART WITH 500"]
;; With IF EXISTS
(sql/format {:alter-sequence [:my_seq {:if-exists true :restart-with 1}]})
;; => ["ALTER SEQUENCE IF EXISTS my_seq RESTART WITH 1"]
;; Modify increment
(sql/format {:alter-sequence [:my_seq {:increment-by 5}]})
;; => ["ALTER SEQUENCE my_seq INCREMENT BY 5"]
;; Basic drop
(sql/format {:drop-sequence :my_seq})
;; => ["DROP SEQUENCE my_seq"]
;; With IF EXISTS
(sql/format {:drop-sequence [:my_seq {:if-exists true}]})
;; => ["DROP SEQUENCE IF EXISTS my_seq"]
Use NEXT VALUE FOR and CURRENT VALUE FOR in queries:
;; In SELECT
(sql/format {:select [[[:next-value-for :my_seq] :id]]})
;; => ["SELECT NEXT VALUE FOR my_seq AS id"]
(sql/format {:select [[[:current-value-for :my_seq] :current_id]]})
;; => ["SELECT CURRENT VALUE FOR my_seq AS current_id"]
;; In INSERT
(sql/format {:insert-into :users
:values [{:id [:next-value-for :my_seq] :name "Alice"}]})
;; => ["INSERT INTO users (id, name) VALUES (NEXT VALUE FOR my_seq, ?)" "Alice"]
;; Schema-qualified sequences
(sql/format {:select [[[:next-value-for :myschema.my_seq] :id]]})
;; => ["SELECT NEXT VALUE FOR myschema.my_seq AS id"]
Convenience functions for building sequence operations:
(require '[honeysql-h2.core :as h2])
;; CREATE SEQUENCE
(h2/create-sequence :my_seq)
;; => {:create-sequence :my_seq}
(h2/create-sequence :my_seq {:start-with 100 :increment-by 10})
;; => {:create-sequence [:my_seq {:start-with 100 :increment-by 10}]}
;; ALTER SEQUENCE
(h2/alter-sequence :my_seq {:restart-with 500})
;; => {:alter-sequence [:my_seq {:restart-with 500}]}
;; DROP SEQUENCE
(h2/drop-sequence :my_seq)
;; => {:drop-sequence :my_seq}
(h2/drop-sequence :my_seq {:if-exists true})
;; => {:drop-sequence [:my_seq {:if-exists true}]}
;; Value expressions for use in queries
(h2/nextval :my_seq)
;; => [:next-value-for :my_seq]
(h2/currval :my_seq)
;; => [:current-value-for :my_seq]
;; Full workflow example
(sql/format (h2/create-sequence :user_id_seq {:start-with 1000}))
;; => ["CREATE SEQUENCE user_id_seq START WITH 1000"]
(sql/format {:insert-into :users
:values [{:id (h2/nextval :user_id_seq) :name "Bob"}]})
;; => ["INSERT INTO users (id, name) VALUES (NEXT VALUE FOR user_id_seq, ?)" "Bob"]
| Option | Description |
|---|---|
| Add IF NOT EXISTS clause (CREATE only) |
| Add IF EXISTS clause (ALTER/DROP only) |
| Data type (e.g., |
| Starting value |
| Restart the sequence (ALTER only) |
| Restart with specific value (ALTER only) |
| Increment value |
| Minimum value |
| Use NO MINVALUE |
| Maximum value |
| Use NO MAXVALUE |
| Enable cycling |
| Disable cycling |
| Cache size |
| Disable caching |
The boolean! function provides sophisticated boolean coercion from various types.
(require '[honeysql-h2.core :refer [boolean! fbool]])
;; Booleans pass through
(boolean! true) ;; => true
(boolean! false) ;; => false
;; nil is false
(boolean! nil) ;; => false
;; Strings (case-insensitive)
(boolean! "true") ;; => true
(boolean! "TRUE") ;; => true
(boolean! "yes") ;; => true
(boolean! "on") ;; => true
(boolean! "1") ;; => true
(boolean! "t") ;; => true
(boolean! "y") ;; => true
(boolean! "false") ;; => false
(boolean! "no") ;; => false
(boolean! "off") ;; => false
(boolean! "0") ;; => false
(boolean! "f") ;; => false
(boolean! "n") ;; => false
(boolean! "") ;; => false
;; Whitespace is trimmed
(boolean! " true ") ;; => true
;; Unknown strings are false (strict mode)
(boolean! "maybe") ;; => false
;; Numbers: positive = true, zero/negative = false
(boolean! 1) ;; => true
(boolean! 42) ;; => true
(boolean! 0.5) ;; => true
(boolean! 0) ;; => false
(boolean! -1) ;; => false
;; Collections: non-empty = true, empty = false
(boolean! [1 2 3]) ;; => true
(boolean! []) ;; => false
(boolean! {:a 1}) ;; => true
(boolean! {}) ;; => false
The fbool function extracts the first element and converts to boolean (useful for query results):
;; Useful for single-value query results
(fbool [{:exists true}]) ;; => true
(fbool [true]) ;; => true
(fbool [1]) ;; => true
(fbool [nil]) ;; => false
(fbool []) ;; => false
Create java.sql.Timestamp instances:
(require '[honeysql-h2.core :refer [timestamp]])
;; Current timestamp
(timestamp)
;; => #inst "2026-..."
;; From milliseconds
(timestamp 1234567890000)
;; => #inst "2009-02-13T23:31:30.000-00:00"
The library extends clojure.java.jdbc to automatically convert JDBC date/time types to Java 8 types when reading result sets:
| JDBC Type | Converted To |
|---|---|
|
|
|
|
|
|
This happens automatically when you require the namespace.
HoneySQL 2.7.x
H2 Database 2.4.x
clojure.java.jdbc 0.7.x
| Function | Description |
|---|---|
| Generate MERGE INTO SQL for upserting records. |
| Build a LISTAGG expression. Options: |
| Build a CREATE SEQUENCE statement map. |
| Build an ALTER SEQUENCE statement map. |
| Build a DROP SEQUENCE statement map. |
| Build a NEXT VALUE FOR expression: |
| Build a CURRENT VALUE FOR expression: |
| Coerce any value to boolean with type-aware handling. |
| Extract first element from collection and coerce to boolean. |
| Create a |
| Keyword | Description |
|---|---|
| LISTAGG function: |
| LISTAGG with DISTINCT: |
| H2 MERGE INTO clause |
| Records clause for MERGE INTO (internal) |
| CREATE SEQUENCE clause: |
| ALTER SEQUENCE clause: |
| DROP SEQUENCE clause: |
| NEXT VALUE FOR function: |
| CURRENT VALUE FOR function: |
Copyright (c) 2022-2024 UPD Consulting Ltd (c) 2024-2026 Fern Flower Lab
Distributed under the MIT License.
Can you improve this documentation? These fine people already did:
A I & MelKoriEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |