Liking cljdoc? Tell your friends :D

duckdb-clj

Clojars Project cljdoc test

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.

Stack

Clojure DuckDB next.jdbc

Installation

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.

Usage

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

Bulk append

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

File helpers

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

Semantics worth knowing

  • STRUCT fields are keywordized on read ({: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 keys are not keywordized (DuckDB map keys can be any type, e.g. MAP(INT, VARCHAR) reads back as {1 "x"}).
  • In-memory databases: each connection to a (memory-datasource) is a separate database. Hold one connection (jdbc/get-connection) for multi-statement work.
  • Result keys are unqualified (:id, not :events/id) - DuckDB's JDBC driver does not report table names for result columns.
  • The 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).
  • Bulk transfer: 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).

Running tests

clojure -M:test

Everything runs against in-memory DuckDB - no services, nothing to download.

License

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

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