@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,1364 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin/duckdb-introspect.ts
|
|
4
|
+
import { DuckDBInstance } from "@duckdb/node-api";
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
|
|
9
|
+
// src/driver.ts
|
|
10
|
+
import { entityKind as entityKind3 } from "drizzle-orm/entity";
|
|
11
|
+
import { DefaultLogger } from "drizzle-orm/logger";
|
|
12
|
+
import { PgDatabase } from "drizzle-orm/pg-core/db";
|
|
13
|
+
import {
|
|
14
|
+
createTableRelationsHelpers,
|
|
15
|
+
extractTablesRelationalConfig
|
|
16
|
+
} from "drizzle-orm/relations";
|
|
17
|
+
|
|
18
|
+
// src/session.ts
|
|
19
|
+
import { entityKind } from "drizzle-orm/entity";
|
|
20
|
+
import { NoopLogger } from "drizzle-orm/logger";
|
|
21
|
+
import { PgTransaction } from "drizzle-orm/pg-core";
|
|
22
|
+
import { PgPreparedQuery, PgSession } from "drizzle-orm/pg-core/session";
|
|
23
|
+
import { fillPlaceholders, sql } from "drizzle-orm/sql/sql";
|
|
24
|
+
|
|
25
|
+
// src/sql/query-rewriters.ts
|
|
26
|
+
var tableIdPropSelectionRegex = new RegExp([
|
|
27
|
+
`("(.+)"\\."(.+)")`,
|
|
28
|
+
`(\\s+as\\s+'?(.+?)'?\\.'?(.+?)'?)?`
|
|
29
|
+
].join(""), "i");
|
|
30
|
+
function adaptArrayOperators(query) {
|
|
31
|
+
const operators = [
|
|
32
|
+
{ token: "@>", fn: "array_has_all" },
|
|
33
|
+
{ token: "<@", fn: "array_has_all", swap: true },
|
|
34
|
+
{ token: "&&", fn: "array_has_any" }
|
|
35
|
+
];
|
|
36
|
+
const isWhitespace = (char) => char !== undefined && /\s/.test(char);
|
|
37
|
+
const walkLeft = (source, start) => {
|
|
38
|
+
let idx = start;
|
|
39
|
+
while (idx >= 0 && isWhitespace(source[idx])) {
|
|
40
|
+
idx--;
|
|
41
|
+
}
|
|
42
|
+
let depth = 0;
|
|
43
|
+
let inString = false;
|
|
44
|
+
for (;idx >= 0; idx--) {
|
|
45
|
+
const ch = source[idx];
|
|
46
|
+
if (ch === "'" && source[idx - 1] !== "\\") {
|
|
47
|
+
inString = !inString;
|
|
48
|
+
}
|
|
49
|
+
if (inString)
|
|
50
|
+
continue;
|
|
51
|
+
if (ch === ")" || ch === "]") {
|
|
52
|
+
depth++;
|
|
53
|
+
} else if (ch === "(" || ch === "[") {
|
|
54
|
+
depth--;
|
|
55
|
+
if (depth < 0) {
|
|
56
|
+
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
57
|
+
}
|
|
58
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
59
|
+
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [0, source.slice(0, start + 1)];
|
|
63
|
+
};
|
|
64
|
+
const walkRight = (source, start) => {
|
|
65
|
+
let idx = start;
|
|
66
|
+
while (idx < source.length && isWhitespace(source[idx])) {
|
|
67
|
+
idx++;
|
|
68
|
+
}
|
|
69
|
+
let depth = 0;
|
|
70
|
+
let inString = false;
|
|
71
|
+
for (;idx < source.length; idx++) {
|
|
72
|
+
const ch = source[idx];
|
|
73
|
+
if (ch === "'" && source[idx - 1] !== "\\") {
|
|
74
|
+
inString = !inString;
|
|
75
|
+
}
|
|
76
|
+
if (inString)
|
|
77
|
+
continue;
|
|
78
|
+
if (ch === "(" || ch === "[") {
|
|
79
|
+
depth++;
|
|
80
|
+
} else if (ch === ")" || ch === "]") {
|
|
81
|
+
depth--;
|
|
82
|
+
if (depth < 0) {
|
|
83
|
+
return [idx, source.slice(start, idx)];
|
|
84
|
+
}
|
|
85
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
86
|
+
return [idx, source.slice(start, idx)];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return [source.length, source.slice(start)];
|
|
90
|
+
};
|
|
91
|
+
let rewritten = query;
|
|
92
|
+
for (const { token, fn, swap } of operators) {
|
|
93
|
+
let idx = rewritten.indexOf(token);
|
|
94
|
+
while (idx !== -1) {
|
|
95
|
+
const [leftStart, leftExpr] = walkLeft(rewritten, idx - 1);
|
|
96
|
+
const [rightEnd, rightExpr] = walkRight(rewritten, idx + token.length);
|
|
97
|
+
const left = leftExpr.trim();
|
|
98
|
+
const right = rightExpr.trim();
|
|
99
|
+
const replacement = `${fn}(${swap ? right : left}, ${swap ? left : right})`;
|
|
100
|
+
rewritten = rewritten.slice(0, leftStart) + replacement + rewritten.slice(rightEnd);
|
|
101
|
+
idx = rewritten.indexOf(token, leftStart + replacement.length);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return rewritten;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/sql/result-mapper.ts
|
|
108
|
+
import {
|
|
109
|
+
Column,
|
|
110
|
+
SQL,
|
|
111
|
+
getTableName,
|
|
112
|
+
is
|
|
113
|
+
} from "drizzle-orm";
|
|
114
|
+
import {
|
|
115
|
+
PgCustomColumn,
|
|
116
|
+
PgDate,
|
|
117
|
+
PgDateString,
|
|
118
|
+
PgInterval,
|
|
119
|
+
PgTime,
|
|
120
|
+
PgTimestamp,
|
|
121
|
+
PgTimestampString
|
|
122
|
+
} from "drizzle-orm/pg-core";
|
|
123
|
+
function toDecoderInput(decoder, value) {
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
function normalizeInet(value) {
|
|
127
|
+
if (value && typeof value === "object" && "address" in value && typeof value.address !== "undefined") {
|
|
128
|
+
const { address, mask } = value;
|
|
129
|
+
if (typeof address === "bigint" || typeof address === "number") {
|
|
130
|
+
const inet = typeof address === "number" ? BigInt(address) : address;
|
|
131
|
+
const maxIpv4 = (1n << 32n) - 1n;
|
|
132
|
+
if (inet >= 0 && inet <= maxIpv4) {
|
|
133
|
+
const num = Number(inet);
|
|
134
|
+
const octets = [
|
|
135
|
+
num >>> 24 & 255,
|
|
136
|
+
num >>> 16 & 255,
|
|
137
|
+
num >>> 8 & 255,
|
|
138
|
+
num & 255
|
|
139
|
+
];
|
|
140
|
+
const suffix = typeof mask === "number" && mask !== 32 ? `/${mask}` : "";
|
|
141
|
+
return `${octets.join(".")}${suffix}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const fallback = value.toString?.();
|
|
145
|
+
if (fallback && fallback !== "[object Object]") {
|
|
146
|
+
return fallback;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
function normalizeTimestampString(value, withTimezone) {
|
|
152
|
+
if (value instanceof Date) {
|
|
153
|
+
const iso = value.toISOString().replace("T", " ");
|
|
154
|
+
return withTimezone ? iso.replace("Z", "+00") : iso.replace("Z", "");
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === "string") {
|
|
157
|
+
const normalized = value.replace("T", " ");
|
|
158
|
+
if (withTimezone) {
|
|
159
|
+
return normalized.includes("+") ? normalized : `${normalized}+00`;
|
|
160
|
+
}
|
|
161
|
+
return normalized.replace(/\+00$/, "");
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
function normalizeTimestamp(value, withTimezone) {
|
|
166
|
+
if (value instanceof Date) {
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
if (typeof value === "string") {
|
|
170
|
+
const hasOffset = value.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(value.trim());
|
|
171
|
+
const spaced = value.replace(" ", "T");
|
|
172
|
+
const normalized = withTimezone || hasOffset ? spaced : `${spaced}+00`;
|
|
173
|
+
return new Date(normalized);
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
function normalizeDateString(value) {
|
|
178
|
+
if (value instanceof Date) {
|
|
179
|
+
return value.toISOString().slice(0, 10);
|
|
180
|
+
}
|
|
181
|
+
if (typeof value === "string") {
|
|
182
|
+
return value.slice(0, 10);
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
function normalizeDateValue(value) {
|
|
187
|
+
if (value instanceof Date) {
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
if (typeof value === "string") {
|
|
191
|
+
return new Date(`${value.slice(0, 10)}T00:00:00Z`);
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
function normalizeTime(value) {
|
|
196
|
+
if (typeof value === "bigint") {
|
|
197
|
+
const totalMillis = Number(value) / 1000;
|
|
198
|
+
const date = new Date(totalMillis);
|
|
199
|
+
return date.toISOString().split("T")[1].replace("Z", "");
|
|
200
|
+
}
|
|
201
|
+
if (value instanceof Date) {
|
|
202
|
+
return value.toISOString().split("T")[1].replace("Z", "");
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
function normalizeInterval(value) {
|
|
207
|
+
if (value && typeof value === "object" && "days" in value && "months" in value) {
|
|
208
|
+
const { months, days, micros } = value;
|
|
209
|
+
if (months === 0 && days !== undefined) {
|
|
210
|
+
if (micros && Number(micros) !== 0) {
|
|
211
|
+
const seconds = Number(micros) / 1e6;
|
|
212
|
+
return `${days} day${days === 1 ? "" : "s"} ${seconds} seconds`.trim();
|
|
213
|
+
}
|
|
214
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
function mapDriverValue(decoder, rawValue) {
|
|
220
|
+
if (is(decoder, PgTimestampString)) {
|
|
221
|
+
return decoder.mapFromDriverValue(toDecoderInput(decoder, normalizeTimestampString(rawValue, decoder.withTimezone)));
|
|
222
|
+
}
|
|
223
|
+
if (is(decoder, PgTimestamp)) {
|
|
224
|
+
const normalized = normalizeTimestamp(rawValue, decoder.withTimezone);
|
|
225
|
+
if (normalized instanceof Date) {
|
|
226
|
+
return normalized;
|
|
227
|
+
}
|
|
228
|
+
return decoder.mapFromDriverValue(toDecoderInput(decoder, normalized));
|
|
229
|
+
}
|
|
230
|
+
if (is(decoder, PgDateString)) {
|
|
231
|
+
return decoder.mapFromDriverValue(toDecoderInput(decoder, normalizeDateString(rawValue)));
|
|
232
|
+
}
|
|
233
|
+
if (is(decoder, PgDate)) {
|
|
234
|
+
return decoder.mapFromDriverValue(toDecoderInput(decoder, normalizeDateValue(rawValue)));
|
|
235
|
+
}
|
|
236
|
+
if (is(decoder, PgTime)) {
|
|
237
|
+
return decoder.mapFromDriverValue(toDecoderInput(decoder, normalizeTime(rawValue)));
|
|
238
|
+
}
|
|
239
|
+
if (is(decoder, PgInterval)) {
|
|
240
|
+
return decoder.mapFromDriverValue(toDecoderInput(decoder, normalizeInterval(rawValue)));
|
|
241
|
+
}
|
|
242
|
+
return decoder.mapFromDriverValue(toDecoderInput(decoder, rawValue));
|
|
243
|
+
}
|
|
244
|
+
function mapResultRow(columns, row, joinsNotNullableMap) {
|
|
245
|
+
const nullifyMap = {};
|
|
246
|
+
const result = columns.reduce((acc, { path, field }, columnIndex) => {
|
|
247
|
+
let decoder;
|
|
248
|
+
if (is(field, Column)) {
|
|
249
|
+
decoder = field;
|
|
250
|
+
} else if (is(field, SQL)) {
|
|
251
|
+
decoder = field.decoder;
|
|
252
|
+
} else {
|
|
253
|
+
const col = field.sql.queryChunks.find((chunk) => is(chunk, Column));
|
|
254
|
+
if (is(col, PgCustomColumn)) {
|
|
255
|
+
decoder = col;
|
|
256
|
+
} else {
|
|
257
|
+
decoder = field.sql.decoder;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
let node = acc;
|
|
261
|
+
for (const [pathChunkIndex, pathChunk] of path.entries()) {
|
|
262
|
+
if (pathChunkIndex < path.length - 1) {
|
|
263
|
+
if (!(pathChunk in node)) {
|
|
264
|
+
node[pathChunk] = {};
|
|
265
|
+
}
|
|
266
|
+
node = node[pathChunk];
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const rawValue = normalizeInet(row[columnIndex]);
|
|
270
|
+
const value = node[pathChunk] = rawValue === null ? null : mapDriverValue(decoder, rawValue);
|
|
271
|
+
if (joinsNotNullableMap && is(field, Column) && path.length === 2) {
|
|
272
|
+
const objectName = path[0];
|
|
273
|
+
if (!(objectName in nullifyMap)) {
|
|
274
|
+
nullifyMap[objectName] = value === null ? getTableName(field.table) : false;
|
|
275
|
+
} else if (typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== getTableName(field.table)) {
|
|
276
|
+
nullifyMap[objectName] = false;
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (joinsNotNullableMap && is(field, SQL.Aliased) && path.length === 2) {
|
|
281
|
+
const col = field.sql.queryChunks.find((chunk) => is(chunk, Column));
|
|
282
|
+
const tableName = col?.table && getTableName(col?.table);
|
|
283
|
+
if (!tableName) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const objectName = path[0];
|
|
287
|
+
if (!(objectName in nullifyMap)) {
|
|
288
|
+
nullifyMap[objectName] = value === null ? tableName : false;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (nullifyMap[objectName] && nullifyMap[objectName] !== tableName) {
|
|
292
|
+
nullifyMap[objectName] = false;
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return acc;
|
|
298
|
+
}, {});
|
|
299
|
+
if (joinsNotNullableMap && Object.keys(nullifyMap).length > 0) {
|
|
300
|
+
for (const [objectName, tableName] of Object.entries(nullifyMap)) {
|
|
301
|
+
if (typeof tableName === "string" && !joinsNotNullableMap[tableName]) {
|
|
302
|
+
result[objectName] = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/session.ts
|
|
310
|
+
import { TransactionRollbackError } from "drizzle-orm/errors";
|
|
311
|
+
|
|
312
|
+
// src/client.ts
|
|
313
|
+
import {
|
|
314
|
+
listValue
|
|
315
|
+
} from "@duckdb/node-api";
|
|
316
|
+
function isPgArrayLiteral(value) {
|
|
317
|
+
return value.startsWith("{") && value.endsWith("}");
|
|
318
|
+
}
|
|
319
|
+
function parsePgArrayLiteral(value) {
|
|
320
|
+
const json = value.replace(/{/g, "[").replace(/}/g, "]");
|
|
321
|
+
try {
|
|
322
|
+
return JSON.parse(json);
|
|
323
|
+
} catch {
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
var warnedArrayLiteral = false;
|
|
328
|
+
function prepareParams(params, options = {}) {
|
|
329
|
+
return params.map((param) => {
|
|
330
|
+
if (typeof param === "string" && isPgArrayLiteral(param)) {
|
|
331
|
+
if (options.rejectStringArrayLiterals) {
|
|
332
|
+
throw new Error("Stringified array literals are not supported. Use duckDbList()/duckDbArray() or pass native arrays.");
|
|
333
|
+
}
|
|
334
|
+
if (!warnedArrayLiteral && options.warnOnStringArrayLiteral) {
|
|
335
|
+
warnedArrayLiteral = true;
|
|
336
|
+
options.warnOnStringArrayLiteral();
|
|
337
|
+
}
|
|
338
|
+
return parsePgArrayLiteral(param);
|
|
339
|
+
}
|
|
340
|
+
return param;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function toNodeApiValue(value) {
|
|
344
|
+
if (Array.isArray(value)) {
|
|
345
|
+
return listValue(value.map((inner) => toNodeApiValue(inner)));
|
|
346
|
+
}
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
async function executeOnClient(client, query, params) {
|
|
350
|
+
const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
|
|
351
|
+
const result = await client.run(query, values);
|
|
352
|
+
const rows = await result.getRowsJS();
|
|
353
|
+
const columns = result.columnNames();
|
|
354
|
+
const seen = {};
|
|
355
|
+
const uniqueColumns = columns.map((col) => {
|
|
356
|
+
const count = seen[col] ?? 0;
|
|
357
|
+
seen[col] = count + 1;
|
|
358
|
+
return count === 0 ? col : `${col}_${count}`;
|
|
359
|
+
});
|
|
360
|
+
return (rows ?? []).map((vals) => {
|
|
361
|
+
const obj = {};
|
|
362
|
+
uniqueColumns.forEach((col, idx) => {
|
|
363
|
+
obj[col] = vals[idx];
|
|
364
|
+
});
|
|
365
|
+
return obj;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/session.ts
|
|
370
|
+
class DuckDBPreparedQuery extends PgPreparedQuery {
|
|
371
|
+
client;
|
|
372
|
+
dialect;
|
|
373
|
+
queryString;
|
|
374
|
+
params;
|
|
375
|
+
logger;
|
|
376
|
+
fields;
|
|
377
|
+
_isResponseInArrayMode;
|
|
378
|
+
customResultMapper;
|
|
379
|
+
rewriteArrays;
|
|
380
|
+
rejectStringArrayLiterals;
|
|
381
|
+
warnOnStringArrayLiteral;
|
|
382
|
+
static [entityKind] = "DuckDBPreparedQuery";
|
|
383
|
+
constructor(client, dialect, queryString, params, logger, fields, _isResponseInArrayMode, customResultMapper, rewriteArrays, rejectStringArrayLiterals, warnOnStringArrayLiteral) {
|
|
384
|
+
super({ sql: queryString, params });
|
|
385
|
+
this.client = client;
|
|
386
|
+
this.dialect = dialect;
|
|
387
|
+
this.queryString = queryString;
|
|
388
|
+
this.params = params;
|
|
389
|
+
this.logger = logger;
|
|
390
|
+
this.fields = fields;
|
|
391
|
+
this._isResponseInArrayMode = _isResponseInArrayMode;
|
|
392
|
+
this.customResultMapper = customResultMapper;
|
|
393
|
+
this.rewriteArrays = rewriteArrays;
|
|
394
|
+
this.rejectStringArrayLiterals = rejectStringArrayLiterals;
|
|
395
|
+
this.warnOnStringArrayLiteral = warnOnStringArrayLiteral;
|
|
396
|
+
}
|
|
397
|
+
async execute(placeholderValues = {}) {
|
|
398
|
+
this.dialect.assertNoPgJsonColumns();
|
|
399
|
+
const params = prepareParams(fillPlaceholders(this.params, placeholderValues), {
|
|
400
|
+
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
401
|
+
warnOnStringArrayLiteral: this.warnOnStringArrayLiteral ? () => this.warnOnStringArrayLiteral?.(this.queryString) : undefined
|
|
402
|
+
});
|
|
403
|
+
const rewrittenQuery = this.rewriteArrays ? adaptArrayOperators(this.queryString) : this.queryString;
|
|
404
|
+
if (this.rewriteArrays && rewrittenQuery !== this.queryString) {
|
|
405
|
+
this.logger.logQuery(`[duckdb] original query before array rewrite: ${this.queryString}`, params);
|
|
406
|
+
}
|
|
407
|
+
this.logger.logQuery(rewrittenQuery, params);
|
|
408
|
+
const {
|
|
409
|
+
fields,
|
|
410
|
+
joinsNotNullableMap,
|
|
411
|
+
customResultMapper
|
|
412
|
+
} = this;
|
|
413
|
+
const rows = await executeOnClient(this.client, rewrittenQuery, params);
|
|
414
|
+
if (rows.length === 0 || !fields) {
|
|
415
|
+
return rows;
|
|
416
|
+
}
|
|
417
|
+
const rowValues = rows.map((row) => Object.values(row));
|
|
418
|
+
return customResultMapper ? customResultMapper(rowValues) : rowValues.map((row) => mapResultRow(fields, row, joinsNotNullableMap));
|
|
419
|
+
}
|
|
420
|
+
all(placeholderValues = {}) {
|
|
421
|
+
return this.execute(placeholderValues);
|
|
422
|
+
}
|
|
423
|
+
isResponseInArrayMode() {
|
|
424
|
+
return this._isResponseInArrayMode;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
class DuckDBSession extends PgSession {
|
|
429
|
+
client;
|
|
430
|
+
schema;
|
|
431
|
+
options;
|
|
432
|
+
static [entityKind] = "DuckDBSession";
|
|
433
|
+
dialect;
|
|
434
|
+
logger;
|
|
435
|
+
rewriteArrays;
|
|
436
|
+
rejectStringArrayLiterals;
|
|
437
|
+
hasWarnedArrayLiteral = false;
|
|
438
|
+
constructor(client, dialect, schema, options = {}) {
|
|
439
|
+
super(dialect);
|
|
440
|
+
this.client = client;
|
|
441
|
+
this.schema = schema;
|
|
442
|
+
this.options = options;
|
|
443
|
+
this.dialect = dialect;
|
|
444
|
+
this.logger = options.logger ?? new NoopLogger;
|
|
445
|
+
this.rewriteArrays = options.rewriteArrays ?? true;
|
|
446
|
+
this.rejectStringArrayLiterals = options.rejectStringArrayLiterals ?? false;
|
|
447
|
+
}
|
|
448
|
+
prepareQuery(query, fields, name, isResponseInArrayMode, customResultMapper) {
|
|
449
|
+
return new DuckDBPreparedQuery(this.client, this.dialect, query.sql, query.params, this.logger, fields, isResponseInArrayMode, customResultMapper, this.rewriteArrays, this.rejectStringArrayLiterals, this.rejectStringArrayLiterals ? undefined : this.warnOnStringArrayLiteral);
|
|
450
|
+
}
|
|
451
|
+
async transaction(transaction) {
|
|
452
|
+
const session = new DuckDBSession(this.client, this.dialect, this.schema, this.options);
|
|
453
|
+
const tx = new DuckDBTransaction(this.dialect, session, this.schema);
|
|
454
|
+
await tx.execute(sql`BEGIN TRANSACTION;`);
|
|
455
|
+
try {
|
|
456
|
+
const result = await transaction(tx);
|
|
457
|
+
await tx.execute(sql`commit`);
|
|
458
|
+
return result;
|
|
459
|
+
} catch (error) {
|
|
460
|
+
await tx.execute(sql`rollback`);
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
warnOnStringArrayLiteral = (query) => {
|
|
465
|
+
if (this.hasWarnedArrayLiteral) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.hasWarnedArrayLiteral = true;
|
|
469
|
+
this.logger.logQuery(`[duckdb] ${arrayLiteralWarning}
|
|
470
|
+
query: ${query}`, []);
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
class DuckDBTransaction extends PgTransaction {
|
|
475
|
+
static [entityKind] = "DuckDBTransaction";
|
|
476
|
+
rollback() {
|
|
477
|
+
throw new TransactionRollbackError;
|
|
478
|
+
}
|
|
479
|
+
getTransactionConfigSQL(config) {
|
|
480
|
+
const chunks = [];
|
|
481
|
+
if (config.isolationLevel) {
|
|
482
|
+
chunks.push(`isolation level ${config.isolationLevel}`);
|
|
483
|
+
}
|
|
484
|
+
if (config.accessMode) {
|
|
485
|
+
chunks.push(config.accessMode);
|
|
486
|
+
}
|
|
487
|
+
if (typeof config.deferrable === "boolean") {
|
|
488
|
+
chunks.push(config.deferrable ? "deferrable" : "not deferrable");
|
|
489
|
+
}
|
|
490
|
+
return sql.raw(chunks.join(" "));
|
|
491
|
+
}
|
|
492
|
+
setTransaction(config) {
|
|
493
|
+
return this.session.execute(sql`set transaction ${this.getTransactionConfigSQL(config)}`);
|
|
494
|
+
}
|
|
495
|
+
async transaction(transaction) {
|
|
496
|
+
const nestedTx = new DuckDBTransaction(this.dialect, this.session, this.schema, this.nestedIndex + 1);
|
|
497
|
+
return transaction(nestedTx);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
var arrayLiteralWarning = "Received a stringified Postgres-style array literal. Use duckDbList()/duckDbArray() or pass native arrays instead. You can also set rejectStringArrayLiterals=true to throw.";
|
|
501
|
+
|
|
502
|
+
// src/dialect.ts
|
|
503
|
+
import { entityKind as entityKind2, is as is2 } from "drizzle-orm/entity";
|
|
504
|
+
import {
|
|
505
|
+
PgDate as PgDate2,
|
|
506
|
+
PgDateString as PgDateString2,
|
|
507
|
+
PgDialect,
|
|
508
|
+
PgJson,
|
|
509
|
+
PgJsonb,
|
|
510
|
+
PgNumeric,
|
|
511
|
+
PgTime as PgTime2,
|
|
512
|
+
PgTimestamp as PgTimestamp2,
|
|
513
|
+
PgTimestampString as PgTimestampString2,
|
|
514
|
+
PgUUID
|
|
515
|
+
} from "drizzle-orm/pg-core";
|
|
516
|
+
import {
|
|
517
|
+
sql as sql2
|
|
518
|
+
} from "drizzle-orm";
|
|
519
|
+
|
|
520
|
+
class DuckDBDialect extends PgDialect {
|
|
521
|
+
static [entityKind2] = "DuckDBPgDialect";
|
|
522
|
+
hasPgJsonColumn = false;
|
|
523
|
+
assertNoPgJsonColumns() {
|
|
524
|
+
if (this.hasPgJsonColumn) {
|
|
525
|
+
throw new Error("Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB’s native JSON type.");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async migrate(migrations, session, config) {
|
|
529
|
+
const migrationConfig = typeof config === "string" ? { migrationsFolder: config } : config;
|
|
530
|
+
const migrationsSchema = migrationConfig.migrationsSchema ?? "drizzle";
|
|
531
|
+
const migrationsTable = migrationConfig.migrationsTable ?? "__drizzle_migrations";
|
|
532
|
+
const migrationsSequence = `${migrationsTable}_id_seq`;
|
|
533
|
+
const legacySequence = "migrations_pk_seq";
|
|
534
|
+
const escapeIdentifier = (value) => value.replace(/"/g, '""');
|
|
535
|
+
const sequenceLiteral = `"${escapeIdentifier(migrationsSchema)}"."${escapeIdentifier(migrationsSequence)}"`;
|
|
536
|
+
const migrationTableCreate = sql2`
|
|
537
|
+
CREATE TABLE IF NOT EXISTS ${sql2.identifier(migrationsSchema)}.${sql2.identifier(migrationsTable)} (
|
|
538
|
+
id integer PRIMARY KEY default nextval('${sql2.raw(sequenceLiteral)}'),
|
|
539
|
+
hash text NOT NULL,
|
|
540
|
+
created_at bigint
|
|
541
|
+
)
|
|
542
|
+
`;
|
|
543
|
+
await session.execute(sql2`CREATE SCHEMA IF NOT EXISTS ${sql2.identifier(migrationsSchema)}`);
|
|
544
|
+
await session.execute(sql2`CREATE SEQUENCE IF NOT EXISTS ${sql2.identifier(migrationsSchema)}.${sql2.identifier(migrationsSequence)}`);
|
|
545
|
+
if (legacySequence !== migrationsSequence) {
|
|
546
|
+
await session.execute(sql2`CREATE SEQUENCE IF NOT EXISTS ${sql2.identifier(migrationsSchema)}.${sql2.identifier(legacySequence)}`);
|
|
547
|
+
}
|
|
548
|
+
await session.execute(migrationTableCreate);
|
|
549
|
+
const dbMigrations = await session.all(sql2`select id, hash, created_at from ${sql2.identifier(migrationsSchema)}.${sql2.identifier(migrationsTable)} order by created_at desc limit 1`);
|
|
550
|
+
const lastDbMigration = dbMigrations[0];
|
|
551
|
+
await session.transaction(async (tx) => {
|
|
552
|
+
for await (const migration of migrations) {
|
|
553
|
+
if (!lastDbMigration || Number(lastDbMigration.created_at) < migration.folderMillis) {
|
|
554
|
+
for (const stmt of migration.sql) {
|
|
555
|
+
await tx.execute(sql2.raw(stmt));
|
|
556
|
+
}
|
|
557
|
+
await tx.execute(sql2`insert into ${sql2.identifier(migrationsSchema)}.${sql2.identifier(migrationsTable)} ("hash", "created_at") values(${migration.hash}, ${migration.folderMillis})`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
prepareTyping(encoder) {
|
|
563
|
+
if (is2(encoder, PgJsonb) || is2(encoder, PgJson)) {
|
|
564
|
+
this.hasPgJsonColumn = true;
|
|
565
|
+
return "none";
|
|
566
|
+
} else if (is2(encoder, PgNumeric)) {
|
|
567
|
+
return "decimal";
|
|
568
|
+
} else if (is2(encoder, PgTime2)) {
|
|
569
|
+
return "time";
|
|
570
|
+
} else if (is2(encoder, PgTimestamp2) || is2(encoder, PgTimestampString2)) {
|
|
571
|
+
return "timestamp";
|
|
572
|
+
} else if (is2(encoder, PgDate2) || is2(encoder, PgDateString2)) {
|
|
573
|
+
return "date";
|
|
574
|
+
} else if (is2(encoder, PgUUID)) {
|
|
575
|
+
return "uuid";
|
|
576
|
+
} else {
|
|
577
|
+
return "none";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/select-builder.ts
|
|
583
|
+
import { is as is4 } from "drizzle-orm/entity";
|
|
584
|
+
import {
|
|
585
|
+
PgSelectBase,
|
|
586
|
+
PgSelectBuilder
|
|
587
|
+
} from "drizzle-orm/pg-core/query-builders";
|
|
588
|
+
import { Subquery, ViewBaseConfig } from "drizzle-orm";
|
|
589
|
+
import { PgViewBase } from "drizzle-orm/pg-core/view-base";
|
|
590
|
+
import { SQL as SQL4 } from "drizzle-orm/sql/sql";
|
|
591
|
+
|
|
592
|
+
// src/sql/selection.ts
|
|
593
|
+
import {
|
|
594
|
+
Column as Column2,
|
|
595
|
+
SQL as SQL3,
|
|
596
|
+
getTableName as getTableName2,
|
|
597
|
+
is as is3,
|
|
598
|
+
sql as sql3
|
|
599
|
+
} from "drizzle-orm";
|
|
600
|
+
function mapEntries(obj, prefix, fullJoin = false) {
|
|
601
|
+
return Object.fromEntries(Object.entries(obj).filter(([key]) => key !== "enableRLS").map(([key, value]) => {
|
|
602
|
+
const qualified = prefix ? `${prefix}.${key}` : key;
|
|
603
|
+
if (fullJoin && is3(value, Column2)) {
|
|
604
|
+
return [
|
|
605
|
+
key,
|
|
606
|
+
sql3`${value}`.mapWith(value).as(`${getTableName2(value.table)}.${value.name}`)
|
|
607
|
+
];
|
|
608
|
+
}
|
|
609
|
+
if (fullJoin && is3(value, SQL3)) {
|
|
610
|
+
const col = value.getSQL().queryChunks.find((chunk) => is3(chunk, Column2));
|
|
611
|
+
const tableName = col?.table && getTableName2(col?.table);
|
|
612
|
+
return [key, value.as(tableName ? `${tableName}.${key}` : key)];
|
|
613
|
+
}
|
|
614
|
+
if (is3(value, SQL3) || is3(value, Column2)) {
|
|
615
|
+
const aliased = is3(value, SQL3) ? value : sql3`${value}`.mapWith(value);
|
|
616
|
+
return [key, aliased.as(qualified)];
|
|
617
|
+
}
|
|
618
|
+
if (is3(value, SQL3.Aliased)) {
|
|
619
|
+
return [key, value];
|
|
620
|
+
}
|
|
621
|
+
if (typeof value === "object" && value !== null) {
|
|
622
|
+
return [
|
|
623
|
+
key,
|
|
624
|
+
mapEntries(value, qualified, fullJoin)
|
|
625
|
+
];
|
|
626
|
+
}
|
|
627
|
+
return [key, value];
|
|
628
|
+
}));
|
|
629
|
+
}
|
|
630
|
+
function aliasFields(fields, fullJoin = false) {
|
|
631
|
+
return mapEntries(fields, undefined, fullJoin);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/select-builder.ts
|
|
635
|
+
import { getTableColumns } from "drizzle-orm/utils";
|
|
636
|
+
|
|
637
|
+
class DuckDBSelectBuilder extends PgSelectBuilder {
|
|
638
|
+
_fields;
|
|
639
|
+
_session;
|
|
640
|
+
_dialect;
|
|
641
|
+
_withList = [];
|
|
642
|
+
_distinct;
|
|
643
|
+
constructor(config) {
|
|
644
|
+
super(config);
|
|
645
|
+
this._fields = config.fields;
|
|
646
|
+
this._session = config.session;
|
|
647
|
+
this._dialect = config.dialect;
|
|
648
|
+
if (config.withList) {
|
|
649
|
+
this._withList = config.withList;
|
|
650
|
+
}
|
|
651
|
+
this._distinct = config.distinct;
|
|
652
|
+
}
|
|
653
|
+
from(source) {
|
|
654
|
+
const isPartialSelect = !!this._fields;
|
|
655
|
+
const src = source;
|
|
656
|
+
let fields;
|
|
657
|
+
if (this._fields) {
|
|
658
|
+
fields = this._fields;
|
|
659
|
+
} else if (is4(src, Subquery)) {
|
|
660
|
+
fields = Object.fromEntries(Object.keys(src._.selectedFields).map((key) => [
|
|
661
|
+
key,
|
|
662
|
+
src[key]
|
|
663
|
+
]));
|
|
664
|
+
} else if (is4(src, PgViewBase)) {
|
|
665
|
+
fields = src[ViewBaseConfig]?.selectedFields;
|
|
666
|
+
} else if (is4(src, SQL4)) {
|
|
667
|
+
fields = {};
|
|
668
|
+
} else {
|
|
669
|
+
fields = aliasFields(getTableColumns(src), !isPartialSelect);
|
|
670
|
+
}
|
|
671
|
+
return new PgSelectBase({
|
|
672
|
+
table: src,
|
|
673
|
+
fields,
|
|
674
|
+
isPartialSelect,
|
|
675
|
+
session: this._session,
|
|
676
|
+
dialect: this._dialect,
|
|
677
|
+
withList: this._withList,
|
|
678
|
+
distinct: this._distinct
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/driver.ts
|
|
684
|
+
class DuckDBDriver {
|
|
685
|
+
client;
|
|
686
|
+
dialect;
|
|
687
|
+
options;
|
|
688
|
+
static [entityKind3] = "DuckDBDriver";
|
|
689
|
+
constructor(client, dialect, options = {}) {
|
|
690
|
+
this.client = client;
|
|
691
|
+
this.dialect = dialect;
|
|
692
|
+
this.options = options;
|
|
693
|
+
}
|
|
694
|
+
createSession(schema) {
|
|
695
|
+
return new DuckDBSession(this.client, this.dialect, schema, {
|
|
696
|
+
logger: this.options.logger,
|
|
697
|
+
rewriteArrays: this.options.rewriteArrays,
|
|
698
|
+
rejectStringArrayLiterals: this.options.rejectStringArrayLiterals
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function drizzle(client, config = {}) {
|
|
703
|
+
const dialect = new DuckDBDialect;
|
|
704
|
+
const logger = config.logger === true ? new DefaultLogger : config.logger || undefined;
|
|
705
|
+
let schema;
|
|
706
|
+
if (config.schema) {
|
|
707
|
+
const tablesConfig = extractTablesRelationalConfig(config.schema, createTableRelationsHelpers);
|
|
708
|
+
schema = {
|
|
709
|
+
fullSchema: config.schema,
|
|
710
|
+
schema: tablesConfig.tables,
|
|
711
|
+
tableNamesMap: tablesConfig.tableNamesMap
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const driver = new DuckDBDriver(client, dialect, {
|
|
715
|
+
logger,
|
|
716
|
+
rewriteArrays: config.rewriteArrays,
|
|
717
|
+
rejectStringArrayLiterals: config.rejectStringArrayLiterals
|
|
718
|
+
});
|
|
719
|
+
const session = driver.createSession(schema);
|
|
720
|
+
return new DuckDBDatabase(dialect, session, schema);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
class DuckDBDatabase extends PgDatabase {
|
|
724
|
+
dialect;
|
|
725
|
+
session;
|
|
726
|
+
static [entityKind3] = "DuckDBDatabase";
|
|
727
|
+
constructor(dialect, session, schema) {
|
|
728
|
+
super(dialect, session, schema);
|
|
729
|
+
this.dialect = dialect;
|
|
730
|
+
this.session = session;
|
|
731
|
+
}
|
|
732
|
+
select(fields) {
|
|
733
|
+
const selectedFields = fields ? aliasFields(fields) : undefined;
|
|
734
|
+
return new DuckDBSelectBuilder({
|
|
735
|
+
fields: selectedFields ?? undefined,
|
|
736
|
+
session: this.session,
|
|
737
|
+
dialect: this.dialect
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
async transaction(transaction) {
|
|
741
|
+
return await this.session.transaction(transaction);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/introspect.ts
|
|
746
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
747
|
+
var SYSTEM_SCHEMAS = new Set(["information_schema", "pg_catalog"]);
|
|
748
|
+
var DEFAULT_IMPORT_BASE = "@leonardovida-md/drizzle-neo-duckdb";
|
|
749
|
+
async function introspect(db, opts = {}) {
|
|
750
|
+
const schemas = await resolveSchemas(db, opts.schemas);
|
|
751
|
+
const includeViews = opts.includeViews ?? false;
|
|
752
|
+
const tables = await loadTables(db, schemas, includeViews);
|
|
753
|
+
const columns = await loadColumns(db, schemas);
|
|
754
|
+
const constraints = await loadConstraints(db, schemas);
|
|
755
|
+
const indexes = await loadIndexes(db, schemas);
|
|
756
|
+
const grouped = buildTables(tables, columns, constraints, indexes);
|
|
757
|
+
const schemaTs = emitSchema(grouped, {
|
|
758
|
+
useCustomTimeTypes: opts.useCustomTimeTypes ?? true,
|
|
759
|
+
mapJsonAsDuckDbJson: opts.mapJsonAsDuckDbJson ?? true,
|
|
760
|
+
importBasePath: opts.importBasePath ?? DEFAULT_IMPORT_BASE
|
|
761
|
+
});
|
|
762
|
+
return {
|
|
763
|
+
files: {
|
|
764
|
+
schemaTs,
|
|
765
|
+
metaJson: grouped
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
async function resolveSchemas(db, targetSchemas) {
|
|
770
|
+
if (targetSchemas?.length) {
|
|
771
|
+
return targetSchemas;
|
|
772
|
+
}
|
|
773
|
+
const rows = await db.execute(sql4`select schema_name from information_schema.schemata`);
|
|
774
|
+
return rows.map((row) => row.schema_name).filter((name) => !SYSTEM_SCHEMAS.has(name));
|
|
775
|
+
}
|
|
776
|
+
async function loadTables(db, schemas, includeViews) {
|
|
777
|
+
const schemaFragments = schemas.map((schema) => sql4`${schema}`);
|
|
778
|
+
return await db.execute(sql4`
|
|
779
|
+
select table_schema as schema_name, table_name, table_type
|
|
780
|
+
from information_schema.tables
|
|
781
|
+
where table_schema in (${sql4.join(schemaFragments, sql4.raw(", "))})
|
|
782
|
+
and ${includeViews ? sql4`1 = 1` : sql4`table_type = 'BASE TABLE'`}
|
|
783
|
+
order by table_schema, table_name
|
|
784
|
+
`);
|
|
785
|
+
}
|
|
786
|
+
async function loadColumns(db, schemas) {
|
|
787
|
+
const schemaFragments = schemas.map((schema) => sql4`${schema}`);
|
|
788
|
+
return await db.execute(sql4`
|
|
789
|
+
select
|
|
790
|
+
schema_name,
|
|
791
|
+
table_name,
|
|
792
|
+
column_name,
|
|
793
|
+
column_index,
|
|
794
|
+
column_default,
|
|
795
|
+
is_nullable,
|
|
796
|
+
data_type,
|
|
797
|
+
character_maximum_length,
|
|
798
|
+
numeric_precision,
|
|
799
|
+
numeric_scale,
|
|
800
|
+
internal
|
|
801
|
+
from duckdb_columns()
|
|
802
|
+
where schema_name in (${sql4.join(schemaFragments, sql4.raw(", "))})
|
|
803
|
+
order by schema_name, table_name, column_index
|
|
804
|
+
`);
|
|
805
|
+
}
|
|
806
|
+
async function loadConstraints(db, schemas) {
|
|
807
|
+
const schemaFragments = schemas.map((schema) => sql4`${schema}`);
|
|
808
|
+
return await db.execute(sql4`
|
|
809
|
+
select
|
|
810
|
+
schema_name,
|
|
811
|
+
table_name,
|
|
812
|
+
constraint_name,
|
|
813
|
+
constraint_type,
|
|
814
|
+
constraint_text,
|
|
815
|
+
constraint_column_names,
|
|
816
|
+
referenced_table,
|
|
817
|
+
referenced_column_names
|
|
818
|
+
from duckdb_constraints()
|
|
819
|
+
where schema_name in (${sql4.join(schemaFragments, sql4.raw(", "))})
|
|
820
|
+
order by schema_name, table_name, constraint_index
|
|
821
|
+
`);
|
|
822
|
+
}
|
|
823
|
+
async function loadIndexes(db, schemas) {
|
|
824
|
+
const schemaFragments = schemas.map((schema) => sql4`${schema}`);
|
|
825
|
+
return await db.execute(sql4`
|
|
826
|
+
select
|
|
827
|
+
schema_name,
|
|
828
|
+
table_name,
|
|
829
|
+
index_name,
|
|
830
|
+
is_unique,
|
|
831
|
+
expressions
|
|
832
|
+
from duckdb_indexes()
|
|
833
|
+
where schema_name in (${sql4.join(schemaFragments, sql4.raw(", "))})
|
|
834
|
+
order by schema_name, table_name, index_name
|
|
835
|
+
`);
|
|
836
|
+
}
|
|
837
|
+
function buildTables(tables, columns, constraints, indexes) {
|
|
838
|
+
const byTable = {};
|
|
839
|
+
for (const table of tables) {
|
|
840
|
+
const key = tableKey(table.schema_name, table.table_name);
|
|
841
|
+
byTable[key] = {
|
|
842
|
+
schema: table.schema_name,
|
|
843
|
+
name: table.table_name,
|
|
844
|
+
kind: table.table_type === "VIEW" ? "view" : "table",
|
|
845
|
+
columns: [],
|
|
846
|
+
constraints: [],
|
|
847
|
+
indexes: []
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
for (const column of columns) {
|
|
851
|
+
if (column.internal) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const key = tableKey(column.schema_name, column.table_name);
|
|
855
|
+
const table = byTable[key];
|
|
856
|
+
if (!table) {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
table.columns.push({
|
|
860
|
+
name: column.column_name,
|
|
861
|
+
dataType: column.data_type,
|
|
862
|
+
columnDefault: column.column_default,
|
|
863
|
+
nullable: column.is_nullable,
|
|
864
|
+
characterLength: column.character_maximum_length,
|
|
865
|
+
numericPrecision: column.numeric_precision,
|
|
866
|
+
numericScale: column.numeric_scale
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
for (const constraint of constraints) {
|
|
870
|
+
const key = tableKey(constraint.schema_name, constraint.table_name);
|
|
871
|
+
const table = byTable[key];
|
|
872
|
+
if (!table) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (!constraint.constraint_column_names?.length) {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
table.constraints.push({
|
|
879
|
+
name: constraint.constraint_name,
|
|
880
|
+
type: constraint.constraint_type,
|
|
881
|
+
columns: constraint.constraint_column_names ?? [],
|
|
882
|
+
referencedTable: constraint.referenced_table && constraint.referenced_column_names ? {
|
|
883
|
+
schema: constraint.schema_name,
|
|
884
|
+
name: constraint.referenced_table,
|
|
885
|
+
columns: constraint.referenced_column_names
|
|
886
|
+
} : undefined,
|
|
887
|
+
rawExpression: constraint.constraint_text
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
for (const index of indexes) {
|
|
891
|
+
const key = tableKey(index.schema_name, index.table_name);
|
|
892
|
+
const table = byTable[key];
|
|
893
|
+
if (!table) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
table.indexes.push(index);
|
|
897
|
+
}
|
|
898
|
+
return Object.values(byTable);
|
|
899
|
+
}
|
|
900
|
+
function emitSchema(catalog, options) {
|
|
901
|
+
const imports = {
|
|
902
|
+
drizzle: new Set,
|
|
903
|
+
pgCore: new Set,
|
|
904
|
+
local: new Set
|
|
905
|
+
};
|
|
906
|
+
imports.pgCore.add("pgSchema");
|
|
907
|
+
const sorted = [...catalog].sort((a, b) => a.schema === b.schema ? a.name.localeCompare(b.name) : a.schema.localeCompare(b.schema));
|
|
908
|
+
const lines = [];
|
|
909
|
+
for (const schema of uniqueSchemas(sorted)) {
|
|
910
|
+
imports.pgCore.add("pgSchema");
|
|
911
|
+
const schemaVar = toSchemaIdentifier(schema);
|
|
912
|
+
lines.push(`export const ${schemaVar} = pgSchema(${JSON.stringify(schema)});`, "");
|
|
913
|
+
const tables = sorted.filter((table) => table.schema === schema);
|
|
914
|
+
for (const table of tables) {
|
|
915
|
+
lines.push(...emitTable(schemaVar, table, imports, options));
|
|
916
|
+
lines.push("");
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const importsBlock = renderImports(imports, options.importBasePath);
|
|
920
|
+
return [importsBlock, ...lines].join(`
|
|
921
|
+
`).trim() + `
|
|
922
|
+
`;
|
|
923
|
+
}
|
|
924
|
+
function emitTable(schemaVar, table, imports, options) {
|
|
925
|
+
const tableVar = toIdentifier(table.name);
|
|
926
|
+
const columnLines = [];
|
|
927
|
+
for (const column of table.columns) {
|
|
928
|
+
columnLines.push(` ${columnProperty(column.name)}: ${emitColumn(column, imports, options)},`);
|
|
929
|
+
}
|
|
930
|
+
const constraintBlock = emitConstraints(table, imports);
|
|
931
|
+
const tableLines = [];
|
|
932
|
+
tableLines.push(`export const ${tableVar} = ${schemaVar}.table(${JSON.stringify(table.name)}, {`);
|
|
933
|
+
tableLines.push(...columnLines);
|
|
934
|
+
tableLines.push(`}${constraintBlock ? "," : ""}${constraintBlock ? ` ${constraintBlock}` : ""});`);
|
|
935
|
+
return tableLines;
|
|
936
|
+
}
|
|
937
|
+
function emitConstraints(table, imports) {
|
|
938
|
+
const constraints = table.constraints.filter((constraint) => ["PRIMARY KEY", "FOREIGN KEY", "UNIQUE"].includes(constraint.type));
|
|
939
|
+
if (!constraints.length) {
|
|
940
|
+
return "";
|
|
941
|
+
}
|
|
942
|
+
const entries = [];
|
|
943
|
+
for (const constraint of constraints) {
|
|
944
|
+
const key = toIdentifier(constraint.name || `${table.name}_constraint`);
|
|
945
|
+
if (constraint.type === "PRIMARY KEY") {
|
|
946
|
+
imports.pgCore.add("primaryKey");
|
|
947
|
+
entries.push(`${key}: primaryKey({ columns: [${constraint.columns.map((col) => `t.${toIdentifier(col)}`).join(", ")}], name: ${JSON.stringify(constraint.name)} })`);
|
|
948
|
+
} else if (constraint.type === "UNIQUE" && constraint.columns.length > 1) {
|
|
949
|
+
imports.pgCore.add("unique");
|
|
950
|
+
entries.push(`${key}: unique(${JSON.stringify(constraint.name)}).on(${constraint.columns.map((col) => `t.${toIdentifier(col)}`).join(", ")})`);
|
|
951
|
+
} else if (constraint.type === "FOREIGN KEY" && constraint.referencedTable) {
|
|
952
|
+
imports.pgCore.add("foreignKey");
|
|
953
|
+
const targetTable = toIdentifier(constraint.referencedTable.name);
|
|
954
|
+
entries.push(`${key}: foreignKey({ columns: [${constraint.columns.map((col) => `t.${toIdentifier(col)}`).join(", ")}], foreignColumns: [${constraint.referencedTable.columns.map((col) => `${targetTable}.${toIdentifier(col)}`).join(", ")}], name: ${JSON.stringify(constraint.name)} })`);
|
|
955
|
+
} else if (constraint.type === "UNIQUE" && constraint.columns.length === 1) {
|
|
956
|
+
const columnName = constraint.columns[0];
|
|
957
|
+
entries.push(`${key}: t.${toIdentifier(columnName)}.unique(${JSON.stringify(constraint.name)})`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (!entries.length) {
|
|
961
|
+
return "";
|
|
962
|
+
}
|
|
963
|
+
const lines = ["(t) => ({"];
|
|
964
|
+
for (const entry of entries) {
|
|
965
|
+
lines.push(` ${entry},`);
|
|
966
|
+
}
|
|
967
|
+
lines.push("})");
|
|
968
|
+
return lines.join(`
|
|
969
|
+
`);
|
|
970
|
+
}
|
|
971
|
+
function emitColumn(column, imports, options) {
|
|
972
|
+
const mapping = mapDuckDbType(column, imports, options);
|
|
973
|
+
let builder = mapping.builder;
|
|
974
|
+
if (!column.nullable) {
|
|
975
|
+
builder += ".notNull()";
|
|
976
|
+
}
|
|
977
|
+
const defaultFragment = buildDefault(column.columnDefault);
|
|
978
|
+
if (defaultFragment) {
|
|
979
|
+
imports.drizzle.add("sql");
|
|
980
|
+
builder += defaultFragment;
|
|
981
|
+
}
|
|
982
|
+
return builder;
|
|
983
|
+
}
|
|
984
|
+
function buildDefault(defaultValue) {
|
|
985
|
+
if (!defaultValue) {
|
|
986
|
+
return "";
|
|
987
|
+
}
|
|
988
|
+
const trimmed = defaultValue.trim();
|
|
989
|
+
if (!trimmed || trimmed.toUpperCase() === "NULL") {
|
|
990
|
+
return "";
|
|
991
|
+
}
|
|
992
|
+
if (/^nextval\(/i.test(trimmed)) {
|
|
993
|
+
return `.default(sql\`${trimmed}\`)`;
|
|
994
|
+
}
|
|
995
|
+
if (/^current_timestamp(?:\(\))?$/i.test(trimmed) || /^now\(\)$/i.test(trimmed)) {
|
|
996
|
+
return `.defaultNow()`;
|
|
997
|
+
}
|
|
998
|
+
if (trimmed === "true" || trimmed === "false") {
|
|
999
|
+
return `.default(${trimmed})`;
|
|
1000
|
+
}
|
|
1001
|
+
const numberValue = Number(trimmed);
|
|
1002
|
+
if (!Number.isNaN(numberValue)) {
|
|
1003
|
+
return `.default(${trimmed})`;
|
|
1004
|
+
}
|
|
1005
|
+
const stringLiteralMatch = /^'(.*)'$/.exec(trimmed);
|
|
1006
|
+
if (stringLiteralMatch) {
|
|
1007
|
+
const value = stringLiteralMatch[1]?.replace(/''/g, "'");
|
|
1008
|
+
return `.default(${JSON.stringify(value)})`;
|
|
1009
|
+
}
|
|
1010
|
+
return "";
|
|
1011
|
+
}
|
|
1012
|
+
function mapDuckDbType(column, imports, options) {
|
|
1013
|
+
const raw = column.dataType.trim();
|
|
1014
|
+
const upper = raw.toUpperCase();
|
|
1015
|
+
if (upper === "BOOLEAN" || upper === "BOOL") {
|
|
1016
|
+
imports.pgCore.add("boolean");
|
|
1017
|
+
return { builder: `boolean(${columnName(column.name)})` };
|
|
1018
|
+
}
|
|
1019
|
+
if (upper === "SMALLINT" || upper === "INT2" || upper === "INT16" || upper === "TINYINT") {
|
|
1020
|
+
imports.pgCore.add("integer");
|
|
1021
|
+
return { builder: `integer(${columnName(column.name)})` };
|
|
1022
|
+
}
|
|
1023
|
+
if (upper === "INTEGER" || upper === "INT" || upper === "INT4" || upper === "SIGNED") {
|
|
1024
|
+
imports.pgCore.add("integer");
|
|
1025
|
+
return { builder: `integer(${columnName(column.name)})` };
|
|
1026
|
+
}
|
|
1027
|
+
if (upper === "BIGINT" || upper === "INT8" || upper === "UBIGINT") {
|
|
1028
|
+
imports.pgCore.add("bigint");
|
|
1029
|
+
return { builder: `bigint(${columnName(column.name)})` };
|
|
1030
|
+
}
|
|
1031
|
+
const decimalMatch = /^DECIMAL\((\d+),(\d+)\)/i.exec(upper);
|
|
1032
|
+
const numericMatch = /^NUMERIC\((\d+),(\d+)\)/i.exec(upper);
|
|
1033
|
+
if (decimalMatch || numericMatch) {
|
|
1034
|
+
imports.pgCore.add("numeric");
|
|
1035
|
+
const [, precision, scale] = decimalMatch ?? numericMatch;
|
|
1036
|
+
return {
|
|
1037
|
+
builder: `numeric(${columnName(column.name)}, { precision: ${precision}, scale: ${scale} })`
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (upper.startsWith("DECIMAL") || upper.startsWith("NUMERIC")) {
|
|
1041
|
+
imports.pgCore.add("numeric");
|
|
1042
|
+
const precision = column.numericPrecision;
|
|
1043
|
+
const scale = column.numericScale;
|
|
1044
|
+
const options2 = [];
|
|
1045
|
+
if (precision !== null && precision !== undefined) {
|
|
1046
|
+
options2.push(`precision: ${precision}`);
|
|
1047
|
+
}
|
|
1048
|
+
if (scale !== null && scale !== undefined) {
|
|
1049
|
+
options2.push(`scale: ${scale}`);
|
|
1050
|
+
}
|
|
1051
|
+
const suffix = options2.length ? `, { ${options2.join(", ")} }` : "";
|
|
1052
|
+
return { builder: `numeric(${columnName(column.name)}${suffix})` };
|
|
1053
|
+
}
|
|
1054
|
+
if (upper === "REAL" || upper === "FLOAT4") {
|
|
1055
|
+
imports.pgCore.add("real");
|
|
1056
|
+
return { builder: `real(${columnName(column.name)})` };
|
|
1057
|
+
}
|
|
1058
|
+
if (upper === "DOUBLE" || upper === "DOUBLE PRECISION" || upper === "FLOAT") {
|
|
1059
|
+
imports.pgCore.add("doublePrecision");
|
|
1060
|
+
return { builder: `doublePrecision(${columnName(column.name)})` };
|
|
1061
|
+
}
|
|
1062
|
+
if (upper.startsWith("CHAR(") || upper === "CHAR") {
|
|
1063
|
+
imports.pgCore.add("char");
|
|
1064
|
+
const length = column.characterLength;
|
|
1065
|
+
const lengthPart = typeof length === "number" ? `, { length: ${length} }` : "";
|
|
1066
|
+
return { builder: `char(${columnName(column.name)}${lengthPart})` };
|
|
1067
|
+
}
|
|
1068
|
+
if (upper.startsWith("VARCHAR")) {
|
|
1069
|
+
imports.pgCore.add("varchar");
|
|
1070
|
+
const length = column.characterLength;
|
|
1071
|
+
const lengthPart = typeof length === "number" ? `, { length: ${length} }` : "";
|
|
1072
|
+
return { builder: `varchar(${columnName(column.name)}${lengthPart})` };
|
|
1073
|
+
}
|
|
1074
|
+
if (upper === "TEXT" || upper === "STRING") {
|
|
1075
|
+
imports.pgCore.add("text");
|
|
1076
|
+
return { builder: `text(${columnName(column.name)})` };
|
|
1077
|
+
}
|
|
1078
|
+
if (upper === "UUID") {
|
|
1079
|
+
imports.pgCore.add("uuid");
|
|
1080
|
+
return { builder: `uuid(${columnName(column.name)})` };
|
|
1081
|
+
}
|
|
1082
|
+
if (upper === "JSON") {
|
|
1083
|
+
if (options.mapJsonAsDuckDbJson) {
|
|
1084
|
+
imports.local.add("duckDbJson");
|
|
1085
|
+
return { builder: `duckDbJson(${columnName(column.name)})` };
|
|
1086
|
+
}
|
|
1087
|
+
imports.pgCore.add("text");
|
|
1088
|
+
return { builder: `text(${columnName(column.name)}) /* JSON */` };
|
|
1089
|
+
}
|
|
1090
|
+
if (upper === "INET") {
|
|
1091
|
+
imports.local.add("duckDbInet");
|
|
1092
|
+
return { builder: `duckDbInet(${columnName(column.name)})` };
|
|
1093
|
+
}
|
|
1094
|
+
if (upper === "INTERVAL") {
|
|
1095
|
+
imports.local.add("duckDbInterval");
|
|
1096
|
+
return { builder: `duckDbInterval(${columnName(column.name)})` };
|
|
1097
|
+
}
|
|
1098
|
+
if (upper === "BLOB" || upper === "BYTEA" || upper === "VARBINARY") {
|
|
1099
|
+
imports.local.add("duckDbBlob");
|
|
1100
|
+
return { builder: `duckDbBlob(${columnName(column.name)})` };
|
|
1101
|
+
}
|
|
1102
|
+
const arrayMatch = /^(.*)\[(\d+)\]$/.exec(upper);
|
|
1103
|
+
if (arrayMatch) {
|
|
1104
|
+
imports.local.add("duckDbArray");
|
|
1105
|
+
const [, base, length] = arrayMatch;
|
|
1106
|
+
return {
|
|
1107
|
+
builder: `duckDbArray(${columnName(column.name)}, ${JSON.stringify(base)}, ${Number(length)})`
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
const listMatch = /^(.*)\[\]$/.exec(upper);
|
|
1111
|
+
if (listMatch) {
|
|
1112
|
+
imports.local.add("duckDbList");
|
|
1113
|
+
const [, base] = listMatch;
|
|
1114
|
+
return {
|
|
1115
|
+
builder: `duckDbList(${columnName(column.name)}, ${JSON.stringify(base)})`
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
if (upper.startsWith("STRUCT")) {
|
|
1119
|
+
imports.local.add("duckDbStruct");
|
|
1120
|
+
const inner = upper.replace(/^STRUCT\s*\(/i, "").replace(/\)$/, "");
|
|
1121
|
+
const fields = parseStructFields(inner);
|
|
1122
|
+
const entries = fields.map(({ name, type }) => `${JSON.stringify(name)}: ${JSON.stringify(type)}`);
|
|
1123
|
+
return {
|
|
1124
|
+
builder: `duckDbStruct(${columnName(column.name)}, { ${entries.join(", ")} })`
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
if (upper.startsWith("MAP(")) {
|
|
1128
|
+
imports.local.add("duckDbMap");
|
|
1129
|
+
const valueType = parseMapValue(upper);
|
|
1130
|
+
return {
|
|
1131
|
+
builder: `duckDbMap(${columnName(column.name)}, ${JSON.stringify(valueType)})`
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
if (upper.startsWith("TIMESTAMP WITH TIME ZONE")) {
|
|
1135
|
+
if (options.useCustomTimeTypes) {
|
|
1136
|
+
imports.local.add("duckDbTimestamp");
|
|
1137
|
+
} else {
|
|
1138
|
+
imports.pgCore.add("timestamp");
|
|
1139
|
+
}
|
|
1140
|
+
const factory = options.useCustomTimeTypes ? `duckDbTimestamp(${columnName(column.name)}, { withTimezone: true })` : `timestamp(${columnName(column.name)}, { withTimezone: true })`;
|
|
1141
|
+
return { builder: factory };
|
|
1142
|
+
}
|
|
1143
|
+
if (upper.startsWith("TIMESTAMP")) {
|
|
1144
|
+
if (options.useCustomTimeTypes) {
|
|
1145
|
+
imports.local.add("duckDbTimestamp");
|
|
1146
|
+
return {
|
|
1147
|
+
builder: `duckDbTimestamp(${columnName(column.name)})`
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
imports.pgCore.add("timestamp");
|
|
1151
|
+
return { builder: `timestamp(${columnName(column.name)})` };
|
|
1152
|
+
}
|
|
1153
|
+
if (upper === "TIME") {
|
|
1154
|
+
if (options.useCustomTimeTypes) {
|
|
1155
|
+
imports.local.add("duckDbTime");
|
|
1156
|
+
return { builder: `duckDbTime(${columnName(column.name)})` };
|
|
1157
|
+
}
|
|
1158
|
+
imports.pgCore.add("time");
|
|
1159
|
+
return { builder: `time(${columnName(column.name)})` };
|
|
1160
|
+
}
|
|
1161
|
+
if (upper === "DATE") {
|
|
1162
|
+
if (options.useCustomTimeTypes) {
|
|
1163
|
+
imports.local.add("duckDbDate");
|
|
1164
|
+
return { builder: `duckDbDate(${columnName(column.name)})` };
|
|
1165
|
+
}
|
|
1166
|
+
imports.pgCore.add("date");
|
|
1167
|
+
return { builder: `date(${columnName(column.name)})` };
|
|
1168
|
+
}
|
|
1169
|
+
imports.pgCore.add("text");
|
|
1170
|
+
return {
|
|
1171
|
+
builder: `text(${columnName(column.name)}) /* TODO: verify type ${upper} */`
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function parseStructFields(inner) {
|
|
1175
|
+
const result = [];
|
|
1176
|
+
for (const part of splitTopLevel(inner, ",")) {
|
|
1177
|
+
const trimmed = part.trim();
|
|
1178
|
+
if (!trimmed)
|
|
1179
|
+
continue;
|
|
1180
|
+
const match = /^"?([^"]+)"?\s+(.*)$/i.exec(trimmed);
|
|
1181
|
+
if (!match) {
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
const [, name, type] = match;
|
|
1185
|
+
result.push({ name, type: type.trim() });
|
|
1186
|
+
}
|
|
1187
|
+
return result;
|
|
1188
|
+
}
|
|
1189
|
+
function parseMapValue(raw) {
|
|
1190
|
+
const inner = raw.replace(/^MAP\(/i, "").replace(/\)$/, "");
|
|
1191
|
+
const parts = splitTopLevel(inner, ",");
|
|
1192
|
+
if (parts.length < 2) {
|
|
1193
|
+
return "TEXT";
|
|
1194
|
+
}
|
|
1195
|
+
return parts[1]?.trim() ?? "TEXT";
|
|
1196
|
+
}
|
|
1197
|
+
function splitTopLevel(input, delimiter) {
|
|
1198
|
+
const parts = [];
|
|
1199
|
+
let depth = 0;
|
|
1200
|
+
let current = "";
|
|
1201
|
+
for (let i = 0;i < input.length; i += 1) {
|
|
1202
|
+
const char = input[i];
|
|
1203
|
+
if (char === "(")
|
|
1204
|
+
depth += 1;
|
|
1205
|
+
if (char === ")")
|
|
1206
|
+
depth = Math.max(0, depth - 1);
|
|
1207
|
+
if (char === delimiter && depth === 0) {
|
|
1208
|
+
parts.push(current);
|
|
1209
|
+
current = "";
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
current += char;
|
|
1213
|
+
}
|
|
1214
|
+
if (current) {
|
|
1215
|
+
parts.push(current);
|
|
1216
|
+
}
|
|
1217
|
+
return parts;
|
|
1218
|
+
}
|
|
1219
|
+
function tableKey(schema, table) {
|
|
1220
|
+
return `${schema}.${table}`;
|
|
1221
|
+
}
|
|
1222
|
+
function toIdentifier(name) {
|
|
1223
|
+
const cleaned = name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
1224
|
+
const parts = cleaned.split("_").filter(Boolean);
|
|
1225
|
+
const base = parts.map((part, index) => index === 0 ? part.toLowerCase() : capitalize(part.toLowerCase())).join("");
|
|
1226
|
+
const candidate = base || "item";
|
|
1227
|
+
return /^[A-Za-z_]/.test(candidate) ? candidate : `t${candidate}`;
|
|
1228
|
+
}
|
|
1229
|
+
function toSchemaIdentifier(schema) {
|
|
1230
|
+
const base = toIdentifier(schema);
|
|
1231
|
+
return base.endsWith("Schema") ? base : `${base}Schema`;
|
|
1232
|
+
}
|
|
1233
|
+
function columnProperty(column) {
|
|
1234
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
|
|
1235
|
+
return toIdentifier(column);
|
|
1236
|
+
}
|
|
1237
|
+
return JSON.stringify(column);
|
|
1238
|
+
}
|
|
1239
|
+
function columnName(name) {
|
|
1240
|
+
return JSON.stringify(name);
|
|
1241
|
+
}
|
|
1242
|
+
function capitalize(value) {
|
|
1243
|
+
if (!value)
|
|
1244
|
+
return value;
|
|
1245
|
+
return value[0].toUpperCase() + value.slice(1);
|
|
1246
|
+
}
|
|
1247
|
+
function uniqueSchemas(tables) {
|
|
1248
|
+
const seen = new Set;
|
|
1249
|
+
const result = [];
|
|
1250
|
+
for (const table of tables) {
|
|
1251
|
+
if (!seen.has(table.schema)) {
|
|
1252
|
+
seen.add(table.schema);
|
|
1253
|
+
result.push(table.schema);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
function renderImports(imports, importBasePath) {
|
|
1259
|
+
const lines = [];
|
|
1260
|
+
const drizzle2 = [...imports.drizzle];
|
|
1261
|
+
if (drizzle2.length) {
|
|
1262
|
+
lines.push(`import { ${drizzle2.sort().join(", ")} } from 'drizzle-orm';`);
|
|
1263
|
+
}
|
|
1264
|
+
const pgCore = [...imports.pgCore];
|
|
1265
|
+
if (pgCore.length) {
|
|
1266
|
+
lines.push(`import { ${pgCore.sort().join(", ")} } from 'drizzle-orm/pg-core';`);
|
|
1267
|
+
}
|
|
1268
|
+
const local = [...imports.local];
|
|
1269
|
+
if (local.length) {
|
|
1270
|
+
lines.push(`import { ${local.sort().join(", ")} } from '${importBasePath}';`);
|
|
1271
|
+
}
|
|
1272
|
+
lines.push("");
|
|
1273
|
+
return lines.join(`
|
|
1274
|
+
`);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/bin/duckdb-introspect.ts
|
|
1278
|
+
function parseArgs(argv) {
|
|
1279
|
+
const options = {
|
|
1280
|
+
outFile: path.resolve(process.cwd(), "drizzle/schema.ts"),
|
|
1281
|
+
includeViews: false,
|
|
1282
|
+
useCustomTimeTypes: true
|
|
1283
|
+
};
|
|
1284
|
+
for (let i = 0;i < argv.length; i += 1) {
|
|
1285
|
+
const arg = argv[i];
|
|
1286
|
+
switch (arg) {
|
|
1287
|
+
case "--url":
|
|
1288
|
+
options.url = argv[++i];
|
|
1289
|
+
break;
|
|
1290
|
+
case "--schema":
|
|
1291
|
+
case "--schemas":
|
|
1292
|
+
options.schemas = argv[++i]?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1293
|
+
break;
|
|
1294
|
+
case "--out":
|
|
1295
|
+
case "--outFile":
|
|
1296
|
+
options.outFile = path.resolve(process.cwd(), argv[++i] ?? "drizzle/schema.ts");
|
|
1297
|
+
break;
|
|
1298
|
+
case "--include-views":
|
|
1299
|
+
case "--includeViews":
|
|
1300
|
+
options.includeViews = true;
|
|
1301
|
+
break;
|
|
1302
|
+
case "--use-pg-time":
|
|
1303
|
+
options.useCustomTimeTypes = false;
|
|
1304
|
+
break;
|
|
1305
|
+
case "--import-base":
|
|
1306
|
+
options.importBasePath = argv[++i];
|
|
1307
|
+
break;
|
|
1308
|
+
case "--help":
|
|
1309
|
+
case "-h":
|
|
1310
|
+
printHelp();
|
|
1311
|
+
process.exit(0);
|
|
1312
|
+
default:
|
|
1313
|
+
if (arg.startsWith("-")) {
|
|
1314
|
+
console.warn(`Unknown option ${arg}`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return options;
|
|
1319
|
+
}
|
|
1320
|
+
function printHelp() {
|
|
1321
|
+
console.log(`duckdb-introspect
|
|
1322
|
+
|
|
1323
|
+
Usage:
|
|
1324
|
+
bun x duckdb-introspect --url <duckdb path|md:> [--schema my_schema] [--out ./drizzle/schema.ts]
|
|
1325
|
+
|
|
1326
|
+
Options:
|
|
1327
|
+
--url DuckDB database path (e.g. :memory:, ./local.duckdb, md:)
|
|
1328
|
+
--schema Comma separated schema list (defaults to all non-system schemas)
|
|
1329
|
+
--out Output file (default: ./drizzle/schema.ts)
|
|
1330
|
+
--include-views Include views in the generated schema
|
|
1331
|
+
--use-pg-time Use pg-core timestamp/date/time instead of DuckDB custom helpers
|
|
1332
|
+
--import-base Override import path for duckdb helpers (default: package name)
|
|
1333
|
+
`);
|
|
1334
|
+
}
|
|
1335
|
+
async function main() {
|
|
1336
|
+
const options = parseArgs(process.argv.slice(2));
|
|
1337
|
+
if (!options.url) {
|
|
1338
|
+
printHelp();
|
|
1339
|
+
throw new Error("Missing required --url");
|
|
1340
|
+
}
|
|
1341
|
+
const instanceOptions = options.url.startsWith("md:") && process.env.MOTHERDUCK_TOKEN ? { motherduck_token: process.env.MOTHERDUCK_TOKEN } : undefined;
|
|
1342
|
+
const instance = await DuckDBInstance.create(options.url, instanceOptions);
|
|
1343
|
+
const connection = await instance.connect();
|
|
1344
|
+
const db = drizzle(connection);
|
|
1345
|
+
try {
|
|
1346
|
+
const result = await introspect(db, {
|
|
1347
|
+
schemas: options.schemas,
|
|
1348
|
+
includeViews: options.includeViews,
|
|
1349
|
+
useCustomTimeTypes: options.useCustomTimeTypes,
|
|
1350
|
+
importBasePath: options.importBasePath
|
|
1351
|
+
});
|
|
1352
|
+
await mkdir(path.dirname(options.outFile), { recursive: true });
|
|
1353
|
+
await writeFile(options.outFile, result.files.schemaTs, "utf8");
|
|
1354
|
+
console.log(`Wrote schema to ${options.outFile}`);
|
|
1355
|
+
} finally {
|
|
1356
|
+
if ("closeSync" in connection && typeof connection.closeSync === "function") {
|
|
1357
|
+
connection.closeSync();
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
main().catch((err) => {
|
|
1362
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1363
|
+
process.exit(1);
|
|
1364
|
+
});
|