@duckdbfan/drizzle-duckdb 0.0.7 → 1.3.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.
Files changed (55) hide show
  1. package/README.md +344 -62
  2. package/dist/bin/duckdb-introspect.d.ts +2 -0
  3. package/dist/client.d.ts +42 -0
  4. package/dist/columns.d.ts +100 -9
  5. package/dist/dialect.d.ts +27 -2
  6. package/dist/driver.d.ts +53 -37
  7. package/dist/duckdb-introspect.mjs +2890 -0
  8. package/dist/helpers.d.ts +1 -0
  9. package/dist/helpers.mjs +360 -0
  10. package/dist/index.d.ts +7 -0
  11. package/dist/index.mjs +3015 -228
  12. package/dist/introspect.d.ts +74 -0
  13. package/dist/migrator.d.ts +3 -2
  14. package/dist/olap.d.ts +46 -0
  15. package/dist/operators.d.ts +8 -0
  16. package/dist/options.d.ts +7 -0
  17. package/dist/pool.d.ts +30 -0
  18. package/dist/select-builder.d.ts +31 -0
  19. package/dist/session.d.ts +33 -8
  20. package/dist/sql/ast-transformer.d.ts +33 -0
  21. package/dist/sql/result-mapper.d.ts +9 -0
  22. package/dist/sql/selection.d.ts +2 -0
  23. package/dist/sql/visitors/array-operators.d.ts +5 -0
  24. package/dist/sql/visitors/column-qualifier.d.ts +10 -0
  25. package/dist/sql/visitors/generate-series-alias.d.ts +13 -0
  26. package/dist/sql/visitors/union-with-hoister.d.ts +11 -0
  27. package/dist/utils.d.ts +2 -5
  28. package/dist/value-wrappers-core.d.ts +42 -0
  29. package/dist/value-wrappers.d.ts +8 -0
  30. package/package.json +53 -16
  31. package/src/bin/duckdb-introspect.ts +181 -0
  32. package/src/client.ts +528 -0
  33. package/src/columns.ts +420 -65
  34. package/src/dialect.ts +111 -15
  35. package/src/driver.ts +266 -180
  36. package/src/helpers.ts +18 -0
  37. package/src/index.ts +8 -1
  38. package/src/introspect.ts +935 -0
  39. package/src/migrator.ts +10 -5
  40. package/src/olap.ts +190 -0
  41. package/src/operators.ts +27 -0
  42. package/src/options.ts +25 -0
  43. package/src/pool.ts +274 -0
  44. package/src/select-builder.ts +110 -0
  45. package/src/session.ts +306 -66
  46. package/src/sql/ast-transformer.ts +170 -0
  47. package/src/sql/result-mapper.ts +303 -0
  48. package/src/sql/selection.ts +60 -0
  49. package/src/sql/visitors/array-operators.ts +214 -0
  50. package/src/sql/visitors/column-qualifier.ts +586 -0
  51. package/src/sql/visitors/generate-series-alias.ts +291 -0
  52. package/src/sql/visitors/union-with-hoister.ts +106 -0
  53. package/src/utils.ts +2 -222
  54. package/src/value-wrappers-core.ts +168 -0
  55. package/src/value-wrappers.ts +165 -0
@@ -0,0 +1,935 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import type { RowData } from './client.ts';
3
+ import type { DuckDBDatabase } from './driver.ts';
4
+
5
+ const SYSTEM_SCHEMAS = new Set(['information_schema', 'pg_catalog']);
6
+
7
+ export interface IntrospectOptions {
8
+ /**
9
+ * Database/catalog to introspect. If not specified, uses the current database
10
+ * (via `SELECT current_database()`). This prevents returning tables from all
11
+ * attached databases in MotherDuck workspaces.
12
+ */
13
+ database?: string;
14
+ /**
15
+ * When true, introspects all attached databases instead of just the current one.
16
+ * Ignored if `database` is explicitly set.
17
+ * @default false
18
+ */
19
+ allDatabases?: boolean;
20
+ schemas?: string[];
21
+ includeViews?: boolean;
22
+ useCustomTimeTypes?: boolean;
23
+ mapJsonAsDuckDbJson?: boolean;
24
+ importBasePath?: string;
25
+ }
26
+
27
+ interface DuckDbTableRow extends RowData {
28
+ schema_name: string;
29
+ table_name: string;
30
+ table_type: string;
31
+ }
32
+
33
+ interface DuckDbColumnRow extends RowData {
34
+ schema_name: string;
35
+ table_name: string;
36
+ column_name: string;
37
+ column_index: number;
38
+ column_default: string | null;
39
+ is_nullable: boolean;
40
+ data_type: string;
41
+ character_maximum_length: number | null;
42
+ numeric_precision: number | null;
43
+ numeric_scale: number | null;
44
+ internal: boolean | null;
45
+ }
46
+
47
+ interface DuckDbConstraintRow extends RowData {
48
+ schema_name: string;
49
+ table_name: string;
50
+ constraint_name: string;
51
+ constraint_type: string;
52
+ constraint_text: string | null;
53
+ constraint_column_names: string[] | null;
54
+ referenced_table: string | null;
55
+ referenced_column_names: string[] | null;
56
+ }
57
+
58
+ interface DuckDbIndexRow extends RowData {
59
+ schema_name: string;
60
+ table_name: string;
61
+ index_name: string;
62
+ is_unique: boolean | null;
63
+ expressions: string | null;
64
+ }
65
+
66
+ export interface IntrospectedColumn {
67
+ name: string;
68
+ dataType: string;
69
+ columnDefault: string | null;
70
+ nullable: boolean;
71
+ characterLength: number | null;
72
+ numericPrecision: number | null;
73
+ numericScale: number | null;
74
+ }
75
+
76
+ export interface IntrospectedConstraint {
77
+ name: string;
78
+ type: string;
79
+ columns: string[];
80
+ referencedTable?: {
81
+ name: string;
82
+ schema: string;
83
+ columns: string[];
84
+ };
85
+ rawExpression?: string | null;
86
+ }
87
+
88
+ export interface IntrospectedTable {
89
+ schema: string;
90
+ name: string;
91
+ kind: 'table' | 'view';
92
+ columns: IntrospectedColumn[];
93
+ constraints: IntrospectedConstraint[];
94
+ indexes: DuckDbIndexRow[];
95
+ }
96
+
97
+ export interface IntrospectResult {
98
+ files: {
99
+ schemaTs: string;
100
+ metaJson: IntrospectedTable[];
101
+ relationsTs?: string;
102
+ };
103
+ }
104
+
105
+ type ImportBuckets = {
106
+ drizzle: Set<string>;
107
+ pgCore: Set<string>;
108
+ local: Set<string>;
109
+ };
110
+
111
+ export const DEFAULT_IMPORT_BASE =
112
+ '@leonardovida-md/drizzle-neo-duckdb/helpers';
113
+
114
+ export async function introspect(
115
+ db: DuckDBDatabase,
116
+ opts: IntrospectOptions = {}
117
+ ): Promise<IntrospectResult> {
118
+ const database = await resolveDatabase(db, opts.database, opts.allDatabases);
119
+ const schemas = await resolveSchemas(db, database, opts.schemas);
120
+ const includeViews = opts.includeViews ?? false;
121
+
122
+ const tables = await loadTables(db, database, schemas, includeViews);
123
+ const columns = await loadColumns(db, database, schemas);
124
+ const constraints = await loadConstraints(db, database, schemas);
125
+ const indexes = await loadIndexes(db, database, schemas);
126
+
127
+ const grouped = buildTables(tables, columns, constraints, indexes);
128
+
129
+ const schemaTs = emitSchema(grouped, {
130
+ useCustomTimeTypes: opts.useCustomTimeTypes ?? true,
131
+ mapJsonAsDuckDbJson: opts.mapJsonAsDuckDbJson ?? true,
132
+ importBasePath: opts.importBasePath ?? DEFAULT_IMPORT_BASE,
133
+ });
134
+
135
+ return {
136
+ files: {
137
+ schemaTs,
138
+ metaJson: grouped,
139
+ },
140
+ };
141
+ }
142
+
143
+ async function resolveDatabase(
144
+ db: DuckDBDatabase,
145
+ targetDatabase?: string,
146
+ allDatabases?: boolean
147
+ ): Promise<string | null> {
148
+ if (allDatabases) {
149
+ return null;
150
+ }
151
+ if (targetDatabase) {
152
+ return targetDatabase;
153
+ }
154
+
155
+ const rows = await db.execute<{ current_database: string }>(
156
+ sql`SELECT current_database() as current_database`
157
+ );
158
+ return rows[0]?.current_database ?? null;
159
+ }
160
+
161
+ async function resolveSchemas(
162
+ db: DuckDBDatabase,
163
+ database: string | null,
164
+ targetSchemas?: string[]
165
+ ): Promise<string[]> {
166
+ if (targetSchemas?.length) {
167
+ return targetSchemas;
168
+ }
169
+
170
+ const databaseFilter = database
171
+ ? sql`catalog_name = ${database}`
172
+ : sql`1 = 1`;
173
+
174
+ const rows = await db.execute<{ schema_name: string }>(
175
+ sql`SELECT schema_name FROM information_schema.schemata WHERE ${databaseFilter}`
176
+ );
177
+
178
+ return rows
179
+ .map((row) => row.schema_name)
180
+ .filter((name) => !SYSTEM_SCHEMAS.has(name));
181
+ }
182
+
183
+ async function loadTables(
184
+ db: DuckDBDatabase,
185
+ database: string | null,
186
+ schemas: string[],
187
+ includeViews: boolean
188
+ ): Promise<DuckDbTableRow[]> {
189
+ const schemaFragments = schemas.map((schema) => sql`${schema}`);
190
+ const databaseFilter = database
191
+ ? sql`table_catalog = ${database}`
192
+ : sql`1 = 1`;
193
+
194
+ return await db.execute<DuckDbTableRow>(
195
+ sql`
196
+ SELECT table_schema as schema_name, table_name, table_type
197
+ FROM information_schema.tables
198
+ WHERE ${databaseFilter}
199
+ AND table_schema IN (${sql.join(schemaFragments, sql.raw(', '))})
200
+ AND ${includeViews ? sql`1 = 1` : sql`table_type = 'BASE TABLE'`}
201
+ ORDER BY table_schema, table_name
202
+ `
203
+ );
204
+ }
205
+
206
+ async function loadColumns(
207
+ db: DuckDBDatabase,
208
+ database: string | null,
209
+ schemas: string[]
210
+ ): Promise<DuckDbColumnRow[]> {
211
+ const schemaFragments = schemas.map((schema) => sql`${schema}`);
212
+ const databaseFilter = database
213
+ ? sql`database_name = ${database}`
214
+ : sql`1 = 1`;
215
+
216
+ return await db.execute<DuckDbColumnRow>(
217
+ sql`
218
+ SELECT
219
+ schema_name,
220
+ table_name,
221
+ column_name,
222
+ column_index,
223
+ column_default,
224
+ is_nullable,
225
+ data_type,
226
+ character_maximum_length,
227
+ numeric_precision,
228
+ numeric_scale,
229
+ internal
230
+ FROM duckdb_columns()
231
+ WHERE ${databaseFilter}
232
+ AND schema_name IN (${sql.join(schemaFragments, sql.raw(', '))})
233
+ ORDER BY schema_name, table_name, column_index
234
+ `
235
+ );
236
+ }
237
+
238
+ async function loadConstraints(
239
+ db: DuckDBDatabase,
240
+ database: string | null,
241
+ schemas: string[]
242
+ ): Promise<DuckDbConstraintRow[]> {
243
+ const schemaFragments = schemas.map((schema) => sql`${schema}`);
244
+ const databaseFilter = database
245
+ ? sql`database_name = ${database}`
246
+ : sql`1 = 1`;
247
+
248
+ return await db.execute<DuckDbConstraintRow>(
249
+ sql`
250
+ SELECT
251
+ schema_name,
252
+ table_name,
253
+ constraint_name,
254
+ constraint_type,
255
+ constraint_text,
256
+ constraint_column_names,
257
+ referenced_table,
258
+ referenced_column_names
259
+ FROM duckdb_constraints()
260
+ WHERE ${databaseFilter}
261
+ AND schema_name IN (${sql.join(schemaFragments, sql.raw(', '))})
262
+ ORDER BY schema_name, table_name, constraint_index
263
+ `
264
+ );
265
+ }
266
+
267
+ async function loadIndexes(
268
+ db: DuckDBDatabase,
269
+ database: string | null,
270
+ schemas: string[]
271
+ ): Promise<DuckDbIndexRow[]> {
272
+ const schemaFragments = schemas.map((schema) => sql`${schema}`);
273
+ const databaseFilter = database
274
+ ? sql`database_name = ${database}`
275
+ : sql`1 = 1`;
276
+
277
+ return await db.execute<DuckDbIndexRow>(
278
+ sql`
279
+ SELECT
280
+ schema_name,
281
+ table_name,
282
+ index_name,
283
+ is_unique,
284
+ expressions
285
+ FROM duckdb_indexes()
286
+ WHERE ${databaseFilter}
287
+ AND schema_name IN (${sql.join(schemaFragments, sql.raw(', '))})
288
+ ORDER BY schema_name, table_name, index_name
289
+ `
290
+ );
291
+ }
292
+
293
+ function buildTables(
294
+ tables: DuckDbTableRow[],
295
+ columns: DuckDbColumnRow[],
296
+ constraints: DuckDbConstraintRow[],
297
+ indexes: DuckDbIndexRow[]
298
+ ): IntrospectedTable[] {
299
+ const byTable: Record<string, IntrospectedTable> = {};
300
+ for (const table of tables) {
301
+ const key = tableKey(table.schema_name, table.table_name);
302
+ byTable[key] = {
303
+ schema: table.schema_name,
304
+ name: table.table_name,
305
+ kind: table.table_type === 'VIEW' ? 'view' : 'table',
306
+ columns: [],
307
+ constraints: [],
308
+ indexes: [],
309
+ };
310
+ }
311
+
312
+ for (const column of columns) {
313
+ if (column.internal) {
314
+ continue;
315
+ }
316
+ const key = tableKey(column.schema_name, column.table_name);
317
+ const table = byTable[key];
318
+ if (!table) {
319
+ continue;
320
+ }
321
+ table.columns.push({
322
+ name: column.column_name,
323
+ dataType: column.data_type,
324
+ columnDefault: column.column_default,
325
+ nullable: column.is_nullable,
326
+ characterLength: column.character_maximum_length,
327
+ numericPrecision: column.numeric_precision,
328
+ numericScale: column.numeric_scale,
329
+ });
330
+ }
331
+
332
+ for (const constraint of constraints) {
333
+ const key = tableKey(constraint.schema_name, constraint.table_name);
334
+ const table = byTable[key];
335
+ if (!table) {
336
+ continue;
337
+ }
338
+ if (!constraint.constraint_column_names?.length) {
339
+ continue;
340
+ }
341
+ table.constraints.push({
342
+ name: constraint.constraint_name,
343
+ type: constraint.constraint_type,
344
+ columns: constraint.constraint_column_names ?? [],
345
+ referencedTable:
346
+ constraint.referenced_table && constraint.referenced_column_names
347
+ ? {
348
+ schema: constraint.schema_name,
349
+ name: constraint.referenced_table,
350
+ columns: constraint.referenced_column_names,
351
+ }
352
+ : undefined,
353
+ rawExpression: constraint.constraint_text,
354
+ });
355
+ }
356
+
357
+ for (const index of indexes) {
358
+ const key = tableKey(index.schema_name, index.table_name);
359
+ const table = byTable[key];
360
+ if (!table) {
361
+ continue;
362
+ }
363
+ table.indexes.push(index);
364
+ }
365
+
366
+ return Object.values(byTable);
367
+ }
368
+
369
+ interface EmitOptions {
370
+ useCustomTimeTypes: boolean;
371
+ mapJsonAsDuckDbJson: boolean;
372
+ importBasePath: string;
373
+ }
374
+
375
+ function emitSchema(
376
+ catalog: IntrospectedTable[],
377
+ options: EmitOptions
378
+ ): string {
379
+ const imports: ImportBuckets = {
380
+ drizzle: new Set(),
381
+ pgCore: new Set(),
382
+ local: new Set(),
383
+ };
384
+
385
+ imports.pgCore.add('pgSchema');
386
+
387
+ const sorted = [...catalog].sort((a, b) =>
388
+ a.schema === b.schema
389
+ ? a.name.localeCompare(b.name)
390
+ : a.schema.localeCompare(b.schema)
391
+ );
392
+
393
+ const lines: string[] = [];
394
+
395
+ for (const schema of uniqueSchemas(sorted)) {
396
+ imports.pgCore.add('pgSchema');
397
+ const schemaVar = toSchemaIdentifier(schema);
398
+ lines.push(
399
+ `export const ${schemaVar} = pgSchema(${JSON.stringify(schema)});`,
400
+ ''
401
+ );
402
+
403
+ const tables = sorted.filter((table) => table.schema === schema);
404
+ for (const table of tables) {
405
+ lines.push(...emitTable(schemaVar, table, imports, options));
406
+ lines.push('');
407
+ }
408
+ }
409
+
410
+ const importsBlock = renderImports(imports, options.importBasePath);
411
+ return [importsBlock, ...lines].join('\n').trim() + '\n';
412
+ }
413
+
414
+ function emitTable(
415
+ schemaVar: string,
416
+ table: IntrospectedTable,
417
+ imports: ImportBuckets,
418
+ options: EmitOptions
419
+ ): string[] {
420
+ const tableVar = toIdentifier(table.name);
421
+ const columnLines: string[] = [];
422
+ for (const column of table.columns) {
423
+ columnLines.push(
424
+ ` ${columnProperty(column.name)}: ${emitColumn(
425
+ column,
426
+ imports,
427
+ options
428
+ )},`
429
+ );
430
+ }
431
+
432
+ const constraintBlock = emitConstraints(table, imports);
433
+
434
+ const tableLines: string[] = [];
435
+ tableLines.push(
436
+ `export const ${tableVar} = ${schemaVar}.table(${JSON.stringify(
437
+ table.name
438
+ )}, {`
439
+ );
440
+ tableLines.push(...columnLines);
441
+ tableLines.push(
442
+ `}${constraintBlock ? ',' : ''}${constraintBlock ? ` ${constraintBlock}` : ''});`
443
+ );
444
+
445
+ return tableLines;
446
+ }
447
+
448
+ function emitConstraints(
449
+ table: IntrospectedTable,
450
+ imports: ImportBuckets
451
+ ): string {
452
+ const constraints = table.constraints.filter((constraint) =>
453
+ ['PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE'].includes(constraint.type)
454
+ );
455
+ if (!constraints.length) {
456
+ return '';
457
+ }
458
+
459
+ const entries: string[] = [];
460
+
461
+ for (const constraint of constraints) {
462
+ const key = toIdentifier(constraint.name || `${table.name}_constraint`);
463
+ if (constraint.type === 'PRIMARY KEY') {
464
+ imports.pgCore.add('primaryKey');
465
+ entries.push(
466
+ `${key}: primaryKey({ columns: [${constraint.columns
467
+ .map((col) => `t.${toIdentifier(col)}`)
468
+ .join(', ')}], name: ${JSON.stringify(constraint.name)} })`
469
+ );
470
+ } else if (constraint.type === 'UNIQUE' && constraint.columns.length > 1) {
471
+ imports.pgCore.add('unique');
472
+ entries.push(
473
+ `${key}: unique(${JSON.stringify(constraint.name)}).on(${constraint.columns
474
+ .map((col) => `t.${toIdentifier(col)}`)
475
+ .join(', ')})`
476
+ );
477
+ } else if (
478
+ constraint.type === 'FOREIGN KEY' &&
479
+ constraint.referencedTable
480
+ ) {
481
+ imports.pgCore.add('foreignKey');
482
+ const targetTable = toIdentifier(constraint.referencedTable.name);
483
+ entries.push(
484
+ `${key}: foreignKey({ columns: [${constraint.columns
485
+ .map((col) => `t.${toIdentifier(col)}`)
486
+ .join(', ')}], foreignColumns: [${constraint.referencedTable.columns
487
+ .map((col) => `${targetTable}.${toIdentifier(col)}`)
488
+ .join(', ')}], name: ${JSON.stringify(constraint.name)} })`
489
+ );
490
+ } else if (
491
+ constraint.type === 'UNIQUE' &&
492
+ constraint.columns.length === 1
493
+ ) {
494
+ const columnName = constraint.columns[0];
495
+ entries.push(
496
+ `${key}: t.${toIdentifier(columnName)}.unique(${JSON.stringify(
497
+ constraint.name
498
+ )})`
499
+ );
500
+ }
501
+ }
502
+
503
+ if (!entries.length) {
504
+ return '';
505
+ }
506
+
507
+ const lines: string[] = ['(t) => ({'];
508
+ for (const entry of entries) {
509
+ lines.push(` ${entry},`);
510
+ }
511
+ lines.push('})');
512
+ return lines.join('\n');
513
+ }
514
+
515
+ interface ColumnEmitOptions extends EmitOptions {}
516
+
517
+ function emitColumn(
518
+ column: IntrospectedColumn,
519
+ imports: ImportBuckets,
520
+ options: ColumnEmitOptions
521
+ ): string {
522
+ const mapping = mapDuckDbType(column, imports, options);
523
+ let builder = mapping.builder;
524
+
525
+ if (!column.nullable) {
526
+ builder += '.notNull()';
527
+ }
528
+
529
+ const defaultFragment = buildDefault(column.columnDefault);
530
+ if (defaultFragment) {
531
+ imports.drizzle.add('sql');
532
+ builder += defaultFragment;
533
+ }
534
+
535
+ return builder;
536
+ }
537
+
538
+ export function buildDefault(defaultValue: string | null): string {
539
+ if (!defaultValue) {
540
+ return '';
541
+ }
542
+ const trimmed = defaultValue.trim();
543
+ if (!trimmed || trimmed.toUpperCase() === 'NULL') {
544
+ return '';
545
+ }
546
+
547
+ if (/^nextval\(/i.test(trimmed)) {
548
+ return `.default(sql\`${trimmed}\`)`;
549
+ }
550
+ if (
551
+ /^current_timestamp(?:\(\))?$/i.test(trimmed) ||
552
+ /^now\(\)$/i.test(trimmed)
553
+ ) {
554
+ return `.defaultNow()`;
555
+ }
556
+ if (trimmed === 'true' || trimmed === 'false') {
557
+ return `.default(${trimmed})`;
558
+ }
559
+ const numberValue = Number(trimmed);
560
+ if (!Number.isNaN(numberValue)) {
561
+ return `.default(${trimmed})`;
562
+ }
563
+ const stringLiteralMatch = /^'(.*)'$/.exec(trimmed);
564
+ if (stringLiteralMatch) {
565
+ const value = stringLiteralMatch[1]?.replace(/''/g, "'");
566
+ return `.default(${JSON.stringify(value)})`;
567
+ }
568
+
569
+ return '';
570
+ }
571
+
572
+ interface TypeMappingResult {
573
+ builder: string;
574
+ }
575
+
576
+ function mapDuckDbType(
577
+ column: IntrospectedColumn,
578
+ imports: ImportBuckets,
579
+ options: ColumnEmitOptions
580
+ ): TypeMappingResult {
581
+ const raw = column.dataType.trim();
582
+ const upper = raw.toUpperCase();
583
+
584
+ if (upper === 'BOOLEAN' || upper === 'BOOL') {
585
+ imports.pgCore.add('boolean');
586
+ return { builder: `boolean(${columnName(column.name)})` };
587
+ }
588
+
589
+ if (
590
+ upper === 'SMALLINT' ||
591
+ upper === 'INT2' ||
592
+ upper === 'INT16' ||
593
+ upper === 'TINYINT'
594
+ ) {
595
+ imports.pgCore.add('integer');
596
+ return { builder: `integer(${columnName(column.name)})` };
597
+ }
598
+
599
+ if (
600
+ upper === 'INTEGER' ||
601
+ upper === 'INT' ||
602
+ upper === 'INT4' ||
603
+ upper === 'SIGNED'
604
+ ) {
605
+ imports.pgCore.add('integer');
606
+ return { builder: `integer(${columnName(column.name)})` };
607
+ }
608
+
609
+ if (upper === 'BIGINT' || upper === 'INT8' || upper === 'UBIGINT') {
610
+ imports.pgCore.add('bigint');
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
+ };
616
+ }
617
+
618
+ const decimalMatch = /^DECIMAL\((\d+),(\d+)\)/i.exec(upper);
619
+ const numericMatch = /^NUMERIC\((\d+),(\d+)\)/i.exec(upper);
620
+ if (decimalMatch || numericMatch) {
621
+ imports.pgCore.add('numeric');
622
+ const [, precision, scale] = decimalMatch ?? numericMatch!;
623
+ return {
624
+ builder: `numeric(${columnName(column.name)}, { precision: ${precision}, scale: ${scale} })`,
625
+ };
626
+ }
627
+
628
+ if (upper.startsWith('DECIMAL') || upper.startsWith('NUMERIC')) {
629
+ imports.pgCore.add('numeric');
630
+ const precision = column.numericPrecision;
631
+ const scale = column.numericScale;
632
+ const options: string[] = [];
633
+ if (precision !== null && precision !== undefined) {
634
+ options.push(`precision: ${precision}`);
635
+ }
636
+ if (scale !== null && scale !== undefined) {
637
+ options.push(`scale: ${scale}`);
638
+ }
639
+ const suffix = options.length ? `, { ${options.join(', ')} }` : '';
640
+ return { builder: `numeric(${columnName(column.name)}${suffix})` };
641
+ }
642
+
643
+ if (upper === 'REAL' || upper === 'FLOAT4') {
644
+ imports.pgCore.add('real');
645
+ return { builder: `real(${columnName(column.name)})` };
646
+ }
647
+
648
+ if (upper === 'DOUBLE' || upper === 'DOUBLE PRECISION' || upper === 'FLOAT') {
649
+ imports.pgCore.add('doublePrecision');
650
+ return { builder: `doublePrecision(${columnName(column.name)})` };
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
+
675
+ if (upper.startsWith('CHAR(') || upper === 'CHAR') {
676
+ imports.pgCore.add('char');
677
+ const length = column.characterLength;
678
+ const lengthPart =
679
+ typeof length === 'number' ? `, { length: ${length} }` : '';
680
+ return { builder: `char(${columnName(column.name)}${lengthPart})` };
681
+ }
682
+
683
+ if (upper.startsWith('VARCHAR')) {
684
+ imports.pgCore.add('varchar');
685
+ const length = column.characterLength;
686
+ const lengthPart =
687
+ typeof length === 'number' ? `, { length: ${length} }` : '';
688
+ return { builder: `varchar(${columnName(column.name)}${lengthPart})` };
689
+ }
690
+
691
+ if (upper === 'TEXT' || upper === 'STRING') {
692
+ imports.pgCore.add('text');
693
+ return { builder: `text(${columnName(column.name)})` };
694
+ }
695
+
696
+ if (upper === 'UUID') {
697
+ imports.pgCore.add('uuid');
698
+ return { builder: `uuid(${columnName(column.name)})` };
699
+ }
700
+
701
+ if (upper === 'JSON') {
702
+ if (options.mapJsonAsDuckDbJson) {
703
+ imports.local.add('duckDbJson');
704
+ return { builder: `duckDbJson(${columnName(column.name)})` };
705
+ }
706
+ imports.pgCore.add('text');
707
+ return { builder: `text(${columnName(column.name)}) /* JSON */` };
708
+ }
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
+
726
+ if (upper === 'INET') {
727
+ imports.local.add('duckDbInet');
728
+ return { builder: `duckDbInet(${columnName(column.name)})` };
729
+ }
730
+
731
+ if (upper === 'INTERVAL') {
732
+ imports.local.add('duckDbInterval');
733
+ return { builder: `duckDbInterval(${columnName(column.name)})` };
734
+ }
735
+
736
+ if (upper === 'BLOB' || upper === 'BYTEA' || upper === 'VARBINARY') {
737
+ imports.local.add('duckDbBlob');
738
+ return { builder: `duckDbBlob(${columnName(column.name)})` };
739
+ }
740
+
741
+ if (upper.startsWith('STRUCT')) {
742
+ imports.local.add('duckDbStruct');
743
+ const inner = upper.replace(/^STRUCT\s*\(/i, '').replace(/\)$/, '');
744
+ const fields = parseStructFields(inner);
745
+ const entries = fields.map(
746
+ ({ name, type }) => `${JSON.stringify(name)}: ${JSON.stringify(type)}`
747
+ );
748
+ return {
749
+ builder: `duckDbStruct(${columnName(
750
+ column.name
751
+ )}, { ${entries.join(', ')} })`,
752
+ };
753
+ }
754
+
755
+ if (upper.startsWith('MAP(')) {
756
+ imports.local.add('duckDbMap');
757
+ const valueType = parseMapValue(upper);
758
+ return {
759
+ builder: `duckDbMap(${columnName(
760
+ column.name
761
+ )}, ${JSON.stringify(valueType)})`,
762
+ };
763
+ }
764
+
765
+ if (upper.startsWith('TIMESTAMP WITH TIME ZONE')) {
766
+ if (options.useCustomTimeTypes) {
767
+ imports.local.add('duckDbTimestamp');
768
+ } else {
769
+ imports.pgCore.add('timestamp');
770
+ }
771
+ const factory = options.useCustomTimeTypes
772
+ ? `duckDbTimestamp(${columnName(column.name)}, { withTimezone: true })`
773
+ : `timestamp(${columnName(column.name)}, { withTimezone: true })`;
774
+ return { builder: factory };
775
+ }
776
+
777
+ if (upper.startsWith('TIMESTAMP')) {
778
+ if (options.useCustomTimeTypes) {
779
+ imports.local.add('duckDbTimestamp');
780
+ return {
781
+ builder: `duckDbTimestamp(${columnName(column.name)})`,
782
+ };
783
+ }
784
+ imports.pgCore.add('timestamp');
785
+ return { builder: `timestamp(${columnName(column.name)})` };
786
+ }
787
+
788
+ if (upper === 'TIME') {
789
+ if (options.useCustomTimeTypes) {
790
+ imports.local.add('duckDbTime');
791
+ return { builder: `duckDbTime(${columnName(column.name)})` };
792
+ }
793
+ imports.pgCore.add('time');
794
+ return { builder: `time(${columnName(column.name)})` };
795
+ }
796
+
797
+ if (upper === 'DATE') {
798
+ if (options.useCustomTimeTypes) {
799
+ imports.local.add('duckDbDate');
800
+ return { builder: `duckDbDate(${columnName(column.name)})` };
801
+ }
802
+ imports.pgCore.add('date');
803
+ return { builder: `date(${columnName(column.name)})` };
804
+ }
805
+
806
+ // Fallback: keep as text to avoid runtime failures.
807
+ // Unknown types are mapped to text with a comment indicating the original type.
808
+ imports.pgCore.add('text');
809
+ return {
810
+ builder: `text(${columnName(
811
+ column.name
812
+ )}) /* unsupported DuckDB type: ${upper} */`,
813
+ };
814
+ }
815
+
816
+ export function parseStructFields(
817
+ inner: string
818
+ ): Array<{ name: string; type: string }> {
819
+ const result: Array<{ name: string; type: string }> = [];
820
+ for (const part of splitTopLevel(inner, ',')) {
821
+ const trimmed = part.trim();
822
+ if (!trimmed) continue;
823
+ const match = /^"?([^"]+)"?\s+(.*)$/i.exec(trimmed);
824
+ if (!match) {
825
+ continue;
826
+ }
827
+ const [, name, type] = match;
828
+ result.push({ name, type: type.trim() });
829
+ }
830
+ return result;
831
+ }
832
+
833
+ export function parseMapValue(raw: string): string {
834
+ const inner = raw.replace(/^MAP\(/i, '').replace(/\)$/, '');
835
+ const parts = splitTopLevel(inner, ',');
836
+ if (parts.length < 2) {
837
+ return 'TEXT';
838
+ }
839
+ return parts[1]?.trim() ?? 'TEXT';
840
+ }
841
+
842
+ export function splitTopLevel(input: string, delimiter: string): string[] {
843
+ const parts: string[] = [];
844
+ let depth = 0;
845
+ let current = '';
846
+ for (let i = 0; i < input.length; i += 1) {
847
+ const char = input[i]!;
848
+ if (char === '(') depth += 1;
849
+ if (char === ')') depth = Math.max(0, depth - 1);
850
+ if (char === delimiter && depth === 0) {
851
+ parts.push(current);
852
+ current = '';
853
+ continue;
854
+ }
855
+ current += char;
856
+ }
857
+ if (current) {
858
+ parts.push(current);
859
+ }
860
+ return parts;
861
+ }
862
+
863
+ function tableKey(schema: string, table: string): string {
864
+ return `${schema}.${table}`;
865
+ }
866
+
867
+ export function toIdentifier(name: string): string {
868
+ const cleaned = name.replace(/[^A-Za-z0-9_]/g, '_');
869
+ const parts = cleaned.split('_').filter(Boolean);
870
+ const base = parts
871
+ .map((part, index) =>
872
+ index === 0 ? part.toLowerCase() : capitalize(part.toLowerCase())
873
+ )
874
+ .join('');
875
+ const candidate = base || 'item';
876
+ return /^[A-Za-z_]/.test(candidate) ? candidate : `t${candidate}`;
877
+ }
878
+
879
+ function toSchemaIdentifier(schema: string): string {
880
+ const base = toIdentifier(schema);
881
+ return base.endsWith('Schema') ? base : `${base}Schema`;
882
+ }
883
+
884
+ function columnProperty(column: string): string {
885
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
886
+ return toIdentifier(column);
887
+ }
888
+ return JSON.stringify(column);
889
+ }
890
+
891
+ function columnName(name: string): string {
892
+ return JSON.stringify(name);
893
+ }
894
+
895
+ function capitalize(value: string): string {
896
+ if (!value) return value;
897
+ return value[0]!.toUpperCase() + value.slice(1);
898
+ }
899
+
900
+ function uniqueSchemas(tables: IntrospectedTable[]): string[] {
901
+ const seen = new Set<string>();
902
+ const result: string[] = [];
903
+ for (const table of tables) {
904
+ if (!seen.has(table.schema)) {
905
+ seen.add(table.schema);
906
+ result.push(table.schema);
907
+ }
908
+ }
909
+ return result;
910
+ }
911
+
912
+ function renderImports(imports: ImportBuckets, importBasePath: string): string {
913
+ const lines: string[] = [];
914
+ const drizzle = [...imports.drizzle];
915
+ if (drizzle.length) {
916
+ lines.push(`import { ${drizzle.sort().join(', ')} } from 'drizzle-orm';`);
917
+ }
918
+
919
+ const pgCore = [...imports.pgCore];
920
+ if (pgCore.length) {
921
+ lines.push(
922
+ `import { ${pgCore.sort().join(', ')} } from 'drizzle-orm/pg-core';`
923
+ );
924
+ }
925
+
926
+ const local = [...imports.local];
927
+ if (local.length) {
928
+ lines.push(
929
+ `import { ${local.sort().join(', ')} } from '${importBasePath}';`
930
+ );
931
+ }
932
+
933
+ lines.push('');
934
+ return lines.join('\n');
935
+ }