@leonardovida-md/drizzle-neo-duckdb 1.0.3 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -5
- package/dist/client.d.ts +7 -1
- package/dist/columns.d.ts +6 -1
- package/dist/dialect.d.ts +21 -0
- package/dist/driver.d.ts +33 -1
- package/dist/duckdb-introspect.mjs +610 -114
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.mjs +319 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +603 -117
- package/dist/introspect.d.ts +9 -0
- package/dist/pool.d.ts +30 -0
- package/dist/session.d.ts +7 -1
- package/dist/sql/query-rewriters.d.ts +1 -1
- package/dist/sql/result-mapper.d.ts +7 -0
- package/dist/utils.d.ts +1 -1
- package/dist/value-wrappers-core.d.ts +42 -0
- package/dist/value-wrappers.d.ts +2 -98
- package/package.json +6 -2
- package/src/bin/duckdb-introspect.ts +27 -0
- package/src/client.ts +54 -13
- package/src/columns.ts +10 -10
- package/src/dialect.ts +51 -3
- package/src/driver.ts +204 -7
- package/src/helpers.ts +18 -0
- package/src/index.ts +1 -0
- package/src/introspect.ts +47 -29
- package/src/migrator.ts +1 -1
- package/src/olap.ts +1 -0
- package/src/pool.ts +274 -0
- package/src/session.ts +134 -15
- package/src/sql/query-rewriters.ts +177 -116
- package/src/sql/result-mapper.ts +7 -7
- package/src/utils.ts +1 -1
- package/src/value-wrappers-core.ts +156 -0
- package/src/value-wrappers.ts +60 -219
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bin/duckdb-introspect.ts
|
|
4
|
-
import { DuckDBInstance } from "@duckdb/node-api";
|
|
4
|
+
import { DuckDBInstance as DuckDBInstance3 } from "@duckdb/node-api";
|
|
5
5
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import process from "node:process";
|
|
8
8
|
|
|
9
9
|
// src/driver.ts
|
|
10
|
+
import { DuckDBInstance as DuckDBInstance2 } from "@duckdb/node-api";
|
|
10
11
|
import { entityKind as entityKind3 } from "drizzle-orm/entity";
|
|
11
12
|
import { DefaultLogger } from "drizzle-orm/logger";
|
|
12
13
|
import { PgDatabase } from "drizzle-orm/pg-core/db";
|
|
@@ -23,83 +24,159 @@ import { PgPreparedQuery, PgSession } from "drizzle-orm/pg-core/session";
|
|
|
23
24
|
import { fillPlaceholders, sql } from "drizzle-orm/sql/sql";
|
|
24
25
|
|
|
25
26
|
// src/sql/query-rewriters.ts
|
|
26
|
-
var
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
let inString = false;
|
|
44
|
-
for (;idx >= 0; idx--) {
|
|
45
|
-
const ch = source[idx];
|
|
46
|
-
if (ch === "'" && source[idx - 1] !== "\\") {
|
|
47
|
-
inString = !inString;
|
|
27
|
+
var OPERATORS = [
|
|
28
|
+
{ token: "@>", fn: "array_has_all" },
|
|
29
|
+
{ token: "<@", fn: "array_has_all", swap: true },
|
|
30
|
+
{ token: "&&", fn: "array_has_any" }
|
|
31
|
+
];
|
|
32
|
+
var isWhitespace = (char) => char !== undefined && /\s/.test(char);
|
|
33
|
+
function scrubForRewrite(query) {
|
|
34
|
+
let scrubbed = "";
|
|
35
|
+
let state = "code";
|
|
36
|
+
for (let i = 0;i < query.length; i += 1) {
|
|
37
|
+
const char = query[i];
|
|
38
|
+
const next = query[i + 1];
|
|
39
|
+
if (state === "code") {
|
|
40
|
+
if (char === "'") {
|
|
41
|
+
scrubbed += "'";
|
|
42
|
+
state = "single";
|
|
43
|
+
continue;
|
|
48
44
|
}
|
|
49
|
-
if (
|
|
45
|
+
if (char === '"') {
|
|
46
|
+
scrubbed += '"';
|
|
47
|
+
state = "double";
|
|
50
48
|
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
49
|
}
|
|
50
|
+
if (char === "-" && next === "-") {
|
|
51
|
+
scrubbed += " ";
|
|
52
|
+
i += 1;
|
|
53
|
+
state = "lineComment";
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (char === "/" && next === "*") {
|
|
57
|
+
scrubbed += " ";
|
|
58
|
+
i += 1;
|
|
59
|
+
state = "blockComment";
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
scrubbed += char;
|
|
63
|
+
continue;
|
|
61
64
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
for (;idx < source.length; idx++) {
|
|
72
|
-
const ch = source[idx];
|
|
73
|
-
if (ch === "'" && source[idx - 1] !== "\\") {
|
|
74
|
-
inString = !inString;
|
|
65
|
+
if (state === "single") {
|
|
66
|
+
if (char === "'" && next === "'") {
|
|
67
|
+
scrubbed += "''";
|
|
68
|
+
i += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
scrubbed += char === "'" ? "'" : ".";
|
|
72
|
+
if (char === "'") {
|
|
73
|
+
state = "code";
|
|
75
74
|
}
|
|
76
|
-
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (state === "double") {
|
|
78
|
+
if (char === '"' && next === '"') {
|
|
79
|
+
scrubbed += '""';
|
|
80
|
+
i += 1;
|
|
77
81
|
continue;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
}
|
|
83
|
+
scrubbed += char === '"' ? '"' : ".";
|
|
84
|
+
if (char === '"') {
|
|
85
|
+
state = "code";
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (state === "lineComment") {
|
|
90
|
+
scrubbed += char === `
|
|
91
|
+
` ? `
|
|
92
|
+
` : " ";
|
|
93
|
+
if (char === `
|
|
94
|
+
`) {
|
|
95
|
+
state = "code";
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (state === "blockComment") {
|
|
100
|
+
if (char === "*" && next === "/") {
|
|
101
|
+
scrubbed += " ";
|
|
102
|
+
i += 1;
|
|
103
|
+
state = "code";
|
|
104
|
+
} else {
|
|
105
|
+
scrubbed += " ";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return scrubbed;
|
|
110
|
+
}
|
|
111
|
+
function findNextOperator(scrubbed, start) {
|
|
112
|
+
for (let idx = start;idx < scrubbed.length; idx += 1) {
|
|
113
|
+
for (const operator of OPERATORS) {
|
|
114
|
+
if (scrubbed.startsWith(operator.token, idx)) {
|
|
115
|
+
return { index: idx, operator };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function walkLeft(source, scrubbed, start) {
|
|
122
|
+
let idx = start;
|
|
123
|
+
while (idx >= 0 && isWhitespace(scrubbed[idx])) {
|
|
124
|
+
idx -= 1;
|
|
125
|
+
}
|
|
126
|
+
let depth = 0;
|
|
127
|
+
for (;idx >= 0; idx -= 1) {
|
|
128
|
+
const ch = scrubbed[idx];
|
|
129
|
+
if (ch === ")" || ch === "]") {
|
|
130
|
+
depth += 1;
|
|
131
|
+
} else if (ch === "(" || ch === "[") {
|
|
132
|
+
if (depth === 0) {
|
|
133
|
+
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
134
|
+
}
|
|
135
|
+
depth = Math.max(0, depth - 1);
|
|
136
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
137
|
+
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return [0, source.slice(0, start + 1)];
|
|
141
|
+
}
|
|
142
|
+
function walkRight(source, scrubbed, start) {
|
|
143
|
+
let idx = start;
|
|
144
|
+
while (idx < scrubbed.length && isWhitespace(scrubbed[idx])) {
|
|
145
|
+
idx += 1;
|
|
146
|
+
}
|
|
147
|
+
let depth = 0;
|
|
148
|
+
for (;idx < scrubbed.length; idx += 1) {
|
|
149
|
+
const ch = scrubbed[idx];
|
|
150
|
+
if (ch === "(" || ch === "[") {
|
|
151
|
+
depth += 1;
|
|
152
|
+
} else if (ch === ")" || ch === "]") {
|
|
153
|
+
if (depth === 0) {
|
|
86
154
|
return [idx, source.slice(start, idx)];
|
|
87
155
|
}
|
|
156
|
+
depth = Math.max(0, depth - 1);
|
|
157
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
158
|
+
return [idx, source.slice(start, idx)];
|
|
88
159
|
}
|
|
89
|
-
|
|
90
|
-
|
|
160
|
+
}
|
|
161
|
+
return [scrubbed.length, source.slice(start)];
|
|
162
|
+
}
|
|
163
|
+
function adaptArrayOperators(query) {
|
|
91
164
|
let rewritten = query;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
165
|
+
let scrubbed = scrubForRewrite(query);
|
|
166
|
+
let searchStart = 0;
|
|
167
|
+
while (true) {
|
|
168
|
+
const next = findNextOperator(scrubbed, searchStart);
|
|
169
|
+
if (!next)
|
|
170
|
+
break;
|
|
171
|
+
const { index, operator } = next;
|
|
172
|
+
const [leftStart, leftExpr] = walkLeft(rewritten, scrubbed, index - 1);
|
|
173
|
+
const [rightEnd, rightExpr] = walkRight(rewritten, scrubbed, index + operator.token.length);
|
|
174
|
+
const left = leftExpr.trim();
|
|
175
|
+
const right = rightExpr.trim();
|
|
176
|
+
const replacement = `${operator.fn}(${operator.swap ? right : left}, ${operator.swap ? left : right})`;
|
|
177
|
+
rewritten = rewritten.slice(0, leftStart) + replacement + rewritten.slice(rightEnd);
|
|
178
|
+
scrubbed = scrubForRewrite(rewritten);
|
|
179
|
+
searchStart = leftStart + replacement.length;
|
|
103
180
|
}
|
|
104
181
|
return rewritten;
|
|
105
182
|
}
|
|
@@ -325,11 +402,21 @@ import {
|
|
|
325
402
|
timestampValue,
|
|
326
403
|
timestampTZValue
|
|
327
404
|
} from "@duckdb/node-api";
|
|
405
|
+
|
|
406
|
+
// src/value-wrappers-core.ts
|
|
328
407
|
var DUCKDB_VALUE_MARKER = Symbol.for("drizzle-duckdb:value");
|
|
408
|
+
|
|
409
|
+
// src/value-wrappers.ts
|
|
329
410
|
function dateToMicros(value) {
|
|
330
411
|
if (value instanceof Date) {
|
|
331
412
|
return BigInt(value.getTime()) * 1000n;
|
|
332
413
|
}
|
|
414
|
+
if (typeof value === "bigint") {
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
if (typeof value === "number") {
|
|
418
|
+
return BigInt(Math.trunc(value)) * 1000n;
|
|
419
|
+
}
|
|
333
420
|
let normalized = value;
|
|
334
421
|
if (!value.includes("T") && value.includes(" ")) {
|
|
335
422
|
normalized = value.replace(" ", "T");
|
|
@@ -383,6 +470,9 @@ function wrapperToNodeApiValue(wrapper, toValue) {
|
|
|
383
470
|
}
|
|
384
471
|
|
|
385
472
|
// src/client.ts
|
|
473
|
+
function isPool(client) {
|
|
474
|
+
return typeof client.acquire === "function";
|
|
475
|
+
}
|
|
386
476
|
function isPgArrayLiteral(value) {
|
|
387
477
|
return value.startsWith("{") && value.endsWith("}");
|
|
388
478
|
}
|
|
@@ -394,18 +484,19 @@ function parsePgArrayLiteral(value) {
|
|
|
394
484
|
return value;
|
|
395
485
|
}
|
|
396
486
|
}
|
|
397
|
-
var warnedArrayLiteral = false;
|
|
398
487
|
function prepareParams(params, options = {}) {
|
|
399
488
|
return params.map((param) => {
|
|
400
|
-
if (typeof param === "string"
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
options.warnOnStringArrayLiteral
|
|
489
|
+
if (typeof param === "string") {
|
|
490
|
+
const trimmed = param.trim();
|
|
491
|
+
if (trimmed && isPgArrayLiteral(trimmed)) {
|
|
492
|
+
if (options.rejectStringArrayLiterals) {
|
|
493
|
+
throw new Error("Stringified array literals are not supported. Use duckDbList()/duckDbArray() or pass native arrays.");
|
|
494
|
+
}
|
|
495
|
+
if (options.warnOnStringArrayLiteral) {
|
|
496
|
+
options.warnOnStringArrayLiteral();
|
|
497
|
+
}
|
|
498
|
+
return parsePgArrayLiteral(trimmed);
|
|
407
499
|
}
|
|
408
|
-
return parsePgArrayLiteral(param);
|
|
409
500
|
}
|
|
410
501
|
return param;
|
|
411
502
|
});
|
|
@@ -445,7 +536,28 @@ function mapRowsToObjects(columns, rows) {
|
|
|
445
536
|
return obj;
|
|
446
537
|
});
|
|
447
538
|
}
|
|
539
|
+
async function closeClientConnection(connection) {
|
|
540
|
+
if ("close" in connection && typeof connection.close === "function") {
|
|
541
|
+
await connection.close();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if ("closeSync" in connection && typeof connection.closeSync === "function") {
|
|
545
|
+
connection.closeSync();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if ("disconnectSync" in connection && typeof connection.disconnectSync === "function") {
|
|
549
|
+
connection.disconnectSync();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
448
552
|
async function executeOnClient(client, query, params) {
|
|
553
|
+
if (isPool(client)) {
|
|
554
|
+
const connection = await client.acquire();
|
|
555
|
+
try {
|
|
556
|
+
return await executeOnClient(connection, query, params);
|
|
557
|
+
} finally {
|
|
558
|
+
await client.release(connection);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
449
561
|
const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
|
|
450
562
|
const result = await client.run(query, values);
|
|
451
563
|
const rows = await result.getRowsJS();
|
|
@@ -454,6 +566,15 @@ async function executeOnClient(client, query, params) {
|
|
|
454
566
|
return rows ? mapRowsToObjects(uniqueColumns, rows) : [];
|
|
455
567
|
}
|
|
456
568
|
async function* executeInBatches(client, query, params, options = {}) {
|
|
569
|
+
if (isPool(client)) {
|
|
570
|
+
const connection = await client.acquire();
|
|
571
|
+
try {
|
|
572
|
+
yield* executeInBatches(connection, query, params, options);
|
|
573
|
+
return;
|
|
574
|
+
} finally {
|
|
575
|
+
await client.release(connection);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
457
578
|
const rowsPerChunk = options.rowsPerChunk && options.rowsPerChunk > 0 ? options.rowsPerChunk : 1e5;
|
|
458
579
|
const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
|
|
459
580
|
const result = await client.stream(query, values);
|
|
@@ -475,6 +596,14 @@ async function* executeInBatches(client, query, params, options = {}) {
|
|
|
475
596
|
}
|
|
476
597
|
}
|
|
477
598
|
async function executeArrowOnClient(client, query, params) {
|
|
599
|
+
if (isPool(client)) {
|
|
600
|
+
const connection = await client.acquire();
|
|
601
|
+
try {
|
|
602
|
+
return await executeArrowOnClient(connection, query, params);
|
|
603
|
+
} finally {
|
|
604
|
+
await client.release(connection);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
478
607
|
const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
|
|
479
608
|
const result = await client.run(query, values);
|
|
480
609
|
const maybeArrow = result.toArrow ?? result.getArrowTable;
|
|
@@ -485,6 +614,13 @@ async function executeArrowOnClient(client, query, params) {
|
|
|
485
614
|
}
|
|
486
615
|
|
|
487
616
|
// src/session.ts
|
|
617
|
+
function isSavepointSyntaxError(error) {
|
|
618
|
+
if (!(error instanceof Error) || !error.message) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
return error.message.toLowerCase().includes("savepoint") && error.message.toLowerCase().includes("syntax error");
|
|
622
|
+
}
|
|
623
|
+
|
|
488
624
|
class DuckDBPreparedQuery extends PgPreparedQuery {
|
|
489
625
|
client;
|
|
490
626
|
dialect;
|
|
@@ -549,6 +685,7 @@ class DuckDBSession extends PgSession {
|
|
|
549
685
|
rewriteArrays;
|
|
550
686
|
rejectStringArrayLiterals;
|
|
551
687
|
hasWarnedArrayLiteral = false;
|
|
688
|
+
rollbackOnly = false;
|
|
552
689
|
constructor(client, dialect, schema, options = {}) {
|
|
553
690
|
super(dialect);
|
|
554
691
|
this.client = client;
|
|
@@ -562,17 +699,46 @@ class DuckDBSession extends PgSession {
|
|
|
562
699
|
prepareQuery(query, fields, name, isResponseInArrayMode, customResultMapper) {
|
|
563
700
|
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);
|
|
564
701
|
}
|
|
565
|
-
|
|
566
|
-
|
|
702
|
+
execute(query) {
|
|
703
|
+
this.dialect.resetPgJsonFlag();
|
|
704
|
+
return super.execute(query);
|
|
705
|
+
}
|
|
706
|
+
all(query) {
|
|
707
|
+
this.dialect.resetPgJsonFlag();
|
|
708
|
+
return super.all(query);
|
|
709
|
+
}
|
|
710
|
+
async transaction(transaction, config) {
|
|
711
|
+
let pinnedConnection;
|
|
712
|
+
let pool;
|
|
713
|
+
let clientForTx = this.client;
|
|
714
|
+
if (isPool(this.client)) {
|
|
715
|
+
pool = this.client;
|
|
716
|
+
pinnedConnection = await pool.acquire();
|
|
717
|
+
clientForTx = pinnedConnection;
|
|
718
|
+
}
|
|
719
|
+
const session = new DuckDBSession(clientForTx, this.dialect, this.schema, this.options);
|
|
567
720
|
const tx = new DuckDBTransaction(this.dialect, session, this.schema);
|
|
568
|
-
await tx.execute(sql`BEGIN TRANSACTION;`);
|
|
569
721
|
try {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
722
|
+
await tx.execute(sql`BEGIN TRANSACTION;`);
|
|
723
|
+
if (config) {
|
|
724
|
+
await tx.setTransaction(config);
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
const result = await transaction(tx);
|
|
728
|
+
if (session.isRollbackOnly()) {
|
|
729
|
+
await tx.execute(sql`rollback`);
|
|
730
|
+
throw new TransactionRollbackError;
|
|
731
|
+
}
|
|
732
|
+
await tx.execute(sql`commit`);
|
|
733
|
+
return result;
|
|
734
|
+
} catch (error) {
|
|
735
|
+
await tx.execute(sql`rollback`);
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
} finally {
|
|
739
|
+
if (pinnedConnection && pool) {
|
|
740
|
+
await pool.release(pinnedConnection);
|
|
741
|
+
}
|
|
576
742
|
}
|
|
577
743
|
}
|
|
578
744
|
warnOnStringArrayLiteral = (query) => {
|
|
@@ -584,7 +750,9 @@ class DuckDBSession extends PgSession {
|
|
|
584
750
|
query: ${query}`, []);
|
|
585
751
|
};
|
|
586
752
|
executeBatches(query, options = {}) {
|
|
753
|
+
this.dialect.resetPgJsonFlag();
|
|
587
754
|
const builtQuery = this.dialect.sqlToQuery(query);
|
|
755
|
+
this.dialect.assertNoPgJsonColumns();
|
|
588
756
|
const params = prepareParams(builtQuery.params, {
|
|
589
757
|
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
590
758
|
warnOnStringArrayLiteral: this.rejectStringArrayLiterals ? undefined : () => this.warnOnStringArrayLiteral(builtQuery.sql)
|
|
@@ -597,7 +765,9 @@ query: ${query}`, []);
|
|
|
597
765
|
return executeInBatches(this.client, rewrittenQuery, params, options);
|
|
598
766
|
}
|
|
599
767
|
async executeArrow(query) {
|
|
768
|
+
this.dialect.resetPgJsonFlag();
|
|
600
769
|
const builtQuery = this.dialect.sqlToQuery(query);
|
|
770
|
+
this.dialect.assertNoPgJsonColumns();
|
|
601
771
|
const params = prepareParams(builtQuery.params, {
|
|
602
772
|
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
603
773
|
warnOnStringArrayLiteral: this.rejectStringArrayLiterals ? undefined : () => this.warnOnStringArrayLiteral(builtQuery.sql)
|
|
@@ -609,6 +779,12 @@ query: ${query}`, []);
|
|
|
609
779
|
this.logger.logQuery(rewrittenQuery, params);
|
|
610
780
|
return executeArrowOnClient(this.client, rewrittenQuery, params);
|
|
611
781
|
}
|
|
782
|
+
markRollbackOnly() {
|
|
783
|
+
this.rollbackOnly = true;
|
|
784
|
+
}
|
|
785
|
+
isRollbackOnly() {
|
|
786
|
+
return this.rollbackOnly;
|
|
787
|
+
}
|
|
612
788
|
}
|
|
613
789
|
|
|
614
790
|
class DuckDBTransaction extends PgTransaction {
|
|
@@ -639,8 +815,46 @@ class DuckDBTransaction extends PgTransaction {
|
|
|
639
815
|
return this.session.executeArrow(query);
|
|
640
816
|
}
|
|
641
817
|
async transaction(transaction) {
|
|
642
|
-
const
|
|
643
|
-
|
|
818
|
+
const internals = this;
|
|
819
|
+
const savepoint = `drizzle_savepoint_${this.nestedIndex + 1}`;
|
|
820
|
+
const savepointSql = sql.raw(`savepoint ${savepoint}`);
|
|
821
|
+
const releaseSql = sql.raw(`release savepoint ${savepoint}`);
|
|
822
|
+
const rollbackSql = sql.raw(`rollback to savepoint ${savepoint}`);
|
|
823
|
+
const nestedTx = new DuckDBTransaction(internals.dialect, internals.session, this.schema, this.nestedIndex + 1);
|
|
824
|
+
if (internals.dialect.areSavepointsUnsupported()) {
|
|
825
|
+
return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
|
|
826
|
+
}
|
|
827
|
+
let createdSavepoint = false;
|
|
828
|
+
try {
|
|
829
|
+
await internals.session.execute(savepointSql);
|
|
830
|
+
internals.dialect.markSavepointsSupported();
|
|
831
|
+
createdSavepoint = true;
|
|
832
|
+
} catch (error) {
|
|
833
|
+
if (!isSavepointSyntaxError(error)) {
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
internals.dialect.markSavepointsUnsupported();
|
|
837
|
+
return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
|
|
838
|
+
}
|
|
839
|
+
try {
|
|
840
|
+
const result = await transaction(nestedTx);
|
|
841
|
+
if (createdSavepoint) {
|
|
842
|
+
await internals.session.execute(releaseSql);
|
|
843
|
+
}
|
|
844
|
+
return result;
|
|
845
|
+
} catch (error) {
|
|
846
|
+
if (createdSavepoint) {
|
|
847
|
+
await internals.session.execute(rollbackSql);
|
|
848
|
+
}
|
|
849
|
+
internals.session.markRollbackOnly();
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
runNestedWithoutSavepoint(transaction, nestedTx, internals) {
|
|
854
|
+
return transaction(nestedTx).catch((error) => {
|
|
855
|
+
internals.session.markRollbackOnly();
|
|
856
|
+
throw error;
|
|
857
|
+
});
|
|
644
858
|
}
|
|
645
859
|
}
|
|
646
860
|
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.";
|
|
@@ -662,15 +876,30 @@ import {
|
|
|
662
876
|
import {
|
|
663
877
|
sql as sql2
|
|
664
878
|
} from "drizzle-orm";
|
|
665
|
-
|
|
666
879
|
class DuckDBDialect extends PgDialect {
|
|
667
880
|
static [entityKind2] = "DuckDBPgDialect";
|
|
668
881
|
hasPgJsonColumn = false;
|
|
882
|
+
savepointsSupported = 0 /* Unknown */;
|
|
883
|
+
resetPgJsonFlag() {
|
|
884
|
+
this.hasPgJsonColumn = false;
|
|
885
|
+
}
|
|
886
|
+
markPgJsonDetected() {
|
|
887
|
+
this.hasPgJsonColumn = true;
|
|
888
|
+
}
|
|
669
889
|
assertNoPgJsonColumns() {
|
|
670
890
|
if (this.hasPgJsonColumn) {
|
|
671
|
-
throw new Error("Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB
|
|
891
|
+
throw new Error("Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB's native JSON type.");
|
|
672
892
|
}
|
|
673
893
|
}
|
|
894
|
+
areSavepointsUnsupported() {
|
|
895
|
+
return this.savepointsSupported === 2 /* No */;
|
|
896
|
+
}
|
|
897
|
+
markSavepointsSupported() {
|
|
898
|
+
this.savepointsSupported = 1 /* Yes */;
|
|
899
|
+
}
|
|
900
|
+
markSavepointsUnsupported() {
|
|
901
|
+
this.savepointsSupported = 2 /* No */;
|
|
902
|
+
}
|
|
674
903
|
async migrate(migrations, session, config) {
|
|
675
904
|
const migrationConfig = typeof config === "string" ? { migrationsFolder: config } : config;
|
|
676
905
|
const migrationsSchema = migrationConfig.migrationsSchema ?? "drizzle";
|
|
@@ -707,8 +936,8 @@ class DuckDBDialect extends PgDialect {
|
|
|
707
936
|
}
|
|
708
937
|
prepareTyping(encoder) {
|
|
709
938
|
if (is2(encoder, PgJsonb) || is2(encoder, PgJson)) {
|
|
710
|
-
this.
|
|
711
|
-
|
|
939
|
+
this.markPgJsonDetected();
|
|
940
|
+
throw new Error("Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB's native JSON type.");
|
|
712
941
|
} else if (is2(encoder, PgNumeric)) {
|
|
713
942
|
return "decimal";
|
|
714
943
|
} else if (is2(encoder, PgTime2)) {
|
|
@@ -820,6 +1049,179 @@ class DuckDBSelectBuilder extends PgSelectBuilder {
|
|
|
820
1049
|
}
|
|
821
1050
|
}
|
|
822
1051
|
|
|
1052
|
+
// src/pool.ts
|
|
1053
|
+
import { DuckDBConnection } from "@duckdb/node-api";
|
|
1054
|
+
var POOL_PRESETS = {
|
|
1055
|
+
pulse: 4,
|
|
1056
|
+
standard: 6,
|
|
1057
|
+
jumbo: 8,
|
|
1058
|
+
mega: 12,
|
|
1059
|
+
giga: 16,
|
|
1060
|
+
local: 8,
|
|
1061
|
+
memory: 4
|
|
1062
|
+
};
|
|
1063
|
+
function resolvePoolSize(pool) {
|
|
1064
|
+
if (pool === false)
|
|
1065
|
+
return false;
|
|
1066
|
+
if (pool === undefined)
|
|
1067
|
+
return 4;
|
|
1068
|
+
if (typeof pool === "string")
|
|
1069
|
+
return POOL_PRESETS[pool];
|
|
1070
|
+
return pool.size ?? 4;
|
|
1071
|
+
}
|
|
1072
|
+
function createDuckDBConnectionPool(instance, options = {}) {
|
|
1073
|
+
const size = options.size && options.size > 0 ? options.size : 4;
|
|
1074
|
+
const acquireTimeout = options.acquireTimeout ?? 30000;
|
|
1075
|
+
const maxWaitingRequests = options.maxWaitingRequests ?? 100;
|
|
1076
|
+
const maxLifetimeMs = options.maxLifetimeMs;
|
|
1077
|
+
const idleTimeoutMs = options.idleTimeoutMs;
|
|
1078
|
+
const metadata = new WeakMap;
|
|
1079
|
+
const idle = [];
|
|
1080
|
+
const waiting = [];
|
|
1081
|
+
let total = 0;
|
|
1082
|
+
let closed = false;
|
|
1083
|
+
let pendingAcquires = 0;
|
|
1084
|
+
const shouldRecycle = (conn, now) => {
|
|
1085
|
+
if (maxLifetimeMs !== undefined && now - conn.createdAt >= maxLifetimeMs) {
|
|
1086
|
+
return true;
|
|
1087
|
+
}
|
|
1088
|
+
if (idleTimeoutMs !== undefined && now - conn.lastUsedAt >= idleTimeoutMs) {
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
return false;
|
|
1092
|
+
};
|
|
1093
|
+
const acquire = async () => {
|
|
1094
|
+
if (closed) {
|
|
1095
|
+
throw new Error("DuckDB connection pool is closed");
|
|
1096
|
+
}
|
|
1097
|
+
while (idle.length > 0) {
|
|
1098
|
+
const pooled = idle.pop();
|
|
1099
|
+
const now = Date.now();
|
|
1100
|
+
if (shouldRecycle(pooled, now)) {
|
|
1101
|
+
await closeClientConnection(pooled.connection);
|
|
1102
|
+
total = Math.max(0, total - 1);
|
|
1103
|
+
metadata.delete(pooled.connection);
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
pooled.lastUsedAt = now;
|
|
1107
|
+
metadata.set(pooled.connection, {
|
|
1108
|
+
createdAt: pooled.createdAt,
|
|
1109
|
+
lastUsedAt: pooled.lastUsedAt
|
|
1110
|
+
});
|
|
1111
|
+
return pooled.connection;
|
|
1112
|
+
}
|
|
1113
|
+
if (total < size) {
|
|
1114
|
+
pendingAcquires += 1;
|
|
1115
|
+
total += 1;
|
|
1116
|
+
try {
|
|
1117
|
+
const connection = await DuckDBConnection.create(instance);
|
|
1118
|
+
if (closed) {
|
|
1119
|
+
await closeClientConnection(connection);
|
|
1120
|
+
total -= 1;
|
|
1121
|
+
throw new Error("DuckDB connection pool is closed");
|
|
1122
|
+
}
|
|
1123
|
+
const now = Date.now();
|
|
1124
|
+
metadata.set(connection, { createdAt: now, lastUsedAt: now });
|
|
1125
|
+
return connection;
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
total -= 1;
|
|
1128
|
+
throw error;
|
|
1129
|
+
} finally {
|
|
1130
|
+
pendingAcquires -= 1;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (waiting.length >= maxWaitingRequests) {
|
|
1134
|
+
throw new Error(`DuckDB connection pool queue is full (max ${maxWaitingRequests} waiting requests)`);
|
|
1135
|
+
}
|
|
1136
|
+
return await new Promise((resolve, reject) => {
|
|
1137
|
+
const timeoutId = setTimeout(() => {
|
|
1138
|
+
const idx = waiting.findIndex((w) => w.timeoutId === timeoutId);
|
|
1139
|
+
if (idx !== -1) {
|
|
1140
|
+
waiting.splice(idx, 1);
|
|
1141
|
+
}
|
|
1142
|
+
reject(new Error(`DuckDB connection pool acquire timeout after ${acquireTimeout}ms`));
|
|
1143
|
+
}, acquireTimeout);
|
|
1144
|
+
waiting.push({ resolve, reject, timeoutId });
|
|
1145
|
+
});
|
|
1146
|
+
};
|
|
1147
|
+
const release = async (connection) => {
|
|
1148
|
+
const waiter = waiting.shift();
|
|
1149
|
+
if (waiter) {
|
|
1150
|
+
clearTimeout(waiter.timeoutId);
|
|
1151
|
+
const now2 = Date.now();
|
|
1152
|
+
const meta = metadata.get(connection) ?? { createdAt: now2, lastUsedAt: now2 };
|
|
1153
|
+
const expired = maxLifetimeMs !== undefined && now2 - meta.createdAt >= maxLifetimeMs;
|
|
1154
|
+
if (closed) {
|
|
1155
|
+
await closeClientConnection(connection);
|
|
1156
|
+
total = Math.max(0, total - 1);
|
|
1157
|
+
metadata.delete(connection);
|
|
1158
|
+
waiter.reject(new Error("DuckDB connection pool is closed"));
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (expired) {
|
|
1162
|
+
await closeClientConnection(connection);
|
|
1163
|
+
total = Math.max(0, total - 1);
|
|
1164
|
+
metadata.delete(connection);
|
|
1165
|
+
try {
|
|
1166
|
+
const replacement = await acquire();
|
|
1167
|
+
waiter.resolve(replacement);
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
waiter.reject(error);
|
|
1170
|
+
}
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
meta.lastUsedAt = now2;
|
|
1174
|
+
metadata.set(connection, meta);
|
|
1175
|
+
waiter.resolve(connection);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (closed) {
|
|
1179
|
+
await closeClientConnection(connection);
|
|
1180
|
+
metadata.delete(connection);
|
|
1181
|
+
total = Math.max(0, total - 1);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
const now = Date.now();
|
|
1185
|
+
const existingMeta = metadata.get(connection) ?? { createdAt: now, lastUsedAt: now };
|
|
1186
|
+
existingMeta.lastUsedAt = now;
|
|
1187
|
+
metadata.set(connection, existingMeta);
|
|
1188
|
+
if (maxLifetimeMs !== undefined && now - existingMeta.createdAt >= maxLifetimeMs) {
|
|
1189
|
+
await closeClientConnection(connection);
|
|
1190
|
+
total -= 1;
|
|
1191
|
+
metadata.delete(connection);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
idle.push({
|
|
1195
|
+
connection,
|
|
1196
|
+
createdAt: existingMeta.createdAt,
|
|
1197
|
+
lastUsedAt: existingMeta.lastUsedAt
|
|
1198
|
+
});
|
|
1199
|
+
};
|
|
1200
|
+
const close = async () => {
|
|
1201
|
+
closed = true;
|
|
1202
|
+
const waiters = waiting.splice(0, waiting.length);
|
|
1203
|
+
for (const waiter of waiters) {
|
|
1204
|
+
clearTimeout(waiter.timeoutId);
|
|
1205
|
+
waiter.reject(new Error("DuckDB connection pool is closed"));
|
|
1206
|
+
}
|
|
1207
|
+
const toClose = idle.splice(0, idle.length);
|
|
1208
|
+
await Promise.allSettled(toClose.map((item) => closeClientConnection(item.connection)));
|
|
1209
|
+
total = Math.max(0, total - toClose.length);
|
|
1210
|
+
toClose.forEach((item) => metadata.delete(item.connection));
|
|
1211
|
+
const maxWait = 5000;
|
|
1212
|
+
const start = Date.now();
|
|
1213
|
+
while (pendingAcquires > 0 && Date.now() - start < maxWait) {
|
|
1214
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
return {
|
|
1218
|
+
acquire,
|
|
1219
|
+
release,
|
|
1220
|
+
close,
|
|
1221
|
+
size
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
823
1225
|
// src/driver.ts
|
|
824
1226
|
class DuckDBDriver {
|
|
825
1227
|
client;
|
|
@@ -839,7 +1241,14 @@ class DuckDBDriver {
|
|
|
839
1241
|
});
|
|
840
1242
|
}
|
|
841
1243
|
}
|
|
842
|
-
function
|
|
1244
|
+
function isConfigObject(data) {
|
|
1245
|
+
if (typeof data !== "object" || data === null)
|
|
1246
|
+
return false;
|
|
1247
|
+
if (data.constructor?.name !== "Object")
|
|
1248
|
+
return false;
|
|
1249
|
+
return "connection" in data || "client" in data || "pool" in data || "schema" in data || "logger" in data;
|
|
1250
|
+
}
|
|
1251
|
+
function createFromClient(client, config = {}, instance) {
|
|
843
1252
|
const dialect = new DuckDBDialect;
|
|
844
1253
|
const logger = config.logger === true ? new DefaultLogger : config.logger || undefined;
|
|
845
1254
|
let schema;
|
|
@@ -857,17 +1266,71 @@ function drizzle(client, config = {}) {
|
|
|
857
1266
|
rejectStringArrayLiterals: config.rejectStringArrayLiterals
|
|
858
1267
|
});
|
|
859
1268
|
const session = driver.createSession(schema);
|
|
860
|
-
|
|
1269
|
+
const db = new DuckDBDatabase(dialect, session, schema, client, instance);
|
|
1270
|
+
return db;
|
|
1271
|
+
}
|
|
1272
|
+
async function createFromConnectionString(path, instanceOptions, config = {}) {
|
|
1273
|
+
const instance = await DuckDBInstance2.create(path, instanceOptions);
|
|
1274
|
+
const poolSize = resolvePoolSize(config.pool);
|
|
1275
|
+
if (poolSize === false) {
|
|
1276
|
+
const connection = await instance.connect();
|
|
1277
|
+
return createFromClient(connection, config, instance);
|
|
1278
|
+
}
|
|
1279
|
+
const pool = createDuckDBConnectionPool(instance, { size: poolSize });
|
|
1280
|
+
return createFromClient(pool, config, instance);
|
|
1281
|
+
}
|
|
1282
|
+
function drizzle(clientOrConfigOrPath, config) {
|
|
1283
|
+
if (typeof clientOrConfigOrPath === "string") {
|
|
1284
|
+
return createFromConnectionString(clientOrConfigOrPath, undefined, config);
|
|
1285
|
+
}
|
|
1286
|
+
if (isConfigObject(clientOrConfigOrPath)) {
|
|
1287
|
+
const configObj = clientOrConfigOrPath;
|
|
1288
|
+
if ("connection" in configObj) {
|
|
1289
|
+
const connConfig = configObj;
|
|
1290
|
+
const { connection, ...restConfig } = connConfig;
|
|
1291
|
+
if (typeof connection === "string") {
|
|
1292
|
+
return createFromConnectionString(connection, undefined, restConfig);
|
|
1293
|
+
}
|
|
1294
|
+
return createFromConnectionString(connection.path, connection.options, restConfig);
|
|
1295
|
+
}
|
|
1296
|
+
if ("client" in configObj) {
|
|
1297
|
+
const clientConfig = configObj;
|
|
1298
|
+
const { client: clientValue, ...restConfig } = clientConfig;
|
|
1299
|
+
return createFromClient(clientValue, restConfig);
|
|
1300
|
+
}
|
|
1301
|
+
throw new Error("Invalid drizzle config: either connection or client must be provided");
|
|
1302
|
+
}
|
|
1303
|
+
return createFromClient(clientOrConfigOrPath, config);
|
|
861
1304
|
}
|
|
862
1305
|
|
|
863
1306
|
class DuckDBDatabase extends PgDatabase {
|
|
864
1307
|
dialect;
|
|
865
1308
|
session;
|
|
866
1309
|
static [entityKind3] = "DuckDBDatabase";
|
|
867
|
-
|
|
1310
|
+
$client;
|
|
1311
|
+
$instance;
|
|
1312
|
+
constructor(dialect, session, schema, client, instance) {
|
|
868
1313
|
super(dialect, session, schema);
|
|
869
1314
|
this.dialect = dialect;
|
|
870
1315
|
this.session = session;
|
|
1316
|
+
this.$client = client;
|
|
1317
|
+
this.$instance = instance;
|
|
1318
|
+
}
|
|
1319
|
+
async close() {
|
|
1320
|
+
if (isPool(this.$client) && this.$client.close) {
|
|
1321
|
+
await this.$client.close();
|
|
1322
|
+
}
|
|
1323
|
+
if (!isPool(this.$client)) {
|
|
1324
|
+
await closeClientConnection(this.$client);
|
|
1325
|
+
}
|
|
1326
|
+
if (this.$instance) {
|
|
1327
|
+
const maybeClosable = this.$instance;
|
|
1328
|
+
if (typeof maybeClosable.close === "function") {
|
|
1329
|
+
await maybeClosable.close();
|
|
1330
|
+
} else if (typeof maybeClosable.closeSync === "function") {
|
|
1331
|
+
maybeClosable.closeSync();
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
871
1334
|
}
|
|
872
1335
|
select(fields) {
|
|
873
1336
|
const selectedFields = fields ? aliasFields(fields) : undefined;
|
|
@@ -891,7 +1354,7 @@ class DuckDBDatabase extends PgDatabase {
|
|
|
891
1354
|
// src/introspect.ts
|
|
892
1355
|
import { sql as sql4 } from "drizzle-orm";
|
|
893
1356
|
var SYSTEM_SCHEMAS = new Set(["information_schema", "pg_catalog"]);
|
|
894
|
-
var DEFAULT_IMPORT_BASE = "@leonardovida-md/drizzle-neo-duckdb";
|
|
1357
|
+
var DEFAULT_IMPORT_BASE = "@leonardovida-md/drizzle-neo-duckdb/helpers";
|
|
895
1358
|
async function introspect(db, opts = {}) {
|
|
896
1359
|
const database = await resolveDatabase(db, opts.database, opts.allDatabases);
|
|
897
1360
|
const schemas = await resolveSchemas(db, database, opts.schemas);
|
|
@@ -1227,6 +1690,22 @@ function mapDuckDbType(column, imports, options) {
|
|
|
1227
1690
|
imports.pgCore.add("doublePrecision");
|
|
1228
1691
|
return { builder: `doublePrecision(${columnName(column.name)})` };
|
|
1229
1692
|
}
|
|
1693
|
+
const arrayMatch = /^(.*)\[(\d+)\]$/.exec(upper);
|
|
1694
|
+
if (arrayMatch) {
|
|
1695
|
+
imports.local.add("duckDbArray");
|
|
1696
|
+
const [, base, length] = arrayMatch;
|
|
1697
|
+
return {
|
|
1698
|
+
builder: `duckDbArray(${columnName(column.name)}, ${JSON.stringify(base)}, ${Number(length)})`
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
const listMatch = /^(.*)\[\]$/.exec(upper);
|
|
1702
|
+
if (listMatch) {
|
|
1703
|
+
imports.local.add("duckDbList");
|
|
1704
|
+
const [, base] = listMatch;
|
|
1705
|
+
return {
|
|
1706
|
+
builder: `duckDbList(${columnName(column.name)}, ${JSON.stringify(base)})`
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1230
1709
|
if (upper.startsWith("CHAR(") || upper === "CHAR") {
|
|
1231
1710
|
imports.pgCore.add("char");
|
|
1232
1711
|
const length = column.characterLength;
|
|
@@ -1255,6 +1734,20 @@ function mapDuckDbType(column, imports, options) {
|
|
|
1255
1734
|
imports.pgCore.add("text");
|
|
1256
1735
|
return { builder: `text(${columnName(column.name)}) /* JSON */` };
|
|
1257
1736
|
}
|
|
1737
|
+
if (upper.startsWith("ENUM")) {
|
|
1738
|
+
imports.pgCore.add("text");
|
|
1739
|
+
const enumLiteral = raw.replace(/^ENUM\s*/i, "").trim();
|
|
1740
|
+
return {
|
|
1741
|
+
builder: `text(${columnName(column.name)}) /* ENUM ${enumLiteral} */`
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
if (upper.startsWith("UNION")) {
|
|
1745
|
+
imports.pgCore.add("text");
|
|
1746
|
+
const unionLiteral = raw.replace(/^UNION\s*/i, "").trim();
|
|
1747
|
+
return {
|
|
1748
|
+
builder: `text(${columnName(column.name)}) /* UNION ${unionLiteral} */`
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1258
1751
|
if (upper === "INET") {
|
|
1259
1752
|
imports.local.add("duckDbInet");
|
|
1260
1753
|
return { builder: `duckDbInet(${columnName(column.name)})` };
|
|
@@ -1267,22 +1760,6 @@ function mapDuckDbType(column, imports, options) {
|
|
|
1267
1760
|
imports.local.add("duckDbBlob");
|
|
1268
1761
|
return { builder: `duckDbBlob(${columnName(column.name)})` };
|
|
1269
1762
|
}
|
|
1270
|
-
const arrayMatch = /^(.*)\[(\d+)\]$/.exec(upper);
|
|
1271
|
-
if (arrayMatch) {
|
|
1272
|
-
imports.local.add("duckDbArray");
|
|
1273
|
-
const [, base, length] = arrayMatch;
|
|
1274
|
-
return {
|
|
1275
|
-
builder: `duckDbArray(${columnName(column.name)}, ${JSON.stringify(base)}, ${Number(length)})`
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
const listMatch = /^(.*)\[\]$/.exec(upper);
|
|
1279
|
-
if (listMatch) {
|
|
1280
|
-
imports.local.add("duckDbList");
|
|
1281
|
-
const [, base] = listMatch;
|
|
1282
|
-
return {
|
|
1283
|
-
builder: `duckDbList(${columnName(column.name)}, ${JSON.stringify(base)})`
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
1763
|
if (upper.startsWith("STRUCT")) {
|
|
1287
1764
|
imports.local.add("duckDbStruct");
|
|
1288
1765
|
const inner = upper.replace(/^STRUCT\s*\(/i, "").replace(/\)$/, "");
|
|
@@ -1336,7 +1813,7 @@ function mapDuckDbType(column, imports, options) {
|
|
|
1336
1813
|
}
|
|
1337
1814
|
imports.pgCore.add("text");
|
|
1338
1815
|
return {
|
|
1339
|
-
builder: `text(${columnName(column.name)}) /*
|
|
1816
|
+
builder: `text(${columnName(column.name)}) /* unsupported DuckDB type: ${upper} */`
|
|
1340
1817
|
};
|
|
1341
1818
|
}
|
|
1342
1819
|
function parseStructFields(inner) {
|
|
@@ -1446,6 +1923,7 @@ function renderImports(imports, importBasePath) {
|
|
|
1446
1923
|
function parseArgs(argv) {
|
|
1447
1924
|
const options = {
|
|
1448
1925
|
outFile: path.resolve(process.cwd(), "drizzle/schema.ts"),
|
|
1926
|
+
outMeta: undefined,
|
|
1449
1927
|
allDatabases: false,
|
|
1450
1928
|
includeViews: false,
|
|
1451
1929
|
useCustomTimeTypes: true
|
|
@@ -1471,6 +1949,11 @@ function parseArgs(argv) {
|
|
|
1471
1949
|
case "--outFile":
|
|
1472
1950
|
options.outFile = path.resolve(process.cwd(), argv[++i] ?? "drizzle/schema.ts");
|
|
1473
1951
|
break;
|
|
1952
|
+
case "--out-json":
|
|
1953
|
+
case "--outJson":
|
|
1954
|
+
case "--json":
|
|
1955
|
+
options.outMeta = path.resolve(process.cwd(), argv[++i] ?? "drizzle/schema.meta.json");
|
|
1956
|
+
break;
|
|
1474
1957
|
case "--include-views":
|
|
1475
1958
|
case "--includeViews":
|
|
1476
1959
|
options.includeViews = true;
|
|
@@ -1505,6 +1988,7 @@ Options:
|
|
|
1505
1988
|
--all-databases Introspect all attached databases (not just current)
|
|
1506
1989
|
--schema Comma separated schema list (defaults to all non-system schemas)
|
|
1507
1990
|
--out Output file (default: ./drizzle/schema.ts)
|
|
1991
|
+
--json Optional JSON metadata output (default: ./drizzle/schema.meta.json)
|
|
1508
1992
|
--include-views Include views in the generated schema
|
|
1509
1993
|
--use-pg-time Use pg-core timestamp/date/time instead of DuckDB custom helpers
|
|
1510
1994
|
--import-base Override import path for duckdb helpers (default: package name)
|
|
@@ -1531,7 +2015,7 @@ async function main() {
|
|
|
1531
2015
|
throw new Error("Missing required --url");
|
|
1532
2016
|
}
|
|
1533
2017
|
const instanceOptions = options.url.startsWith("md:") && process.env.MOTHERDUCK_TOKEN ? { motherduck_token: process.env.MOTHERDUCK_TOKEN } : undefined;
|
|
1534
|
-
const instance = await
|
|
2018
|
+
const instance = await DuckDBInstance3.create(options.url, instanceOptions);
|
|
1535
2019
|
const connection = await instance.connect();
|
|
1536
2020
|
const db = drizzle(connection);
|
|
1537
2021
|
try {
|
|
@@ -1545,11 +2029,23 @@ async function main() {
|
|
|
1545
2029
|
});
|
|
1546
2030
|
await mkdir(path.dirname(options.outFile), { recursive: true });
|
|
1547
2031
|
await writeFile(options.outFile, result.files.schemaTs, "utf8");
|
|
2032
|
+
if (options.outMeta) {
|
|
2033
|
+
await mkdir(path.dirname(options.outMeta), { recursive: true });
|
|
2034
|
+
await writeFile(options.outMeta, JSON.stringify(result.files.metaJson, null, 2), "utf8");
|
|
2035
|
+
}
|
|
1548
2036
|
console.log(`Wrote schema to ${options.outFile}`);
|
|
2037
|
+
if (options.outMeta) {
|
|
2038
|
+
console.log(`Wrote metadata to ${options.outMeta}`);
|
|
2039
|
+
}
|
|
1549
2040
|
} finally {
|
|
1550
2041
|
if ("closeSync" in connection && typeof connection.closeSync === "function") {
|
|
1551
2042
|
connection.closeSync();
|
|
1552
2043
|
}
|
|
2044
|
+
if ("closeSync" in instance && typeof instance.closeSync === "function") {
|
|
2045
|
+
instance.closeSync();
|
|
2046
|
+
} else if ("close" in instance && typeof instance.close === "function") {
|
|
2047
|
+
await instance.close();
|
|
2048
|
+
}
|
|
1553
2049
|
}
|
|
1554
2050
|
}
|
|
1555
2051
|
main().catch((err) => {
|