@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/README.md +98 -259
- package/bin/rip-db +5 -10
- package/db.rip +217 -237
- package/lib/duckdb-binary.rip +34 -13
- package/lib/duckdb.mjs +694 -304
- package/package.json +10 -7
- package/PROTOCOL.md +0 -258
- package/db.html +0 -122
- package/lib/darwin-arm64/duckdb.node +0 -0
package/lib/duckdb.mjs
CHANGED
|
@@ -1,95 +1,141 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DuckDB
|
|
2
|
+
* DuckDB Pure Bun FFI Wrapper
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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 {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
//
|
|
87
|
-
//
|
|
134
|
+
// ==============================================================================
|
|
135
|
+
// DuckDB Type Constants
|
|
136
|
+
// ==============================================================================
|
|
88
137
|
|
|
89
|
-
const
|
|
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
|
-
|
|
167
|
+
UNION: 28,
|
|
168
|
+
BIT: 29,
|
|
169
|
+
TIMESTAMP_TZ: 32,
|
|
122
170
|
};
|
|
123
171
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
151
|
-
const
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
197
|
+
function fromCString(p) {
|
|
198
|
+
if (!p) return null;
|
|
199
|
+
return new CString(p).toString();
|
|
200
|
+
}
|
|
193
201
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
#
|
|
263
|
+
#ptrBuf = null;
|
|
264
|
+
#handle = null;
|
|
229
265
|
|
|
230
266
|
constructor(path) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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.#
|
|
244
|
-
|
|
245
|
-
this.#
|
|
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
|
-
#
|
|
293
|
+
#ptrBuf = null;
|
|
294
|
+
#handle = null;
|
|
295
|
+
#db = null;
|
|
252
296
|
|
|
253
297
|
constructor(db) {
|
|
254
|
-
this.#
|
|
255
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
385
|
+
const resultPtr = new Uint8Array(64); // duckdb_result struct is ~48 bytes
|
|
386
|
+
lib.duckdb_execute_prepared(stmtHandle, ptr(resultPtr));
|
|
308
387
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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.#
|
|
406
|
-
|
|
407
|
-
this.#
|
|
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
|
-
|
|
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 };
|