@rip-lang/db 0.10.0 → 1.0.1

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,31 +1,49 @@
1
1
  /**
2
- * DuckDB bindings for Bun using Zig FFI
2
+ * DuckDB High-Performance Zig Bindings
3
3
  *
4
- * Clean, type-safe wrapper with full type support.
4
+ * Minimal JavaScript wrapper for the Zig implementation.
5
+ * The Zig code handles all binary serialization directly - no JS overhead!
6
+ *
7
+ * Usage:
8
+ * import { open, queryBinary, tokenize } from './duckdb.mjs';
9
+ *
10
+ * const db = open(':memory:');
11
+ * const conn = db.connect();
12
+ *
13
+ * // Returns binary ArrayBuffer - ready for HTTP response!
14
+ * const result = queryBinary(conn.handle, 'SELECT 42', 10000);
15
+ *
16
+ * // Returns binary ArrayBuffer for tokenize endpoint
17
+ * const tokens = tokenize('SELECT * FROM users');
5
18
  */
6
19
 
7
- import { dlopen, ptr, CString } from 'bun:ffi';
20
+ import { dlopen, ptr } from 'bun:ffi';
8
21
  import { fileURLToPath } from 'url';
9
22
  import { dirname, join } from 'path';
10
23
  import { platform, arch } from 'process';
11
24
 
12
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
26
 
14
- // Map Node.js arch names
27
+ // Map platform/arch to library path
15
28
  const archMap = { 'arm64': 'arm64', 'x64': 'x64', 'x86_64': 'x64' };
16
29
  const target = `${platform}-${archMap[arch] || arch}`;
17
-
18
- // Load the platform-specific native module
19
30
  const libPath = join(__dirname, `${target}/duckdb.node`);
20
31
 
21
- const duck = dlopen(libPath, {
22
- // Database operations (usize = pointer-sized integer)
32
+ // Load the Zig library
33
+ const lib = dlopen(libPath, {
34
+ // Database lifecycle
23
35
  duck_open: { args: ['ptr'], returns: 'usize' },
24
36
  duck_close: { args: ['usize'], returns: 'void' },
25
37
  duck_connect: { args: ['usize'], returns: 'usize' },
26
38
  duck_disconnect: { args: ['usize'], returns: 'void' },
27
39
 
28
- // Query operations
40
+ // Binary serialization (for DuckDB UI protocol)
41
+ duck_query_binary: { args: ['usize', 'ptr', 'ptr', 'usize', 'u64'], returns: 'usize' },
42
+ duck_tokenize: { args: ['ptr', 'usize', 'ptr', 'usize'], returns: 'usize' },
43
+ duck_empty_result: { args: ['ptr', 'usize'], returns: 'usize' },
44
+ duck_error_result: { args: ['ptr', 'ptr', 'usize'], returns: 'usize' },
45
+
46
+ // Basic query API (for JSON endpoints)
29
47
  duck_query: { args: ['usize', 'ptr'], returns: 'usize' },
30
48
  duck_free_result: { args: ['usize'], returns: 'void' },
31
49
  duck_result_error: { args: ['usize'], returns: 'ptr' },
@@ -33,380 +51,275 @@ const duck = dlopen(libPath, {
33
51
  duck_column_count: { args: ['usize'], returns: 'u64' },
34
52
  duck_column_name: { args: ['usize', 'u64'], returns: 'ptr' },
35
53
  duck_column_type: { args: ['usize', 'u64'], returns: 'u32' },
36
-
37
- // Value extraction
38
54
  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' },
54
-
55
- // 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
55
+ duck_value_varchar: { args: ['usize', 'u64', 'u64'], returns: 'ptr' },
56
+ duck_value_int64: { args: ['usize', 'u64', 'u64'], returns: 'i64' },
57
+ duck_value_double: { args: ['usize', 'u64', 'u64'], returns: 'f64' },
58
+ duck_value_boolean: { args: ['usize', 'u64', 'u64'], returns: 'bool' },
83
59
  duck_free: { args: ['usize'], returns: 'void' },
84
60
  }).symbols;
85
61
 
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.
88
-
89
- const utf8 = new TextEncoder();
90
-
91
- // DuckDB type enum
92
- const Type = {
93
- INVALID: 0,
94
- BOOLEAN: 1,
95
- TINYINT: 2,
96
- SMALLINT: 3,
97
- INTEGER: 4,
98
- BIGINT: 5,
99
- UTINYINT: 6,
100
- USMALLINT: 7,
101
- UINTEGER: 8,
102
- UBIGINT: 9,
103
- FLOAT: 10,
104
- DOUBLE: 11,
105
- TIMESTAMP: 12,
106
- DATE: 13,
107
- TIME: 14,
108
- INTERVAL: 15,
109
- HUGEINT: 16,
110
- VARCHAR: 17,
111
- BLOB: 18,
112
- DECIMAL: 19,
113
- TIMESTAMP_S: 20,
114
- TIMESTAMP_MS: 21,
115
- TIMESTAMP_NS: 22,
116
- ENUM: 23,
117
- LIST: 24,
118
- STRUCT: 25,
119
- MAP: 26,
120
- UUID: 27,
121
- JSON: 28,
122
- };
62
+ import { CString } from 'bun:ffi';
123
63
 
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
- };
64
+ // Pre-allocated buffer for query results (16MB default)
65
+ const DEFAULT_BUFFER_SIZE = 16 * 1024 * 1024;
66
+ let sharedBuffer = new Uint8Array(DEFAULT_BUFFER_SIZE);
149
67
 
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
- };
181
-
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;
186
- }
187
-
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
- }
193
-
194
- const binder = binders[paramType];
195
- if (binder) {
196
- return binder(stmt, idx, val);
197
- }
198
-
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));
205
- }
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);
212
- }
213
-
214
- // Object -> JSON
215
- const bytes = utf8.encode(JSON.stringify(val));
216
- return duck.duck_bind_string(stmt, idx, ptr(bytes), bytes.length);
217
- }
68
+ const utf8Encoder = new TextEncoder();
218
69
 
219
70
  /**
220
71
  * Open a DuckDB database
221
72
  * @param {string|null} path - Path to database file, or null/:memory: for in-memory
73
+ * @returns {Database}
222
74
  */
223
75
  export function open(path) {
224
76
  return new Database(path);
225
77
  }
226
78
 
227
79
  class Database {
228
- #ptr;
80
+ #handle;
229
81
 
230
82
  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');
83
+ const dbPath = path === null || path === ':memory:' ? null : path;
84
+ const pathBytes = dbPath ? utf8Encoder.encode(dbPath + '\0') : null;
85
+ this.#handle = lib.duck_open(pathBytes ? ptr(pathBytes) : null);
86
+ if (this.#handle === 0n || this.#handle === 0) {
87
+ throw new Error('Failed to open database');
88
+ }
236
89
  }
237
90
 
91
+ get handle() { return this.#handle; }
92
+
238
93
  connect() {
239
- return new Connection(this.#ptr);
94
+ return new Connection(this.#handle);
240
95
  }
241
96
 
242
97
  close() {
243
- if (this.#ptr) {
244
- duck.duck_close(this.#ptr);
245
- this.#ptr = 0;
98
+ if (this.#handle) {
99
+ lib.duck_close(this.#handle);
100
+ this.#handle = null;
246
101
  }
247
102
  }
248
103
  }
249
104
 
250
- class Connection {
251
- #ptr;
105
+ // DuckDB type constants
106
+ const DUCKDB_TYPE = {
107
+ INVALID: 0, BOOLEAN: 1, TINYINT: 2, SMALLINT: 3, INTEGER: 4, BIGINT: 5,
108
+ UTINYINT: 6, USMALLINT: 7, UINTEGER: 8, UBIGINT: 9, FLOAT: 10, DOUBLE: 11,
109
+ TIMESTAMP: 12, DATE: 13, TIME: 14, INTERVAL: 15, HUGEINT: 16, VARCHAR: 17,
110
+ BLOB: 18, DECIMAL: 19, TIMESTAMP_S: 20, TIMESTAMP_MS: 21, TIMESTAMP_NS: 22,
111
+ ENUM: 23, LIST: 24, STRUCT: 25, MAP: 26, UUID: 27,
112
+ };
252
113
 
253
- constructor(db) {
254
- this.#ptr = duck.duck_connect(db);
255
- if (this.#ptr === 0) throw new Error('Failed to connect to database');
256
- }
114
+ class Connection {
115
+ #handle;
257
116
 
258
- query(sql) {
259
- const sqlPtr = ptr(utf8.encode(sql + '\0'));
260
- const result = duck.duck_query(this.#ptr, sqlPtr);
261
-
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());
117
+ constructor(dbHandle) {
118
+ this.#handle = lib.duck_connect(dbHandle);
119
+ if (this.#handle === 0n || this.#handle === 0) {
120
+ throw new Error('Failed to create connection');
268
121
  }
122
+ }
269
123
 
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 [];
277
- }
124
+ get handle() { return this.#handle; }
278
125
 
279
- // Get column info
280
- const columns = [];
281
- const types = [];
282
- const extract = [];
126
+ /**
127
+ * Execute SQL and return rows as array of objects
128
+ * @param {string} sql - SQL query
129
+ * @returns {object[]} Array of row objects
130
+ */
131
+ query(sql) {
132
+ const sqlBytes = utf8Encoder.encode(sql + '\0');
133
+ const result = lib.duck_query(this.#handle, ptr(sqlBytes));
134
+
135
+ try {
136
+ // Check for error
137
+ const errorPtr = lib.duck_result_error(result);
138
+ if (errorPtr) {
139
+ throw new Error(new CString(errorPtr).toString());
140
+ }
283
141
 
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);
289
- }
142
+ const rowCount = Number(lib.duck_row_count(result));
143
+ const colCount = Number(lib.duck_column_count(result));
290
144
 
291
- // Extract all rows
292
- const rows = new Array(rowCount);
293
- for (let r = 0; r < rowCount; r++) {
294
- const row = {};
145
+ // Get column names and types
146
+ const columns = [];
147
+ const types = [];
295
148
  for (let c = 0; c < colCount; c++) {
296
- if (duck.duck_value_is_null(result, c, r)) {
297
- row[columns[c]] = null;
298
- } else {
299
- row[columns[c]] = extract[c](result, c, r);
300
- }
149
+ const namePtr = lib.duck_column_name(result, BigInt(c));
150
+ columns.push(namePtr ? new CString(namePtr).toString() : `col${c}`);
151
+ types.push(Number(lib.duck_column_type(result, BigInt(c))));
301
152
  }
302
- rows[r] = row;
303
- }
304
-
305
- duck.duck_free_result(result);
306
- return rows;
307
- }
308
153
 
309
- prepare(sql) {
310
- const sqlPtr = ptr(utf8.encode(sql + '\0'));
311
- const stmt = duck.duck_prepare(this.#ptr, sqlPtr);
154
+ // Build row objects
155
+ const rows = [];
156
+ for (let r = 0; r < rowCount; r++) {
157
+ const row = {};
158
+ for (let c = 0; c < colCount; c++) {
159
+ const col = BigInt(c);
160
+ const rowIdx = BigInt(r);
161
+
162
+ if (lib.duck_value_is_null(result, col, rowIdx)) {
163
+ row[columns[c]] = null;
164
+ } else {
165
+ const type = types[c];
166
+ switch (type) {
167
+ case DUCKDB_TYPE.BOOLEAN:
168
+ row[columns[c]] = lib.duck_value_boolean(result, col, rowIdx);
169
+ break;
170
+ case DUCKDB_TYPE.TINYINT:
171
+ case DUCKDB_TYPE.SMALLINT:
172
+ case DUCKDB_TYPE.INTEGER:
173
+ case DUCKDB_TYPE.BIGINT:
174
+ case DUCKDB_TYPE.UTINYINT:
175
+ case DUCKDB_TYPE.USMALLINT:
176
+ case DUCKDB_TYPE.UINTEGER:
177
+ case DUCKDB_TYPE.UBIGINT:
178
+ case DUCKDB_TYPE.DATE:
179
+ case DUCKDB_TYPE.TIME:
180
+ case DUCKDB_TYPE.TIMESTAMP:
181
+ case DUCKDB_TYPE.TIMESTAMP_S:
182
+ case DUCKDB_TYPE.TIMESTAMP_MS:
183
+ case DUCKDB_TYPE.TIMESTAMP_NS:
184
+ row[columns[c]] = Number(lib.duck_value_int64(result, col, rowIdx));
185
+ break;
186
+ case DUCKDB_TYPE.FLOAT:
187
+ case DUCKDB_TYPE.DOUBLE:
188
+ case DUCKDB_TYPE.DECIMAL:
189
+ row[columns[c]] = lib.duck_value_double(result, col, rowIdx);
190
+ break;
191
+ default:
192
+ // VARCHAR, BLOB, UUID, etc - get as string
193
+ const strPtr = lib.duck_value_varchar(result, col, rowIdx);
194
+ if (strPtr) {
195
+ row[columns[c]] = new CString(strPtr).toString();
196
+ lib.duck_free(strPtr); // Free the allocated string
197
+ } else {
198
+ row[columns[c]] = null;
199
+ }
200
+ }
201
+ }
202
+ }
203
+ rows.push(row);
204
+ }
312
205
 
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());
206
+ return rows;
207
+ } finally {
208
+ lib.duck_free_result(result);
318
209
  }
319
-
320
- return new PreparedStatement(stmt);
321
210
  }
322
211
 
323
212
  close() {
324
- if (this.#ptr) {
325
- duck.duck_disconnect(this.#ptr);
326
- this.#ptr = 0;
213
+ if (this.#handle) {
214
+ lib.duck_disconnect(this.#handle);
215
+ this.#handle = null;
327
216
  }
328
217
  }
329
218
  }
330
219
 
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));
342
- }
343
- }
344
-
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}`);
351
- }
352
- }
353
-
354
- // Execute
355
- const result = duck.duck_execute_prepared(this.#ptr);
356
-
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
- }
364
-
365
- // Extract rows
366
- const rowCount = Number(duck.duck_row_count(result));
367
- const colCount = Number(duck.duck_column_count(result));
368
-
369
- if (rowCount === 0) {
370
- duck.duck_free_result(result);
371
- return [];
372
- }
373
-
374
- // Get column info
375
- const columns = [];
376
- const types = [];
377
- const extract = [];
220
+ /**
221
+ * Execute SQL and return binary result (DuckDB UI protocol format)
222
+ *
223
+ * @param {bigint|number} connHandle - Connection handle from conn.handle
224
+ * @param {string} sql - SQL query to execute
225
+ * @param {number} rowLimit - Maximum rows to return (default 10000)
226
+ * @returns {ArrayBuffer} - Binary result ready for HTTP response
227
+ */
228
+ export function queryBinary(connHandle, sql, rowLimit = 10000) {
229
+ const sqlBytes = utf8Encoder.encode(sql + '\0');
230
+ const bytesWritten = lib.duck_query_binary(
231
+ connHandle,
232
+ ptr(sqlBytes),
233
+ ptr(sharedBuffer),
234
+ sharedBuffer.length,
235
+ BigInt(rowLimit)
236
+ );
237
+
238
+ // Return a copy of the used portion
239
+ return sharedBuffer.slice(0, Number(bytesWritten)).buffer;
240
+ }
378
241
 
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
- }
242
+ /**
243
+ * Tokenize SQL and return binary result (DuckDB UI protocol format)
244
+ *
245
+ * @param {string} sql - SQL to tokenize
246
+ * @returns {ArrayBuffer} - Binary result ready for HTTP response
247
+ */
248
+ export function tokenize(sql) {
249
+ const sqlBytes = utf8Encoder.encode(sql);
250
+ const bytesWritten = lib.duck_tokenize(
251
+ ptr(sqlBytes),
252
+ sqlBytes.length,
253
+ ptr(sharedBuffer),
254
+ sharedBuffer.length
255
+ );
256
+
257
+ return sharedBuffer.slice(0, Number(bytesWritten)).buffer;
258
+ }
385
259
 
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);
395
- }
396
- }
397
- rows[r] = row;
398
- }
260
+ /**
261
+ * Create an empty binary result (for /ddb/interrupt)
262
+ * @returns {ArrayBuffer}
263
+ */
264
+ export function emptyResult() {
265
+ const bytesWritten = lib.duck_empty_result(
266
+ ptr(sharedBuffer),
267
+ sharedBuffer.length
268
+ );
269
+ return sharedBuffer.slice(0, Number(bytesWritten)).buffer;
270
+ }
399
271
 
400
- duck.duck_free_result(result);
401
- return rows;
402
- }
272
+ /**
273
+ * Create an error binary result
274
+ * @param {string} message - Error message
275
+ * @returns {ArrayBuffer}
276
+ */
277
+ export function errorResult(message) {
278
+ const msgBytes = utf8Encoder.encode(message + '\0');
279
+ const bytesWritten = lib.duck_error_result(
280
+ ptr(msgBytes),
281
+ ptr(sharedBuffer),
282
+ sharedBuffer.length
283
+ );
284
+ return sharedBuffer.slice(0, Number(bytesWritten)).buffer;
285
+ }
403
286
 
404
- close() {
405
- if (this.#ptr) {
406
- duck.duck_free_prepare(this.#ptr);
407
- this.#ptr = 0;
408
- }
409
- }
287
+ /**
288
+ * Resize the shared buffer (for very large results)
289
+ * @param {number} size - New buffer size in bytes
290
+ */
291
+ export function setBufferSize(size) {
292
+ sharedBuffer = new Uint8Array(size);
410
293
  }
411
294
 
412
- export { Type };
295
+ // Export types for compatibility
296
+ export const Type = {
297
+ BOOLEAN: 10,
298
+ TINYINT: 11,
299
+ SMALLINT: 12,
300
+ INTEGER: 13,
301
+ BIGINT: 14,
302
+ DATE: 15,
303
+ TIME: 16,
304
+ TIMESTAMP_SEC: 17,
305
+ TIMESTAMP_MS: 18,
306
+ TIMESTAMP: 19,
307
+ TIMESTAMP_NS: 20,
308
+ DECIMAL: 21,
309
+ FLOAT: 22,
310
+ DOUBLE: 23,
311
+ CHAR: 24,
312
+ VARCHAR: 25,
313
+ BLOB: 26,
314
+ INTERVAL: 27,
315
+ UTINYINT: 28,
316
+ USMALLINT: 29,
317
+ UINTEGER: 30,
318
+ UBIGINT: 31,
319
+ TIMESTAMP_TZ: 32,
320
+ TIME_TZ: 34,
321
+ HUGEINT: 50,
322
+ UUID: 54,
323
+ };
324
+
325
+ export default { open, queryBinary, tokenize, emptyResult, errorResult, setBufferSize, Type };