@rip-lang/db 0.10.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/duckdb.mjs CHANGED
@@ -1,95 +1,141 @@
1
1
  /**
2
- * DuckDB bindings for Bun using Zig FFI
2
+ * DuckDB Pure Bun FFI Wrapper
3
3
  *
4
- * Clean, type-safe wrapper with full type support.
4
+ * Direct FFI bindings to DuckDB's C API using the modern chunk-based API.
5
+ * No deprecated per-value functions. No Zig, no npm package.
6
+ *
7
+ * Usage:
8
+ * import { open } from './duckdb.mjs';
9
+ *
10
+ * const db = open(':memory:');
11
+ * const conn = db.connect();
12
+ * const rows = await conn.query('SELECT 42 as num');
13
+ * conn.close();
14
+ * db.close();
5
15
  */
6
16
 
7
- import { dlopen, ptr, CString } from 'bun:ffi';
8
- import { fileURLToPath } from 'url';
9
- import { dirname, join } from 'path';
10
- import { platform, arch } from 'process';
11
-
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
-
14
- // Map Node.js arch names
15
- const archMap = { 'arm64': 'arm64', 'x64': 'x64', 'x86_64': 'x64' };
16
- const target = `${platform}-${archMap[arch] || arch}`;
17
-
18
- // Load the platform-specific native module
19
- const libPath = join(__dirname, `${target}/duckdb.node`);
20
-
21
- const duck = dlopen(libPath, {
22
- // Database operations (usize = pointer-sized integer)
23
- duck_open: { args: ['ptr'], returns: 'usize' },
24
- duck_close: { args: ['usize'], returns: 'void' },
25
- duck_connect: { args: ['usize'], returns: 'usize' },
26
- duck_disconnect: { args: ['usize'], returns: 'void' },
27
-
28
- // Query operations
29
- duck_query: { args: ['usize', 'ptr'], returns: 'usize' },
30
- duck_free_result: { args: ['usize'], returns: 'void' },
31
- duck_result_error: { args: ['usize'], returns: 'ptr' },
32
- duck_row_count: { args: ['usize'], returns: 'u64' },
33
- duck_column_count: { args: ['usize'], returns: 'u64' },
34
- duck_column_name: { args: ['usize', 'u64'], returns: 'ptr' },
35
- duck_column_type: { args: ['usize', 'u64'], returns: 'u32' },
36
-
37
- // Value extraction
38
- duck_value_is_null: { args: ['usize', 'u64', 'u64'], returns: 'bool' },
39
- duck_value_bool: { args: ['usize', 'u64', 'u64'], returns: 'bool' },
40
- duck_value_i8: { args: ['usize', 'u64', 'u64'], returns: 'i8' },
41
- duck_value_i16: { args: ['usize', 'u64', 'u64'], returns: 'i16' },
42
- duck_value_i32: { args: ['usize', 'u64', 'u64'], returns: 'i32' },
43
- duck_value_i64: { args: ['usize', 'u64', 'u64'], returns: 'i64' },
44
- duck_value_u8: { args: ['usize', 'u64', 'u64'], returns: 'u8' },
45
- duck_value_u16: { args: ['usize', 'u64', 'u64'], returns: 'u16' },
46
- duck_value_u32: { args: ['usize', 'u64', 'u64'], returns: 'u32' },
47
- duck_value_u64: { args: ['usize', 'u64', 'u64'], returns: 'u64' },
48
- duck_value_f32: { args: ['usize', 'u64', 'u64'], returns: 'f32' },
49
- duck_value_f64: { args: ['usize', 'u64', 'u64'], returns: 'f64' },
50
- duck_value_string_internal: { args: ['usize', 'u64', 'u64'], returns: 'ptr' },
51
- duck_value_date_days: { args: ['usize', 'u64', 'u64'], returns: 'i32' },
52
- duck_value_time_micros: { args: ['usize', 'u64', 'u64'], returns: 'i64' },
53
- duck_value_timestamp_micros: { args: ['usize', 'u64', 'u64'], returns: 'i64' },
17
+ import { dlopen, ptr, CString, read as ffiRead } from 'bun:ffi';
18
+ import { platform } from 'process';
19
+ import { existsSync } from 'fs';
20
+
21
+ // ==============================================================================
22
+ // Find DuckDB Library
23
+ // ==============================================================================
24
+
25
+ function findDuckDBLibrary() {
26
+ const candidates = [];
27
+
28
+ if (platform === 'darwin') {
29
+ candidates.push(
30
+ '/opt/homebrew/lib/libduckdb.dylib',
31
+ '/usr/local/lib/libduckdb.dylib',
32
+ '/usr/lib/libduckdb.dylib',
33
+ );
34
+ } else if (platform === 'linux') {
35
+ candidates.push(
36
+ '/usr/lib/libduckdb.so',
37
+ '/usr/local/lib/libduckdb.so',
38
+ '/usr/lib/x86_64-linux-gnu/libduckdb.so',
39
+ '/usr/lib/aarch64-linux-gnu/libduckdb.so',
40
+ );
41
+ } else if (platform === 'win32') {
42
+ candidates.push(
43
+ 'C:\\Program Files\\DuckDB\\duckdb.dll',
44
+ 'duckdb.dll',
45
+ );
46
+ }
47
+
48
+ if (process.env.DUCKDB_LIB_PATH) {
49
+ candidates.unshift(process.env.DUCKDB_LIB_PATH);
50
+ }
51
+
52
+ for (const path of candidates) {
53
+ if (existsSync(path)) return path;
54
+ }
55
+
56
+ throw new Error(
57
+ `Could not find DuckDB library. Tried:\n${candidates.join('\n')}\n\n` +
58
+ `Install DuckDB or set DUCKDB_LIB_PATH environment variable.`
59
+ );
60
+ }
61
+
62
+ const libPath = findDuckDBLibrary();
63
+
64
+ // ==============================================================================
65
+ // Load DuckDB C API (modern chunk-based + lifecycle functions)
66
+ // ==============================================================================
67
+
68
+ const lib = dlopen(libPath, {
69
+ // Database lifecycle
70
+ duckdb_open: { args: ['ptr', 'ptr'], returns: 'i32' },
71
+ duckdb_close: { args: ['ptr'], returns: 'void' },
72
+
73
+ // Connection lifecycle
74
+ duckdb_connect: { args: ['ptr', 'ptr'], returns: 'i32' },
75
+ duckdb_disconnect: { args: ['ptr'], returns: 'void' },
76
+
77
+ // Query execution
78
+ duckdb_query: { args: ['ptr', 'ptr', 'ptr'], returns: 'i32' },
79
+ duckdb_destroy_result: { args: ['ptr'], returns: 'void' },
54
80
 
55
81
  // Prepared statements
56
- duck_prepare: { args: ['usize', 'ptr'], returns: 'usize' },
57
- duck_prepare_error: { args: ['usize'], returns: 'ptr' },
58
- duck_free_prepare: { args: ['usize'], returns: 'void' },
59
- duck_nparams: { args: ['usize'], returns: 'u64' },
60
- duck_param_type: { args: ['usize', 'u64'], returns: 'u32' },
61
- duck_execute_prepared: { args: ['usize'], returns: 'usize' },
62
-
63
- // Parameter binding
64
- duck_bind_null: { args: ['usize', 'u64'], returns: 'bool' },
65
- duck_bind_bool: { args: ['usize', 'u64', 'bool'], returns: 'bool' },
66
- duck_bind_i8: { args: ['usize', 'u64', 'i8'], returns: 'bool' },
67
- duck_bind_i16: { args: ['usize', 'u64', 'i16'], returns: 'bool' },
68
- duck_bind_i32: { args: ['usize', 'u64', 'i32'], returns: 'bool' },
69
- duck_bind_i64: { args: ['usize', 'u64', 'i64'], returns: 'bool' },
70
- duck_bind_u8: { args: ['usize', 'u64', 'u8'], returns: 'bool' },
71
- duck_bind_u16: { args: ['usize', 'u64', 'u16'], returns: 'bool' },
72
- duck_bind_u32: { args: ['usize', 'u64', 'u32'], returns: 'bool' },
73
- duck_bind_u64: { args: ['usize', 'u64', 'u64'], returns: 'bool' },
74
- duck_bind_f32: { args: ['usize', 'u64', 'f32'], returns: 'bool' },
75
- duck_bind_f64: { args: ['usize', 'u64', 'f64'], returns: 'bool' },
76
- duck_bind_string: { args: ['usize', 'u64', 'ptr', 'u64'], returns: 'bool' },
77
- duck_bind_blob: { args: ['usize', 'u64', 'ptr', 'u64'], returns: 'bool' },
78
- duck_bind_timestamp: { args: ['usize', 'u64', 'i64'], returns: 'bool' },
79
- duck_bind_date: { args: ['usize', 'u64', 'i32'], returns: 'bool' },
80
- duck_bind_time: { args: ['usize', 'u64', 'i64'], returns: 'bool' },
81
-
82
- // Utility
83
- duck_free: { args: ['usize'], returns: 'void' },
82
+ duckdb_prepare: { args: ['ptr', 'ptr', 'ptr'], returns: 'i32' },
83
+ duckdb_prepare_error: { args: ['ptr'], returns: 'ptr' },
84
+ duckdb_destroy_prepare: { args: ['ptr'], returns: 'void' },
85
+ duckdb_bind_null: { args: ['ptr', 'u64'], returns: 'i32' },
86
+ duckdb_bind_boolean: { args: ['ptr', 'u64', 'bool'], returns: 'i32' },
87
+ duckdb_bind_int32: { args: ['ptr', 'u64', 'i32'], returns: 'i32' },
88
+ duckdb_bind_int64: { args: ['ptr', 'u64', 'i64'], returns: 'i32' },
89
+ duckdb_bind_double: { args: ['ptr', 'u64', 'f64'], returns: 'i32' },
90
+ duckdb_bind_varchar: { args: ['ptr', 'u64', 'ptr'], returns: 'i32' },
91
+ duckdb_execute_prepared: { args: ['ptr', 'ptr'], returns: 'i32' },
92
+
93
+ // Result inspection
94
+ duckdb_column_count: { args: ['ptr'], returns: 'u64' },
95
+ duckdb_column_name: { args: ['ptr', 'u64'], returns: 'ptr' },
96
+ duckdb_column_type: { args: ['ptr', 'u64'], returns: 'i32' },
97
+ duckdb_result_error: { args: ['ptr'], returns: 'ptr' },
98
+
99
+ // Modern chunk-based API (non-deprecated)
100
+ duckdb_fetch_chunk: { args: ['ptr'], returns: 'ptr' },
101
+ duckdb_data_chunk_get_size: { args: ['ptr'], returns: 'u64' },
102
+ duckdb_data_chunk_get_vector: { args: ['ptr', 'u64'], returns: 'ptr' },
103
+ duckdb_vector_get_data: { args: ['ptr'], returns: 'ptr' },
104
+ duckdb_vector_get_validity: { args: ['ptr'], returns: 'ptr' },
105
+ duckdb_destroy_data_chunk: { args: ['ptr'], returns: 'void' },
106
+
107
+ // Logical type introspection (for DECIMAL, ENUM, LIST, STRUCT)
108
+ duckdb_column_logical_type: { args: ['ptr', 'u64'], returns: 'ptr' },
109
+ duckdb_destroy_logical_type: { args: ['ptr'], returns: 'void' },
110
+ duckdb_get_type_id: { args: ['ptr'], returns: 'i32' },
111
+ duckdb_decimal_width: { args: ['ptr'], returns: 'u8' },
112
+ duckdb_decimal_scale: { args: ['ptr'], returns: 'u8' },
113
+ duckdb_decimal_internal_type: { args: ['ptr'], returns: 'i32' },
114
+ duckdb_enum_internal_type: { args: ['ptr'], returns: 'i32' },
115
+ duckdb_enum_dictionary_size: { args: ['ptr'], returns: 'u32' },
116
+ duckdb_enum_dictionary_value: { args: ['ptr', 'u64'], returns: 'ptr' },
117
+
118
+ // Nested type vector access (LIST, STRUCT)
119
+ duckdb_list_vector_get_child: { args: ['ptr'], returns: 'ptr' },
120
+ duckdb_list_vector_get_size: { args: ['ptr'], returns: 'u64' }, // Not yet used; available for LIST size checks
121
+ duckdb_struct_vector_get_child: { args: ['ptr', 'u64'], returns: 'ptr' },
122
+ duckdb_struct_type_child_count: { args: ['ptr'], returns: 'u64' },
123
+ duckdb_struct_type_child_name: { args: ['ptr', 'u64'], returns: 'ptr' },
124
+ duckdb_struct_type_child_type: { args: ['ptr', 'u64'], returns: 'ptr' },
125
+ duckdb_list_type_child_type: { args: ['ptr'], returns: 'ptr' },
126
+
127
+ // Memory
128
+ duckdb_free: { args: ['ptr'], returns: 'void' },
129
+
130
+ // Library info
131
+ duckdb_library_version: { args: [], returns: 'ptr' },
84
132
  }).symbols;
85
133
 
86
- // Note: Don't use .native versions as they convert BigInts to Numbers,
87
- // which can cause precision loss for pointer values on 64-bit systems.
134
+ // ==============================================================================
135
+ // DuckDB Type Constants
136
+ // ==============================================================================
88
137
 
89
- const utf8 = new TextEncoder();
90
-
91
- // DuckDB type enum
92
- const Type = {
138
+ const DUCKDB_TYPE = {
93
139
  INVALID: 0,
94
140
  BOOLEAN: 1,
95
141
  TINYINT: 2,
@@ -118,295 +164,639 @@ const Type = {
118
164
  STRUCT: 25,
119
165
  MAP: 26,
120
166
  UUID: 27,
121
- JSON: 28,
167
+ UNION: 28,
168
+ BIT: 29,
169
+ TIMESTAMP_TZ: 32,
122
170
  };
123
171
 
124
- // Value extractors by type
125
- const extractors = {
126
- [Type.BOOLEAN]: (r, c, row) => duck.duck_value_bool(r, c, row),
127
- [Type.TINYINT]: (r, c, row) => duck.duck_value_i8(r, c, row),
128
- [Type.SMALLINT]: (r, c, row) => duck.duck_value_i16(r, c, row),
129
- [Type.INTEGER]: (r, c, row) => duck.duck_value_i32(r, c, row),
130
- [Type.BIGINT]: (r, c, row) => Number(duck.duck_value_i64(r, c, row)), // Convert to Number for JSON compatibility
131
- [Type.UTINYINT]: (r, c, row) => duck.duck_value_u8(r, c, row),
132
- [Type.USMALLINT]: (r, c, row) => duck.duck_value_u16(r, c, row),
133
- [Type.UINTEGER]: (r, c, row) => duck.duck_value_u32(r, c, row),
134
- [Type.UBIGINT]: (r, c, row) => Number(duck.duck_value_u64(r, c, row)), // Convert to Number for JSON compatibility
135
- [Type.FLOAT]: (r, c, row) => duck.duck_value_f32(r, c, row),
136
- [Type.DOUBLE]: (r, c, row) => duck.duck_value_f64(r, c, row),
137
- [Type.DECIMAL]: (r, c, row) => duck.duck_value_f64(r, c, row), // Treat as double
138
- [Type.VARCHAR]: (r, c, row) => new CString(duck.duck_value_string_internal(r, c, row)).toString(),
139
- [Type.JSON]: (r, c, row) => new CString(duck.duck_value_string_internal(r, c, row)).toString(),
140
- [Type.UUID]: (r, c, row) => new CString(duck.duck_value_string_internal(r, c, row)).toString(),
141
- [Type.DATE]: (r, c, row) => duck.duck_value_date_days(r, c, row) * 86400000, // ms since epoch
142
- [Type.TIME]: (r, c, row) => Number(duck.duck_value_time_micros(r, c, row)) / 1000, // ms
143
- [Type.TIMESTAMP]: (r, c, row) => Number(duck.duck_value_timestamp_micros(r, c, row)) / 1000,
144
- [Type.TIMESTAMP_S]: (r, c, row) => Number(duck.duck_value_timestamp_micros(r, c, row)) / 1000,
145
- [Type.TIMESTAMP_MS]: (r, c, row) => Number(duck.duck_value_timestamp_micros(r, c, row)) / 1000,
146
- [Type.TIMESTAMP_NS]: (r, c, row) => Number(duck.duck_value_timestamp_micros(r, c, row)) / 1000,
147
- [Type.HUGEINT]: (r, c, row) => Number(duck.duck_value_i64(r, c, row)), // Simplified, Number for JSON
148
- };
172
+ export { DUCKDB_TYPE };
173
+
174
+ // ==============================================================================
175
+ // Helper Functions
176
+ // ==============================================================================
177
+
178
+ // Async mutex to serialize FFI calls
179
+ let ffiLock = Promise.resolve();
180
+ function withLock(fn) {
181
+ const prev = ffiLock;
182
+ let resolve;
183
+ ffiLock = new Promise(r => resolve = r);
184
+ return prev.then(() => {
185
+ try { return fn(); }
186
+ finally { resolve(); }
187
+ });
188
+ }
149
189
 
150
- // Parameter binders by type
151
- const binders = {
152
- [Type.BOOLEAN]: (stmt, idx, val) => duck.duck_bind_bool(stmt, idx, val),
153
- [Type.TINYINT]: (stmt, idx, val) => duck.duck_bind_i8(stmt, idx, val | 0),
154
- [Type.SMALLINT]: (stmt, idx, val) => duck.duck_bind_i16(stmt, idx, val | 0),
155
- [Type.INTEGER]: (stmt, idx, val) => duck.duck_bind_i32(stmt, idx, val | 0),
156
- [Type.BIGINT]: (stmt, idx, val) => duck.duck_bind_i64(stmt, idx, BigInt(val)),
157
- [Type.UTINYINT]: (stmt, idx, val) => duck.duck_bind_u8(stmt, idx, val >>> 0),
158
- [Type.USMALLINT]: (stmt, idx, val) => duck.duck_bind_u16(stmt, idx, val >>> 0),
159
- [Type.UINTEGER]: (stmt, idx, val) => duck.duck_bind_u32(stmt, idx, val >>> 0),
160
- [Type.UBIGINT]: (stmt, idx, val) => duck.duck_bind_u64(stmt, idx, BigInt(val)),
161
- [Type.FLOAT]: (stmt, idx, val) => duck.duck_bind_f32(stmt, idx, val),
162
- [Type.DOUBLE]: (stmt, idx, val) => duck.duck_bind_f64(stmt, idx, val),
163
- [Type.DECIMAL]: (stmt, idx, val) => duck.duck_bind_f64(stmt, idx, val),
164
- [Type.VARCHAR]: (stmt, idx, val) => {
165
- const bytes = utf8.encode(String(val));
166
- return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
167
- },
168
- [Type.JSON]: (stmt, idx, val) => {
169
- const str = typeof val === 'string' ? val : JSON.stringify(val);
170
- const bytes = utf8.encode(str);
171
- return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
172
- },
173
- [Type.UUID]: (stmt, idx, val) => {
174
- const bytes = utf8.encode(String(val));
175
- return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
176
- },
177
- [Type.TIMESTAMP]: (stmt, idx, val) => duck.duck_bind_timestamp(stmt, idx, BigInt(val) * 1000n),
178
- [Type.DATE]: (stmt, idx, val) => duck.duck_bind_date(stmt, idx, Math.floor(val / 86400000)),
179
- [Type.TIME]: (stmt, idx, val) => duck.duck_bind_time(stmt, idx, BigInt(val) * 1000n),
180
- };
190
+ const encoder = new TextEncoder();
191
+ const decoder = new TextDecoder();
181
192
 
182
- // Default extractor for unknown types
183
- function defaultExtractor(r, c, row) {
184
- const p = duck.duck_value_string_internal(r, c, row);
185
- return p ? new CString(p) : null;
193
+ function toCString(str) {
194
+ return encoder.encode(str + '\0');
186
195
  }
187
196
 
188
- // Bind a value based on its JS type and param type
189
- function bindValue(stmt, idx, val, paramType) {
190
- if (val === null || val === undefined) {
191
- return duck.duck_bind_null(stmt, idx);
192
- }
197
+ function fromCString(p) {
198
+ if (!p) return null;
199
+ return new CString(p).toString();
200
+ }
193
201
 
194
- const binder = binders[paramType];
195
- if (binder) {
196
- return binder(stmt, idx, val);
197
- }
202
+ function allocPtr() {
203
+ return new Uint8Array(8);
204
+ }
205
+
206
+ function readPtr(buf) {
207
+ return ffiRead.ptr(ptr(buf), 0);
208
+ }
198
209
 
199
- // Fallback: infer from JS type
200
- const t = typeof val;
201
- if (t === 'boolean') return duck.duck_bind_bool(stmt, idx, val);
202
- if (t === 'number') {
203
- if (Number.isInteger(val)) {
204
- return duck.duck_bind_i64(stmt, idx, BigInt(val));
210
+ // Read a duckdb_string_t (16 bytes) from a data pointer at a given row offset
211
+ function readString(dataPtr, row) {
212
+ const offset = row * 16; // duckdb_string_t is 16 bytes
213
+ const length = ffiRead.u32(dataPtr, offset);
214
+
215
+ if (length <= 12) {
216
+ // Inlined: bytes 4-15 contain the string data
217
+ const bytes = new Uint8Array(length);
218
+ for (let i = 0; i < length; i++) {
219
+ bytes[i] = ffiRead.u8(dataPtr, offset + 4 + i);
205
220
  }
206
- return duck.duck_bind_f64(stmt, idx, val);
207
- }
208
- if (t === 'bigint') return duck.duck_bind_i64(stmt, idx, val);
209
- if (t === 'string') {
210
- const bytes = utf8.encode(val);
211
- return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
221
+ return decoder.decode(bytes);
222
+ } else {
223
+ // Pointer: bytes 4-7 are prefix, bytes 8-15 are pointer to string data
224
+ const strPtr = ffiRead.ptr(dataPtr, offset + 8);
225
+ if (!strPtr) return null;
226
+ const bytes = new Uint8Array(length);
227
+ for (let i = 0; i < length; i++) {
228
+ bytes[i] = ffiRead.u8(strPtr, i);
229
+ }
230
+ return decoder.decode(bytes);
212
231
  }
232
+ }
213
233
 
214
- // Object -> JSON
215
- const bytes = utf8.encode(JSON.stringify(val));
216
- return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
234
+ // Check if a row is valid (not NULL) in a validity mask
235
+ function isValid(validityPtr, row) {
236
+ if (!validityPtr) return true; // NULL validity = all valid
237
+ const entryIdx = Math.floor(row / 64);
238
+ const bitIdx = row % 64;
239
+ const entry = ffiRead.u64(validityPtr, entryIdx * 8);
240
+ return (entry & (1n << BigInt(bitIdx))) !== 0n;
217
241
  }
218
242
 
219
- /**
220
- * Open a DuckDB database
221
- * @param {string|null} path - Path to database file, or null/:memory: for in-memory
222
- */
223
- export function open(path) {
224
- return new Database(path);
243
+ // Format a hugeint (16 bytes: lower uint64 at offset 0, upper int64 at offset 8) as UUID
244
+ function readUUID(dataPtr, row) {
245
+ const offset = row * 16;
246
+ const lower = ffiRead.u64(dataPtr, offset);
247
+ const upper = ffiRead.i64(dataPtr, offset + 8);
248
+
249
+ // DuckDB stores UUID as hugeint with XOR on the upper bits
250
+ // Upper 64 bits have sign bit flipped for sorting
251
+ const hi = BigInt(upper) ^ (1n << 63n);
252
+ const lo = BigInt(lower);
253
+
254
+ const hex = ((hi << 64n) | lo).toString(16).padStart(32, '0');
255
+ return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
225
256
  }
226
257
 
258
+ // ==============================================================================
259
+ // Database Class
260
+ // ==============================================================================
261
+
227
262
  class Database {
228
- #ptr;
263
+ #ptrBuf = null;
264
+ #handle = null;
229
265
 
230
266
  constructor(path) {
231
- const p = path === null || path === ':memory:'
232
- ? 0
233
- : ptr(utf8.encode(path + '\0'));
234
- this.#ptr = duck.duck_open(p);
235
- if (this.#ptr === 0) throw new Error('Failed to open database');
267
+ this.#ptrBuf = allocPtr();
268
+ const pathBytes = path && path !== ':memory:' ? toCString(path) : null;
269
+ const result = lib.duckdb_open(pathBytes ? ptr(pathBytes) : null, ptr(this.#ptrBuf));
270
+ if (result !== 0) throw new Error('Failed to open database');
271
+ this.#handle = readPtr(this.#ptrBuf);
236
272
  }
237
273
 
238
- connect() {
239
- return new Connection(this.#ptr);
240
- }
274
+ get handle() { return this.#handle; }
275
+ get ptrBuf() { return this.#ptrBuf; }
276
+
277
+ connect() { return new Connection(this); }
241
278
 
242
279
  close() {
243
- if (this.#ptr) {
244
- duck.duck_close(this.#ptr);
245
- this.#ptr = 0;
280
+ if (this.#ptrBuf) {
281
+ lib.duckdb_close(ptr(this.#ptrBuf));
282
+ this.#ptrBuf = null;
283
+ this.#handle = null;
246
284
  }
247
285
  }
248
286
  }
249
287
 
288
+ // ==============================================================================
289
+ // Connection Class
290
+ // ==============================================================================
291
+
250
292
  class Connection {
251
- #ptr;
293
+ #ptrBuf = null;
294
+ #handle = null;
295
+ #db = null;
252
296
 
253
297
  constructor(db) {
254
- this.#ptr = duck.duck_connect(db);
255
- if (this.#ptr === 0) throw new Error('Failed to connect to database');
298
+ this.#db = db;
299
+ this.#ptrBuf = allocPtr();
300
+ const result = lib.duckdb_connect(db.handle, ptr(this.#ptrBuf));
301
+ if (result !== 0) throw new Error('Failed to create connection');
302
+ this.#handle = readPtr(this.#ptrBuf);
256
303
  }
257
304
 
258
- query(sql) {
259
- const sqlPtr = ptr(utf8.encode(sql + '\0'));
260
- const result = duck.duck_query(this.#ptr, sqlPtr);
305
+ get handle() { return this.#handle; }
306
+ get ptrBuf() { return this.#ptrBuf; }
307
+
308
+ /**
309
+ * Execute a SQL query and return results as array of objects
310
+ * @param {string} sql - SQL query
311
+ * @param {any[]} params - Optional parameters for prepared statement
312
+ * @returns {Promise<object[]>} Array of row objects
313
+ */
314
+ query(sql, params = []) {
315
+ return withLock(() => {
316
+ if (params.length > 0) return this.#queryPrepared(sql, params);
317
+ return this.#querySimple(sql);
318
+ });
319
+ }
261
320
 
262
- // Check for error
263
- const errPtr = duck.duck_result_error(result);
264
- if (errPtr) {
265
- const err = new CString(errPtr);
266
- duck.duck_free_result(result);
267
- throw new Error(err.toString());
321
+ #querySimple(sql) {
322
+ const resultPtr = new Uint8Array(64); // duckdb_result struct is ~48 bytes
323
+ const sqlBytes = toCString(sql);
324
+ lib.duckdb_query(this.#handle, ptr(sqlBytes), ptr(resultPtr));
325
+
326
+ const rp = ptr(resultPtr);
327
+ const errorPtr = lib.duckdb_result_error(rp);
328
+ if (errorPtr) {
329
+ const error = fromCString(errorPtr);
330
+ lib.duckdb_destroy_result(rp);
331
+ throw new Error(error);
268
332
  }
269
333
 
270
- // Extract rows
271
- const rowCount = Number(duck.duck_row_count(result));
272
- const colCount = Number(duck.duck_column_count(result));
273
-
274
- if (rowCount === 0) {
275
- duck.duck_free_result(result);
276
- return [];
334
+ try {
335
+ return this.#extractChunks(resultPtr);
336
+ } finally {
337
+ lib.duckdb_destroy_result(rp);
277
338
  }
339
+ }
278
340
 
279
- // Get column info
280
- const columns = [];
281
- const types = [];
282
- const extract = [];
283
-
284
- for (let c = 0; c < colCount; c++) {
285
- columns.push(new CString(duck.duck_column_name(result, c)).toString());
286
- const type = duck.duck_column_type(result, c);
287
- types.push(type);
288
- extract.push(extractors[type] || defaultExtractor);
341
+ #queryPrepared(sql, params) {
342
+ const stmtPtr = allocPtr();
343
+ const sqlBytes = toCString(sql);
344
+
345
+ const prepStatus = lib.duckdb_prepare(this.#handle, ptr(sqlBytes), ptr(stmtPtr));
346
+ if (prepStatus !== 0) {
347
+ const stmtHandle = readPtr(stmtPtr);
348
+ if (stmtHandle) {
349
+ const errPtr = lib.duckdb_prepare_error(stmtHandle);
350
+ const errMsg = errPtr ? fromCString(errPtr) : 'Failed to prepare statement';
351
+ lib.duckdb_destroy_prepare(ptr(stmtPtr));
352
+ throw new Error(errMsg);
353
+ }
354
+ throw new Error('Failed to prepare statement');
289
355
  }
290
356
 
291
- // Extract all rows
292
- const rows = new Array(rowCount);
293
- for (let r = 0; r < rowCount; r++) {
294
- const row = {};
295
- for (let c = 0; c < colCount; c++) {
296
- if (duck.duck_value_is_null(result, c, r)) {
297
- row[columns[c]] = null;
357
+ const stmtHandle = readPtr(stmtPtr);
358
+
359
+ try {
360
+ for (let i = 0; i < params.length; i++) {
361
+ const paramIdx = BigInt(i + 1);
362
+ const value = params[i];
363
+
364
+ if (value === null || value === undefined) {
365
+ lib.duckdb_bind_null(stmtHandle, paramIdx);
366
+ } else if (typeof value === 'boolean') {
367
+ lib.duckdb_bind_boolean(stmtHandle, paramIdx, value);
368
+ } else if (typeof value === 'number') {
369
+ if (Number.isInteger(value)) {
370
+ lib.duckdb_bind_int64(stmtHandle, paramIdx, BigInt(value));
371
+ } else {
372
+ lib.duckdb_bind_double(stmtHandle, paramIdx, value);
373
+ }
374
+ } else if (typeof value === 'bigint') {
375
+ lib.duckdb_bind_int64(stmtHandle, paramIdx, value);
376
+ } else if (value instanceof Date) {
377
+ const strBytes = toCString(value.toISOString());
378
+ lib.duckdb_bind_varchar(stmtHandle, paramIdx, ptr(strBytes));
298
379
  } else {
299
- row[columns[c]] = extract[c](result, c, r);
380
+ const strBytes = toCString(String(value));
381
+ lib.duckdb_bind_varchar(stmtHandle, paramIdx, ptr(strBytes));
300
382
  }
301
383
  }
302
- rows[r] = row;
303
- }
304
384
 
305
- duck.duck_free_result(result);
306
- return rows;
307
- }
385
+ const resultPtr = new Uint8Array(64); // duckdb_result struct is ~48 bytes
386
+ lib.duckdb_execute_prepared(stmtHandle, ptr(resultPtr));
308
387
 
309
- prepare(sql) {
310
- const sqlPtr = ptr(utf8.encode(sql + '\0'));
311
- const stmt = duck.duck_prepare(this.#ptr, sqlPtr);
388
+ const rp = ptr(resultPtr);
389
+ const errorPtr = lib.duckdb_result_error(rp);
390
+ if (errorPtr) {
391
+ const error = fromCString(errorPtr);
392
+ lib.duckdb_destroy_result(rp);
393
+ throw new Error(error);
394
+ }
312
395
 
313
- const errPtr = duck.duck_prepare_error(stmt);
314
- if (errPtr) {
315
- const err = new CString(errPtr);
316
- duck.duck_free_prepare(stmt);
317
- throw new Error(err.toString());
396
+ try {
397
+ return this.#extractChunks(resultPtr);
398
+ } finally {
399
+ lib.duckdb_destroy_result(rp);
400
+ }
401
+ } finally {
402
+ lib.duckdb_destroy_prepare(ptr(stmtPtr));
318
403
  }
319
-
320
- return new PreparedStatement(stmt);
321
404
  }
322
405
 
323
- close() {
324
- if (this.#ptr) {
325
- duck.duck_disconnect(this.#ptr);
326
- this.#ptr = 0;
406
+ // ---------------------------------------------------------------------------
407
+ // Modern chunk-based result extraction
408
+ //
409
+ // Uses duckdb_fetch_chunk + duckdb_vector_get_data to read values directly
410
+ // from DuckDB's columnar memory. No deprecated duckdb_value_* functions.
411
+ //
412
+ // Contract:
413
+ // BIGINT/UBIGINT → number (lossy above 2^53, JSON-safe)
414
+ // DECIMAL/HUGEINT → string (preserves precision)
415
+ // All timestamps → Date (UTC)
416
+ // UUID → string (formatted)
417
+ // VARCHAR/BLOB → string
418
+ // ENUM → string (dictionary lookup)
419
+ // LIST → array, STRUCT → object, MAP → object
420
+ // ---------------------------------------------------------------------------
421
+
422
+ #extractChunks(resultPtr) {
423
+ const rp = ptr(resultPtr);
424
+ const colCount = Number(lib.duckdb_column_count(rp));
425
+
426
+ // Get column info + logical type metadata for complex types
427
+ const columns = [];
428
+ for (let c = 0; c < colCount; c++) {
429
+ const namePtr = lib.duckdb_column_name(rp, BigInt(c));
430
+ const type = lib.duckdb_column_type(rp, BigInt(c));
431
+ const col = {
432
+ name: fromCString(namePtr) || `col${c}`,
433
+ type,
434
+ typeName: this.#typeName(type)
435
+ };
436
+
437
+ // Get logical type metadata for complex types
438
+ if (type === DUCKDB_TYPE.DECIMAL || type === DUCKDB_TYPE.ENUM ||
439
+ type === DUCKDB_TYPE.LIST || type === DUCKDB_TYPE.STRUCT || type === DUCKDB_TYPE.MAP) {
440
+ const logType = lib.duckdb_column_logical_type(rp, BigInt(c));
441
+ if (logType) {
442
+ if (type === DUCKDB_TYPE.DECIMAL) {
443
+ col.decimalScale = lib.duckdb_decimal_scale(logType);
444
+ col.decimalInternalType = lib.duckdb_decimal_internal_type(logType);
445
+ } else if (type === DUCKDB_TYPE.ENUM) {
446
+ col.enumInternalType = lib.duckdb_enum_internal_type(logType);
447
+ const dictSize = lib.duckdb_enum_dictionary_size(logType);
448
+ col.enumDict = [];
449
+ for (let d = 0; d < dictSize; d++) {
450
+ const vp = lib.duckdb_enum_dictionary_value(logType, BigInt(d));
451
+ col.enumDict.push(fromCString(vp));
452
+ if (vp) lib.duckdb_free(vp);
453
+ }
454
+ } else if (type === DUCKDB_TYPE.LIST) {
455
+ const childLogType = lib.duckdb_list_type_child_type(logType);
456
+ if (childLogType) {
457
+ col.childType = lib.duckdb_get_type_id(childLogType);
458
+ const ltBuf2 = allocPtr();
459
+ new DataView(ltBuf2.buffer).setBigUint64(0, BigInt(childLogType), true);
460
+ lib.duckdb_destroy_logical_type(ptr(ltBuf2));
461
+ }
462
+ } else if (type === DUCKDB_TYPE.STRUCT) {
463
+ const childCount = Number(lib.duckdb_struct_type_child_count(logType));
464
+ col.structChildren = [];
465
+ for (let i = 0; i < childCount; i++) {
466
+ const np = lib.duckdb_struct_type_child_name(logType, BigInt(i));
467
+ const ct = lib.duckdb_struct_type_child_type(logType, BigInt(i));
468
+ const childType = ct ? lib.duckdb_get_type_id(ct) : DUCKDB_TYPE.VARCHAR;
469
+ col.structChildren.push({ name: fromCString(np) || `f${i}`, type: childType });
470
+ if (np) lib.duckdb_free(np);
471
+ if (ct) {
472
+ const ltBuf2 = allocPtr();
473
+ new DataView(ltBuf2.buffer).setBigUint64(0, BigInt(ct), true);
474
+ lib.duckdb_destroy_logical_type(ptr(ltBuf2));
475
+ }
476
+ }
477
+ } else if (type === DUCKDB_TYPE.MAP) {
478
+ const keyLogType = lib.duckdb_list_type_child_type(logType); // MAP child is STRUCT
479
+ if (keyLogType) {
480
+ // MAP's child is a STRUCT with key (0) and value (1)
481
+ const keyType = lib.duckdb_struct_type_child_type(keyLogType, 0n);
482
+ const valType = lib.duckdb_struct_type_child_type(keyLogType, 1n);
483
+ col.keyType = keyType ? lib.duckdb_get_type_id(keyType) : DUCKDB_TYPE.VARCHAR;
484
+ col.valueType = valType ? lib.duckdb_get_type_id(valType) : DUCKDB_TYPE.VARCHAR;
485
+ if (keyType) {
486
+ const b = allocPtr(); new DataView(b.buffer).setBigUint64(0, BigInt(keyType), true);
487
+ lib.duckdb_destroy_logical_type(ptr(b));
488
+ }
489
+ if (valType) {
490
+ const b = allocPtr(); new DataView(b.buffer).setBigUint64(0, BigInt(valType), true);
491
+ lib.duckdb_destroy_logical_type(ptr(b));
492
+ }
493
+ const b = allocPtr(); new DataView(b.buffer).setBigUint64(0, BigInt(keyLogType), true);
494
+ lib.duckdb_destroy_logical_type(ptr(b));
495
+ }
496
+ }
497
+ const ltBuf = allocPtr();
498
+ new DataView(ltBuf.buffer).setBigUint64(0, BigInt(logType), true);
499
+ lib.duckdb_destroy_logical_type(ptr(ltBuf));
500
+ }
501
+ }
502
+
503
+ columns.push(col);
327
504
  }
328
- }
329
- }
330
505
 
331
- class PreparedStatement {
332
- #ptr;
333
- #paramCount;
334
- #paramTypes;
335
-
336
- constructor(ptr) {
337
- this.#ptr = ptr;
338
- this.#paramCount = Number(duck.duck_nparams(ptr));
339
- this.#paramTypes = [];
340
- for (let i = 0; i < this.#paramCount; i++) {
341
- this.#paramTypes.push(duck.duck_param_type(ptr, i + 1));
506
+ // Fetch chunks and extract rows
507
+ const rows = [];
508
+ const chunkBuf = allocPtr();
509
+
510
+ while (true) {
511
+ const chunk = lib.duckdb_fetch_chunk(rp);
512
+ if (!chunk) break;
513
+
514
+ const chunkSize = Number(lib.duckdb_data_chunk_get_size(chunk));
515
+ if (chunkSize === 0) {
516
+ const dv = new DataView(chunkBuf.buffer);
517
+ dv.setBigUint64(0, BigInt(chunk), true);
518
+ lib.duckdb_destroy_data_chunk(ptr(chunkBuf));
519
+ break;
520
+ }
521
+
522
+ // Get vectors for each column (data + validity + handle for nested types)
523
+ const colVec = [];
524
+ const colData = [];
525
+ const colValidity = [];
526
+ for (let c = 0; c < colCount; c++) {
527
+ const vec = lib.duckdb_data_chunk_get_vector(chunk, BigInt(c));
528
+ colVec.push(vec);
529
+ colData.push(lib.duckdb_vector_get_data(vec));
530
+ colValidity.push(lib.duckdb_vector_get_validity(vec));
531
+ }
532
+
533
+ // Extract rows from this chunk
534
+ for (let r = 0; r < chunkSize; r++) {
535
+ const row = {};
536
+ for (let c = 0; c < colCount; c++) {
537
+ const col = columns[c];
538
+ if (!isValid(colValidity[c], r)) {
539
+ row[col.name] = null;
540
+ } else {
541
+ row[col.name] = this.#readValue(colData[c], r, col.type, col, colVec[c]);
542
+ }
543
+ }
544
+ rows.push(row);
545
+ }
546
+
547
+ // Destroy chunk
548
+ const dv = new DataView(chunkBuf.buffer);
549
+ dv.setBigUint64(0, BigInt(chunk), true);
550
+ lib.duckdb_destroy_data_chunk(ptr(chunkBuf));
342
551
  }
552
+
553
+ rows.columns = columns;
554
+ return rows;
343
555
  }
344
556
 
345
- query(...params) {
346
- // Bind parameters
347
- for (let i = 0; i < params.length; i++) {
348
- const paramType = this.#paramTypes[i] || Type.VARCHAR;
349
- if (!bindValue(this.#ptr, i + 1, params[i], paramType)) {
350
- throw new Error(`Failed to bind parameter ${i + 1}`);
557
+ // ---------------------------------------------------------------------------
558
+ // Read a single value from raw vector memory at a given row index.
559
+ // This is the core type dispatch reads directly from DuckDB's columnar
560
+ // memory layout without any deprecated per-value API calls.
561
+ // ---------------------------------------------------------------------------
562
+
563
+ // col = column metadata (includes decimalScale, enumDict, etc.)
564
+ // vec = vector handle (for nested type child access)
565
+ #readValue(dataPtr, row, type, col, vec) {
566
+ switch (type) {
567
+ case DUCKDB_TYPE.BOOLEAN:
568
+ return ffiRead.u8(dataPtr, row) !== 0;
569
+
570
+ case DUCKDB_TYPE.TINYINT:
571
+ return ffiRead.i8(dataPtr, row);
572
+ case DUCKDB_TYPE.SMALLINT:
573
+ return ffiRead.i16(dataPtr, row * 2);
574
+ case DUCKDB_TYPE.INTEGER:
575
+ return ffiRead.i32(dataPtr, row * 4);
576
+ case DUCKDB_TYPE.UTINYINT:
577
+ return ffiRead.u8(dataPtr, row);
578
+ case DUCKDB_TYPE.USMALLINT:
579
+ return ffiRead.u16(dataPtr, row * 2);
580
+ case DUCKDB_TYPE.UINTEGER:
581
+ return ffiRead.u32(dataPtr, row * 4);
582
+
583
+ case DUCKDB_TYPE.BIGINT:
584
+ return Number(ffiRead.i64(dataPtr, row * 8));
585
+ case DUCKDB_TYPE.UBIGINT:
586
+ return Number(ffiRead.u64(dataPtr, row * 8));
587
+
588
+ case DUCKDB_TYPE.FLOAT:
589
+ return ffiRead.f32(dataPtr, row * 4);
590
+ case DUCKDB_TYPE.DOUBLE:
591
+ return ffiRead.f64(dataPtr, row * 8);
592
+
593
+ case DUCKDB_TYPE.HUGEINT: {
594
+ const lo = ffiRead.u64(dataPtr, row * 16);
595
+ const hi = ffiRead.i64(dataPtr, row * 16 + 8);
596
+ const value = (BigInt(hi) << 64n) | BigInt(lo);
597
+ return value.toString();
351
598
  }
352
- }
353
599
 
354
- // Execute
355
- const result = duck.duck_execute_prepared(this.#ptr);
600
+ case DUCKDB_TYPE.DECIMAL: {
601
+ // Read based on internal type, divide by 10^scale, return as string
602
+ const scale = col?.decimalScale || 0;
603
+ const internalType = col?.decimalInternalType || DUCKDB_TYPE.DOUBLE;
604
+ let raw;
605
+ switch (internalType) {
606
+ case DUCKDB_TYPE.SMALLINT:
607
+ raw = BigInt(ffiRead.i16(dataPtr, row * 2)); break;
608
+ case DUCKDB_TYPE.INTEGER:
609
+ raw = BigInt(ffiRead.i32(dataPtr, row * 4)); break;
610
+ case DUCKDB_TYPE.BIGINT:
611
+ raw = ffiRead.i64(dataPtr, row * 8); break;
612
+ case DUCKDB_TYPE.HUGEINT: {
613
+ const lo = ffiRead.u64(dataPtr, row * 16);
614
+ const hi = ffiRead.i64(dataPtr, row * 16 + 8);
615
+ raw = (BigInt(hi) << 64n) | BigInt(lo);
616
+ break;
617
+ }
618
+ default:
619
+ return ffiRead.f64(dataPtr, row * 8);
620
+ }
621
+ if (scale === 0) return raw.toString();
622
+ const divisor = 10n ** BigInt(scale);
623
+ const sign = raw < 0n ? '-' : '';
624
+ const abs = raw < 0n ? -raw : raw;
625
+ const intPart = abs / divisor;
626
+ const fracPart = abs % divisor;
627
+ return `${sign}${intPart}.${fracPart.toString().padStart(scale, '0')}`;
628
+ }
356
629
 
357
- // Check for error
358
- const errPtr = duck.duck_result_error(result);
359
- if (errPtr) {
360
- const err = new CString(errPtr);
361
- duck.duck_free_result(result);
362
- throw new Error(err.toString());
363
- }
630
+ case DUCKDB_TYPE.DATE: {
631
+ const days = ffiRead.i32(dataPtr, row * 4);
632
+ const ms = days * 86400000;
633
+ const d = new Date(ms);
634
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth()+1).padStart(2,'0')}-${String(d.getUTCDate()).padStart(2,'0')}`;
635
+ }
364
636
 
365
- // Extract rows
366
- const rowCount = Number(duck.duck_row_count(result));
367
- const colCount = Number(duck.duck_column_count(result));
637
+ case DUCKDB_TYPE.TIMESTAMP:
638
+ case DUCKDB_TYPE.TIMESTAMP_S:
639
+ case DUCKDB_TYPE.TIMESTAMP_MS:
640
+ case DUCKDB_TYPE.TIMESTAMP_NS:
641
+ case DUCKDB_TYPE.TIMESTAMP_TZ: {
642
+ const micros = ffiRead.i64(dataPtr, row * 8);
643
+ return new Date(Number(micros / 1000n));
644
+ }
368
645
 
369
- if (rowCount === 0) {
370
- duck.duck_free_result(result);
371
- return [];
372
- }
646
+ case DUCKDB_TYPE.TIME: {
647
+ const us = Number(ffiRead.i64(dataPtr, row * 8));
648
+ const totalSec = Math.floor(us / 1000000);
649
+ const h = Math.floor(totalSec / 3600);
650
+ const m = Math.floor((totalSec % 3600) / 60);
651
+ const s = totalSec % 60;
652
+ const frac = us % 1000000;
653
+ return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}` +
654
+ (frac > 0 ? `.${String(frac).padStart(6,'0').replace(/0+$/, '')}` : '');
655
+ }
373
656
 
374
- // Get column info
375
- const columns = [];
376
- const types = [];
377
- const extract = [];
657
+ case DUCKDB_TYPE.UUID:
658
+ return readUUID(dataPtr, row);
659
+
660
+ case DUCKDB_TYPE.VARCHAR:
661
+ case DUCKDB_TYPE.BLOB:
662
+ return readString(dataPtr, row);
663
+
664
+ case DUCKDB_TYPE.INTERVAL: {
665
+ const months = ffiRead.i32(dataPtr, row * 16);
666
+ const days = ffiRead.i32(dataPtr, row * 16 + 4);
667
+ const micros = Number(ffiRead.i64(dataPtr, row * 16 + 8));
668
+ const parts = [];
669
+ if (months) parts.push(`${months} month${months !== 1 ? 's' : ''}`);
670
+ if (days) parts.push(`${days} day${days !== 1 ? 's' : ''}`);
671
+ if (micros) {
672
+ const secs = micros / 1000000;
673
+ parts.push(`${secs} second${secs !== 1 ? 's' : ''}`);
674
+ }
675
+ return parts.join(' ') || '0 seconds';
676
+ }
378
677
 
379
- for (let c = 0; c < colCount; c++) {
380
- columns.push(new CString(duck.duck_column_name(result, c)).toString());
381
- const type = duck.duck_column_type(result, c);
382
- types.push(type);
383
- extract.push(extractors[type] || defaultExtractor);
384
- }
678
+ case DUCKDB_TYPE.ENUM: {
679
+ // Read integer index, look up string from pre-built dictionary
680
+ const dict = col?.enumDict;
681
+ if (!dict) return null;
682
+ const enumType = col?.enumInternalType || DUCKDB_TYPE.UTINYINT;
683
+ let idx;
684
+ switch (enumType) {
685
+ case DUCKDB_TYPE.UTINYINT: idx = ffiRead.u8(dataPtr, row); break;
686
+ case DUCKDB_TYPE.USMALLINT: idx = ffiRead.u16(dataPtr, row * 2); break;
687
+ case DUCKDB_TYPE.UINTEGER: idx = ffiRead.u32(dataPtr, row * 4); break;
688
+ default: idx = ffiRead.u32(dataPtr, row * 4); break;
689
+ }
690
+ return dict[idx] ?? null;
691
+ }
385
692
 
386
- // Extract all rows
387
- const rows = new Array(rowCount);
388
- for (let r = 0; r < rowCount; r++) {
389
- const row = {};
390
- for (let c = 0; c < colCount; c++) {
391
- if (duck.duck_value_is_null(result, c, r)) {
392
- row[columns[c]] = null;
393
- } else {
394
- row[columns[c]] = extract[c](result, c, r);
693
+ case DUCKDB_TYPE.LIST: {
694
+ // Read list entry (offset + length) from parent, then read child values
695
+ if (!vec) return null;
696
+ const entryOffset = row * 16; // duckdb_list_entry is 16 bytes (uint64 offset + uint64 length)
697
+ const listOffset = Number(ffiRead.u64(dataPtr, entryOffset));
698
+ const listLength = Number(ffiRead.u64(dataPtr, entryOffset + 8));
699
+ const childVec = lib.duckdb_list_vector_get_child(vec);
700
+ const childData = lib.duckdb_vector_get_data(childVec);
701
+ const childValidity = lib.duckdb_vector_get_validity(childVec);
702
+ // Determine child type from the vector's column type
703
+ const childType = col?.childType || DUCKDB_TYPE.VARCHAR;
704
+ const result = [];
705
+ for (let i = 0; i < listLength; i++) {
706
+ const childRow = listOffset + i;
707
+ if (!isValid(childValidity, childRow)) {
708
+ result.push(null);
709
+ } else {
710
+ result.push(this.#readValue(childData, childRow, childType, null, childVec));
711
+ }
712
+ }
713
+ return result;
714
+ }
715
+
716
+ case DUCKDB_TYPE.STRUCT: {
717
+ // Read each child vector at the row index
718
+ if (!vec) return null;
719
+ const obj = {};
720
+ const childCount = col?.structChildren?.length || 0;
721
+ for (let i = 0; i < childCount; i++) {
722
+ const child = col.structChildren[i];
723
+ const childVec = lib.duckdb_struct_vector_get_child(vec, BigInt(i));
724
+ const childData = lib.duckdb_vector_get_data(childVec);
725
+ const childValidity = lib.duckdb_vector_get_validity(childVec);
726
+ if (!isValid(childValidity, row)) {
727
+ obj[child.name] = null;
728
+ } else {
729
+ obj[child.name] = this.#readValue(childData, row, child.type, null, childVec);
730
+ }
731
+ }
732
+ return obj;
733
+ }
734
+
735
+ case DUCKDB_TYPE.MAP: {
736
+ // MAP is internally a LIST of STRUCTs with 'key' and 'value' fields
737
+ if (!vec) return null;
738
+ const entryOffset = row * 16;
739
+ const listOffset = Number(ffiRead.u64(dataPtr, entryOffset));
740
+ const listLength = Number(ffiRead.u64(dataPtr, entryOffset + 8));
741
+ const childVec = lib.duckdb_list_vector_get_child(vec);
742
+ // MAP child is a STRUCT with key (child 0) and value (child 1)
743
+ const keyVec = lib.duckdb_struct_vector_get_child(childVec, 0n);
744
+ const valVec = lib.duckdb_struct_vector_get_child(childVec, 1n);
745
+ const keyData = lib.duckdb_vector_get_data(keyVec);
746
+ const valData = lib.duckdb_vector_get_data(valVec);
747
+ const keyValidity = lib.duckdb_vector_get_validity(keyVec);
748
+ const valValidity = lib.duckdb_vector_get_validity(valVec);
749
+ const keyType = col?.keyType || DUCKDB_TYPE.VARCHAR;
750
+ const valType = col?.valueType || DUCKDB_TYPE.VARCHAR;
751
+ const obj = {};
752
+ for (let i = 0; i < listLength; i++) {
753
+ const childRow = listOffset + i;
754
+ const k = isValid(keyValidity, childRow)
755
+ ? this.#readValue(keyData, childRow, keyType, null, keyVec) : null;
756
+ const v = isValid(valValidity, childRow)
757
+ ? this.#readValue(valData, childRow, valType, null, valVec) : null;
758
+ if (k !== null) obj[String(k)] = v;
395
759
  }
760
+ return obj;
396
761
  }
397
- rows[r] = row;
762
+
763
+ case DUCKDB_TYPE.UNION:
764
+ case DUCKDB_TYPE.BIT:
765
+ return null; // Rarely used types
766
+
767
+ default:
768
+ try { return readString(dataPtr, row); }
769
+ catch { return null; }
398
770
  }
771
+ }
399
772
 
400
- duck.duck_free_result(result);
401
- return rows;
773
+ #typeName(type) {
774
+ for (const [name, value] of Object.entries(DUCKDB_TYPE)) {
775
+ if (value === type) return name;
776
+ }
777
+ return 'UNKNOWN';
402
778
  }
403
779
 
404
780
  close() {
405
- if (this.#ptr) {
406
- duck.duck_free_prepare(this.#ptr);
407
- this.#ptr = 0;
781
+ if (this.#ptrBuf) {
782
+ lib.duckdb_disconnect(ptr(this.#ptrBuf));
783
+ this.#ptrBuf = null;
784
+ this.#handle = null;
408
785
  }
409
786
  }
410
787
  }
411
788
 
412
- export { Type };
789
+ // ==============================================================================
790
+ // Public API
791
+ // ==============================================================================
792
+
793
+ export function open(path) {
794
+ return new Database(path);
795
+ }
796
+
797
+ export function version() {
798
+ const versionPtr = lib.duckdb_library_version();
799
+ return fromCString(versionPtr);
800
+ }
801
+
802
+ export { Database, Connection };