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