Liking cljdoc? Tell your friends :D

honeysql-h2

Installation

Add to your project.clj:

[ai.z7/honeysql-h2 "0.0.6"]

Or deps.edn:

ai.z7/honeysql-h2 {:mvn/version "0.0.6"}

Features

  • 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

Usage

(require '[honeysql-h2.core :as h2])
(require '[honey.sql :as sql])

UPSERT (MERGE INTO)

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"]

LISTAGG Aggregate Function

Full support for H2’s LISTAGG function with all optional clauses.

H2 Syntax Reference

LISTAGG ( [ DISTINCT | ALL ] expression [, separator]
  [ ON OVERFLOW { ERROR | TRUNCATE [filler] { WITH | WITHOUT } COUNT } ] )
  WITHIN GROUP (ORDER BY ...)
  [ FILTER (WHERE ...) ]
  [ OVER (...) ]

Low-Level Usage

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"]

ON OVERFLOW Clause

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)

Helper Function

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]})

Window Function Support

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]})

Sequences

Full support for H2’s SEQUENCE operations including CREATE, ALTER, DROP, and value expressions.

H2 Syntax Reference

-- 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

CREATE SEQUENCE

;; 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"]

ALTER SEQUENCE

;; 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"]

DROP SEQUENCE

;; 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"]

Value Expressions

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"]

Helper Functions

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"]

Sequence Options Reference

OptionDescription

:if-not-exists

Add IF NOT EXISTS clause (CREATE only)

:if-exists

Add IF EXISTS clause (ALTER/DROP only)

:as

Data type (e.g., :bigint, :int)

:start-with

Starting value

:restart

Restart the sequence (ALTER only)

:restart-with

Restart with specific value (ALTER only)

:increment-by

Increment value

:minvalue

Minimum value

:no-minvalue

Use NO MINVALUE

:maxvalue

Maximum value

:no-maxvalue

Use NO MAXVALUE

:cycle

Enable cycling

:no-cycle

Disable cycling

:cache

Cache size

:no-cache

Disable caching

Boolean Coercion

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

Timestamp Helper

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"

Automatic Date/Time Conversion

The library extends clojure.java.jdbc to automatically convert JDBC date/time types to Java 8 types when reading result sets:

JDBC TypeConverted To

java.sql.Date

java.time.LocalDate

java.sql.Time

java.time.LocalTime

java.sql.Timestamp

java.time.LocalDateTime

This happens automatically when you require the namespace.

API Reference

Functions

FunctionDescription

(upsert data table)
(upsert data table pk)

Generate MERGE INTO SQL for upserting records. pk can be a keyword (single key) or vector (composite key).

(listagg expr & opts)

Build a LISTAGG expression. Options: :separator, :distinct, :on-overflow, :within-group, :filter, :over

(create-sequence name)
(create-sequence name opts)

Build a CREATE SEQUENCE statement map.

(alter-sequence name opts)

Build an ALTER SEQUENCE statement map.

(drop-sequence name)
(drop-sequence name opts)

Build a DROP SEQUENCE statement map.

(nextval name)

Build a NEXT VALUE FOR expression: [:next-value-for name]

(currval name)

Build a CURRENT VALUE FOR expression: [:current-value-for name]

(boolean! v)

Coerce any value to boolean with type-aware handling.

(fbool coll)

Extract first element from collection and coerce to boolean.

(timestamp)
(timestamp millis)

Create a java.sql.Timestamp from current time or milliseconds.

HoneySQL Extensions

KeywordDescription

:listagg

LISTAGG function: [:listagg expr separator? overflow?]

:listagg-distinct

LISTAGG with DISTINCT: [:listagg-distinct expr separator? overflow?]

:merge-into

H2 MERGE INTO clause

:records

Records clause for MERGE INTO (internal)

:create-sequence

CREATE SEQUENCE clause: {:create-sequence name} or {:create-sequence [name opts]}

:alter-sequence

ALTER SEQUENCE clause: {:alter-sequence [name opts]}

:drop-sequence

DROP SEQUENCE clause: {:drop-sequence name} or {:drop-sequence [name opts]}

:next-value-for

NEXT VALUE FOR function: [:next-value-for seq-name]

:current-value-for

CURRENT VALUE FOR function: [:current-value-for seq-name]

License

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 & MelKori
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close