@leonardovida-md/drizzle-neo-duckdb 1.0.3 → 1.1.1

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/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';
@@ -23,6 +24,13 @@ import { DuckDBDialect } from './dialect.ts';
23
24
  import { DuckDBSelectBuilder } from './select-builder.ts';
24
25
  import { aliasFields } from './sql/selection.ts';
25
26
  import type { ExecuteInBatchesOptions, RowData } from './client.ts';
27
+ import { closeClientConnection, isPool } from './client.ts';
28
+ import {
29
+ createDuckDBConnectionPool,
30
+ resolvePoolSize,
31
+ type DuckDBPoolConfig,
32
+ type PoolPreset,
33
+ } from './pool.ts';
26
34
 
27
35
  export interface PgDriverOptions {
28
36
  logger?: Logger;
@@ -50,18 +58,57 @@ export class DuckDBDriver {
50
58
  }
51
59
  }
52
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
+
53
69
  export interface DuckDBDrizzleConfig<
54
70
  TSchema extends Record<string, unknown> = Record<string, never>,
55
71
  > extends DrizzleConfig<TSchema> {
56
72
  rewriteArrays?: boolean;
57
73
  rejectStringArrayLiterals?: boolean;
74
+ /** Pool configuration. Use preset name, size config, or false to disable. */
75
+ pool?: DuckDBPoolConfig | PoolPreset | false;
58
76
  }
59
77
 
60
- export function drizzle<
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<
61
107
  TSchema extends Record<string, unknown> = Record<string, never>,
62
108
  >(
63
109
  client: DuckDBClientLike,
64
- config: DuckDBDrizzleConfig<TSchema> = {}
110
+ config: DuckDBDrizzleConfig<TSchema> = {},
111
+ instance?: DuckDBInstance
65
112
  ): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>> {
66
113
  const dialect = new DuckDBDialect();
67
114
 
@@ -89,10 +136,125 @@ export function drizzle<
89
136
  });
90
137
  const session = driver.createSession(schema);
91
138
 
92
- return new DuckDBDatabase(dialect, session, schema) as DuckDBDatabase<
93
- TSchema,
94
- ExtractTablesWithRelations<TSchema>
95
- >;
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);
96
258
  }
97
259
 
98
260
  export class DuckDBDatabase<
@@ -102,12 +264,46 @@ export class DuckDBDatabase<
102
264
  > extends PgDatabase<DuckDBQueryResultHKT, TFullSchema, TSchema> {
103
265
  static readonly [entityKind]: string = 'DuckDBDatabase';
104
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
+
105
273
  constructor(
106
274
  readonly dialect: DuckDBDialect,
107
275
  readonly session: DuckDBSession<TFullSchema, TSchema>,
108
- schema: RelationalSchemaConfig<TSchema> | undefined
276
+ schema: RelationalSchemaConfig<TSchema> | undefined,
277
+ client: DuckDBClientLike,
278
+ instance?: DuckDBInstance
109
279
  ) {
110
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
+ if (!isPool(this.$client)) {
294
+ await closeClientConnection(this.$client);
295
+ }
296
+ if (this.$instance) {
297
+ const maybeClosable = this.$instance as unknown as {
298
+ close?: () => Promise<void> | void;
299
+ closeSync?: () => void;
300
+ };
301
+ if (typeof maybeClosable.close === 'function') {
302
+ await maybeClosable.close();
303
+ } else if (typeof maybeClosable.closeSync === 'function') {
304
+ maybeClosable.closeSync();
305
+ }
306
+ }
111
307
  }
112
308
 
113
309
  select(): DuckDBSelectBuilder<undefined>;
@@ -119,6 +315,7 @@ export class DuckDBDatabase<
119
315
  ): DuckDBSelectBuilder<SelectedFields | undefined> {
120
316
  const selectedFields = fields ? aliasFields(fields) : undefined;
121
317
 
318
+ // Cast needed: DuckDBSession is compatible but types don't align exactly with PgSession
122
319
  return new DuckDBSelectBuilder({
123
320
  fields: selectedFields ?? undefined,
124
321
  session: this.session as unknown as PgSession<DuckDBQueryResultHKT>,
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
@@ -4,5 +4,6 @@ export * from './columns.ts';
4
4
  export * from './migrator.ts';
5
5
  export * from './introspect.ts';
6
6
  export * from './client.ts';
7
+ export * from './pool.ts';
7
8
  export * from './olap.ts';
8
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,
@@ -534,7 +535,7 @@ function emitColumn(
534
535
  return builder;
535
536
  }
536
537
 
537
- function buildDefault(defaultValue: string | null): string {
538
+ export function buildDefault(defaultValue: string | null): string {
538
539
  if (!defaultValue) {
539
540
  return '';
540
541
  }
@@ -649,6 +650,28 @@ function mapDuckDbType(
649
650
  return { builder: `doublePrecision(${columnName(column.name)})` };
650
651
  }
651
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
+
652
675
  if (upper.startsWith('CHAR(') || upper === 'CHAR') {
653
676
  imports.pgCore.add('char');
654
677
  const length = column.characterLength;
@@ -684,6 +707,22 @@ function mapDuckDbType(
684
707
  return { builder: `text(${columnName(column.name)}) /* JSON */` };
685
708
  }
686
709
 
710
+ if (upper.startsWith('ENUM')) {
711
+ imports.pgCore.add('text');
712
+ const enumLiteral = raw.replace(/^ENUM\s*/i, '').trim();
713
+ return {
714
+ builder: `text(${columnName(column.name)}) /* ENUM ${enumLiteral} */`,
715
+ };
716
+ }
717
+
718
+ if (upper.startsWith('UNION')) {
719
+ imports.pgCore.add('text');
720
+ const unionLiteral = raw.replace(/^UNION\s*/i, '').trim();
721
+ return {
722
+ builder: `text(${columnName(column.name)}) /* UNION ${unionLiteral} */`,
723
+ };
724
+ }
725
+
687
726
  if (upper === 'INET') {
688
727
  imports.local.add('duckDbInet');
689
728
  return { builder: `duckDbInet(${columnName(column.name)})` };
@@ -699,28 +738,6 @@ function mapDuckDbType(
699
738
  return { builder: `duckDbBlob(${columnName(column.name)})` };
700
739
  }
701
740
 
702
- const arrayMatch = /^(.*)\[(\d+)\]$/.exec(upper);
703
- if (arrayMatch) {
704
- imports.local.add('duckDbArray');
705
- const [, base, length] = arrayMatch;
706
- return {
707
- builder: `duckDbArray(${columnName(
708
- column.name
709
- )}, ${JSON.stringify(base)}, ${Number(length)})`,
710
- };
711
- }
712
-
713
- const listMatch = /^(.*)\[\]$/.exec(upper);
714
- if (listMatch) {
715
- imports.local.add('duckDbList');
716
- const [, base] = listMatch;
717
- return {
718
- builder: `duckDbList(${columnName(
719
- column.name
720
- )}, ${JSON.stringify(base)})`,
721
- };
722
- }
723
-
724
741
  if (upper.startsWith('STRUCT')) {
725
742
  imports.local.add('duckDbStruct');
726
743
  const inner = upper.replace(/^STRUCT\s*\(/i, '').replace(/\)$/, '');
@@ -787,15 +804,16 @@ function mapDuckDbType(
787
804
  }
788
805
 
789
806
  // Fallback: keep as text to avoid runtime failures.
807
+ // Unknown types are mapped to text with a comment indicating the original type.
790
808
  imports.pgCore.add('text');
791
809
  return {
792
810
  builder: `text(${columnName(
793
811
  column.name
794
- )}) /* TODO: verify type ${upper} */`,
812
+ )}) /* unsupported DuckDB type: ${upper} */`,
795
813
  };
796
814
  }
797
815
 
798
- function parseStructFields(
816
+ export function parseStructFields(
799
817
  inner: string
800
818
  ): Array<{ name: string; type: string }> {
801
819
  const result: Array<{ name: string; type: string }> = [];
@@ -812,7 +830,7 @@ function parseStructFields(
812
830
  return result;
813
831
  }
814
832
 
815
- function parseMapValue(raw: string): string {
833
+ export function parseMapValue(raw: string): string {
816
834
  const inner = raw.replace(/^MAP\(/i, '').replace(/\)$/, '');
817
835
  const parts = splitTopLevel(inner, ',');
818
836
  if (parts.length < 2) {
@@ -821,7 +839,7 @@ function parseMapValue(raw: string): string {
821
839
  return parts[1]?.trim() ?? 'TEXT';
822
840
  }
823
841
 
824
- function splitTopLevel(input: string, delimiter: string): string[] {
842
+ export function splitTopLevel(input: string, delimiter: string): string[] {
825
843
  const parts: string[] = [];
826
844
  let depth = 0;
827
845
  let current = '';
@@ -846,7 +864,7 @@ function tableKey(schema: string, table: string): string {
846
864
  return `${schema}.${table}`;
847
865
  }
848
866
 
849
- function toIdentifier(name: string): string {
867
+ export function toIdentifier(name: string): string {
850
868
  const cleaned = name.replace(/[^A-Za-z0-9_]/g, '_');
851
869
  const parts = cleaned.split('_').filter(Boolean);
852
870
  const base = parts
package/src/migrator.ts CHANGED
@@ -14,9 +14,9 @@ export async function migrate<TSchema extends Record<string, unknown>>(
14
14
 
15
15
  const migrations = readMigrationFiles(migrationConfig);
16
16
 
17
+ // Cast needed: Drizzle's internal PgSession type differs from exported type
17
18
  await db.dialect.migrate(
18
19
  migrations,
19
- // Need to work around omitted internal types from drizzle...
20
20
  db.session as unknown as PgSession,
21
21
  migrationConfig
22
22
  );
package/src/olap.ts CHANGED
@@ -169,6 +169,7 @@ export class OlapBuilder {
169
169
 
170
170
  Object.assign(selection, this.measureMap);
171
171
 
172
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Drizzle's query builder types don't allow reassignment after groupBy
172
173
  let query: any = this.db
173
174
  .select(selection as SelectedFields)
174
175
  .from(this.source!)