@leonardovida-md/drizzle-neo-duckdb 1.0.2 → 1.0.3
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 +36 -18
- package/dist/client.d.ts +12 -0
- package/dist/columns.d.ts +18 -10
- package/dist/driver.d.ts +4 -0
- package/dist/duckdb-introspect.mjs +171 -23
- package/dist/index.d.ts +3 -0
- package/dist/index.mjs +387 -37
- package/dist/olap.d.ts +46 -0
- package/dist/session.d.ts +5 -0
- package/dist/value-wrappers.d.ts +104 -0
- package/package.json +7 -3
- package/src/bin/duckdb-introspect.ts +12 -3
- package/src/client.ts +135 -18
- package/src/columns.ts +65 -36
- package/src/dialect.ts +2 -2
- package/src/driver.ts +20 -6
- package/src/index.ts +3 -0
- package/src/introspect.ts +15 -10
- package/src/migrator.ts +1 -3
- package/src/olap.ts +189 -0
- package/src/select-builder.ts +3 -7
- package/src/session.ts +87 -18
- package/src/sql/query-rewriters.ts +5 -8
- package/src/sql/result-mapper.ts +6 -6
- package/src/sql/selection.ts +2 -9
- package/src/value-wrappers.ts +324 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type DuckDBValue } from '@duckdb/node-api';
|
|
2
|
+
/**
|
|
3
|
+
* Symbol used to identify wrapped DuckDB values for native binding.
|
|
4
|
+
* Uses Symbol.for() to ensure cross-module compatibility.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DUCKDB_VALUE_MARKER: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* Type identifier for each wrapper kind.
|
|
9
|
+
*/
|
|
10
|
+
export type DuckDBValueKind = 'list' | 'array' | 'struct' | 'map' | 'timestamp' | 'blob' | 'json';
|
|
11
|
+
/**
|
|
12
|
+
* Base interface for all tagged DuckDB value wrappers.
|
|
13
|
+
*/
|
|
14
|
+
export interface DuckDBValueWrapper<TKind extends DuckDBValueKind = DuckDBValueKind, TData = unknown> {
|
|
15
|
+
readonly [DUCKDB_VALUE_MARKER]: true;
|
|
16
|
+
readonly kind: TKind;
|
|
17
|
+
readonly data: TData;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* List wrapper - maps to DuckDBListValue
|
|
21
|
+
*/
|
|
22
|
+
export interface ListValueWrapper extends DuckDBValueWrapper<'list', unknown[]> {
|
|
23
|
+
readonly elementType?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Array wrapper (fixed size) - maps to DuckDBArrayValue
|
|
27
|
+
*/
|
|
28
|
+
export interface ArrayValueWrapper extends DuckDBValueWrapper<'array', unknown[]> {
|
|
29
|
+
readonly elementType?: string;
|
|
30
|
+
readonly fixedLength?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Struct wrapper - maps to DuckDBStructValue
|
|
34
|
+
*/
|
|
35
|
+
export interface StructValueWrapper extends DuckDBValueWrapper<'struct', Record<string, unknown>> {
|
|
36
|
+
readonly schema?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Map wrapper - maps to DuckDBMapValue
|
|
40
|
+
*/
|
|
41
|
+
export interface MapValueWrapper extends DuckDBValueWrapper<'map', Record<string, unknown>> {
|
|
42
|
+
readonly valueType?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Timestamp wrapper - maps to DuckDBTimestampValue or DuckDBTimestampTZValue
|
|
46
|
+
*/
|
|
47
|
+
export interface TimestampValueWrapper extends DuckDBValueWrapper<'timestamp', Date | string> {
|
|
48
|
+
readonly withTimezone: boolean;
|
|
49
|
+
readonly precision?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Blob wrapper - maps to DuckDBBlobValue
|
|
53
|
+
*/
|
|
54
|
+
export interface BlobValueWrapper extends DuckDBValueWrapper<'blob', Buffer | Uint8Array> {
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* JSON wrapper - delays JSON.stringify() to binding time.
|
|
58
|
+
* DuckDB stores JSON as VARCHAR internally.
|
|
59
|
+
*/
|
|
60
|
+
export interface JsonValueWrapper extends DuckDBValueWrapper<'json', unknown> {
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Union of all wrapper types for exhaustive type checking.
|
|
64
|
+
*/
|
|
65
|
+
export type AnyDuckDBValueWrapper = ListValueWrapper | ArrayValueWrapper | StructValueWrapper | MapValueWrapper | TimestampValueWrapper | BlobValueWrapper | JsonValueWrapper;
|
|
66
|
+
/**
|
|
67
|
+
* Type guard to check if a value is a tagged DuckDB wrapper.
|
|
68
|
+
* Optimized for fast detection in the hot path.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isDuckDBWrapper(value: unknown): value is AnyDuckDBValueWrapper;
|
|
71
|
+
/**
|
|
72
|
+
* Create a list wrapper for variable-length lists.
|
|
73
|
+
*/
|
|
74
|
+
export declare function wrapList(data: unknown[], elementType?: string): ListValueWrapper;
|
|
75
|
+
/**
|
|
76
|
+
* Create an array wrapper for fixed-length arrays.
|
|
77
|
+
*/
|
|
78
|
+
export declare function wrapArray(data: unknown[], elementType?: string, fixedLength?: number): ArrayValueWrapper;
|
|
79
|
+
/**
|
|
80
|
+
* Create a struct wrapper for named field structures.
|
|
81
|
+
*/
|
|
82
|
+
export declare function wrapStruct(data: Record<string, unknown>, schema?: Record<string, string>): StructValueWrapper;
|
|
83
|
+
/**
|
|
84
|
+
* Create a map wrapper for key-value maps.
|
|
85
|
+
*/
|
|
86
|
+
export declare function wrapMap(data: Record<string, unknown>, valueType?: string): MapValueWrapper;
|
|
87
|
+
/**
|
|
88
|
+
* Create a timestamp wrapper.
|
|
89
|
+
*/
|
|
90
|
+
export declare function wrapTimestamp(data: Date | string, withTimezone: boolean, precision?: number): TimestampValueWrapper;
|
|
91
|
+
/**
|
|
92
|
+
* Create a blob wrapper for binary data.
|
|
93
|
+
*/
|
|
94
|
+
export declare function wrapBlob(data: Buffer | Uint8Array): BlobValueWrapper;
|
|
95
|
+
/**
|
|
96
|
+
* Create a JSON wrapper that delays JSON.stringify() to binding time.
|
|
97
|
+
* This ensures consistent handling with other wrapped types.
|
|
98
|
+
*/
|
|
99
|
+
export declare function wrapJson(data: unknown): JsonValueWrapper;
|
|
100
|
+
/**
|
|
101
|
+
* Convert a wrapper to a DuckDB Node API value.
|
|
102
|
+
* Uses exhaustive switch for compile-time safety.
|
|
103
|
+
*/
|
|
104
|
+
export declare function wrapperToNodeApiValue(wrapper: AnyDuckDBValueWrapper, toValue: (v: unknown) => DuckDBValue): DuckDBValue;
|
package/package.json
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
"module": "./dist/index.mjs",
|
|
4
4
|
"main": "./dist/index.mjs",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.3",
|
|
7
7
|
"description": "A drizzle ORM client for use with DuckDB. Based on drizzle's Postgres client.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "bun build --target=node ./src/index.ts --outfile=./dist/index.mjs --packages=external && bun build --target=node ./src/bin/duckdb-introspect.ts --outfile=./dist/duckdb-introspect.mjs --packages=external && bun run build:declarations",
|
|
11
11
|
"build:declarations": "tsc --emitDeclarationOnly --project tsconfig.types.json",
|
|
12
12
|
"test": "vitest",
|
|
13
|
-
"t": "vitest --watch --ui"
|
|
13
|
+
"t": "vitest --watch --ui",
|
|
14
|
+
"bench": "vitest bench --runInBand test/perf",
|
|
15
|
+
"perf:run": "bun run scripts/run-perf.ts",
|
|
16
|
+
"perf:compare": "bun run scripts/compare-perf.ts"
|
|
14
17
|
},
|
|
15
18
|
"bin": {
|
|
16
19
|
"duckdb-introspect": "dist/duckdb-introspect.mjs"
|
|
@@ -33,7 +36,8 @@
|
|
|
33
36
|
"prettier": "^3.5.3",
|
|
34
37
|
"typescript": "^5.8.2",
|
|
35
38
|
"uuid": "^10.0.0",
|
|
36
|
-
"vitest": "^1.6.0"
|
|
39
|
+
"vitest": "^1.6.0",
|
|
40
|
+
"tinybench": "^2.7.1"
|
|
37
41
|
},
|
|
38
42
|
"repository": {
|
|
39
43
|
"type": "git",
|
|
@@ -40,11 +40,17 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
40
40
|
break;
|
|
41
41
|
case '--schema':
|
|
42
42
|
case '--schemas':
|
|
43
|
-
options.schemas = argv[++i]
|
|
43
|
+
options.schemas = argv[++i]
|
|
44
|
+
?.split(',')
|
|
45
|
+
.map((s) => s.trim())
|
|
46
|
+
.filter(Boolean);
|
|
44
47
|
break;
|
|
45
48
|
case '--out':
|
|
46
49
|
case '--outFile':
|
|
47
|
-
options.outFile = path.resolve(
|
|
50
|
+
options.outFile = path.resolve(
|
|
51
|
+
process.cwd(),
|
|
52
|
+
argv[++i] ?? 'drizzle/schema.ts'
|
|
53
|
+
);
|
|
48
54
|
break;
|
|
49
55
|
case '--include-views':
|
|
50
56
|
case '--includeViews':
|
|
@@ -133,7 +139,10 @@ async function main() {
|
|
|
133
139
|
|
|
134
140
|
console.log(`Wrote schema to ${options.outFile}`);
|
|
135
141
|
} finally {
|
|
136
|
-
if (
|
|
142
|
+
if (
|
|
143
|
+
'closeSync' in connection &&
|
|
144
|
+
typeof connection.closeSync === 'function'
|
|
145
|
+
) {
|
|
137
146
|
connection.closeSync();
|
|
138
147
|
}
|
|
139
148
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
listValue,
|
|
3
|
+
timestampValue,
|
|
3
4
|
type DuckDBConnection,
|
|
4
5
|
type DuckDBValue,
|
|
5
6
|
} from '@duckdb/node-api';
|
|
7
|
+
import {
|
|
8
|
+
DUCKDB_VALUE_MARKER,
|
|
9
|
+
wrapperToNodeApiValue,
|
|
10
|
+
type AnyDuckDBValueWrapper,
|
|
11
|
+
} from './value-wrappers.ts';
|
|
6
12
|
|
|
7
13
|
export type DuckDBClientLike = DuckDBConnection;
|
|
8
14
|
export type RowData = Record<string, unknown>;
|
|
@@ -50,13 +56,62 @@ export function prepareParams(
|
|
|
50
56
|
});
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Convert a value to DuckDB Node API value.
|
|
61
|
+
* Handles wrapper types and plain values for backward compatibility.
|
|
62
|
+
* Optimized for the common case (primitives) in the hot path.
|
|
63
|
+
*/
|
|
53
64
|
function toNodeApiValue(value: unknown): DuckDBValue {
|
|
65
|
+
// Fast path 1: null/undefined
|
|
66
|
+
if (value == null) return null;
|
|
67
|
+
|
|
68
|
+
// Fast path 2: primitives (most common)
|
|
69
|
+
const t = typeof value;
|
|
70
|
+
if (t === 'string' || t === 'number' || t === 'bigint' || t === 'boolean') {
|
|
71
|
+
return value as DuckDBValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fast path 3: pre-wrapped DuckDB value (Symbol check ~2-3ns)
|
|
75
|
+
if (t === 'object' && DUCKDB_VALUE_MARKER in (value as object)) {
|
|
76
|
+
return wrapperToNodeApiValue(
|
|
77
|
+
value as AnyDuckDBValueWrapper,
|
|
78
|
+
toNodeApiValue
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Legacy path: plain arrays (backward compatibility)
|
|
54
83
|
if (Array.isArray(value)) {
|
|
55
84
|
return listValue(value.map((inner) => toNodeApiValue(inner)));
|
|
56
85
|
}
|
|
86
|
+
|
|
87
|
+
// Date conversion to timestamp
|
|
88
|
+
if (value instanceof Date) {
|
|
89
|
+
return timestampValue(BigInt(value.getTime()) * 1000n);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fallback for unknown objects
|
|
57
93
|
return value as DuckDBValue;
|
|
58
94
|
}
|
|
59
95
|
|
|
96
|
+
function deduplicateColumns(columns: string[]): string[] {
|
|
97
|
+
const seen: Record<string, number> = {};
|
|
98
|
+
return columns.map((col) => {
|
|
99
|
+
const count = seen[col] ?? 0;
|
|
100
|
+
seen[col] = count + 1;
|
|
101
|
+
return count === 0 ? col : `${col}_${count}`;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function mapRowsToObjects(columns: string[], rows: unknown[][]): RowData[] {
|
|
106
|
+
return rows.map((vals) => {
|
|
107
|
+
const obj: Record<string, unknown> = {};
|
|
108
|
+
columns.forEach((col, idx) => {
|
|
109
|
+
obj[col] = vals[idx];
|
|
110
|
+
});
|
|
111
|
+
return obj;
|
|
112
|
+
}) as RowData[];
|
|
113
|
+
}
|
|
114
|
+
|
|
60
115
|
export async function closeClientConnection(
|
|
61
116
|
connection: DuckDBConnection
|
|
62
117
|
): Promise<void> {
|
|
@@ -65,10 +120,7 @@ export async function closeClientConnection(
|
|
|
65
120
|
return;
|
|
66
121
|
}
|
|
67
122
|
|
|
68
|
-
if (
|
|
69
|
-
'closeSync' in connection &&
|
|
70
|
-
typeof connection.closeSync === 'function'
|
|
71
|
-
) {
|
|
123
|
+
if ('closeSync' in connection && typeof connection.closeSync === 'function') {
|
|
72
124
|
connection.closeSync();
|
|
73
125
|
return;
|
|
74
126
|
}
|
|
@@ -92,19 +144,84 @@ export async function executeOnClient(
|
|
|
92
144
|
: undefined;
|
|
93
145
|
const result = await client.run(query, values);
|
|
94
146
|
const rows = await result.getRowsJS();
|
|
95
|
-
const columns =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
seen[col] = count + 1;
|
|
100
|
-
return count === 0 ? col : `${col}_${count}`;
|
|
101
|
-
});
|
|
147
|
+
const columns =
|
|
148
|
+
// prefer deduplicated names when available (Node API >=1.4.2)
|
|
149
|
+
result.deduplicatedColumnNames?.() ?? result.columnNames();
|
|
150
|
+
const uniqueColumns = deduplicateColumns(columns);
|
|
102
151
|
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
152
|
+
return rows ? mapRowsToObjects(uniqueColumns, rows) : [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface ExecuteInBatchesOptions {
|
|
156
|
+
rowsPerChunk?: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Stream results from DuckDB in batches to avoid fully materializing rows in JS.
|
|
161
|
+
*/
|
|
162
|
+
export async function* executeInBatches(
|
|
163
|
+
client: DuckDBClientLike,
|
|
164
|
+
query: string,
|
|
165
|
+
params: unknown[],
|
|
166
|
+
options: ExecuteInBatchesOptions = {}
|
|
167
|
+
): AsyncGenerator<RowData[], void, void> {
|
|
168
|
+
const rowsPerChunk =
|
|
169
|
+
options.rowsPerChunk && options.rowsPerChunk > 0
|
|
170
|
+
? options.rowsPerChunk
|
|
171
|
+
: 100_000;
|
|
172
|
+
const values =
|
|
173
|
+
params.length > 0
|
|
174
|
+
? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
|
|
175
|
+
: undefined;
|
|
176
|
+
|
|
177
|
+
const result = await client.stream(query, values);
|
|
178
|
+
const columns =
|
|
179
|
+
// prefer deduplicated names when available (Node API >=1.4.2)
|
|
180
|
+
result.deduplicatedColumnNames?.() ?? result.columnNames();
|
|
181
|
+
const uniqueColumns = deduplicateColumns(columns);
|
|
182
|
+
|
|
183
|
+
let buffer: RowData[] = [];
|
|
184
|
+
|
|
185
|
+
for await (const chunk of result.yieldRowsJs()) {
|
|
186
|
+
const objects = mapRowsToObjects(uniqueColumns, chunk);
|
|
187
|
+
for (const row of objects) {
|
|
188
|
+
buffer.push(row);
|
|
189
|
+
if (buffer.length >= rowsPerChunk) {
|
|
190
|
+
yield buffer;
|
|
191
|
+
buffer = [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (buffer.length > 0) {
|
|
197
|
+
yield buffer;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Return columnar results when the underlying node-api exposes an Arrow/columnar API.
|
|
203
|
+
* Falls back to column-major JS arrays when Arrow is unavailable.
|
|
204
|
+
*/
|
|
205
|
+
export async function executeArrowOnClient(
|
|
206
|
+
client: DuckDBClientLike,
|
|
207
|
+
query: string,
|
|
208
|
+
params: unknown[]
|
|
209
|
+
): Promise<unknown> {
|
|
210
|
+
const values =
|
|
211
|
+
params.length > 0
|
|
212
|
+
? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
|
|
213
|
+
: undefined;
|
|
214
|
+
const result = await client.run(query, values);
|
|
215
|
+
|
|
216
|
+
const maybeArrow =
|
|
217
|
+
(result as unknown as { toArrow?: () => Promise<unknown> }).toArrow ??
|
|
218
|
+
(result as unknown as { getArrowTable?: () => Promise<unknown> })
|
|
219
|
+
.getArrowTable;
|
|
220
|
+
|
|
221
|
+
if (typeof maybeArrow === 'function') {
|
|
222
|
+
return await maybeArrow.call(result);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Fallback: return column-major JS arrays to avoid per-row object creation.
|
|
226
|
+
return result.getColumnsObjectJS();
|
|
110
227
|
}
|
package/src/columns.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { sql, type SQL } from 'drizzle-orm';
|
|
2
2
|
import type { SQLWrapper } from 'drizzle-orm/sql/sql';
|
|
3
3
|
import { customType } from 'drizzle-orm/pg-core';
|
|
4
|
+
import {
|
|
5
|
+
wrapList,
|
|
6
|
+
wrapArray,
|
|
7
|
+
wrapMap,
|
|
8
|
+
wrapBlob,
|
|
9
|
+
wrapJson,
|
|
10
|
+
type ListValueWrapper,
|
|
11
|
+
type ArrayValueWrapper,
|
|
12
|
+
type MapValueWrapper,
|
|
13
|
+
type BlobValueWrapper,
|
|
14
|
+
type JsonValueWrapper,
|
|
15
|
+
} from './value-wrappers.ts';
|
|
4
16
|
|
|
5
17
|
type IntColType =
|
|
6
18
|
| 'SMALLINT'
|
|
@@ -93,7 +105,9 @@ function formatLiteral(value: unknown, typeHint?: string): string {
|
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
const str =
|
|
96
|
-
typeof value === 'string'
|
|
108
|
+
typeof value === 'string'
|
|
109
|
+
? value
|
|
110
|
+
: (JSON.stringify(value) ?? String(value));
|
|
97
111
|
|
|
98
112
|
const escaped = str.replace(/'/g, "''");
|
|
99
113
|
// Simple quoting based on hint.
|
|
@@ -140,7 +154,10 @@ function buildStructLiteral(
|
|
|
140
154
|
return sql`struct_pack(${sql.join(parts, sql.raw(', '))})`;
|
|
141
155
|
}
|
|
142
156
|
|
|
143
|
-
function buildMapLiteral(
|
|
157
|
+
function buildMapLiteral(
|
|
158
|
+
value: Record<string, unknown>,
|
|
159
|
+
valueType?: string
|
|
160
|
+
): SQL {
|
|
144
161
|
const keys = Object.keys(value);
|
|
145
162
|
const vals = Object.values(value);
|
|
146
163
|
const keyList = buildListLiteral(keys, 'TEXT');
|
|
@@ -155,14 +172,17 @@ export const duckDbList = <TData = unknown>(
|
|
|
155
172
|
name: string,
|
|
156
173
|
elementType: AnyColType
|
|
157
174
|
) =>
|
|
158
|
-
customType<{
|
|
175
|
+
customType<{
|
|
176
|
+
data: TData[];
|
|
177
|
+
driverData: ListValueWrapper | unknown[] | string;
|
|
178
|
+
}>({
|
|
159
179
|
dataType() {
|
|
160
180
|
return `${elementType}[]`;
|
|
161
181
|
},
|
|
162
|
-
toDriver(value: TData[]) {
|
|
163
|
-
return
|
|
182
|
+
toDriver(value: TData[]): ListValueWrapper {
|
|
183
|
+
return wrapList(value, elementType);
|
|
164
184
|
},
|
|
165
|
-
fromDriver(value: unknown[] | string |
|
|
185
|
+
fromDriver(value: unknown[] | string | ListValueWrapper): TData[] {
|
|
166
186
|
if (Array.isArray(value)) {
|
|
167
187
|
return value as TData[];
|
|
168
188
|
}
|
|
@@ -181,16 +201,19 @@ export const duckDbArray = <TData = unknown>(
|
|
|
181
201
|
elementType: AnyColType,
|
|
182
202
|
fixedLength?: number
|
|
183
203
|
) =>
|
|
184
|
-
customType<{
|
|
204
|
+
customType<{
|
|
205
|
+
data: TData[];
|
|
206
|
+
driverData: ArrayValueWrapper | unknown[] | string;
|
|
207
|
+
}>({
|
|
185
208
|
dataType() {
|
|
186
209
|
return fixedLength
|
|
187
210
|
? `${elementType}[${fixedLength}]`
|
|
188
211
|
: `${elementType}[]`;
|
|
189
212
|
},
|
|
190
|
-
toDriver(value: TData[]) {
|
|
191
|
-
return
|
|
213
|
+
toDriver(value: TData[]): ArrayValueWrapper {
|
|
214
|
+
return wrapArray(value, elementType, fixedLength);
|
|
192
215
|
},
|
|
193
|
-
fromDriver(value: unknown[] | string |
|
|
216
|
+
fromDriver(value: unknown[] | string | ArrayValueWrapper): TData[] {
|
|
194
217
|
if (Array.isArray(value)) {
|
|
195
218
|
return value as TData[];
|
|
196
219
|
}
|
|
@@ -208,15 +231,15 @@ export const duckDbMap = <TData extends Record<string, any>>(
|
|
|
208
231
|
name: string,
|
|
209
232
|
valueType: AnyColType | ListColType | ArrayColType
|
|
210
233
|
) =>
|
|
211
|
-
customType<{ data: TData; driverData: TData }>({
|
|
212
|
-
|
|
234
|
+
customType<{ data: TData; driverData: MapValueWrapper | TData }>({
|
|
235
|
+
dataType() {
|
|
213
236
|
return `MAP (STRING, ${valueType})`;
|
|
214
237
|
},
|
|
215
|
-
toDriver(value: TData) {
|
|
216
|
-
return
|
|
238
|
+
toDriver(value: TData): MapValueWrapper {
|
|
239
|
+
return wrapMap(value, valueType);
|
|
217
240
|
},
|
|
218
|
-
fromDriver(value: TData): TData {
|
|
219
|
-
return value;
|
|
241
|
+
fromDriver(value: TData | MapValueWrapper): TData {
|
|
242
|
+
return value as TData;
|
|
220
243
|
},
|
|
221
244
|
})(name);
|
|
222
245
|
|
|
@@ -233,6 +256,8 @@ export const duckDbStruct = <TData extends Record<string, any>>(
|
|
|
233
256
|
return `STRUCT (${fields.join(', ')})`;
|
|
234
257
|
},
|
|
235
258
|
toDriver(value: TData) {
|
|
259
|
+
// Use SQL literals for structs due to DuckDB type inference issues
|
|
260
|
+
// with nested empty lists
|
|
236
261
|
return buildStructLiteral(value, schema);
|
|
237
262
|
},
|
|
238
263
|
fromDriver(value: TData | string): TData {
|
|
@@ -247,15 +272,24 @@ export const duckDbStruct = <TData extends Record<string, any>>(
|
|
|
247
272
|
},
|
|
248
273
|
})(name);
|
|
249
274
|
|
|
275
|
+
/**
|
|
276
|
+
* JSON column type that wraps values and delays JSON.stringify() to binding time.
|
|
277
|
+
* This ensures consistent handling with other wrapped types.
|
|
278
|
+
*
|
|
279
|
+
* Note: DuckDB stores JSON as VARCHAR internally, so the final binding
|
|
280
|
+
* is always a stringified JSON value.
|
|
281
|
+
*/
|
|
250
282
|
export const duckDbJson = <TData = unknown>(name: string) =>
|
|
251
|
-
customType<{ data: TData; driverData: SQL | string }>({
|
|
283
|
+
customType<{ data: TData; driverData: JsonValueWrapper | SQL | string }>({
|
|
252
284
|
dataType() {
|
|
253
285
|
return 'JSON';
|
|
254
286
|
},
|
|
255
|
-
toDriver(value: TData) {
|
|
287
|
+
toDriver(value: TData): JsonValueWrapper | SQL | string {
|
|
288
|
+
// Pass through strings directly
|
|
256
289
|
if (typeof value === 'string') {
|
|
257
290
|
return value;
|
|
258
291
|
}
|
|
292
|
+
// Pass through SQL objects (for raw SQL expressions)
|
|
259
293
|
if (
|
|
260
294
|
value !== null &&
|
|
261
295
|
typeof value === 'object' &&
|
|
@@ -263,9 +297,10 @@ export const duckDbJson = <TData = unknown>(name: string) =>
|
|
|
263
297
|
) {
|
|
264
298
|
return value as unknown as SQL;
|
|
265
299
|
}
|
|
266
|
-
|
|
300
|
+
// Wrap non-string values for delayed stringify at binding time
|
|
301
|
+
return wrapJson(value);
|
|
267
302
|
},
|
|
268
|
-
fromDriver(value: SQL | string) {
|
|
303
|
+
fromDriver(value: SQL | string | JsonValueWrapper) {
|
|
269
304
|
if (typeof value !== 'string') {
|
|
270
305
|
return value as unknown as TData;
|
|
271
306
|
}
|
|
@@ -283,14 +318,14 @@ export const duckDbJson = <TData = unknown>(name: string) =>
|
|
|
283
318
|
|
|
284
319
|
export const duckDbBlob = customType<{
|
|
285
320
|
data: Buffer;
|
|
321
|
+
driverData: BlobValueWrapper;
|
|
286
322
|
default: false;
|
|
287
323
|
}>({
|
|
288
324
|
dataType() {
|
|
289
325
|
return 'BLOB';
|
|
290
326
|
},
|
|
291
|
-
toDriver(value: Buffer) {
|
|
292
|
-
|
|
293
|
-
return sql`from_hex(${hexString})`;
|
|
327
|
+
toDriver(value: Buffer): BlobValueWrapper {
|
|
328
|
+
return wrapBlob(value);
|
|
294
329
|
},
|
|
295
330
|
});
|
|
296
331
|
|
|
@@ -322,10 +357,7 @@ interface TimestampOptions {
|
|
|
322
357
|
precision?: number;
|
|
323
358
|
}
|
|
324
359
|
|
|
325
|
-
export const duckDbTimestamp = (
|
|
326
|
-
name: string,
|
|
327
|
-
options: TimestampOptions = {}
|
|
328
|
-
) =>
|
|
360
|
+
export const duckDbTimestamp = (name: string, options: TimestampOptions = {}) =>
|
|
329
361
|
customType<{
|
|
330
362
|
data: Date | string;
|
|
331
363
|
driverData: SQL | string | Date;
|
|
@@ -338,6 +370,7 @@ export const duckDbTimestamp = (
|
|
|
338
370
|
return `TIMESTAMP${precision}`;
|
|
339
371
|
},
|
|
340
372
|
toDriver(value: Date | string) {
|
|
373
|
+
// Use SQL literals for timestamps due to Bun/DuckDB bigint binding issues
|
|
341
374
|
const iso = value instanceof Date ? value.toISOString() : value;
|
|
342
375
|
const normalized = iso.replace('T', ' ').replace('Z', '+00');
|
|
343
376
|
const typeKeyword = options.withTimezone ? 'TIMESTAMPTZ' : 'TIMESTAMP';
|
|
@@ -353,11 +386,9 @@ export const duckDbTimestamp = (
|
|
|
353
386
|
if (value instanceof Date) {
|
|
354
387
|
return value;
|
|
355
388
|
}
|
|
356
|
-
const stringValue =
|
|
357
|
-
typeof value === 'string' ? value : value.toString();
|
|
389
|
+
const stringValue = typeof value === 'string' ? value : value.toString();
|
|
358
390
|
const hasOffset =
|
|
359
|
-
stringValue.endsWith('Z') ||
|
|
360
|
-
/[+-]\d{2}:?\d{2}$/.test(stringValue);
|
|
391
|
+
stringValue.endsWith('Z') || /[+-]\d{2}:?\d{2}$/.test(stringValue);
|
|
361
392
|
const normalized = hasOffset
|
|
362
393
|
? stringValue.replace(' ', 'T')
|
|
363
394
|
: `${stringValue.replace(' ', 'T')}Z`;
|
|
@@ -374,11 +405,9 @@ export const duckDbDate = (name: string) =>
|
|
|
374
405
|
return value;
|
|
375
406
|
},
|
|
376
407
|
fromDriver(value: string | Date) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
: value;
|
|
381
|
-
return str;
|
|
408
|
+
const str =
|
|
409
|
+
value instanceof Date ? value.toISOString().slice(0, 10) : value;
|
|
410
|
+
return str;
|
|
382
411
|
},
|
|
383
412
|
})(name);
|
|
384
413
|
|
package/src/dialect.ts
CHANGED
|
@@ -52,8 +52,8 @@ export class DuckDBDialect extends PgDialect {
|
|
|
52
52
|
|
|
53
53
|
const migrationTableCreate = sql`
|
|
54
54
|
CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsSchema)}.${sql.identifier(
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
migrationsTable
|
|
56
|
+
)} (
|
|
57
57
|
id integer PRIMARY KEY default nextval('${sql.raw(sequenceLiteral)}'),
|
|
58
58
|
hash text NOT NULL,
|
|
59
59
|
created_at bigint
|
package/src/driver.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type TablesRelationalConfig,
|
|
13
13
|
} from 'drizzle-orm/relations';
|
|
14
14
|
import { type DrizzleConfig } from 'drizzle-orm/utils';
|
|
15
|
+
import type { SQL } from 'drizzle-orm/sql/sql';
|
|
15
16
|
import type {
|
|
16
17
|
DuckDBClientLike,
|
|
17
18
|
DuckDBQueryResultHKT,
|
|
@@ -21,6 +22,7 @@ import { DuckDBSession } from './session.ts';
|
|
|
21
22
|
import { DuckDBDialect } from './dialect.ts';
|
|
22
23
|
import { DuckDBSelectBuilder } from './select-builder.ts';
|
|
23
24
|
import { aliasFields } from './sql/selection.ts';
|
|
25
|
+
import type { ExecuteInBatchesOptions, RowData } from './client.ts';
|
|
24
26
|
|
|
25
27
|
export interface PgDriverOptions {
|
|
26
28
|
logger?: Logger;
|
|
@@ -49,14 +51,14 @@ export class DuckDBDriver {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export interface DuckDBDrizzleConfig<
|
|
52
|
-
TSchema extends Record<string, unknown> = Record<string, never
|
|
54
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
53
55
|
> extends DrizzleConfig<TSchema> {
|
|
54
56
|
rewriteArrays?: boolean;
|
|
55
57
|
rejectStringArrayLiterals?: boolean;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export function drizzle<
|
|
59
|
-
TSchema extends Record<string, unknown> = Record<string, never
|
|
61
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
60
62
|
>(
|
|
61
63
|
client: DuckDBClientLike,
|
|
62
64
|
config: DuckDBDrizzleConfig<TSchema> = {}
|
|
@@ -95,7 +97,8 @@ export function drizzle<
|
|
|
95
97
|
|
|
96
98
|
export class DuckDBDatabase<
|
|
97
99
|
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
98
|
-
TSchema extends
|
|
100
|
+
TSchema extends
|
|
101
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
99
102
|
> extends PgDatabase<DuckDBQueryResultHKT, TFullSchema, TSchema> {
|
|
100
103
|
static readonly [entityKind]: string = 'DuckDBDatabase';
|
|
101
104
|
|
|
@@ -111,9 +114,9 @@ export class DuckDBDatabase<
|
|
|
111
114
|
select<TSelection extends SelectedFields>(
|
|
112
115
|
fields: TSelection
|
|
113
116
|
): DuckDBSelectBuilder<TSelection>;
|
|
114
|
-
select(
|
|
115
|
-
SelectedFields
|
|
116
|
-
> {
|
|
117
|
+
select(
|
|
118
|
+
fields?: SelectedFields
|
|
119
|
+
): DuckDBSelectBuilder<SelectedFields | undefined> {
|
|
117
120
|
const selectedFields = fields ? aliasFields(fields) : undefined;
|
|
118
121
|
|
|
119
122
|
return new DuckDBSelectBuilder({
|
|
@@ -123,6 +126,17 @@ export class DuckDBDatabase<
|
|
|
123
126
|
});
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
executeBatches<T extends RowData = RowData>(
|
|
130
|
+
query: SQL,
|
|
131
|
+
options: ExecuteInBatchesOptions = {}
|
|
132
|
+
): AsyncGenerator<T[], void, void> {
|
|
133
|
+
return this.session.executeBatches<T>(query, options);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
executeArrow(query: SQL): Promise<unknown> {
|
|
137
|
+
return this.session.executeArrow(query);
|
|
138
|
+
}
|
|
139
|
+
|
|
126
140
|
override async transaction<T>(
|
|
127
141
|
transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
|
|
128
142
|
): Promise<T> {
|