DuckDB type coercion and helpers for Clojure over next.jdbc: LIST, STRUCT,
MAP, and ENUM columns round-trip as plain Clojure data, plus thin wrappers for
read_parquet, read_csv, ATTACH, extensions, and Appender bulk inserts.
deps.edn:
net.clojars.savya/duckdb-clj {:mvn/version "0.2.0"}
Leiningen:
[net.clojars.savya/duckdb-clj "0.2.0"]
Bundles org.duckdb/duckdb_jdbc (the embedded database - no server) and
extends next.jdbc's protocols on load.
(require '[duckdb.core :as duck] ; requiring this also activates the type coercion
'[next.jdbc :as jdbc])
(def ds (duck/file-datasource "analytics.db")) ; or (duck/memory-datasource)
(jdbc/execute! ds ["create type mood as enum ('sad', 'ok', 'happy')"])
(jdbc/execute! ds ["create table events (
id int,
tags varchar[],
user struct(name varchar, age int),
counts map(varchar, int),
mood mood)"])
;; write: plain Clojure data binds into LIST / STRUCT / MAP / ENUM parameters
(jdbc/execute! ds ["insert into events values (?, ?, ?, ?, ?)"
1
["signup" "mobile"]
{:name "alice" :age 30}
{"clicks" 12 "views" 40}
:happy])
;; read: they come back as Clojure data
(jdbc/execute! ds ["select * from events"])
;; => [{:id 1
;; :tags ["signup" "mobile"]
;; :user {:name "alice" :age 30}
;; :counts {"clicks" 12 "views" 40}
;; :mood "happy"}]
Nesting works in both directions (LIST of STRUCT, STRUCT containing MAP, ...). ENUM columns read as strings; write keywords or strings as parameters.
(jdbc/execute! ds ["create table metrics (
id int,
name varchar,
score double,
tags varchar[],
info struct(source varchar, batch int))"])
(duck/append!
ds
:metrics
[{:name "alpha" :score 1.5 :id 1
:tags ["daily" "mobile"]
:info {:source "api" :batch 7}}
{:id 2 :name "beta" :score 2.25
:tags []
:info {:source "job" :batch 8}}])
;; => 2
append! takes rows as maps and appends values in the table's declared column
order, not the map's iteration order. It uses DuckDB's Appender API and supports
common scalar values plus nested LIST and STRUCT values.
(duck/read-parquet ds "data/*.parquet")
(duck/read-parquet ds "data/*.parquet" {:union-by-name true :file-row-number true})
(duck/read-csv ds "data.csv" {:header true})
(duck/attach! ds "other.db" "other") ; then: select * from other.t
(duck/attach! ds "other.db" "other" {:read-only true})
(duck/detach! ds "other")
(duck/install-extension! ds "httpfs")
(duck/load-extension! ds "httpfs")
(duck/duckdb-version ds)
Option maps render to DuckDB named arguments ({:union-by-name true} →
union_by_name = true); option names are validated as identifiers and string
values are SQL-escaped.
{:name "alice"}); on write, the
Clojure map is bound positionally against the column's declared field order,
and an absent field key throws (:duckdb/error :missing-struct-field) -
explicit nil values are fine.MAP(INT, VARCHAR) reads back as {1 "x"}).(memory-datasource) is a
separate database. Hold one connection (jdbc/get-connection) for
multi-statement work.:id, not :events/id) - DuckDB's JDBC driver
does not report table names for result columns.java.util.Map ReadableColumn extension is process-global across all
next.jdbc usage in the JVM (it converts Java maps to Clojure maps - benign,
but noted).append! uses DuckDB's Appender API for faster row ingest
from Clojure maps. For bulk columnar extracts and very large analytical
transfers, use
tmducken (DuckDB C API ->
tech.ml.dataset) or DuckDB's
ADBC client instead.Errors are ex-info maps keyed :duckdb/error
(:missing-struct-field, :invalid-option, :invalid-alias,
:append-failed).
clojure -M:test
Everything runs against in-memory DuckDB - no services, nothing to download.
Copyright © 2026 Savyasachi.
Distributed under the Eclipse Public License 2.0.
Can you improve this documentation?Edit 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 |