@leonardovida-md/drizzle-neo-duckdb 1.0.2 → 1.1.0
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 +51 -18
- package/dist/client.d.ts +19 -1
- package/dist/columns.d.ts +18 -10
- package/dist/driver.d.ts +37 -1
- package/dist/duckdb-introspect.mjs +382 -60
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.mjs +319 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +588 -72
- package/dist/introspect.d.ts +1 -0
- package/dist/olap.d.ts +46 -0
- package/dist/pool.d.ts +22 -0
- package/dist/session.d.ts +5 -0
- package/dist/sql/query-rewriters.d.ts +0 -1
- package/dist/utils.d.ts +1 -1
- package/dist/value-wrappers-core.d.ts +42 -0
- package/dist/value-wrappers.d.ts +8 -0
- package/package.json +12 -4
- package/src/bin/duckdb-introspect.ts +12 -3
- package/src/client.ts +178 -23
- package/src/columns.ts +65 -36
- package/src/dialect.ts +2 -2
- package/src/driver.ts +211 -13
- package/src/helpers.ts +18 -0
- package/src/index.ts +4 -0
- package/src/introspect.ts +39 -33
- package/src/migrator.ts +2 -4
- package/src/olap.ts +190 -0
- package/src/pool.ts +104 -0
- package/src/select-builder.ts +3 -7
- package/src/session.ts +123 -28
- package/src/sql/query-rewriters.ts +4 -54
- package/src/sql/result-mapper.ts +6 -6
- package/src/sql/selection.ts +2 -9
- package/src/utils.ts +1 -1
- package/src/value-wrappers-core.ts +156 -0
- package/src/value-wrappers.ts +155 -0
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-core.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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DuckDBInstance } from '@duckdb/node-api';
|
|
1
2
|
import { entityKind } from 'drizzle-orm/entity';
|
|
2
3
|
import type { Logger } from 'drizzle-orm/logger';
|
|
3
4
|
import { DefaultLogger } from 'drizzle-orm/logger';
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
type TablesRelationalConfig,
|
|
13
14
|
} from 'drizzle-orm/relations';
|
|
14
15
|
import { type DrizzleConfig } from 'drizzle-orm/utils';
|
|
16
|
+
import type { SQL } from 'drizzle-orm/sql/sql';
|
|
15
17
|
import type {
|
|
16
18
|
DuckDBClientLike,
|
|
17
19
|
DuckDBQueryResultHKT,
|
|
@@ -21,6 +23,14 @@ import { DuckDBSession } from './session.ts';
|
|
|
21
23
|
import { DuckDBDialect } from './dialect.ts';
|
|
22
24
|
import { DuckDBSelectBuilder } from './select-builder.ts';
|
|
23
25
|
import { aliasFields } from './sql/selection.ts';
|
|
26
|
+
import type { ExecuteInBatchesOptions, RowData } from './client.ts';
|
|
27
|
+
import { isPool } from './client.ts';
|
|
28
|
+
import {
|
|
29
|
+
createDuckDBConnectionPool,
|
|
30
|
+
resolvePoolSize,
|
|
31
|
+
type DuckDBPoolConfig,
|
|
32
|
+
type PoolPreset,
|
|
33
|
+
} from './pool.ts';
|
|
24
34
|
|
|
25
35
|
export interface PgDriverOptions {
|
|
26
36
|
logger?: Logger;
|
|
@@ -48,18 +58,57 @@ export class DuckDBDriver {
|
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
|
|
61
|
+
/** Connection configuration when using path-based connection */
|
|
62
|
+
export interface DuckDBConnectionConfig {
|
|
63
|
+
/** Database path: ':memory:', './file.duckdb', 'md:', 'md:database' */
|
|
64
|
+
path: string;
|
|
65
|
+
/** DuckDB instance options (e.g., motherduck_token) */
|
|
66
|
+
options?: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
export interface DuckDBDrizzleConfig<
|
|
52
|
-
TSchema extends Record<string, unknown> = Record<string, never
|
|
70
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
53
71
|
> extends DrizzleConfig<TSchema> {
|
|
54
72
|
rewriteArrays?: boolean;
|
|
55
73
|
rejectStringArrayLiterals?: boolean;
|
|
74
|
+
/** Pool configuration. Use preset name, size config, or false to disable. */
|
|
75
|
+
pool?: DuckDBPoolConfig | PoolPreset | false;
|
|
56
76
|
}
|
|
57
77
|
|
|
58
|
-
export
|
|
59
|
-
TSchema extends Record<string, unknown> = Record<string, never
|
|
78
|
+
export interface DuckDBDrizzleConfigWithConnection<
|
|
79
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
80
|
+
> extends DuckDBDrizzleConfig<TSchema> {
|
|
81
|
+
/** Connection string or config object */
|
|
82
|
+
connection: string | DuckDBConnectionConfig;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface DuckDBDrizzleConfigWithClient<
|
|
86
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
87
|
+
> extends DuckDBDrizzleConfig<TSchema> {
|
|
88
|
+
/** Explicit client (connection or pool) */
|
|
89
|
+
client: DuckDBClientLike;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Check if a value looks like a config object (not a client) */
|
|
93
|
+
function isConfigObject(data: unknown): data is Record<string, unknown> {
|
|
94
|
+
if (typeof data !== 'object' || data === null) return false;
|
|
95
|
+
if (data.constructor?.name !== 'Object') return false;
|
|
96
|
+
return (
|
|
97
|
+
'connection' in data ||
|
|
98
|
+
'client' in data ||
|
|
99
|
+
'pool' in data ||
|
|
100
|
+
'schema' in data ||
|
|
101
|
+
'logger' in data
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Internal: create database from a client (connection or pool) */
|
|
106
|
+
function createFromClient<
|
|
107
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
60
108
|
>(
|
|
61
109
|
client: DuckDBClientLike,
|
|
62
|
-
config: DuckDBDrizzleConfig<TSchema> = {}
|
|
110
|
+
config: DuckDBDrizzleConfig<TSchema> = {},
|
|
111
|
+
instance?: DuckDBInstance
|
|
63
112
|
): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>> {
|
|
64
113
|
const dialect = new DuckDBDialect();
|
|
65
114
|
|
|
@@ -87,35 +136,173 @@ export function drizzle<
|
|
|
87
136
|
});
|
|
88
137
|
const session = driver.createSession(schema);
|
|
89
138
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
139
|
+
const db = new DuckDBDatabase(dialect, session, schema, client, instance);
|
|
140
|
+
return db as DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Internal: create database from a connection string */
|
|
144
|
+
async function createFromConnectionString<
|
|
145
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
146
|
+
>(
|
|
147
|
+
path: string,
|
|
148
|
+
instanceOptions: Record<string, string> | undefined,
|
|
149
|
+
config: DuckDBDrizzleConfig<TSchema> = {}
|
|
150
|
+
): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>> {
|
|
151
|
+
const instance = await DuckDBInstance.create(path, instanceOptions);
|
|
152
|
+
const poolSize = resolvePoolSize(config.pool);
|
|
153
|
+
|
|
154
|
+
if (poolSize === false) {
|
|
155
|
+
const connection = await instance.connect();
|
|
156
|
+
return createFromClient(connection, config, instance);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const pool = createDuckDBConnectionPool(instance, { size: poolSize });
|
|
160
|
+
return createFromClient(pool, config, instance);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Overload 1: Connection string (async, auto-pools)
|
|
164
|
+
export function drizzle<
|
|
165
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
166
|
+
>(
|
|
167
|
+
connectionString: string
|
|
168
|
+
): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
|
|
169
|
+
|
|
170
|
+
// Overload 2: Connection string + config (async, auto-pools)
|
|
171
|
+
export function drizzle<
|
|
172
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
173
|
+
>(
|
|
174
|
+
connectionString: string,
|
|
175
|
+
config: DuckDBDrizzleConfig<TSchema>
|
|
176
|
+
): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
|
|
177
|
+
|
|
178
|
+
// Overload 3: Config with connection (async, auto-pools)
|
|
179
|
+
export function drizzle<
|
|
180
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
181
|
+
>(
|
|
182
|
+
config: DuckDBDrizzleConfigWithConnection<TSchema>
|
|
183
|
+
): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
|
|
184
|
+
|
|
185
|
+
// Overload 4: Config with explicit client (sync)
|
|
186
|
+
export function drizzle<
|
|
187
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
188
|
+
>(
|
|
189
|
+
config: DuckDBDrizzleConfigWithClient<TSchema>
|
|
190
|
+
): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
|
|
191
|
+
|
|
192
|
+
// Overload 5: Explicit client (sync, backward compatible)
|
|
193
|
+
export function drizzle<
|
|
194
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
195
|
+
>(
|
|
196
|
+
client: DuckDBClientLike,
|
|
197
|
+
config?: DuckDBDrizzleConfig<TSchema>
|
|
198
|
+
): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
|
|
199
|
+
|
|
200
|
+
// Implementation
|
|
201
|
+
export function drizzle<
|
|
202
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
203
|
+
>(
|
|
204
|
+
clientOrConfigOrPath:
|
|
205
|
+
| string
|
|
206
|
+
| DuckDBClientLike
|
|
207
|
+
| DuckDBDrizzleConfigWithConnection<TSchema>
|
|
208
|
+
| DuckDBDrizzleConfigWithClient<TSchema>,
|
|
209
|
+
config?: DuckDBDrizzleConfig<TSchema>
|
|
210
|
+
):
|
|
211
|
+
| DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>
|
|
212
|
+
| Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>> {
|
|
213
|
+
// String path -> async with auto-pool
|
|
214
|
+
if (typeof clientOrConfigOrPath === 'string') {
|
|
215
|
+
return createFromConnectionString(clientOrConfigOrPath, undefined, config);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Config object with connection or client
|
|
219
|
+
if (isConfigObject(clientOrConfigOrPath)) {
|
|
220
|
+
const configObj = clientOrConfigOrPath as
|
|
221
|
+
| DuckDBDrizzleConfigWithConnection<TSchema>
|
|
222
|
+
| DuckDBDrizzleConfigWithClient<TSchema>;
|
|
223
|
+
|
|
224
|
+
if ('connection' in configObj) {
|
|
225
|
+
const connConfig =
|
|
226
|
+
configObj as DuckDBDrizzleConfigWithConnection<TSchema>;
|
|
227
|
+
const { connection, ...restConfig } = connConfig;
|
|
228
|
+
if (typeof connection === 'string') {
|
|
229
|
+
return createFromConnectionString(
|
|
230
|
+
connection,
|
|
231
|
+
undefined,
|
|
232
|
+
restConfig as DuckDBDrizzleConfig<TSchema>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return createFromConnectionString(
|
|
236
|
+
connection.path,
|
|
237
|
+
connection.options,
|
|
238
|
+
restConfig as DuckDBDrizzleConfig<TSchema>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if ('client' in configObj) {
|
|
243
|
+
const clientConfig = configObj as DuckDBDrizzleConfigWithClient<TSchema>;
|
|
244
|
+
const { client: clientValue, ...restConfig } = clientConfig;
|
|
245
|
+
return createFromClient(
|
|
246
|
+
clientValue,
|
|
247
|
+
restConfig as DuckDBDrizzleConfig<TSchema>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
throw new Error(
|
|
252
|
+
'Invalid drizzle config: either connection or client must be provided'
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Direct client (backward compatible)
|
|
257
|
+
return createFromClient(clientOrConfigOrPath as DuckDBClientLike, config);
|
|
94
258
|
}
|
|
95
259
|
|
|
96
260
|
export class DuckDBDatabase<
|
|
97
261
|
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
98
|
-
TSchema extends
|
|
262
|
+
TSchema extends
|
|
263
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
99
264
|
> extends PgDatabase<DuckDBQueryResultHKT, TFullSchema, TSchema> {
|
|
100
265
|
static readonly [entityKind]: string = 'DuckDBDatabase';
|
|
101
266
|
|
|
267
|
+
/** The underlying connection or pool */
|
|
268
|
+
readonly $client: DuckDBClientLike;
|
|
269
|
+
|
|
270
|
+
/** The DuckDB instance (when created from connection string) */
|
|
271
|
+
readonly $instance?: DuckDBInstance;
|
|
272
|
+
|
|
102
273
|
constructor(
|
|
103
274
|
readonly dialect: DuckDBDialect,
|
|
104
275
|
readonly session: DuckDBSession<TFullSchema, TSchema>,
|
|
105
|
-
schema: RelationalSchemaConfig<TSchema> | undefined
|
|
276
|
+
schema: RelationalSchemaConfig<TSchema> | undefined,
|
|
277
|
+
client: DuckDBClientLike,
|
|
278
|
+
instance?: DuckDBInstance
|
|
106
279
|
) {
|
|
107
280
|
super(dialect, session, schema);
|
|
281
|
+
this.$client = client;
|
|
282
|
+
this.$instance = instance;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Close the database connection pool and instance.
|
|
287
|
+
* Should be called when shutting down the application.
|
|
288
|
+
*/
|
|
289
|
+
async close(): Promise<void> {
|
|
290
|
+
if (isPool(this.$client) && this.$client.close) {
|
|
291
|
+
await this.$client.close();
|
|
292
|
+
}
|
|
293
|
+
// Note: DuckDBInstance doesn't have a close method in the current API
|
|
108
294
|
}
|
|
109
295
|
|
|
110
296
|
select(): DuckDBSelectBuilder<undefined>;
|
|
111
297
|
select<TSelection extends SelectedFields>(
|
|
112
298
|
fields: TSelection
|
|
113
299
|
): DuckDBSelectBuilder<TSelection>;
|
|
114
|
-
select(
|
|
115
|
-
SelectedFields
|
|
116
|
-
> {
|
|
300
|
+
select(
|
|
301
|
+
fields?: SelectedFields
|
|
302
|
+
): DuckDBSelectBuilder<SelectedFields | undefined> {
|
|
117
303
|
const selectedFields = fields ? aliasFields(fields) : undefined;
|
|
118
304
|
|
|
305
|
+
// Cast needed: DuckDBSession is compatible but types don't align exactly with PgSession
|
|
119
306
|
return new DuckDBSelectBuilder({
|
|
120
307
|
fields: selectedFields ?? undefined,
|
|
121
308
|
session: this.session as unknown as PgSession<DuckDBQueryResultHKT>,
|
|
@@ -123,6 +310,17 @@ export class DuckDBDatabase<
|
|
|
123
310
|
});
|
|
124
311
|
}
|
|
125
312
|
|
|
313
|
+
executeBatches<T extends RowData = RowData>(
|
|
314
|
+
query: SQL,
|
|
315
|
+
options: ExecuteInBatchesOptions = {}
|
|
316
|
+
): AsyncGenerator<T[], void, void> {
|
|
317
|
+
return this.session.executeBatches<T>(query, options);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
executeArrow(query: SQL): Promise<unknown> {
|
|
321
|
+
return this.session.executeArrow(query);
|
|
322
|
+
}
|
|
323
|
+
|
|
126
324
|
override async transaction<T>(
|
|
127
325
|
transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
|
|
128
326
|
): Promise<T> {
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Client-safe entrypoint exposing schema builder utilities without pulling
|
|
2
|
+
// the DuckDB Node API bindings. Intended for generated schemas and browser use.
|
|
3
|
+
export {
|
|
4
|
+
duckDbList,
|
|
5
|
+
duckDbArray,
|
|
6
|
+
duckDbMap,
|
|
7
|
+
duckDbStruct,
|
|
8
|
+
duckDbJson,
|
|
9
|
+
duckDbBlob,
|
|
10
|
+
duckDbInet,
|
|
11
|
+
duckDbInterval,
|
|
12
|
+
duckDbTimestamp,
|
|
13
|
+
duckDbDate,
|
|
14
|
+
duckDbTime,
|
|
15
|
+
duckDbArrayContains,
|
|
16
|
+
duckDbArrayContained,
|
|
17
|
+
duckDbArrayOverlaps,
|
|
18
|
+
} from './columns.ts';
|
package/src/index.ts
CHANGED
package/src/introspect.ts
CHANGED
|
@@ -108,7 +108,8 @@ type ImportBuckets = {
|
|
|
108
108
|
local: Set<string>;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
const DEFAULT_IMPORT_BASE =
|
|
111
|
+
export const DEFAULT_IMPORT_BASE =
|
|
112
|
+
'@leonardovida-md/drizzle-neo-duckdb/helpers';
|
|
112
113
|
|
|
113
114
|
export async function introspect(
|
|
114
115
|
db: DuckDBDatabase,
|
|
@@ -466,17 +467,17 @@ function emitConstraints(
|
|
|
466
467
|
.map((col) => `t.${toIdentifier(col)}`)
|
|
467
468
|
.join(', ')}], name: ${JSON.stringify(constraint.name)} })`
|
|
468
469
|
);
|
|
469
|
-
} else if (
|
|
470
|
-
constraint.type === 'UNIQUE' &&
|
|
471
|
-
constraint.columns.length > 1
|
|
472
|
-
) {
|
|
470
|
+
} else if (constraint.type === 'UNIQUE' && constraint.columns.length > 1) {
|
|
473
471
|
imports.pgCore.add('unique');
|
|
474
472
|
entries.push(
|
|
475
473
|
`${key}: unique(${JSON.stringify(constraint.name)}).on(${constraint.columns
|
|
476
474
|
.map((col) => `t.${toIdentifier(col)}`)
|
|
477
475
|
.join(', ')})`
|
|
478
476
|
);
|
|
479
|
-
} else if (
|
|
477
|
+
} else if (
|
|
478
|
+
constraint.type === 'FOREIGN KEY' &&
|
|
479
|
+
constraint.referencedTable
|
|
480
|
+
) {
|
|
480
481
|
imports.pgCore.add('foreignKey');
|
|
481
482
|
const targetTable = toIdentifier(constraint.referencedTable.name);
|
|
482
483
|
entries.push(
|
|
@@ -546,7 +547,10 @@ function buildDefault(defaultValue: string | null): string {
|
|
|
546
547
|
if (/^nextval\(/i.test(trimmed)) {
|
|
547
548
|
return `.default(sql\`${trimmed}\`)`;
|
|
548
549
|
}
|
|
549
|
-
if (
|
|
550
|
+
if (
|
|
551
|
+
/^current_timestamp(?:\(\))?$/i.test(trimmed) ||
|
|
552
|
+
/^now\(\)$/i.test(trimmed)
|
|
553
|
+
) {
|
|
550
554
|
return `.defaultNow()`;
|
|
551
555
|
}
|
|
552
556
|
if (trimmed === 'true' || trimmed === 'false') {
|
|
@@ -604,7 +608,11 @@ function mapDuckDbType(
|
|
|
604
608
|
|
|
605
609
|
if (upper === 'BIGINT' || upper === 'INT8' || upper === 'UBIGINT') {
|
|
606
610
|
imports.pgCore.add('bigint');
|
|
607
|
-
|
|
611
|
+
// Drizzle's bigint helper requires an explicit mode. Default to 'number'
|
|
612
|
+
// to mirror DuckDB's typical 64-bit integer behavior in JS.
|
|
613
|
+
return {
|
|
614
|
+
builder: `bigint(${columnName(column.name)}, { mode: 'number' })`,
|
|
615
|
+
};
|
|
608
616
|
}
|
|
609
617
|
|
|
610
618
|
const decimalMatch = /^DECIMAL\((\d+),(\d+)\)/i.exec(upper);
|
|
@@ -642,6 +650,28 @@ function mapDuckDbType(
|
|
|
642
650
|
return { builder: `doublePrecision(${columnName(column.name)})` };
|
|
643
651
|
}
|
|
644
652
|
|
|
653
|
+
const arrayMatch = /^(.*)\[(\d+)\]$/.exec(upper);
|
|
654
|
+
if (arrayMatch) {
|
|
655
|
+
imports.local.add('duckDbArray');
|
|
656
|
+
const [, base, length] = arrayMatch;
|
|
657
|
+
return {
|
|
658
|
+
builder: `duckDbArray(${columnName(
|
|
659
|
+
column.name
|
|
660
|
+
)}, ${JSON.stringify(base)}, ${Number(length)})`,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const listMatch = /^(.*)\[\]$/.exec(upper);
|
|
665
|
+
if (listMatch) {
|
|
666
|
+
imports.local.add('duckDbList');
|
|
667
|
+
const [, base] = listMatch;
|
|
668
|
+
return {
|
|
669
|
+
builder: `duckDbList(${columnName(
|
|
670
|
+
column.name
|
|
671
|
+
)}, ${JSON.stringify(base)})`,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
645
675
|
if (upper.startsWith('CHAR(') || upper === 'CHAR') {
|
|
646
676
|
imports.pgCore.add('char');
|
|
647
677
|
const length = column.characterLength;
|
|
@@ -692,28 +722,6 @@ function mapDuckDbType(
|
|
|
692
722
|
return { builder: `duckDbBlob(${columnName(column.name)})` };
|
|
693
723
|
}
|
|
694
724
|
|
|
695
|
-
const arrayMatch = /^(.*)\[(\d+)\]$/.exec(upper);
|
|
696
|
-
if (arrayMatch) {
|
|
697
|
-
imports.local.add('duckDbArray');
|
|
698
|
-
const [, base, length] = arrayMatch;
|
|
699
|
-
return {
|
|
700
|
-
builder: `duckDbArray(${columnName(
|
|
701
|
-
column.name
|
|
702
|
-
)}, ${JSON.stringify(base)}, ${Number(length)})`,
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const listMatch = /^(.*)\[\]$/.exec(upper);
|
|
707
|
-
if (listMatch) {
|
|
708
|
-
imports.local.add('duckDbList');
|
|
709
|
-
const [, base] = listMatch;
|
|
710
|
-
return {
|
|
711
|
-
builder: `duckDbList(${columnName(
|
|
712
|
-
column.name
|
|
713
|
-
)}, ${JSON.stringify(base)})`,
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
|
|
717
725
|
if (upper.startsWith('STRUCT')) {
|
|
718
726
|
imports.local.add('duckDbStruct');
|
|
719
727
|
const inner = upper.replace(/^STRUCT\s*\(/i, '').replace(/\)$/, '');
|
|
@@ -894,9 +902,7 @@ function renderImports(imports: ImportBuckets, importBasePath: string): string {
|
|
|
894
902
|
const pgCore = [...imports.pgCore];
|
|
895
903
|
if (pgCore.length) {
|
|
896
904
|
lines.push(
|
|
897
|
-
`import { ${pgCore
|
|
898
|
-
.sort()
|
|
899
|
-
.join(', ')} } from 'drizzle-orm/pg-core';`
|
|
905
|
+
`import { ${pgCore.sort().join(', ')} } from 'drizzle-orm/pg-core';`
|
|
900
906
|
);
|
|
901
907
|
}
|
|
902
908
|
|
package/src/migrator.ts
CHANGED
|
@@ -10,15 +10,13 @@ export async function migrate<TSchema extends Record<string, unknown>>(
|
|
|
10
10
|
config: DuckDbMigrationConfig
|
|
11
11
|
) {
|
|
12
12
|
const migrationConfig: MigrationConfig =
|
|
13
|
-
typeof config === 'string'
|
|
14
|
-
? { migrationsFolder: config }
|
|
15
|
-
: config;
|
|
13
|
+
typeof config === 'string' ? { migrationsFolder: config } : config;
|
|
16
14
|
|
|
17
15
|
const migrations = readMigrationFiles(migrationConfig);
|
|
18
16
|
|
|
17
|
+
// Cast needed: Drizzle's internal PgSession type differs from exported type
|
|
19
18
|
await db.dialect.migrate(
|
|
20
19
|
migrations,
|
|
21
|
-
// Need to work around omitted internal types from drizzle...
|
|
22
20
|
db.session as unknown as PgSession,
|
|
23
21
|
migrationConfig
|
|
24
22
|
);
|