@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/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' ? value : JSON.stringify(value) ?? String(value);
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(value: Record<string, unknown>, valueType?: string): SQL {
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<{ data: TData[]; driverData: SQL | unknown[] | string }>({
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 buildListLiteral(value, elementType);
182
+ toDriver(value: TData[]): ListValueWrapper {
183
+ return wrapList(value, elementType);
164
184
  },
165
- fromDriver(value: unknown[] | string | SQL): TData[] {
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<{ data: TData[]; driverData: SQL | unknown[] | string }>({
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 buildListLiteral(value, elementType);
213
+ toDriver(value: TData[]): ArrayValueWrapper {
214
+ return wrapArray(value, elementType, fixedLength);
192
215
  },
193
- fromDriver(value: unknown[] | string | SQL): TData[] {
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
- dataType() {
234
+ customType<{ data: TData; driverData: MapValueWrapper | TData }>({
235
+ dataType() {
213
236
  return `MAP (STRING, ${valueType})`;
214
237
  },
215
- toDriver(value: TData) {
216
- return buildMapLiteral(value, valueType);
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
- return JSON.stringify(value ?? null);
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
- const hexString = value.toString('hex');
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
- const str =
378
- value instanceof Date
379
- ? value.toISOString().slice(0, 10)
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
- migrationsTable
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 function drizzle<
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
- return new DuckDBDatabase(dialect, session, schema) as DuckDBDatabase<
91
- TSchema,
92
- ExtractTablesWithRelations<TSchema>
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 TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>
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(fields?: SelectedFields): DuckDBSelectBuilder<
115
- SelectedFields | undefined
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
@@ -3,3 +3,7 @@ export * from './session.ts';
3
3
  export * from './columns.ts';
4
4
  export * from './migrator.ts';
5
5
  export * from './introspect.ts';
6
+ export * from './client.ts';
7
+ export * from './pool.ts';
8
+ export * from './olap.ts';
9
+ export * from './value-wrappers.ts';
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 = '@leonardovida-md/drizzle-neo-duckdb';
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 (constraint.type === 'FOREIGN KEY' && constraint.referencedTable) {
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 (/^current_timestamp(?:\(\))?$/i.test(trimmed) || /^now\(\)$/i.test(trimmed)) {
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
- return { builder: `bigint(${columnName(column.name)})` };
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
  );