@rip-lang/db 1.0.1 → 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/README.md +103 -120
- package/bin/rip-db +4 -9
- package/db.rip +224 -230
- package/lib/duckdb-binary.rip +546 -0
- package/lib/duckdb.mjs +727 -250
- package/package.json +8 -11
- package/INTERNALS.md +0 -324
- package/build.zig +0 -88
- package/lib/darwin-arm64/duckdb.node +0 -0
- package/src/duckdb.zig +0 -1156
package/lib/duckdb.mjs
CHANGED
|
@@ -1,325 +1,802 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DuckDB
|
|
2
|
+
* DuckDB Pure Bun FFI Wrapper
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
* import { open
|
|
8
|
+
* import { open } from './duckdb.mjs';
|
|
9
9
|
*
|
|
10
10
|
* const db = open(':memory:');
|
|
11
11
|
* const conn = db.connect();
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* // Returns binary ArrayBuffer for tokenize endpoint
|
|
17
|
-
* const tokens = tokenize('SELECT * FROM users');
|
|
12
|
+
* const rows = await conn.query('SELECT 42 as num');
|
|
13
|
+
* conn.close();
|
|
14
|
+
* db.close();
|
|
18
15
|
*/
|
|
19
16
|
|
|
20
|
-
import { dlopen, ptr } from 'bun:ffi';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
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
|
+
}
|
|
24
61
|
|
|
25
|
-
const
|
|
62
|
+
const libPath = findDuckDBLibrary();
|
|
26
63
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const libPath = join(__dirname, `${target}/duckdb.node`);
|
|
64
|
+
// ==============================================================================
|
|
65
|
+
// Load DuckDB C API (modern chunk-based + lifecycle functions)
|
|
66
|
+
// ==============================================================================
|
|
31
67
|
|
|
32
|
-
// Load the Zig library
|
|
33
68
|
const lib = dlopen(libPath, {
|
|
34
69
|
// Database lifecycle
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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' },
|
|
80
|
+
|
|
81
|
+
// Prepared statements
|
|
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' },
|
|
60
132
|
}).symbols;
|
|
61
133
|
|
|
62
|
-
|
|
134
|
+
// ==============================================================================
|
|
135
|
+
// DuckDB Type Constants
|
|
136
|
+
// ==============================================================================
|
|
63
137
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
138
|
+
const DUCKDB_TYPE = {
|
|
139
|
+
INVALID: 0,
|
|
140
|
+
BOOLEAN: 1,
|
|
141
|
+
TINYINT: 2,
|
|
142
|
+
SMALLINT: 3,
|
|
143
|
+
INTEGER: 4,
|
|
144
|
+
BIGINT: 5,
|
|
145
|
+
UTINYINT: 6,
|
|
146
|
+
USMALLINT: 7,
|
|
147
|
+
UINTEGER: 8,
|
|
148
|
+
UBIGINT: 9,
|
|
149
|
+
FLOAT: 10,
|
|
150
|
+
DOUBLE: 11,
|
|
151
|
+
TIMESTAMP: 12,
|
|
152
|
+
DATE: 13,
|
|
153
|
+
TIME: 14,
|
|
154
|
+
INTERVAL: 15,
|
|
155
|
+
HUGEINT: 16,
|
|
156
|
+
VARCHAR: 17,
|
|
157
|
+
BLOB: 18,
|
|
158
|
+
DECIMAL: 19,
|
|
159
|
+
TIMESTAMP_S: 20,
|
|
160
|
+
TIMESTAMP_MS: 21,
|
|
161
|
+
TIMESTAMP_NS: 22,
|
|
162
|
+
ENUM: 23,
|
|
163
|
+
LIST: 24,
|
|
164
|
+
STRUCT: 25,
|
|
165
|
+
MAP: 26,
|
|
166
|
+
UUID: 27,
|
|
167
|
+
UNION: 28,
|
|
168
|
+
BIT: 29,
|
|
169
|
+
TIMESTAMP_TZ: 32,
|
|
170
|
+
};
|
|
67
171
|
|
|
68
|
-
|
|
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
|
+
}
|
|
69
189
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
export function open(path) {
|
|
76
|
-
return new Database(path);
|
|
190
|
+
const encoder = new TextEncoder();
|
|
191
|
+
const decoder = new TextDecoder();
|
|
192
|
+
|
|
193
|
+
function toCString(str) {
|
|
194
|
+
return encoder.encode(str + '\0');
|
|
77
195
|
}
|
|
78
196
|
|
|
197
|
+
function fromCString(p) {
|
|
198
|
+
if (!p) return null;
|
|
199
|
+
return new CString(p).toString();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function allocPtr() {
|
|
203
|
+
return new Uint8Array(8);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function readPtr(buf) {
|
|
207
|
+
return ffiRead.ptr(ptr(buf), 0);
|
|
208
|
+
}
|
|
209
|
+
|
|
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);
|
|
220
|
+
}
|
|
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);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
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;
|
|
241
|
+
}
|
|
242
|
+
|
|
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)}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ==============================================================================
|
|
259
|
+
// Database Class
|
|
260
|
+
// ==============================================================================
|
|
261
|
+
|
|
79
262
|
class Database {
|
|
80
|
-
#
|
|
263
|
+
#ptrBuf = null;
|
|
264
|
+
#handle = null;
|
|
81
265
|
|
|
82
266
|
constructor(path) {
|
|
83
|
-
|
|
84
|
-
const pathBytes =
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
}
|
|
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);
|
|
89
272
|
}
|
|
90
273
|
|
|
91
274
|
get handle() { return this.#handle; }
|
|
275
|
+
get ptrBuf() { return this.#ptrBuf; }
|
|
92
276
|
|
|
93
|
-
connect() {
|
|
94
|
-
return new Connection(this.#handle);
|
|
95
|
-
}
|
|
277
|
+
connect() { return new Connection(this); }
|
|
96
278
|
|
|
97
279
|
close() {
|
|
98
|
-
if (this.#
|
|
99
|
-
lib.
|
|
280
|
+
if (this.#ptrBuf) {
|
|
281
|
+
lib.duckdb_close(ptr(this.#ptrBuf));
|
|
282
|
+
this.#ptrBuf = null;
|
|
100
283
|
this.#handle = null;
|
|
101
284
|
}
|
|
102
285
|
}
|
|
103
286
|
}
|
|
104
287
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
};
|
|
288
|
+
// ==============================================================================
|
|
289
|
+
// Connection Class
|
|
290
|
+
// ==============================================================================
|
|
113
291
|
|
|
114
292
|
class Connection {
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
293
|
+
#ptrBuf = null;
|
|
294
|
+
#handle = null;
|
|
295
|
+
#db = null;
|
|
296
|
+
|
|
297
|
+
constructor(db) {
|
|
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);
|
|
122
303
|
}
|
|
123
304
|
|
|
124
305
|
get handle() { return this.#handle; }
|
|
306
|
+
get ptrBuf() { return this.#ptrBuf; }
|
|
125
307
|
|
|
126
308
|
/**
|
|
127
|
-
* Execute SQL and return
|
|
309
|
+
* Execute a SQL query and return results as array of objects
|
|
128
310
|
* @param {string} sql - SQL query
|
|
129
|
-
* @
|
|
311
|
+
* @param {any[]} params - Optional parameters for prepared statement
|
|
312
|
+
* @returns {Promise<object[]>} Array of row objects
|
|
130
313
|
*/
|
|
131
|
-
query(sql) {
|
|
132
|
-
|
|
133
|
-
|
|
314
|
+
query(sql, params = []) {
|
|
315
|
+
return withLock(() => {
|
|
316
|
+
if (params.length > 0) return this.#queryPrepared(sql, params);
|
|
317
|
+
return this.#querySimple(sql);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
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);
|
|
332
|
+
}
|
|
134
333
|
|
|
135
334
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
335
|
+
return this.#extractChunks(resultPtr);
|
|
336
|
+
} finally {
|
|
337
|
+
lib.duckdb_destroy_result(rp);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
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');
|
|
355
|
+
}
|
|
356
|
+
|
|
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));
|
|
379
|
+
} else {
|
|
380
|
+
const strBytes = toCString(String(value));
|
|
381
|
+
lib.duckdb_bind_varchar(stmtHandle, paramIdx, ptr(strBytes));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const resultPtr = new Uint8Array(64); // duckdb_result struct is ~48 bytes
|
|
386
|
+
lib.duckdb_execute_prepared(stmtHandle, ptr(resultPtr));
|
|
387
|
+
|
|
388
|
+
const rp = ptr(resultPtr);
|
|
389
|
+
const errorPtr = lib.duckdb_result_error(rp);
|
|
138
390
|
if (errorPtr) {
|
|
139
|
-
|
|
391
|
+
const error = fromCString(errorPtr);
|
|
392
|
+
lib.duckdb_destroy_result(rp);
|
|
393
|
+
throw new Error(error);
|
|
394
|
+
}
|
|
395
|
+
|
|
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));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
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
|
+
}
|
|
140
501
|
}
|
|
141
502
|
|
|
142
|
-
|
|
143
|
-
|
|
503
|
+
columns.push(col);
|
|
504
|
+
}
|
|
505
|
+
|
|
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
|
+
}
|
|
144
521
|
|
|
145
|
-
// Get column
|
|
146
|
-
const
|
|
147
|
-
const
|
|
522
|
+
// Get vectors for each column (data + validity + handle for nested types)
|
|
523
|
+
const colVec = [];
|
|
524
|
+
const colData = [];
|
|
525
|
+
const colValidity = [];
|
|
148
526
|
for (let c = 0; c < colCount; c++) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
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));
|
|
152
531
|
}
|
|
153
532
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
for (let r = 0; r < rowCount; r++) {
|
|
533
|
+
// Extract rows from this chunk
|
|
534
|
+
for (let r = 0; r < chunkSize; r++) {
|
|
157
535
|
const row = {};
|
|
158
536
|
for (let c = 0; c < colCount; c++) {
|
|
159
|
-
const col =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (lib.duck_value_is_null(result, col, rowIdx)) {
|
|
163
|
-
row[columns[c]] = null;
|
|
537
|
+
const col = columns[c];
|
|
538
|
+
if (!isValid(colValidity[c], r)) {
|
|
539
|
+
row[col.name] = null;
|
|
164
540
|
} else {
|
|
165
|
-
|
|
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
|
-
}
|
|
541
|
+
row[col.name] = this.#readValue(colData[c], r, col.type, col, colVec[c]);
|
|
201
542
|
}
|
|
202
543
|
}
|
|
203
544
|
rows.push(row);
|
|
204
545
|
}
|
|
205
546
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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));
|
|
209
551
|
}
|
|
210
|
-
}
|
|
211
552
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
lib.duck_disconnect(this.#handle);
|
|
215
|
-
this.#handle = null;
|
|
216
|
-
}
|
|
553
|
+
rows.columns = columns;
|
|
554
|
+
return rows;
|
|
217
555
|
}
|
|
218
|
-
}
|
|
219
556
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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();
|
|
598
|
+
}
|
|
237
599
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|
|
241
629
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
*
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
);
|
|
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
|
+
}
|
|
256
636
|
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
}
|
|
259
645
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
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
|
+
}
|
|
271
656
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
+
}
|
|
677
|
+
|
|
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
|
+
}
|
|
692
|
+
|
|
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;
|
|
759
|
+
}
|
|
760
|
+
return obj;
|
|
761
|
+
}
|
|
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; }
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
#typeName(type) {
|
|
774
|
+
for (const [name, value] of Object.entries(DUCKDB_TYPE)) {
|
|
775
|
+
if (value === type) return name;
|
|
776
|
+
}
|
|
777
|
+
return 'UNKNOWN';
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
close() {
|
|
781
|
+
if (this.#ptrBuf) {
|
|
782
|
+
lib.duckdb_disconnect(ptr(this.#ptrBuf));
|
|
783
|
+
this.#ptrBuf = null;
|
|
784
|
+
this.#handle = null;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
285
787
|
}
|
|
286
788
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
export function
|
|
292
|
-
|
|
789
|
+
// ==============================================================================
|
|
790
|
+
// Public API
|
|
791
|
+
// ==============================================================================
|
|
792
|
+
|
|
793
|
+
export function open(path) {
|
|
794
|
+
return new Database(path);
|
|
293
795
|
}
|
|
294
796
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
};
|
|
797
|
+
export function version() {
|
|
798
|
+
const versionPtr = lib.duckdb_library_version();
|
|
799
|
+
return fromCString(versionPtr);
|
|
800
|
+
}
|
|
324
801
|
|
|
325
|
-
export
|
|
802
|
+
export { Database, Connection };
|