///|
extern "js" fn js_connect(
path : String,
backend : JsBackend,
on_ok : (Connection) -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(path, backend, on_ok, on_err) => {
#| const toError = (err) => err && err.message ? err.message : String(err);
#| const isNode = typeof process !== "undefined" && !!(process.versions && process.versions.node);
#| let mode = backend | 0;
#| if (mode === 0) {
#| mode = isNode ? 1 : 2;
#| }
#| const connectNode = async () => {
#| const api = await import("@duckdb/node-api");
#| const instance = await api.DuckDBInstance.create(path || ":memory:");
#| const connection = await instance.connect();
#| on_ok({ kind: "node", instance, connection });
#| };
#| const connectWasm = async () => {
#| if (typeof Worker === "undefined") {
#| throw new Error("duckdb-wasm requires Worker support");
#| }
#| const duckdb = await import("@duckdb/duckdb-wasm");
#| const bundles = duckdb.getJsDelivrBundles();
#| const bundle = await duckdb.selectBundle(bundles);
#| const logger = new duckdb.ConsoleLogger();
#| const worker = new Worker(bundle.mainWorker);
#| const db = new duckdb.AsyncDuckDB(logger, worker);
#| await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
#| const conn = await db.connect();
#| on_ok({ kind: "wasm", db, conn, worker });
#| };
#| const run = async () => {
#| if (mode === 1) {
#| await connectNode();
#| return;
#| }
#| if (mode === 2) {
#| await connectWasm();
#| return;
#| }
#| throw new Error("unknown JsBackend");
#| };
#| run().catch((err) => on_err(toError(err)));
#|}
///|
extern "js" fn js_query(
conn : Connection,
sql : String,
on_ok : (Array[String], Array[Array[String]], Array[Array[Bool]], Array[Int]) -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(conn, sql, on_ok, on_err) => {
#| const toError = (err) => err && err.message ? err.message : String(err);
#| let floatTypeId = null;
#| let doubleTypeId = null;
#| const toCell = (value, typeId) => {
#| if (value === null || value === undefined) {
#| return ["", true];
#| }
#| if (typeof value === "string") {
#| if (typeId !== null && (typeId === floatTypeId || typeId === doubleTypeId)) {
#| if (value === "NaN") {
#| return ["nan", false];
#| }
#| if (value === "Infinity") {
#| return ["inf", false];
#| }
#| if (value === "-Infinity") {
#| return ["-inf", false];
#| }
#| }
#| return [value, false];
#| }
#| if (typeof value === "number") {
#| if (Number.isNaN(value)) {
#| return ["nan", false];
#| }
#| if (value === Infinity) {
#| return ["inf", false];
#| }
#| if (value === -Infinity) {
#| return ["-inf", false];
#| }
#| return [String(value), false];
#| }
#| if (typeof value === "boolean" || typeof value === "bigint") {
#| return [String(value), false];
#| }
#| return [JSON.stringify(value), false];
#| };
#| const pushRow = (values, rows, nulls, typeIds) => {
#| const row = [];
#| const rowNulls = [];
#| for (let i = 0; i < values.length; i++) {
#| const cell = toCell(values[i], typeIds ? typeIds[i] : null);
#| row.push(cell[0]);
#| rowNulls.push(cell[1]);
#| }
#| rows.push(row);
#| nulls.push(rowNulls);
#| };
#| const runNode = async () => {
#| const api = await import('@duckdb/node-api');
#| floatTypeId = api.DuckDBTypeId.FLOAT;
#| doubleTypeId = api.DuckDBTypeId.DOUBLE;
#| const result = await conn.connection.run(sql);
#| const columns = result.columnNames();
#| const rowsJson = await result.getRowsJson();
#| const typeIds = columns.map((_, idx) => result.columnTypeId(idx));
#| const rows = [];
#| const nulls = [];
#| for (const row of rowsJson) {
#| const values = Array.isArray(row) ? row : columns.map((name) => row[name]);
#| pushRow(values, rows, nulls, typeIds);
#| }
#| on_ok(columns, rows, nulls, typeIds);
#| };
#| const runWasm = async () => {
#| const arrowResult = await conn.conn.query(sql);
#| let columns = [];
#| if (arrowResult && arrowResult.schema && arrowResult.schema.fields) {
#| columns = arrowResult.schema.fields.map((field) => field.name);
#| }
#| const rowObjects = arrowResult.toArray().map((row) => row.toJSON());
#| if (columns.length === 0 && rowObjects.length > 0) {
#| columns = Object.keys(rowObjects[0]);
#| }
#| const rows = [];
#| const nulls = [];
#| for (const row of rowObjects) {
#| const values = columns.map((name) => row[name]);
#| pushRow(values, rows, nulls, null);
#| }
#| const typeIds = [];
#| on_ok(columns, rows, nulls, typeIds);
#| };
#| const run = async () => {
#| if (conn && conn.kind === "node") {
#| await runNode();
#| return;
#| }
#| if (conn && conn.kind === "wasm") {
#| await runWasm();
#| return;
#| }
#| throw new Error("unknown connection backend");
#| };
#| run().catch((err) => on_err(toError(err)));
#|}
///|
fn column_types_from_ids(
columns : Array[String],
type_ids : Array[Int],
) -> Array[ColumnType] {
let column_types : Array[ColumnType] = []
let count = columns.length()
for col = 0; col < count; col = col + 1 {
if col < type_ids.length() {
column_types.push(column_type_from_id(type_ids[col]))
} else {
column_types.push(ColumnType::Unknown(-1))
}
} else {
()
}
column_types
}
///|
extern "js" fn js_query_stream(
conn : Connection,
sql : String,
on_ok : (ResultStream) -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(conn, sql, on_ok, on_err) => {
#| const toError = (err) => err && err.message ? err.message : String(err);
#| const runNode = async () => {
#| const api = await import('@duckdb/node-api');
#| const result = await conn.connection.stream(sql);
#| const columns = result.columnNames();
#| const typeIds = columns.map((_, idx) => result.columnTypeId(idx));
#| on_ok({
#| kind: "node",
#| result,
#| columns,
#| typeIds,
#| floatTypeId: api.DuckDBTypeId.FLOAT,
#| doubleTypeId: api.DuckDBTypeId.DOUBLE,
#| });
#| };
#| const runWasm = async () => {
#| const reader = await conn.conn.send(sql, true);
#| let columns = [];
#| if (reader && reader.schema && reader.schema.fields) {
#| columns = reader.schema.fields.map((field) => field.name);
#| }
#| on_ok({ kind: "wasm", reader, columns, done: false });
#| };
#| const run = async () => {
#| if (conn && conn.kind === "node") {
#| await runNode();
#| return;
#| }
#| if (conn && conn.kind === "wasm") {
#| await runWasm();
#| return;
#| }
#| throw new Error("unknown connection backend");
#| };
#| run().catch((err) => on_err(toError(err)));
#|}
///|
extern "js" fn js_stream_columns(stream : ResultStream) -> Array[String] =
#|(stream) => {
#| if (stream && Array.isArray(stream.columns)) {
#| return stream.columns;
#| }
#| return [];
#|}
///|
extern "js" fn js_stream_column_count(stream : ResultStream) -> Int =
#|(stream) => {
#| if (stream && Array.isArray(stream.columns)) {
#| return stream.columns.length;
#| }
#| return 0;
#|}
///|
extern "js" fn js_stream_next(
stream : ResultStream,
on_ok : (Array[Array[String]], Array[Array[Bool]]) -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(stream, on_ok, on_err) => {
#| const toError = (err) => err && err.message ? err.message : String(err);
#| const toCell = (value, typeId) => {
#| const floatTypeId = stream.floatTypeId;
#| const doubleTypeId = stream.doubleTypeId;
#| if (value === null || value === undefined) {
#| return ["", true];
#| }
#| if (typeof value === "string") {
#| if (typeId !== null && typeId !== undefined &&
#| (typeId === floatTypeId || typeId === doubleTypeId)) {
#| if (value === "NaN") return ["nan", false];
#| if (value === "Infinity") return ["inf", false];
#| if (value === "-Infinity") return ["-inf", false];
#| }
#| return [value, false];
#| }
#| if (typeof value === "number") {
#| if (Number.isNaN(value)) return ["nan", false];
#| if (value === Infinity) return ["inf", false];
#| if (value === -Infinity) return ["-inf", false];
#| return [String(value), false];
#| }
#| if (typeof value === "boolean" || typeof value === "bigint") {
#| return [String(value), false];
#| }
#| return [JSON.stringify(value), false];
#| };
#| const pushRow = (values, rows, nulls, typeIds) => {
#| const row = [];
#| const rowNulls = [];
#| for (let i = 0; i < values.length; i++) {
#| const cell = toCell(values[i], typeIds ? typeIds[i] : null);
#| row.push(cell[0]);
#| rowNulls.push(cell[1]);
#| }
#| rows.push(row);
#| nulls.push(rowNulls);
#| };
#| const runNode = async () => {
#| const chunk = await stream.result.fetchChunk();
#| if (!chunk) {
#| throw new Error("fetchChunk failed");
#| }
#| if (chunk.rowCount <= 0) {
#| on_ok([], []);
#| return;
#| }
#| const rows = [];
#| const nulls = [];
#| const values = chunk.getRows();
#| for (const row of values) {
#| pushRow(row, rows, nulls, stream.typeIds || null);
#| }
#| on_ok(rows, nulls);
#| };
#| const runWasm = async () => {
#| if (stream.done) {
#| on_ok([], []);
#| return;
#| }
#| const next = await stream.reader.next();
#| if (!next || next.done || !next.value) {
#| stream.done = true;
#| on_ok([], []);
#| return;
#| }
#| const batch = next.value;
#| const rowCount = batch.numRows ?? batch.length ?? 0;
#| let columnCount = batch.numCols ?? 0;
#| if (!columnCount && batch.schema && batch.schema.fields) {
#| columnCount = batch.schema.fields.length;
#| }
#| if ((!stream.columns || stream.columns.length === 0) &&
#| batch.schema && batch.schema.fields) {
#| stream.columns = batch.schema.fields.map((field) => field.name);
#| }
#| const rows = [];
#| const nulls = [];
#| for (let r = 0; r < rowCount; r++) {
#| const row = [];
#| const rowNulls = [];
#| for (let c = 0; c < columnCount; c++) {
#| const vector = batch.getChildAt ? batch.getChildAt(c) : batch.getChild(c);
#| const value = vector ? vector.get(r) : null;
#| const cell = toCell(value, null);
#| row.push(cell[0]);
#| rowNulls.push(cell[1]);
#| }
#| rows.push(row);
#| nulls.push(rowNulls);
#| }
#| on_ok(rows, nulls);
#| };
#| const run = async () => {
#| if (stream && stream.kind === "node") {
#| await runNode();
#| return;
#| }
#| if (stream && stream.kind === "wasm") {
#| await runWasm();
#| return;
#| }
#| throw new Error("unknown stream backend");
#| };
#| run().catch((err) => on_err(toError(err)));
#|}
///|
extern "js" fn js_stream_close(
stream : ResultStream,
on_ok : () -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(stream, on_ok, on_err) => {
#| const run = async () => {
#| if (stream && stream.kind === "wasm") {
#| if (stream.reader && typeof stream.reader.cancel === "function") {
#| await stream.reader.cancel();
#| }
#| stream.done = true;
#| }
#| on_ok();
#| };
#| run().catch((err) => on_err(err && err.message ? err.message : String(err)));
#|}
///|
extern "js" fn js_close(
conn : Connection,
on_ok : () -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(conn, on_ok, on_err) => {
#| const toError = (err) => err && err.message ? err.message : String(err);
#| const run = async () => {
#| if (conn && conn.kind === "node") {
#| if (conn.connection && typeof conn.connection.closeSync === "function") {
#| conn.connection.closeSync();
#| }
#| if (conn.instance && typeof conn.instance.close === "function") {
#| await conn.instance.close();
#| }
#| on_ok();
#| return;
#| }
#| if (conn && conn.kind === "wasm") {
#| if (conn.conn && typeof conn.conn.close === "function") {
#| await conn.conn.close();
#| }
#| if (conn.db && typeof conn.db.terminate === "function") {
#| await conn.db.terminate();
#| }
#| if (conn.worker && typeof conn.worker.terminate === "function") {
#| conn.worker.terminate();
#| }
#| on_ok();
#| return;
#| }
#| throw new Error("unknown connection backend");
#| };
#| run().catch((err) => on_err(toError(err)));
#|}
///|
pub fn connect(
on_ready~ : (Result[Connection, DuckDBError]) -> Unit,
path? : String = ":memory:",
backend? : JsBackend = JsBackend::Auto,
) -> Unit {
touch_public_types(backend)
js_connect(path, backend, fn(conn) { on_ready(Ok(conn)) }, fn(message) {
on_ready(Err(DuckDBError::Message(message)))
})
}
///|
pub fn Connection::close(
self : Connection,
on_done~ : (Result[Unit, DuckDBError]) -> Unit,
) -> Unit {
js_close(self, fn() { on_done(Ok(())) }, fn(message) {
on_done(Err(DuckDBError::Message(message)))
})
}
///|
pub fn Connection::query(
self : Connection,
sql : String,
on_done~ : (Result[QueryResult, DuckDBError]) -> Unit,
) -> Unit {
js_query(
self,
sql,
fn(columns, rows, nulls, type_ids) {
let column_types = column_types_from_ids(columns, type_ids)
on_done(Ok({ columns, column_types, rows, nulls }))
},
fn(message) { on_done(Err(DuckDBError::Message(message))) },
)
}
///|
pub fn Connection::query_stream(
self : Connection,
sql : String,
on_done~ : (Result[ResultStream, DuckDBError]) -> Unit,
) -> Unit {
js_query_stream(self, sql, fn(stream) { on_done(Ok(stream)) }, fn(message) {
on_done(Err(DuckDBError::Message(message)))
})
}
///|
pub fn ResultStream::columns(self : ResultStream) -> Array[String] {
js_stream_columns(self)
}
///|
pub fn ResultStream::column_count(self : ResultStream) -> Int {
js_stream_column_count(self)
}
///|
pub fn ResultStream::next(
self : ResultStream,
on_done~ : (Result[DataChunk?, DuckDBError]) -> Unit,
) -> Unit {
js_stream_next(
self,
fn(rows, nulls) {
if rows.length() == 0 {
on_done(Ok(None))
} else {
on_done(Ok(Some({ columns: self.columns(), rows, nulls })))
}
},
fn(message) { on_done(Err(DuckDBError::Message(message))) },
)
}
///|
pub fn ResultStream::close(
self : ResultStream,
on_done~ : (Result[Unit, DuckDBError]) -> Unit,
) -> Unit {
js_stream_close(self, fn() { on_done(Ok(())) }, fn(message) {
on_done(Err(DuckDBError::Message(message)))
})
}
// ============================================================================
// Prepared Statement JS FFI
// ============================================================================
///|
extern "js" fn js_prepare(
conn : Connection,
sql : String,
on_ok : (PreparedStatement) -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(conn, sql, on_ok, on_err) => {
#| const toError = (err) => err && err.message ? err.message : String(err);
#| const run = async () => {
#| if (conn && conn.kind === "node") {
#| try {
#| const statement = await conn.connection.prepare(sql);
#| on_ok({ kind: "prepared", connection: conn, backend: conn.kind, statement });
#| return;
#| } catch (e) {
#| on_err(toError(e));
#| return;
#| }
#| }
#| if (conn && conn.kind === "wasm") {
#| try {
#| // Use real duckdb-wasm prepared statements
#| const statement = await conn.conn.prepare(sql);
#| on_ok({
#| kind: "prepared",
#| connection: conn,
#| backend: conn.kind,
#| statement,
#| sql,
#| params: {},
#| });
#| return;
#| } catch (e) {
#| on_err(toError(e));
#| return;
#| }
#| }
#| throw new Error("unknown connection backend");
#| };
#| run().catch((err) => on_err(toError(err)));
#|}
///|
extern "js" fn js_bind_int(
stmt : PreparedStatement,
index : Int,
value : Int,
) -> Result[Unit, String] =
#|(stmt, index, value) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bindInteger(index, value);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| // WASM: store parameters for later use in execute
#| if (!stmt.params) stmt.params = {};
#| stmt.params[index] = { type: "int", value };
#| return { ok: true };
#| }
#| return { ok: false, error: "unknown statement backend" };
#|}
///|
extern "js" fn js_bind_bigint(
stmt : PreparedStatement,
index : Int,
value : Int,
) -> Result[Unit, String] =
#|(stmt, index, value) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bindBigInt(index, BigInt(value));
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| if (!stmt.params) stmt.params = {};
#| stmt.params[index] = { type: "bigint", value };
#| return { ok: true };
#| }
#| return { ok: false, error: "unknown statement backend" };
#|}
///|
extern "js" fn js_bind_double(
stmt : PreparedStatement,
index : Int,
value : Double,
) -> Result[Unit, String] =
#|(stmt, index, value) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bindDouble(index, value);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| if (!stmt.params) stmt.params = {};
#| stmt.params[index] = { type: "double", value };
#| return { ok: true };
#| }
#| return { ok: false, error: "unknown statement backend" };
#|}
///|
extern "js" fn js_bind_varchar(
stmt : PreparedStatement,
index : Int,
value : String,
) -> Result[Unit, String] =
#|(stmt, index, value) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bindVarchar(index, value);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| if (!stmt.params) stmt.params = {};
#| stmt.params[index] = { type: "varchar", value };
#| return { ok: true };
#| }
#| return { ok: false, error: "unknown statement backend" };
#|}
///|
extern "js" fn js_bind_bool(
stmt : PreparedStatement,
index : Int,
value : Bool,
) -> Result[Unit, String] =
#|(stmt, index, value) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bindBoolean(index, value);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| if (!stmt.params) stmt.params = {};
#| stmt.params[index] = { type: "bool", value };
#| return { ok: true };
#| }
#| return { ok: false, error: "unknown statement backend" };
#|}
///|
extern "js" fn js_bind_null(
stmt : PreparedStatement,
index : Int,
) -> Result[Unit, String] =
#|(stmt, index) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bindNull(index);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| if (!stmt.params) stmt.params = {};
#| stmt.params[index] = { type: "null", value: null };
#| return { ok: true };
#| }
#| return { ok: false, error: "unknown statement backend" };
#|}
///|
extern "js" fn js_clear_bindings(
stmt : PreparedStatement,
) -> Result[Unit, String] =
#|(stmt) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.clearBindings();
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| stmt.params = {};
#| return { ok: true };
#| }
#| return { ok: false, error: "unknown statement backend" };
#|}
///|
extern "js" fn js_bind_date(
stmt : PreparedStatement,
index : Int,
date_str : String,
) -> Result[Unit, String] =
#|(stmt, index, date_str) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bindDate(index, date_str);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| if (!stmt.params) stmt.params = {};
#| stmt.params[index] = { type: "date", value: date_str };
#| return { ok: true };
#| }
#| return { ok: false, error: "unknown statement backend" };
#|}
///|
extern "js" fn js_bind_timestamp(
stmt : PreparedStatement,
index : Int,
timestamp_str : String,
) -> Result[Unit, String] =
#|(stmt, index, timestamp_str) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bindTimestamp(index, timestamp_str);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| if (!stmt.params) stmt.params = {};
#| stmt.params[index] = { type: "timestamp", value: timestamp_str };
#| return { ok: true };
#| }
#| return { ok: false, error: "unknown statement backend" };
#|}
// ============================================================================
// Advanced Type Bindings (Node API only)
// ============================================================================
///|
extern "js" fn js_bind_blob(
stmt : PreparedStatement,
index : Int,
data : Bytes,
) -> Result[Unit, String] =
#|(stmt, index, data) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| const uint8Array = new Uint8Array(data.byteLength);
#| for (let i = 0; i < data.byteLength; i++) {
#| uint8Array[i] = data.readUint8(i);
#| }
#| stmt.statement.bindBlob(index, { bytes: uint8Array });
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| return { ok: false, error: "bind_blob is not supported for WASM backend. Use INSERT statements with CAST or hex encoding instead." };
#| }
#| return { ok: false, error: "bind_blob is only supported for Node backend" };
#|}
///|
extern "js" fn js_bind_decimal(
stmt : PreparedStatement,
index : Int,
width : Int,
scale : Int,
value : Int,
) -> Result[Unit, String] =
#|(stmt, index, width, scale, value) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bindDecimal(index, { width, scale, value: BigInt(value) });
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| return { ok: false, error: "bind_decimal is not supported for WASM backend. Use INSERT statements with DECIMAL literals instead." };
#| }
#| return { ok: false, error: "bind_decimal is only supported for Node backend" };
#|}
///|
extern "js" fn js_bind_interval(
stmt : PreparedStatement,
index : Int,
months : Int,
days : Int,
micros : Int64,
) -> Result[Unit, String] =
#|(stmt, index, months, days, micros) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bindInterval(index, { months, days, micros: BigInt(micros) });
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| return { ok: false, error: "bind_interval is not supported for WASM backend. Use INSERT statements with INTERVAL literals instead." };
#| }
#| return { ok: false, error: "bind_interval is only supported for Node backend" };
#|}
///|
extern "js" fn js_bind_list_varchar(
stmt : PreparedStatement,
index : Int,
values : Array[String],
) -> Result[Unit, String] =
#|(stmt, index, values) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| stmt.statement.bind(index, { items: values }, { typeId: 24 }); // LIST type
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| return { ok: false, error: "bind_list_varchar is not supported for WASM backend. Use INSERT statements with LIST literals instead." };
#| }
#| return { ok: false, error: "bind_list_varchar is only supported for Node backend" };
#|}
///|
extern "js" fn js_bind_struct(
stmt : PreparedStatement,
index : Int,
fields : Array[String],
values : Array[String],
) -> Result[Unit, String] =
#|(stmt, index, fields, values) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| const entries = fields.map((name, i) => ({ name, value: values[i] }));
#| stmt.statement.bindStruct(index, { entries });
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| return { ok: false, error: "bind_struct is not supported for WASM backend. Use INSERT statements with STRUCT literals instead." };
#| }
#| return { ok: false, error: "bind_struct is only supported for Node backend" };
#|}
///|
extern "js" fn js_bind_map(
stmt : PreparedStatement,
index : Int,
keys : Array[String],
values : Array[String],
) -> Result[Unit, String] =
#|(stmt, index, keys, values) => {
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "node" && stmt.statement) {
#| try {
#| const entries = keys.map((key, i) => ({ key, value: values[i] }));
#| stmt.statement.bindMap(index, { entries });
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| if (stmt && stmt.kind === "prepared" && stmt.connection && stmt.connection.kind === "wasm") {
#| return { ok: false, error: "bind_map is not supported for WASM backend. Use INSERT statements with MAP literals instead." };
#| }
#| return { ok: false, error: "bind_map is only supported for Node backend" };
#|}
// ============================================================================
// Appender API (Node API only)
// ============================================================================
///|
extern "js" fn js_create_appender(
conn : Connection,
table : String,
on_ok : (Appender) -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(conn, table, on_ok, on_err) => {
#| const toError = (err) => err && err.message ? err.message : String(err);
#| const run = async () => {
#| if (conn && conn.kind === "node") {
#| try {
#| const appender = await conn.connection.createAppender(table);
#| on_ok({ kind: "appender", connection: conn, appender });
#| return;
#| } catch (e) {
#| on_err(toError(e));
#| return;
#| }
#| }
#| if (conn && conn.kind === "wasm") {
#| on_err("Appender is not supported for WASM backend. Use INSERT statements, insertCSVFromPath(), insertJSONFromPath(), or insertArrowTable() instead.");
#| return;
#| }
#| on_err("unknown connection backend");
#| };
#| run().catch((err) => on_err(toError(err)));
#|}
///|
extern "js" fn js_appender_begin_row(
appender : Appender,
) -> Result[Unit, String] =
#|(appender) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| // No-op in Node API, but kept for API compatibility
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_append_int(
appender : Appender,
value : Int,
) -> Result[Unit, String] =
#|(appender, value) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.appendInteger(value);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_append_bigint(
appender : Appender,
value : Int,
) -> Result[Unit, String] =
#|(appender, value) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.appendBigInt(BigInt(value));
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_append_double(
appender : Appender,
value : Double,
) -> Result[Unit, String] =
#|(appender, value) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.appendDouble(value);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_append_varchar(
appender : Appender,
value : String,
) -> Result[Unit, String] =
#|(appender, value) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.appendVarchar(value);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_append_bool(
appender : Appender,
value : Bool,
) -> Result[Unit, String] =
#|(appender, value) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.appendBoolean(value);
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_append_null(
appender : Appender,
) -> Result[Unit, String] =
#|(appender) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.appendNull();
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_append_blob(
appender : Appender,
data : Bytes,
) -> Result[Unit, String] =
#|(appender, data) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| const uint8Array = new Uint8Array(data.byteLength);
#| for (let i = 0; i < data.byteLength; i++) {
#| uint8Array[i] = data.readUint8(i);
#| }
#| appender.appender.appendBlob({ bytes: uint8Array });
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_append_decimal(
appender : Appender,
width : Int,
scale : Int,
value : Int,
) -> Result[Unit, String] =
#|(appender, width, scale, value) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.appendDecimal({ width, scale, value: BigInt(value) });
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_append_interval(
appender : Appender,
months : Int,
days : Int,
micros : Int64,
) -> Result[Unit, String] =
#|(appender, months, days, micros) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.appendInterval({ months, days, micros: BigInt(micros) });
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_end_row(appender : Appender) -> Result[Unit, String] =
#|(appender) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.endRow();
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_flush(appender : Appender) -> Result[Unit, String] =
#|(appender) => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.flushSync();
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#| }
#| return { ok: false, error: "invalid appender" };
#|}
///|
extern "js" fn js_appender_close(
appender : Appender,
on_ok : () -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(appender, on_ok, on_err) => {
#| const toError = (err) => err && err.message ? err.message : String(err);
#| const run = async () => {
#| if (appender && appender.kind === "appender" && appender.appender) {
#| try {
#| appender.appender.closeSync();
#| on_ok();
#| return;
#| } catch (e) {
#| on_err(toError(e));
#| return;
#| }
#| }
#| on_err("invalid appender");
#| };
#| run().catch((err) => on_err(toError(err)));
#|}
///|
extern "js" fn js_execute_prepared(
stmt : PreparedStatement,
on_ok : (Array[String], Array[Array[String]], Array[Array[Bool]], Array[Int]) -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(stmt, on_ok, on_err) => {
#| let floatTypeId = null;
#| let doubleTypeId = null;
#| const toCell = (value, typeId) => {
#| if (value === null || value === undefined) {
#| return ["", true];
#| }
#| if (typeof value === "string") {
#| if (typeId !== null && (typeId === floatTypeId || typeId === doubleTypeId)) {
#| if (value === "NaN") {
#| return ["nan", false];
#| }
#| if (value === "Infinity") {
#| return ["inf", false];
#| }
#| if (value === "-Infinity") {
#| return ["-inf", false];
#| }
#| }
#| return [value, false];
#| }
#| if (typeof value === "number") {
#| if (Number.isNaN(value)) {
#| return ["nan", false];
#| }
#| if (value === Infinity) {
#| return ["inf", false];
#| }
#| if (value === -Infinity) {
#| return ["-inf", false];
#| }
#| return [String(value), false];
#| }
#| if (typeof value === "boolean" || typeof value === "bigint") {
#| return [String(value), false];
#| }
#| return [JSON.stringify(value), false];
#| };
#| const pushRow = (values, rows, nulls, typeIds) => {
#| const row = [];
#| const rowNulls = [];
#| for (let i = 0; i < values.length; i++) {
#| const cell = toCell(values[i], typeIds ? typeIds[i] : null);
#| row.push(cell[0]);
#| rowNulls.push(cell[1]);
#| }
#| rows.push(row);
#| nulls.push(rowNulls);
#| };
#| const run = async () => {
#| const backend = stmt && (stmt.backend || (stmt.connection && stmt.connection.kind));
#| const inferred = backend || (stmt && stmt.statement && typeof stmt.statement.query === "function" ? "wasm" : null);
#| const kind = inferred || (stmt && stmt.statement && typeof stmt.statement.run === "function" ? "node" : null);
#| if (kind === "node" && stmt && stmt.kind === "prepared" && stmt.statement) {
#| try {
#| const api = await import('@duckdb/node-api');
#| floatTypeId = api.DuckDBTypeId.FLOAT;
#| doubleTypeId = api.DuckDBTypeId.DOUBLE;
#| const result = await stmt.statement.run();
#| const columns = result.columnNames();
#| const rowsJson = await result.getRowsJson();
#| const typeIds = columns.map((_, idx) => result.columnTypeId(idx));
#| const rows = [];
#| const nulls = [];
#| for (const row of rowsJson) {
#| const values = Array.isArray(row) ? row : columns.map((name) => row[name]);
#| pushRow(values, rows, nulls, typeIds);
#| }
#| on_ok(columns, rows, nulls, typeIds);
#| return;
#| } catch (e) {
#| on_err(e.message || String(e));
#| return;
#| }
#| }
#| if (kind === "wasm" && stmt && stmt.kind === "prepared" && stmt.statement) {
#| try {
#| // For WASM, use prepared statement with parameters
#| const params = [];
#| if (stmt.params) {
#| const indices = Object.keys(stmt.params).map(Number).sort((a, b) => a - b);
#| for (const index of indices) {
#| const param = stmt.params[index];
#| // Convert typed parameters to values for duckdb-wasm
#| if (param.type === "null") {
#| params.push(null);
#| } else if (param.type === "bigint") {
#| params.push(BigInt(param.value));
#| } else if (param.type === "date" || param.type === "timestamp") {
#| params.push(param.value); // ISO string format
#| } else {
#| params.push(param.value);
#| }
#| }
#| }
#| const arrowResult = await stmt.statement.query(...params);
#| let columns = [];
#| if (arrowResult && arrowResult.schema && arrowResult.schema.fields) {
#| columns = arrowResult.schema.fields.map((field) => field.name);
#| }
#| const rowObjects = arrowResult.toArray().map((row) => row.toJSON());
#| if (columns.length === 0 && rowObjects.length > 0) {
#| columns = Object.keys(rowObjects[0]);
#| }
#| const rows = [];
#| const nulls = [];
#| for (const row of rowObjects) {
#| const values = columns.map((name) => row[name]);
#| pushRow(values, rows, nulls, null);
#| }
#| const typeIds = [];
#| on_ok(columns, rows, nulls, typeIds);
#| return;
#| } catch (e) {
#| on_err(e.message || String(e));
#| return;
#| }
#| }
#| on_err("unknown statement backend");
#| };
#| run().catch((err) => on_err(err.message || String(err)));
#|}
///|
extern "js" fn js_execute_prepared_stream(
stmt : PreparedStatement,
on_ok : (ResultStream) -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(stmt, on_ok, on_err) => {
#| const run = async () => {
#| const backend = stmt && (stmt.backend || (stmt.connection && stmt.connection.kind));
#| const inferred = backend || (stmt && stmt.statement && typeof stmt.statement.query === "function" ? "wasm" : null);
#| const kind = inferred || (stmt && stmt.statement && typeof stmt.statement.run === "function" ? "node" : null);
#| if (kind === "node" && stmt && stmt.kind === "prepared" && stmt.statement) {
#| try {
#| const api = await import('@duckdb/node-api');
#| const result = await stmt.statement.stream();
#| const columns = result.columnNames();
#| const typeIds = columns.map((_, idx) => result.columnTypeId(idx));
#| on_ok({
#| kind: "node",
#| result,
#| columns,
#| typeIds,
#| floatTypeId: api.DuckDBTypeId.FLOAT,
#| doubleTypeId: api.DuckDBTypeId.DOUBLE,
#| });
#| return;
#| } catch (e) {
#| on_err(e.message || String(e));
#| return;
#| }
#| }
#| if (kind === "wasm" && stmt && stmt.kind === "prepared" && stmt.statement) {
#| try {
#| const params = [];
#| if (stmt.params) {
#| const indices = Object.keys(stmt.params).map(Number).sort((a, b) => a - b);
#| for (const index of indices) {
#| const param = stmt.params[index];
#| if (param.type === "null") {
#| params.push(null);
#| } else if (param.type === "bigint") {
#| params.push(BigInt(param.value));
#| } else if (param.type === "date" || param.type === "timestamp") {
#| params.push(param.value);
#| } else {
#| params.push(param.value);
#| }
#| }
#| }
#| const reader = await stmt.statement.send(...params);
#| let columns = [];
#| if (reader && reader.schema && reader.schema.fields) {
#| columns = reader.schema.fields.map((field) => field.name);
#| }
#| on_ok({ kind: "wasm", reader, columns, done: false });
#| return;
#| } catch (e) {
#| on_err(e.message || String(e));
#| return;
#| }
#| }
#| on_err("unknown statement backend");
#| };
#| run().catch((err) => on_err(err.message || String(err)));
#|}
///|
extern "js" fn js_close_prepared(
stmt : PreparedStatement,
on_ok : () -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(stmt, on_ok, on_err) => {
#| const run = async () => {
#| const backend = stmt && (stmt.backend || (stmt.connection && stmt.connection.kind));
#| const inferred = backend || (stmt && stmt.statement && typeof stmt.statement.query === "function" ? "wasm" : null);
#| const kind = inferred || (stmt && stmt.statement && typeof stmt.statement.run === "function" ? "node" : null);
#| if (kind === "node" && stmt && stmt.kind === "prepared" && stmt.statement) {
#| try {
#| // Node-api statements are automatically cleaned up
#| on_ok();
#| return;
#| } catch (e) {
#| on_err(e.message || String(e));
#| return;
#| }
#| }
#| if (kind === "wasm" && stmt && stmt.kind === "prepared" && stmt.statement) {
#| try {
#| if (typeof stmt.statement.close === "function") {
#| await stmt.statement.close();
#| }
#| stmt.params = {};
#| on_ok();
#| return;
#| } catch (e) {
#| on_err(e.message || String(e));
#| return;
#| }
#| }
#| on_err("unknown statement backend");
#| };
#| run().catch((err) => on_err(err.message || String(err)));
#|}
// ============================================================================
// Prepared Statement API Implementation
// ============================================================================
///|
pub fn Connection::prepare(
self : Connection,
sql : String,
on_done~ : (Result[PreparedStatement, DuckDBError]) -> Unit,
) -> Unit {
js_prepare(self, sql, fn(stmt) { on_done(Ok(stmt)) }, fn(message) {
on_done(Err(DuckDBError::Message(message)))
})
}
///|
pub fn PreparedStatement::bind_int(
self : PreparedStatement,
index : Int,
value : Int,
) -> Result[Unit, DuckDBError] {
match js_bind_int(self, index, value) {
Ok(_) => Ok(())
Err(message) => Err(DuckDBError::Message(message))
}
}
///|
pub fn PreparedStatement::bind_bigint(
self : PreparedStatement,
index : Int,
value : Int,
) -> Result[Unit, DuckDBError] {
match js_bind_bigint(self, index, value) {
Ok(_) => Ok(())
Err(message) => Err(DuckDBError::Message(message))
}
}
///|
pub fn PreparedStatement::bind_double(
self : PreparedStatement,
index : Int,
value : Double,
) -> Result[Unit, DuckDBError] {
match js_bind_double(self, index, value) {
Ok(_) => Ok(())
Err(message) => Err(DuckDBError::Message(message))
}
}
///|
pub fn PreparedStatement::bind_varchar(
self : PreparedStatement,
index : Int,
value : String,
) -> Result[Unit, DuckDBError] {
match js_bind_varchar(self, index, value) {
Ok(_) => Ok(())
Err(message) => Err(DuckDBError::Message(message))
}
}
///|
pub fn PreparedStatement::bind_bool(
self : PreparedStatement,
index : Int,
value : Bool,
) -> Result[Unit, DuckDBError] {
match js_bind_bool(self, index, value) {
Ok(_) => Ok(())
Err(message) => Err(DuckDBError::Message(message))
}
}
///|
pub fn PreparedStatement::bind_null(
self : PreparedStatement,
index : Int,
) -> Result[Unit, DuckDBError] {
match js_bind_null(self, index) {
Ok(_) => Ok(())
Err(message) => Err(DuckDBError::Message(message))
}
}
///|
pub fn PreparedStatement::clear_bindings(
self : PreparedStatement,
) -> Result[Unit, DuckDBError] {
match js_clear_bindings(self) {
Ok(_) => Ok(())
Err(message) => Err(DuckDBError::Message(message))
}
}
// ============================================================================
// Date/Timestamp API Implementation (JS Backend)
// ============================================================================
///|
pub fn PreparedStatement::bind_date(
self : PreparedStatement,
index : Int,
days : Int,
) -> Result[Unit, DuckDBError] {
// Convert days to ISO date string for JS
let (year, month, day) = date_to_ymd(days)
let date_str = "\{year}-\{String::pad_start(month.to_string(), 2, '0')}-\{String::pad_start(day.to_string(), 2, '0')}"
match js_bind_date(self, index, date_str) {
Ok(_) => Ok(())
Err(message) => Err(DuckDBError::Message(message))
}
}
///|
pub fn PreparedStatement::bind_timestamp(
self : PreparedStatement,
index : Int,
micros : Int64,
) -> Result[Unit, DuckDBError] {
// Convert microseconds to ISO timestamp string for JS
let (year, month, day, hour, minute, second) = timestamp_to_ymd_hms(micros)
let date_str = "\{year}-\{String::pad_start(month.to_string(), 2, '0')}-\{String::pad_start(day.to_string(), 2, '0')}"
let time_str = "\{String::pad_start(hour.to_string(), 2, '0')}:\{String::pad_start(minute.to_string(), 2, '0')}:\{String::pad_start(second.to_string(), 2, '0')}"
let timestamp_str = "\{date_str} \{time_str}"
match js_bind_timestamp(self, index, timestamp_str) {
Ok(_) => Ok(())
Err(message) => Err(DuckDBError::Message(message))
}
}
///|
pub fn Appender::append_date(
self : Appender,
days : Int,
) -> Result[Unit, DuckDBError] {
let _ = self
let _ = days
// Appender not supported in JS backend
Err(DuckDBError::Message("Appender not yet supported for JS backend"))
}
///|
pub fn Appender::append_timestamp(
self : Appender,
micros : Int64,
) -> Result[Unit, DuckDBError] {
let _ = self
let _ = micros
// Appender not supported in JS backend
Err(DuckDBError::Message("Appender not yet supported for JS backend"))
}
///|
/// Cumulative days before each month (for non-leap years).
fn days_before_month(m : Int) -> Int {
match m {
1 => 0
2 => 31
3 => 59
4 => 90
5 => 120
6 => 151
7 => 181
8 => 212
9 => 243
10 => 273
11 => 304
12 => 334
_ => 0
}
}
///|
/// Count leap years between year 1 and the given year (exclusive).
fn count_leap_years_before(year : Int) -> Int {
let y = year - 1
y / 4 - y / 100 + y / 400
}
///|
/// Convert year, month, day to days since 1970-01-01 (Unix epoch).
/// Uses accurate Gregorian calendar calculation with leap year support.
pub fn date_from_ymd(year : Int, month : Int, day : Int) -> Int {
// Days from 1970 to the beginning of the given year
let year_delta = year - 1970
let leap_years = count_leap_years_before(year) - count_leap_years_before(1970)
let year_days = year_delta * 365 + leap_years
// Days from beginning of year to beginning of month
let month_days = days_before_month(month)
// Add leap day if past February in a leap year
let leap_day = if month > 2 && is_leap_year(year) { 1 } else { 0 }
// Day of month (0-indexed)
let day_days = day - 1
year_days + month_days + leap_day + day_days
}
///|
/// Convert days since 1970-01-01 to year, month, day.
/// Uses accurate Gregorian calendar calculation with leap year support.
pub fn date_to_ymd(total_days : Int) -> (Int, Int, Int) {
// Use a simpler algorithm: count years from 1970
let mut days = total_days
let mut year = 1970
// Count years forward
while days >= 365 {
let leap = if is_leap_year(year) { 1 } else { 0 }
let days_in_year = 365 + leap
if days >= days_in_year {
days = days - days_in_year
year = year + 1
} else {
break
}
}
// Find month
let is_leap = is_leap_year(year)
let mut month = 1
let mut remaining = days
// Cumulative days for each month
while month <= 12 {
let dim = match month {
2 => if is_leap { 29 } else { 28 }
4 | 6 | 9 | 11 => 30
_ => 31
}
if remaining < dim {
break
}
remaining = remaining - dim
month = month + 1
}
let day = remaining + 1
(year, month, day)
}
///|
/// Convert date components to a timestamp (microseconds since 1970-01-01).
/// Note: This is a simplified calculation. For production use, use a proper date library.
pub fn timestamp_from_ymd_hms(
year : Int,
month : Int,
day : Int,
hour : Int,
minute : Int,
second : Int,
) -> Int64 {
let date_days = date_from_ymd(year, month, day)
let date_days_64 = date_days.to_int64()
let day_micros = date_days_64 * 86400L * 1000000L
let hour_64 = hour.to_int64()
let minute_64 = minute.to_int64()
let second_64 = second.to_int64()
let time_micros = (hour_64 * 3600L + minute_64 * 60L + second_64) * 1000000L
day_micros + time_micros
}
///|
/// Convert timestamp (microseconds since 1970-01-01) to date components.
/// Note: This is a simplified calculation. For production use, use a proper date library.
pub fn timestamp_to_ymd_hms(micros : Int64) -> (Int, Int, Int, Int, Int, Int) {
let total_seconds = micros / 1000000L
let days = (total_seconds / 86400L).to_int()
let seconds_in_day = (total_seconds % 86400L).to_int()
let hour = seconds_in_day / 3600
let remaining_seconds = seconds_in_day % 3600
let minute = remaining_seconds / 60
let second = remaining_seconds % 60
let (year, month, day) = date_to_ymd(days)
(year, month, day, hour, minute, second)
}
///|
pub fn PreparedStatement::execute(
self : PreparedStatement,
on_done~ : (Result[QueryResult, DuckDBError]) -> Unit,
) -> Unit {
js_execute_prepared(
self,
fn(columns, rows, nulls, type_ids) {
let column_types = column_types_from_ids(columns, type_ids)
on_done(Ok({ columns, column_types, rows, nulls }))
},
fn(message) { on_done(Err(DuckDBError::Message(message))) },
)
}
///|
pub fn PreparedStatement::execute_stream(
self : PreparedStatement,
on_done~ : (Result[ResultStream, DuckDBError]) -> Unit,
) -> Unit {
js_execute_prepared_stream(self, fn(stream) { on_done(Ok(stream)) }, fn(
message,
) {
on_done(Err(DuckDBError::Message(message)))
})
}
///|
pub fn PreparedStatement::close(
self : PreparedStatement,
on_done~ : (Result[Unit, DuckDBError]) -> Unit,
) -> Unit {
js_close_prepared(self, fn() { on_done(Ok(())) }, fn(message) {
on_done(Err(DuckDBError::Message(message)))
})
}
// ============================================================================
// Configuration API Implementation
// ============================================================================
///|
extern "js" fn js_config_create() -> Config =
#|() => {
#| return { kind: "config", options: {} };
#|}
///|
extern "js" fn js_config_set(
cfg : Config,
key : String,
value : String,
) -> Result[Unit, String] =
#|(cfg, key, value) => {
#| try {
#| cfg.options[key] = value;
#| return { ok: true };
#| } catch (e) {
#| return { ok: false, error: e.message || String(e) };
#| }
#|}
///|
extern "js" fn js_connect_with_config(
path : String,
config : Config,
backend : JsBackend,
on_ok : (Connection) -> Unit,
on_err : (String) -> Unit,
) -> Unit =
#|(path, config, backend, on_ok, on_err) => {
#| const toError = (err) => err && err.message ? err.message : String(err);
#| const isNode = typeof process !== "undefined" && !!(process.versions && process.versions.node);
#| let mode = backend | 0;
#| if (mode === 0) {
#| mode = isNode ? 1 : 2;
#| }
#|
#| const configOptions = config.options || {};
#|
#| const connectNode = async () => {
#| const api = await import("@duckdb/node-api");
#| const instance = await api.DuckDBInstance.create(path || ":memory:", configOptions);
#| const connection = await instance.connect();
#| on_ok({ kind: "node", instance, connection });
#| };
#|
#| const connectWasm = async () => {
#| if (typeof Worker === "undefined") {
#| throw new Error("duckdb-wasm requires Worker support");
#| }
#| const duckdb = await import("@duckdb/duckdb-wasm");
#| const bundles = duckdb.getJsDelivrBundles();
#| const bundle = await duckdb.selectBundle(bundles);
#| const logger = new duckdb.ConsoleLogger();
#| const worker = new Worker(bundle.mainWorker);
#| const db = new duckdb.AsyncDuckDB(logger, worker);
#| await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
#| const conn = await db.connect();
#| on_ok({ kind: "wasm", db, conn, worker });
#| };
#|
#| const run = async () => {
#| if (mode === 1) {
#| await connectNode();
#| return;
#| }
#| if (mode === 2) {
#| await connectWasm();
#| return;
#| }
#| throw new Error("unknown JsBackend");
#| };
#| run().catch((err) => on_err(toError(err)));
#|}
///|
pub fn Config::create() -> Config {
js_config_create()
}
///|
pub fn Config::set(
self : Config,
key : String,
value : String,
) -> Result[Unit, DuckDBError] {
match js_config_set(self, key, value) {
Ok(_) => Ok(())
Err(message) => Err(DuckDBError::Message(message))
}
}
///|
pub fn connect_with_config(
on_ready~ : (Result[Connection, DuckDBError]) -> Unit,
config : Config?,
path? : String = ":memory:",
backend? : JsBackend = JsBackend::Auto,
) -> Unit {
touch_public_types(backend)
match config {
Some(cfg) =>
js_connect_with_config(
path,
cfg,
backend,
fn(conn) { on_ready(Ok(conn)) },
fn(message) { on_ready(Err(DuckDBError::Message(message))) },
)
None =>
// Call the original connect function - need to create a wrapper
js_connect(path, backend, fn(conn) { on_ready(Ok(conn)) }, fn(message) {
on_ready(Err(DuckDBError::Message(message)))
})
}
}
// ============================================================================
// Appender API Implementation (Node backend only)
// ============================================================================
///|
pub fn Connection::create_appender(
self : Connection,
schema : String,
table : String,
on_done~ : (Result[Appender, DuckDBError]) -> Unit,
) -> Unit {
// Note: schema parameter is ignored for Node API (uses table name only)
let _ = schema
js_create_appender(self, table, fn(a) { on_done(Ok(a)) }, fn(e) {
on_done(Err(DuckDBError::Message(e)))
})
}
///|
pub fn Appender::begin_row(self : Appender) -> Result[Unit, DuckDBError] {
match js_appender_begin_row(self) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::append_int(
self : Appender,
value : Int,
) -> Result[Unit, DuckDBError] {
match js_appender_append_int(self, value) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::append_bigint(
self : Appender,
value : Int,
) -> Result[Unit, DuckDBError] {
match js_appender_append_bigint(self, value) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::append_double(
self : Appender,
value : Double,
) -> Result[Unit, DuckDBError] {
match js_appender_append_double(self, value) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::append_varchar(
self : Appender,
value : String,
) -> Result[Unit, DuckDBError] {
match js_appender_append_varchar(self, value) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::append_bool(
self : Appender,
value : Bool,
) -> Result[Unit, DuckDBError] {
match js_appender_append_bool(self, value) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::append_null(self : Appender) -> Result[Unit, DuckDBError] {
match js_appender_append_null(self) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::end_row(self : Appender) -> Result[Unit, DuckDBError] {
match js_appender_end_row(self) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::flush(self : Appender) -> Result[Unit, DuckDBError] {
match js_appender_flush(self) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::close(
self : Appender,
on_done~ : (Result[Unit, DuckDBError]) -> Unit,
) -> Unit {
js_appender_close(self, fn() { on_done(Ok(())) }, fn(e) {
on_done(Err(DuckDBError::Message(e)))
})
}
// ============================================================================
// Advanced Data Types for JS Backend
// ============================================================================
// ----------------------------------------------------------------------------
// Blob Type
// ----------------------------------------------------------------------------
///|
pub fn PreparedStatement::bind_blob(
self : PreparedStatement,
index : Int,
value : Bytes,
) -> Result[Unit, DuckDBError] {
match js_bind_blob(self, index, value) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::append_blob(
self : Appender,
value : Bytes,
) -> Result[Unit, DuckDBError] {
match js_appender_append_blob(self, value) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
// ----------------------------------------------------------------------------
// Decimal Type
// ----------------------------------------------------------------------------
///|
pub fn PreparedStatement::bind_decimal(
self : PreparedStatement,
index : Int,
value : Decimal,
) -> Result[Unit, DuckDBError] {
// For JS Node backend, convert to BigInt from lower/upper
// Note: This loses true 128-bit precision, but provides best effort
let bigint_value = if value.upper < 0 {
// Negative: construct two's complement
BigInt::from_int(value.lower) - (BigInt::from_int(1) << 64)
} else {
// Positive or zero
BigInt::from_int(value.lower) + (BigInt::from_int(value.upper) << 64)
}
// Use bind_decimal with the BigInt value
match
js_bind_decimal(
self,
index,
value.width,
value.scale,
bigint_value.to_int(),
) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::append_decimal(
self : Appender,
value : Decimal,
) -> Result[Unit, DuckDBError] {
// For JS Node backend, convert to BigInt from lower/upper
let bigint_value = if value.upper < 0 {
// Negative: construct two's complement
BigInt::from_int(value.lower) - (BigInt::from_int(1) << 64)
} else {
// Positive or zero
BigInt::from_int(value.lower) + (BigInt::from_int(value.upper) << 64)
}
match
js_appender_append_decimal(
self,
value.width,
value.scale,
bigint_value.to_int(),
) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn decimal_from_double(value : Double, width : Int, scale : Int) -> Decimal {
let multiplier = int_pow10(scale).to_int()
let scaled = (value * Double::from_int(multiplier)).to_int()
let upper = if scaled >= 0 { 0 } else { -1 }
{ width, scale, lower: scaled, upper }
}
///|
pub fn decimal_to_double(decimal : Decimal) -> Double {
let divisor = Double::from_int(int_pow10(decimal.scale).to_int())
Double::from_int(decimal.lower) / divisor
}
///|
pub fn decimal_from_parts(
whole : Int,
fractional : Int,
scale : Int,
) -> Decimal {
let divisor = int_pow10(scale).to_int()
let value = if whole < 0 {
whole * divisor - fractional
} else {
whole * divisor + fractional
}
let width = if whole == 0 {
String::length(fractional.to_string())
} else {
String::length(whole.to_string()) + scale
}
let upper = if value >= 0 { 0 } else { -1 }
{ width: width.max(1), scale, lower: value, upper }
}
///|
pub fn decimal_to_parts(decimal : Decimal) -> (Int, Int) {
let divisor = int_pow10(decimal.scale).to_int()
let whole = decimal.lower / divisor
let remainder = decimal.lower % divisor
let fractional = if remainder < 0 {
divisor - Int::abs(remainder)
} else {
remainder
}
(whole, fractional)
}
///|
/// Create a decimal from 128-bit parts.
pub fn decimal_from_hugeint(
lower : Int,
upper : Int,
width : Int,
scale : Int,
) -> Decimal {
{ width, scale, lower, upper }
}
// ----------------------------------------------------------------------------
// Interval Type
// ----------------------------------------------------------------------------
///|
pub fn PreparedStatement::bind_interval(
self : PreparedStatement,
index : Int,
value : Interval,
) -> Result[Unit, DuckDBError] {
match js_bind_interval(self, index, value.months, value.days, value.micros) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn Appender::append_interval(
self : Appender,
value : Interval,
) -> Result[Unit, DuckDBError] {
match
js_appender_append_interval(self, value.months, value.days, value.micros) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn interval_from_parts(
months : Int,
days : Int,
micros : Int64,
) -> Interval {
{ months, days, micros }
}
///|
pub fn interval_from_days(days : Int) -> Interval {
{ months: 0, days, micros: 0 }
}
///|
pub fn interval_from_hours(hours : Int) -> Interval {
let micros = hours.to_int64() * 3600L * 1000000L
{ months: 0, days: 0, micros }
}
///|
pub fn interval_from_minutes(minutes : Int) -> Interval {
let micros = minutes.to_int64() * 60L * 1000000L
{ months: 0, days: 0, micros }
}
///|
pub fn interval_from_seconds(seconds : Int) -> Interval {
let micros = seconds.to_int64() * 1000000L
{ months: 0, days: 0, micros }
}
///|
pub fn interval_from_months(months : Int) -> Interval {
{ months, days: 0, micros: 0 }
}
///|
pub fn interval_to_micros(interval : Interval) -> Int64 {
let days_micros = interval.days.to_int64() * 86400L * 1000000L
interval.micros + days_micros
}
// ----------------------------------------------------------------------------
// List Type
// ----------------------------------------------------------------------------
///|
pub fn PreparedStatement::bind_list_varchar(
self : PreparedStatement,
index : Int,
values : Array[String],
) -> Result[Unit, DuckDBError] {
match js_bind_list_varchar(self, index, values) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn list_from_strings(elements : Array[String]) -> List {
{ elements, }
}
///|
pub fn list_length(list : List) -> Int {
list.elements.length()
}
///|
pub fn list_get(list : List, index : Int) -> String? {
if index >= 0 && index < list.elements.length() {
Some(list.elements[index])
} else {
None
}
}
// ----------------------------------------------------------------------------
// Struct Type
// ----------------------------------------------------------------------------
///|
pub fn PreparedStatement::bind_struct(
self : PreparedStatement,
index : Int,
value : Struct,
) -> Result[Unit, DuckDBError] {
match js_bind_struct(self, index, value.fields, value.values) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn struct_from_arrays(
fields : Array[String],
values : Array[String],
) -> Struct {
{ fields, values }
}
///|
pub fn struct_from_pairs(pairs : Array[(String, String)]) -> Struct {
let fields = pairs.map(fn(p) { p.0 })
let values = pairs.map(fn(p) { p.1 })
{ fields, values }
}
// Helper function to find index in array
///|
fn array_index_of(arr : Array[String], val : String) -> Int? {
let mut i = 0
while i < arr.length() {
if arr[i] == val {
return Some(i)
}
i = i + 1
}
None
}
///|
pub fn struct_get(s : Struct, field_name : String) -> String? {
let idx = array_index_of(s.fields, field_name)
match idx {
Some(i) => if i < s.values.length() { Some(s.values[i]) } else { None }
None => None
}
}
///|
pub fn struct_field_count(s : Struct) -> Int {
s.fields.length()
}
// ----------------------------------------------------------------------------
// Map Type
// ----------------------------------------------------------------------------
///|
pub fn PreparedStatement::bind_map(
self : PreparedStatement,
index : Int,
map : Map,
) -> Result[Unit, DuckDBError] {
match js_bind_map(self, index, map.keys, map.values) {
Ok(_) => Ok(())
Err(e) => Err(DuckDBError::Message(e))
}
}
///|
pub fn map_from_arrays(keys : Array[String], values : Array[String]) -> Map {
{ keys, values }
}
///|
pub fn map_from_pairs(pairs : Array[(String, String)]) -> Map {
let keys = pairs.map(fn(p) { p.0 })
let values = pairs.map(fn(p) { p.1 })
{ keys, values }
}
///|
pub fn map_get(m : Map, key : String) -> String? {
let idx = array_index_of(m.keys, key)
match idx {
Some(i) => if i < m.values.length() { Some(m.values[i]) } else { None }
None => None
}
}
///|
pub fn map_size(m : Map) -> Int {
m.keys.length()
}