MoonBit bindings for DuckDB on native and JavaScript targets (Node.js and browser WASM backends)
Dependencies
Upgrading from 0.6.4? See MIGRATION.md for the 0.6.4 → 0.7.0 breaking changes (structured errors, 128-bit Decimal, typed Arrow vectors, the quack package move) with before/after examples.
| Feature | Native | JS (Node) | JS (WASM) |
|---|---|---|---|
| Connection & Query | ✅ | ✅ | ✅ |
| Prepared Statements | ✅ | ✅ | ✅ |
| Streaming Results | ✅ | ✅ | ✅ |
| Appender | ✅ | ✅ (Node only) | ❌ |
| Appender DataChunk | ✅ | ✅ (Node only) | ❌ |
| Arrow Integration | ✅ | ✅ | ✅ |
| Advanced Types | ⚠️ | ⚠️ | ⚠️ |
| Type | Native Bind | Native Append | Node Bind | Node Append | WASM Bind |
|---|---|---|---|---|---|
| Decimal | ✅ 128-bit | ✅ 128-bit | ✅ 128-bit | ✅ 128-bit | ✅ direct string param + SQL cast |
| Interval | ✅ | ✅ | ✅ | ✅ | ✅ direct string param + SQL cast |
| Blob | ✅ | ✅ | ✅ | ✅ | ❌ unsupported |
| List | ✅ VARCHAR | ✅ VARCHAR | ✅ VARCHAR (Node only) | ❌ | ❌ unsupported |
| Struct | ✅ VARCHAR | ✅ VARCHAR | ✅ VARCHAR (Node only) | ❌ | ❌ unsupported |
| Map | ✅ VARCHAR | ✅ VARCHAR | ✅ VARCHAR (Node only) | ❌ | ❌ unsupported |
| Backend | Command | Runner | DuckDB under test |
|---|---|---|---|
| Native | moon check --target native + moon test --target native | macos-latest, ubuntu-latest | libduckdb 1.4.5, 1.5.6 |
| JS (Node) | moon check --target js + moon test --target js | ubuntu-latest, Node 24 | @duckdb/node-api 1.4.3-r.3 (minimum), 1.5.6-r.1 (pinned) |
| JS (WASM) | pnpm test:wasm-browser (Playwright Chromium) | ubuntu-latest | @duckdb/duckdb-wasm 1.33.1-dev18.0 (minimum), 1.33.1-dev65.0 (pinned) |
brew install duckdb# Download a release that matches your platform
wget https://github.com/duckdb/duckdb/releases/download/<version>/libduckdb-linux-amd64.zip
unzip libduckdb-linux-amd64.zip
sudo cp libduckdb.so /usr/local/lib/
sudo ldconfiggit clone https://github.com/duckdb/duckdb.git
cd duckdb
mkdir build && cd build
cmake ..
make -j$(nproc)
sudo make install
sudo ldconfigmoon build --target-native -- -L/usr/local/lib -Wl,-rpath,/usr/local/lib -lduckdbpnpm installpnpm installpnpm test:wasm-browserpnpm test:wasm-browser:installconnect(on_ready=fn(result) {
match result {
Ok(conn) => {
@quack.install(conn, on_done=fn(_) { () })
@quack.load(conn, on_done=fn(_) { () })
@quack.serve(
conn,
"quack:localhost",
token="super_secret",
on_done=fn(started) {
match started {
Ok(info) => println("quack server: \{info.rows}")
Err(err) => println("quack serve failed: \{err}")
}
},
)
}
Err(err) => println("connect failed: \{err}")
}
})@quack.create_secret(
conn,
"super_secret",
scope="quack:localhost",
on_done=fn(_) { () },
)
@quack.query(
conn,
"quack:localhost",
"SELECT 42 AS answer",
token="super_secret",
on_done=fn(result) {
match result {
Ok(rows) => println("\{rows.rows}")
Err(err) => println("quack query failed: \{err}")
}
},
)
@quack.attach(
conn,
"quack:localhost",
"remote_db",
token="super_secret",
on_done=fn(_) { () },
)connect(on_ready=fn (result) {
match result {
Ok(conn) => {
conn.query(
"select 1 as a, NULL as b, 'duck' as c",
on_done=fn (query_result) {
match query_result {
Ok(result) => {
println("columns: \{result.columns}")
println("rows: \{result.rows}")
println("nulls: \{result.nulls}")
}
Err(err) => println("query failed: \{err}")
}
},
)
conn.close(on_done=fn (closed) {
match closed {
Ok(_) => ()
Err(err) => println("close failed: \{err}")
}
})
}
Err(err) => println("connect failed: \{err}")
}
})conn.query("select 1 as a, 2.5 as b, NULL as c", on_done=fn (query_result) {
match query_result {
Ok(result) => {
let value = result.get_int(0, 0) // Some(1)
let typed = result.to_typed()
let b0 = typed.get_double(0, 1)
let c0 = typed.get_string(0, 2) // None
println("\{value} \{b0} \{c0}")
}
Err(err) => println("query failed: \{err}")
}
})connect(on_ready=fn (result) {
match result {
Ok(conn) => {
conn.query_stream(
"SELECT i FROM RANGE(1000000) tbl(i)",
on_done=fn (stream_result) {
match stream_result {
Ok(stream) => {
let mut total = 0
let done = Ref::new(false)
while !done.val {
stream.next(on_done=fn (chunk_result) {
match chunk_result {
Ok(Some(chunk)) => total = total + chunk.row_count()
Ok(None) => done.val = true
Err(err) => {
done.val = true
println("stream failed: \{err}")
}
}
})
}
stream.close(on_done=fn (_) { () })
println("rows: \{total}")
}
Err(err) => println("stream failed: \{err}")
}
},
)
}
Err(err) => println("connect failed: \{err}")
}
})// Aggregate state to track running totals
pub struct Aggregates {
mut total_rows : Int
mut sum_values : Int
mut min_value : Int?
mut max_value : Int?
}
let agg_ref = Ref::new({ total_rows: 0, sum_values: 0, min_value: None, max_value: None })
let done_ref = Ref::new(false)
conn.query_stream(
"SELECT value FROM measurements",
on_done=fn (stream_result) {
match stream_result {
Ok(stream) => {
while !done_ref.val {
stream.next(on_done=fn (chunk_result) {
match chunk_result {
Ok(Some(chunk)) => {
// Process each row in the chunk
for row = 0; row < chunk.row_count(); row = row + 1 {
match chunk.cell(row, 0) {
Some(v) => {
let value = parse_int(v)
agg_ref.val.total_rows = agg_ref.val.total_rows + 1
agg_ref.val.sum_values = agg_ref.val.sum_values + value
// Update min/max...
}
None => ()
}
}
}
Ok(None) => done_ref.val = true
Err(err) => { done_ref.val = true; println("error: \{err}") }
}
})
}
stream.close(on_done=fn (_) {
println("Total: \{agg_ref.val.total_rows}")
println("Sum: \{agg_ref.val.sum_values}")
})
}
Err(err) => println("stream failed: \{err}")
}
},
)connect(
on_ready=fn (result) { /* ... */ },
backend=JsBackend::Wasm,
)connect(on_ready=fn (result) {
match result {
Ok(conn) => {
let caps = conn.capabilities()
if caps.appender {
// safe to call conn.create_appender(...)
}
match caps.require(BackendFeature::BlobBind) {
Ok(_) => () // bind_blob is available
Err(err) => println("gated: \{err.message()}")
}
}
Err(err) => println("connect failed: \{err}")
}
})let config = Config::create()
match config.set("memory_limit", "1GB") {
Ok(_) => ()
Err(err) => println("config set failed: \{err}")
}
connect_with_config(
on_ready=fn (result) { /* ... */ },
config=Some(config),
path=":memory:",
)match stmt.bind_int(1, 42) {
Ok(_) =>
match stmt.bind_varchar(2, "hello") {
Ok(_) => ()
Err(e) => println("bind failed: \{e}")
}
Err(e) => println("bind failed: \{e}")
}pub suberror DuckDBError {
Query(String)
Prepare(String)
Bind(String)
Append(String)
Unsupported(feature~ : String, backend~ : String, message~ : String)
Closed(resource~ : String)
InvalidArgument(argument~ : String, reason~ : String)
Backend(backend~ : String, message~ : String)
DuckDB(error_type~ : String, message~ : String)
}#deprecated("DuckDBError::Message was replaced by structured variants in 0.7.0; use err.message() for the diagnostic text or match on the Query/Prepare/Bind/Append/Unsupported/Closed/InvalidArgument/Backend/DuckDB variants")
fn DuckDBError::to_message(self : DuckDBError) -> Stringpub type Appenderfn Appender::append_list_varchar(self : Appender, values : Array[String]) -> Result[Unit, DuckDBError]fn Appender::append_list_varchar_value(self : Appender, values : Array[String]) -> Result[Unit, DuckDBError]pub struct ArrowField {
name : String
nullable : Bool
type_id : String
}pub struct ArrowResult {
// private fields
}#deprecated("arrow getters now read canonical typed vectors; use ArrowResult::to_chunks with Vector::bools/value_at for new code")
fn ArrowResult::get_column_bool(self : ArrowResult, col : Int) -> Array[Bool]#deprecated("arrow getters now read canonical typed vectors; use ArrowResult::to_chunks with Vector::bools and Vector::is_null for new code")
fn ArrowResult::get_column_bool_nullable(self : ArrowResult, col : Int) -> (Array[Bool], Array[Bool])#deprecated("arrow getters now read canonical typed vectors; use ArrowResult::to_chunks with Vector::doubles/value_at for new code")
fn ArrowResult::get_column_double(self : ArrowResult, col : Int) -> Array[Double]#deprecated("arrow getters now read canonical typed vectors; use ArrowResult::to_chunks with Vector::doubles and Vector::is_null for new code")
fn ArrowResult::get_column_double_nullable(self : ArrowResult, col : Int) -> (Array[Double], Array[Bool])#deprecated("arrow getters now read canonical typed vectors; use ArrowResult::to_chunks with Vector::ints/value_at for new code")
fn ArrowResult::get_column_int32(self : ArrowResult, col : Int) -> Array[Int]#deprecated("arrow getters now read canonical typed vectors; use ArrowResult::to_chunks with Vector::ints and Vector::is_null for new code")
fn ArrowResult::get_column_int32_nullable(self : ArrowResult, col : Int) -> (Array[Int], Array[Bool])#deprecated("arrow getters now read canonical typed vectors; use ArrowResult::to_chunks with Vector::int64s/value_at for new code")
fn ArrowResult::get_column_int64(self : ArrowResult, col : Int) -> Array[Int64]#deprecated("arrow getters now read canonical typed vectors; use ArrowResult::to_chunks with Vector::int64s and Vector::is_null for new code")
fn ArrowResult::get_column_int64_nullable(self : ArrowResult, col : Int) -> (Array[Int64], Array[Bool])#deprecated("arrow getters now read canonical typed vectors; use ArrowResult::to_chunks with Vector::strings/string_at for new code")
fn ArrowResult::get_column_string(self : ArrowResult, col : Int) -> Array[String]#deprecated("arrow getters now read canonical typed vectors; use ArrowResult::to_chunks with Vector::strings and Vector::is_null for new code")
fn ArrowResult::get_column_string_nullable(self : ArrowResult, col : Int) -> (Array[String], Array[Bool])pub struct BackendCapabilities {
backend : Backend
connect : Bool
prepared_statements : Bool
streaming : Bool
appender : Bool
appender_chunk : Bool
appender_date_timestamp : Bool
arrow : Bool
persistent_storage : Bool
quack : Bool
decimal_bind : Bool
decimal_append : Bool
interval_bind : Bool
interval_append : Bool
blob_bind : Bool
blob_append : Bool
nested_bind : Bool
nested_append : Bool
nested_typed : Bool
}fn BackendCapabilities::require(self : BackendCapabilities, feature : BackendFeature) -> Result[Unit, DuckDBError]fn BackendCapabilities::unsupported_error(self : BackendCapabilities, feature : BackendFeature) -> DuckDBErrortype CheckConfigfn CheckConfig::new(cases : Int, max_size : Int, seed : Int, max_shrinks : Int, discard_ratio? : Int) -> CheckConfigtype CheckResultpub(all) enum ColumnType {
Invalid
Boolean
TinyInt
SmallInt
Integer
BigInt
UTinyInt
USmallInt
UInteger
UBigInt
Float
Double
Timestamp
Date
Time
Interval
HugeInt
UHugeInt
Varchar
Blob
Decimal
TimestampS
TimestampMs
TimestampNs
Enum
List
Struct
Map
Array
Uuid
Union
Bit
TimeTz
TimestampTz
Any
Bignum
SqlNull
StringLiteral
IntegerLiteral
TimeNs
Unknown(Int)
} derive(Eq)pub type Configpub type Connectionfn Connection::attach(self : Connection, uri : String, db_alias? : String, options? : String, on_done~ : (Result[Unit, DuckDBError]) -> Unit) -> Unit#deprecated("moved to the f4ah6o/duckdb/quack package (quack::attach); or use Connection::attach with a 'TYPE quack' options clause")
fn Connection::attach_quack(self : Connection, uri : String, db_alias : String, token? : String, disable_ssl? : Bool, on_done~ : (Result[Unit, DuckDBError]) -> Unit) -> Unitfn Connection::create_appender(self : Connection, schema : String, table : String, on_done~ : (Result[Appender, DuckDBError]) -> Unit) -> Unit#deprecated("moved to the f4ah6o/duckdb/quack package (quack::create_secret)")
fn Connection::create_quack_secret(self : Connection, token : String, scope? : String, on_done~ : (Result[Unit, DuckDBError]) -> Unit) -> Unitfn Connection::install_extension(self : Connection, name : String, repository? : String, force? : Bool, on_done~ : (Result[Unit, DuckDBError]) -> Unit) -> Unit#deprecated("moved to the f4ah6o/duckdb/quack package (quack::install); or use Connection::install_extension(\"quack\", repository=\"core_nightly\", force=true)")
fn Connection::install_quack(self : Connection, on_done~ : (Result[Unit, DuckDBError]) -> Unit) -> Unitfn Connection::load_extension(self : Connection, name : String, on_done~ : (Result[Unit, DuckDBError]) -> Unit) -> Unit#deprecated("moved to the f4ah6o/duckdb/quack package (quack::load); or use Connection::load_extension(\"quack\")")
fn Connection::load_quack(self : Connection, on_done~ : (Result[Unit, DuckDBError]) -> Unit) -> Unitfn Connection::prepare(self : Connection, sql : String, on_done~ : (Result[PreparedStatement, DuckDBError]) -> Unit) -> Unit#deprecated("moved to the f4ah6o/duckdb/quack package (quack::query)")
fn Connection::quack_query(self : Connection, uri : String, sql : String, token? : String, disable_ssl? : Bool, on_done~ : (Result[QueryResult, DuckDBError]) -> Unit) -> Unitfn Connection::query(self : Connection, sql : String, on_done~ : (Result[QueryResult, DuckDBError]) -> Unit) -> Unitfn Connection::query_arrow(self : Connection, sql : String, on_done~ : (Result[ArrowResult, DuckDBError]) -> Unit) -> Unitfn Connection::query_chunks(self : Connection, sql : String, on_done~ : (Result[Array[VectorChunk], DuckDBError]) -> Unit) -> Unitfn Connection::query_stream(self : Connection, sql : String, on_done~ : (Result[ResultStream, DuckDBError]) -> Unit) -> Unit#deprecated("moved to the f4ah6o/duckdb/quack package (quack::serve)")
fn Connection::start_quack_server(self : Connection, uri : String, token? : String, allow_other_hostname? : Bool, on_done~ : (Result[QueryResult, DuckDBError]) -> Unit) -> Unit#deprecated("moved to the f4ah6o/duckdb/quack package (quack::stop)")
fn Connection::stop_quack_server(self : Connection, uri : String, on_done~ : (Result[Unit, DuckDBError]) -> Unit) -> Unitpub struct Decimal {
width : Int
scale : Int
lower : UInt64
upper : Int64
}pub struct Interval {
months : Int
days : Int
micros : Int64
}#external
pub type NativeLogicalType#external
pub type NativeVectorpub type PreparedStatementfn PreparedStatement::bind_bigint(self : PreparedStatement, index : Int, value : Int) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_blob(self : PreparedStatement, index : Int, value : Bytes) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_bool(self : PreparedStatement, index : Int, value : Bool) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_date(self : PreparedStatement, index : Int, days : Int) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_decimal(self : PreparedStatement, index : Int, value : Decimal) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_double(self : PreparedStatement, index : Int, value : Double) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_int(self : PreparedStatement, index : Int, value : Int) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_interval(self : PreparedStatement, index : Int, value : Interval) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_list_varchar(self : PreparedStatement, index : Int, values : Array[String]) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_map(self : PreparedStatement, index : Int, map : Map) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_struct(self : PreparedStatement, index : Int, value : Struct) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_timestamp(self : PreparedStatement, index : Int, micros : Int64) -> Result[Unit, DuckDBError]fn PreparedStatement::bind_varchar(self : PreparedStatement, index : Int, value : String) -> Result[Unit, DuckDBError]fn PreparedStatement::close(self : PreparedStatement, on_done~ : (Result[Unit, DuckDBError]) -> Unit) -> Unitfn PreparedStatement::execute(self : PreparedStatement, on_done~ : (Result[QueryResult, DuckDBError]) -> Unit) -> Unitfn PreparedStatement::execute_chunks(self : PreparedStatement, on_done~ : (Result[Array[VectorChunk], DuckDBError]) -> Unit) -> Unitfn PreparedStatement::execute_stream(self : PreparedStatement, on_done~ : (Result[ResultStream, DuckDBError]) -> Unit) -> Unitpub type ResultStreamfn ResultStream::next(self : ResultStream, on_done~ : (Result[DataChunk?, DuckDBError]) -> Unit) -> Unitfn ResultStream::next_chunk(self : ResultStream, on_done~ : (Result[VectorChunk?, DuckDBError]) -> Unit) -> Unitpub(all) enum Value {
Int(Int)
Int64(Int64)
UInt64(UInt64)
Double(Double)
Bool(Bool)
String(String)
Date(Int)
Timestamp(Int64)
TimestampNs(Int64)
Decimal(Decimal)
Interval(Interval)
HugeInt(lower~ : UInt64, upper~ : Int64)
Blob(Bytes)
List(Array[Value])
Struct(fields~ : Array[String], values~ : Array[Value])
Map(keys~ : Array[Value], values~ : Array[Value])
Null
}fn VectorChunk::new(columns : Array[String], vectors : Array[Vector]) -> Result[VectorChunk, DuckDBError]pub(all) enum VectorData {
Bool(FixedArray[Bool])
Int32(FixedArray[Int])
Int64(FixedArray[Int64])
UInt64(FixedArray[UInt64])
Float(FixedArray[Float])
Double(FixedArray[Double])
Varchar(Array[String])
Blob(Array[Bytes])
Decimal(FixedArray[Decimal])
Interval(FixedArray[Interval])
HugeInt(lower~ : FixedArray[UInt64], upper~ : FixedArray[Int64])
List(child~ : Vector, offsets~ : FixedArray[UInt64], lengths~ : FixedArray[UInt64])
Struct(fields~ : Array[String], children~ : Array[Vector])
Map(offsets~ : FixedArray[UInt64], lengths~ : FixedArray[UInt64], keys~ : Vector, values~ : Vector)
Any(Array[Value])
}fn[A : Debug] assert_check(name : String, gen : Gen[A], check : (A) -> Result[Unit, String], config? : CheckConfig, shrink? : (A) -> Iter[A]) -> Unitfn[A : Debug] check_with_stats(gen : Gen[A], check : (A) -> (Result[Unit, String], String?), config? : CheckConfig) -> CheckResultfn connect(on_ready~ : (Result[Connection, DuckDBError]) -> Unit, path? : String, backend? : JsBackend) -> Unitfn connect_with_config(on_ready~ : (Result[Connection, DuckDBError]) -> Unit, config : Config?, path? : String, backend? : JsBackend) -> Unitfn date_from_ymd(year : Int, month : Int, day : Int) -> Intfn date_to_days(year : Int, month : Int, day : Int) -> Intfn date_to_ymd(total_days : Int) -> (Int, Int, Int)fn days_in_month(year : Int, month : Int) -> Intfn days_to_ymd(days : Int) -> (Int, Int, Int)#deprecated("DuckDBError::Message was replaced by structured variants in 0.7.0; construct the matching variant (Query/Prepare/Bind/Append/Unsupported/Closed/InvalidArgument/Backend/DuckDB) instead")
fn duckdb_message_error(message : String) -> DuckDBErrorfn parse_date(s : String) -> Result[Int, String]fn parse_fraction_to_micros(s : String) -> Intfn parse_time_to_micros(s : String) -> Result[Int64, String]fn parse_timestamp(s : String) -> Result[Int64, String]fn support_matrix_markdown() -> Stringfn timestamp_from_ymd_hms(year : Int, month : Int, day : Int, hour : Int, minute : Int, second : Int) -> Int64fn timestamp_to_ymd_hms(micros : Int64) -> (Int, Int, Int, Int, Int, Int)Install
Download zipMoonBit bindings for DuckDB on native and JavaScript targets (Node.js and browser WASM backends)
Dependencies