@leonardovida-md/drizzle-neo-duckdb 1.1.0 → 1.1.2
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 +44 -85
- package/dist/client.d.ts +15 -1
- package/dist/columns.d.ts +8 -2
- package/dist/dialect.d.ts +21 -0
- package/dist/driver.d.ts +7 -3
- package/dist/duckdb-introspect.mjs +667 -133
- package/dist/helpers.mjs +35 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +695 -137
- package/dist/introspect.d.ts +8 -0
- package/dist/options.d.ts +10 -0
- package/dist/pool.d.ts +8 -0
- package/dist/session.d.ts +18 -6
- package/dist/sql/query-rewriters.d.ts +1 -0
- package/dist/sql/result-mapper.d.ts +7 -0
- package/dist/value-wrappers-core.d.ts +2 -2
- package/package.json +1 -1
- package/src/bin/duckdb-introspect.ts +27 -0
- package/src/client.ts +301 -38
- package/src/columns.ts +60 -13
- package/src/dialect.ts +51 -3
- package/src/driver.ts +45 -7
- package/src/index.ts +1 -0
- package/src/introspect.ts +23 -6
- package/src/options.ts +40 -0
- package/src/pool.ts +182 -12
- package/src/session.ts +206 -31
- package/src/sql/query-rewriters.ts +191 -75
- package/src/sql/result-mapper.ts +7 -7
- package/src/value-wrappers-core.ts +2 -2
- package/src/value-wrappers.ts +13 -3
|
@@ -24,83 +24,162 @@ import { PgPreparedQuery, PgSession } from "drizzle-orm/pg-core/session";
|
|
|
24
24
|
import { fillPlaceholders, sql } from "drizzle-orm/sql/sql";
|
|
25
25
|
|
|
26
26
|
// src/sql/query-rewriters.ts
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
break;
|
|
45
|
-
if (ch === "'" && source[idx - 1] !== "\\") {
|
|
46
|
-
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;
|
|
47
44
|
}
|
|
48
|
-
if (
|
|
45
|
+
if (char === '"') {
|
|
46
|
+
scrubbed += '"';
|
|
47
|
+
state = "double";
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (char === "-" && next === "-") {
|
|
51
|
+
scrubbed += " ";
|
|
52
|
+
i += 1;
|
|
53
|
+
state = "lineComment";
|
|
49
54
|
continue;
|
|
50
|
-
if (ch === ")" || ch === "]") {
|
|
51
|
-
depth++;
|
|
52
|
-
} else if (ch === "(" || ch === "[") {
|
|
53
|
-
depth--;
|
|
54
|
-
if (depth < 0) {
|
|
55
|
-
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
56
|
-
}
|
|
57
|
-
} else if (depth === 0 && isWhitespace(ch)) {
|
|
58
|
-
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
59
55
|
}
|
|
56
|
+
if (char === "/" && next === "*") {
|
|
57
|
+
scrubbed += " ";
|
|
58
|
+
i += 1;
|
|
59
|
+
state = "blockComment";
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
scrubbed += char;
|
|
63
|
+
continue;
|
|
60
64
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
idx++;
|
|
67
|
-
}
|
|
68
|
-
let depth = 0;
|
|
69
|
-
let inString = false;
|
|
70
|
-
for (;idx < source.length; idx++) {
|
|
71
|
-
const ch = source[idx];
|
|
72
|
-
if (ch === undefined)
|
|
73
|
-
break;
|
|
74
|
-
if (ch === "'" && source[idx - 1] !== "\\") {
|
|
75
|
-
inString = !inString;
|
|
65
|
+
if (state === "single") {
|
|
66
|
+
if (char === "'" && next === "'") {
|
|
67
|
+
scrubbed += "''";
|
|
68
|
+
i += 1;
|
|
69
|
+
continue;
|
|
76
70
|
}
|
|
77
|
-
|
|
71
|
+
scrubbed += char === "'" ? "'" : ".";
|
|
72
|
+
if (char === "'") {
|
|
73
|
+
state = "code";
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (state === "double") {
|
|
78
|
+
if (char === '"' && next === '"') {
|
|
79
|
+
scrubbed += '""';
|
|
80
|
+
i += 1;
|
|
78
81
|
continue;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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) {
|
|
87
154
|
return [idx, source.slice(start, idx)];
|
|
88
155
|
}
|
|
156
|
+
depth = Math.max(0, depth - 1);
|
|
157
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
158
|
+
return [idx, source.slice(start, idx)];
|
|
89
159
|
}
|
|
90
|
-
|
|
91
|
-
|
|
160
|
+
}
|
|
161
|
+
return [scrubbed.length, source.slice(start)];
|
|
162
|
+
}
|
|
163
|
+
function adaptArrayOperators(query) {
|
|
164
|
+
if (query.indexOf("@>") === -1 && query.indexOf("<@") === -1 && query.indexOf("&&") === -1) {
|
|
165
|
+
return query;
|
|
166
|
+
}
|
|
92
167
|
let rewritten = query;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
168
|
+
let scrubbed = scrubForRewrite(query);
|
|
169
|
+
let searchStart = 0;
|
|
170
|
+
while (true) {
|
|
171
|
+
const next = findNextOperator(scrubbed, searchStart);
|
|
172
|
+
if (!next)
|
|
173
|
+
break;
|
|
174
|
+
const { index, operator } = next;
|
|
175
|
+
const [leftStart, leftExpr] = walkLeft(rewritten, scrubbed, index - 1);
|
|
176
|
+
const [rightEnd, rightExpr] = walkRight(rewritten, scrubbed, index + operator.token.length);
|
|
177
|
+
const left = leftExpr.trim();
|
|
178
|
+
const right = rightExpr.trim();
|
|
179
|
+
const replacement = `${operator.fn}(${operator.swap ? right : left}, ${operator.swap ? left : right})`;
|
|
180
|
+
rewritten = rewritten.slice(0, leftStart) + replacement + rewritten.slice(rightEnd);
|
|
181
|
+
scrubbed = scrubForRewrite(rewritten);
|
|
182
|
+
searchStart = leftStart + replacement.length;
|
|
104
183
|
}
|
|
105
184
|
return rewritten;
|
|
106
185
|
}
|
|
@@ -335,6 +414,12 @@ function dateToMicros(value) {
|
|
|
335
414
|
if (value instanceof Date) {
|
|
336
415
|
return BigInt(value.getTime()) * 1000n;
|
|
337
416
|
}
|
|
417
|
+
if (typeof value === "bigint") {
|
|
418
|
+
return value;
|
|
419
|
+
}
|
|
420
|
+
if (typeof value === "number") {
|
|
421
|
+
return BigInt(Math.trunc(value)) * 1000n;
|
|
422
|
+
}
|
|
338
423
|
let normalized = value;
|
|
339
424
|
if (!value.includes("T") && value.includes(" ")) {
|
|
340
425
|
normalized = value.replace(" ", "T");
|
|
@@ -391,6 +476,7 @@ function wrapperToNodeApiValue(wrapper, toValue) {
|
|
|
391
476
|
function isPool(client) {
|
|
392
477
|
return typeof client.acquire === "function";
|
|
393
478
|
}
|
|
479
|
+
var PREPARED_CACHE = Symbol.for("drizzle-duckdb:prepared-cache");
|
|
394
480
|
function isPgArrayLiteral(value) {
|
|
395
481
|
return value.startsWith("{") && value.endsWith("}");
|
|
396
482
|
}
|
|
@@ -404,14 +490,21 @@ function parsePgArrayLiteral(value) {
|
|
|
404
490
|
}
|
|
405
491
|
function prepareParams(params, options = {}) {
|
|
406
492
|
return params.map((param) => {
|
|
407
|
-
if (typeof param === "string" &&
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
493
|
+
if (typeof param === "string" && param.length > 0) {
|
|
494
|
+
const firstChar = param[0];
|
|
495
|
+
const maybeArrayLiteral = firstChar === "{" || firstChar === "[" || firstChar === " " || firstChar === "\t";
|
|
496
|
+
if (maybeArrayLiteral) {
|
|
497
|
+
const trimmed = firstChar === "{" || firstChar === "[" ? param : param.trim();
|
|
498
|
+
if (trimmed && isPgArrayLiteral(trimmed)) {
|
|
499
|
+
if (options.rejectStringArrayLiterals) {
|
|
500
|
+
throw new Error("Stringified array literals are not supported. Use duckDbList()/duckDbArray() or pass native arrays.");
|
|
501
|
+
}
|
|
502
|
+
if (options.warnOnStringArrayLiteral) {
|
|
503
|
+
options.warnOnStringArrayLiteral();
|
|
504
|
+
}
|
|
505
|
+
return parsePgArrayLiteral(trimmed);
|
|
506
|
+
}
|
|
413
507
|
}
|
|
414
|
-
return parsePgArrayLiteral(param);
|
|
415
508
|
}
|
|
416
509
|
return param;
|
|
417
510
|
});
|
|
@@ -435,13 +528,121 @@ function toNodeApiValue(value) {
|
|
|
435
528
|
return value;
|
|
436
529
|
}
|
|
437
530
|
function deduplicateColumns(columns) {
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
531
|
+
const counts = new Map;
|
|
532
|
+
let hasDuplicates = false;
|
|
533
|
+
for (const column of columns) {
|
|
534
|
+
const next = (counts.get(column) ?? 0) + 1;
|
|
535
|
+
counts.set(column, next);
|
|
536
|
+
if (next > 1) {
|
|
537
|
+
hasDuplicates = true;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!hasDuplicates) {
|
|
542
|
+
return columns;
|
|
543
|
+
}
|
|
544
|
+
counts.clear();
|
|
545
|
+
return columns.map((column) => {
|
|
546
|
+
const count = counts.get(column) ?? 0;
|
|
547
|
+
counts.set(column, count + 1);
|
|
548
|
+
return count === 0 ? column : `${column}_${count}`;
|
|
443
549
|
});
|
|
444
550
|
}
|
|
551
|
+
function destroyPreparedStatement(entry) {
|
|
552
|
+
if (!entry)
|
|
553
|
+
return;
|
|
554
|
+
try {
|
|
555
|
+
entry.statement.destroySync();
|
|
556
|
+
} catch {}
|
|
557
|
+
}
|
|
558
|
+
function getPreparedCache(connection, size) {
|
|
559
|
+
const store = connection;
|
|
560
|
+
const existing = store[PREPARED_CACHE];
|
|
561
|
+
if (existing) {
|
|
562
|
+
existing.size = size;
|
|
563
|
+
return existing;
|
|
564
|
+
}
|
|
565
|
+
const cache = { size, entries: new Map };
|
|
566
|
+
store[PREPARED_CACHE] = cache;
|
|
567
|
+
return cache;
|
|
568
|
+
}
|
|
569
|
+
function evictOldest(cache) {
|
|
570
|
+
const oldest = cache.entries.keys().next();
|
|
571
|
+
if (!oldest.done) {
|
|
572
|
+
const key = oldest.value;
|
|
573
|
+
const entry = cache.entries.get(key);
|
|
574
|
+
cache.entries.delete(key);
|
|
575
|
+
destroyPreparedStatement(entry);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function evictCacheEntry(cache, key) {
|
|
579
|
+
const entry = cache.entries.get(key);
|
|
580
|
+
cache.entries.delete(key);
|
|
581
|
+
destroyPreparedStatement(entry);
|
|
582
|
+
}
|
|
583
|
+
async function getOrPrepareStatement(connection, query, cacheConfig) {
|
|
584
|
+
const cache = getPreparedCache(connection, cacheConfig.size);
|
|
585
|
+
const cached = cache.entries.get(query);
|
|
586
|
+
if (cached) {
|
|
587
|
+
cache.entries.delete(query);
|
|
588
|
+
cache.entries.set(query, cached);
|
|
589
|
+
return cached.statement;
|
|
590
|
+
}
|
|
591
|
+
const statement = await connection.prepare(query);
|
|
592
|
+
cache.entries.set(query, { statement });
|
|
593
|
+
while (cache.entries.size > cache.size) {
|
|
594
|
+
evictOldest(cache);
|
|
595
|
+
}
|
|
596
|
+
return statement;
|
|
597
|
+
}
|
|
598
|
+
async function materializeResultRows(result) {
|
|
599
|
+
const rows = await result.getRowsJS() ?? [];
|
|
600
|
+
const baseColumns = typeof result.deduplicatedColumnNames === "function" ? result.deduplicatedColumnNames() : result.columnNames();
|
|
601
|
+
const columns = typeof result.deduplicatedColumnNames === "function" ? baseColumns : deduplicateColumns(baseColumns);
|
|
602
|
+
return { columns, rows };
|
|
603
|
+
}
|
|
604
|
+
async function materializeRows(client, query, params, options = {}) {
|
|
605
|
+
if (isPool(client)) {
|
|
606
|
+
const connection2 = await client.acquire();
|
|
607
|
+
try {
|
|
608
|
+
return await materializeRows(connection2, query, params, options);
|
|
609
|
+
} finally {
|
|
610
|
+
await client.release(connection2);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
|
|
614
|
+
const connection = client;
|
|
615
|
+
if (options.prepareCache && typeof connection.prepare === "function") {
|
|
616
|
+
const cache = getPreparedCache(connection, options.prepareCache.size);
|
|
617
|
+
try {
|
|
618
|
+
const statement = await getOrPrepareStatement(connection, query, options.prepareCache);
|
|
619
|
+
if (values) {
|
|
620
|
+
statement.bind(values);
|
|
621
|
+
} else {
|
|
622
|
+
statement.clearBindings?.();
|
|
623
|
+
}
|
|
624
|
+
const result2 = await statement.run();
|
|
625
|
+
cache.entries.delete(query);
|
|
626
|
+
cache.entries.set(query, { statement });
|
|
627
|
+
return await materializeResultRows(result2);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
evictCacheEntry(cache, query);
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const result = await connection.run(query, values);
|
|
634
|
+
return await materializeResultRows(result);
|
|
635
|
+
}
|
|
636
|
+
function clearPreparedCache(connection) {
|
|
637
|
+
const store = connection;
|
|
638
|
+
const cache = store[PREPARED_CACHE];
|
|
639
|
+
if (!cache)
|
|
640
|
+
return;
|
|
641
|
+
for (const entry of cache.entries.values()) {
|
|
642
|
+
destroyPreparedStatement(entry);
|
|
643
|
+
}
|
|
644
|
+
cache.entries.clear();
|
|
645
|
+
}
|
|
445
646
|
function mapRowsToObjects(columns, rows) {
|
|
446
647
|
return rows.map((vals) => {
|
|
447
648
|
const obj = {};
|
|
@@ -452,6 +653,7 @@ function mapRowsToObjects(columns, rows) {
|
|
|
452
653
|
});
|
|
453
654
|
}
|
|
454
655
|
async function closeClientConnection(connection) {
|
|
656
|
+
clearPreparedCache(connection);
|
|
455
657
|
if ("close" in connection && typeof connection.close === "function") {
|
|
456
658
|
await connection.close();
|
|
457
659
|
return;
|
|
@@ -464,27 +666,51 @@ async function closeClientConnection(connection) {
|
|
|
464
666
|
connection.disconnectSync();
|
|
465
667
|
}
|
|
466
668
|
}
|
|
467
|
-
async function executeOnClient(client, query, params) {
|
|
669
|
+
async function executeOnClient(client, query, params, options = {}) {
|
|
670
|
+
const { columns, rows } = await materializeRows(client, query, params, options);
|
|
671
|
+
if (!rows || rows.length === 0) {
|
|
672
|
+
return [];
|
|
673
|
+
}
|
|
674
|
+
return mapRowsToObjects(columns, rows);
|
|
675
|
+
}
|
|
676
|
+
async function executeArraysOnClient(client, query, params, options = {}) {
|
|
677
|
+
return await materializeRows(client, query, params, options);
|
|
678
|
+
}
|
|
679
|
+
async function* executeInBatches(client, query, params, options = {}) {
|
|
468
680
|
if (isPool(client)) {
|
|
469
681
|
const connection = await client.acquire();
|
|
470
682
|
try {
|
|
471
|
-
|
|
683
|
+
yield* executeInBatches(connection, query, params, options);
|
|
684
|
+
return;
|
|
472
685
|
} finally {
|
|
473
686
|
await client.release(connection);
|
|
474
687
|
}
|
|
475
688
|
}
|
|
689
|
+
const rowsPerChunk = options.rowsPerChunk && options.rowsPerChunk > 0 ? options.rowsPerChunk : 1e5;
|
|
476
690
|
const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
|
|
477
|
-
const result = await client.
|
|
478
|
-
const
|
|
479
|
-
const columns = result.deduplicatedColumnNames
|
|
480
|
-
|
|
481
|
-
|
|
691
|
+
const result = await client.stream(query, values);
|
|
692
|
+
const rawColumns = typeof result.deduplicatedColumnNames === "function" ? result.deduplicatedColumnNames() : result.columnNames();
|
|
693
|
+
const columns = typeof result.deduplicatedColumnNames === "function" ? rawColumns : deduplicateColumns(rawColumns);
|
|
694
|
+
let buffer = [];
|
|
695
|
+
for await (const chunk of result.yieldRowsJs()) {
|
|
696
|
+
const objects = mapRowsToObjects(columns, chunk);
|
|
697
|
+
for (const row of objects) {
|
|
698
|
+
buffer.push(row);
|
|
699
|
+
if (buffer.length >= rowsPerChunk) {
|
|
700
|
+
yield buffer;
|
|
701
|
+
buffer = [];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (buffer.length > 0) {
|
|
706
|
+
yield buffer;
|
|
707
|
+
}
|
|
482
708
|
}
|
|
483
|
-
async function*
|
|
709
|
+
async function* executeInBatchesRaw(client, query, params, options = {}) {
|
|
484
710
|
if (isPool(client)) {
|
|
485
711
|
const connection = await client.acquire();
|
|
486
712
|
try {
|
|
487
|
-
yield*
|
|
713
|
+
yield* executeInBatchesRaw(connection, query, params, options);
|
|
488
714
|
return;
|
|
489
715
|
} finally {
|
|
490
716
|
await client.release(connection);
|
|
@@ -493,21 +719,20 @@ async function* executeInBatches(client, query, params, options = {}) {
|
|
|
493
719
|
const rowsPerChunk = options.rowsPerChunk && options.rowsPerChunk > 0 ? options.rowsPerChunk : 1e5;
|
|
494
720
|
const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
|
|
495
721
|
const result = await client.stream(query, values);
|
|
496
|
-
const
|
|
497
|
-
const
|
|
722
|
+
const rawColumns = typeof result.deduplicatedColumnNames === "function" ? result.deduplicatedColumnNames() : result.columnNames();
|
|
723
|
+
const columns = typeof result.deduplicatedColumnNames === "function" ? rawColumns : deduplicateColumns(rawColumns);
|
|
498
724
|
let buffer = [];
|
|
499
725
|
for await (const chunk of result.yieldRowsJs()) {
|
|
500
|
-
const
|
|
501
|
-
for (const row of objects) {
|
|
726
|
+
for (const row of chunk) {
|
|
502
727
|
buffer.push(row);
|
|
503
728
|
if (buffer.length >= rowsPerChunk) {
|
|
504
|
-
yield buffer;
|
|
729
|
+
yield { columns, rows: buffer };
|
|
505
730
|
buffer = [];
|
|
506
731
|
}
|
|
507
732
|
}
|
|
508
733
|
}
|
|
509
734
|
if (buffer.length > 0) {
|
|
510
|
-
yield buffer;
|
|
735
|
+
yield { columns, rows: buffer };
|
|
511
736
|
}
|
|
512
737
|
}
|
|
513
738
|
async function executeArrowOnClient(client, query, params) {
|
|
@@ -529,6 +754,20 @@ async function executeArrowOnClient(client, query, params) {
|
|
|
529
754
|
}
|
|
530
755
|
|
|
531
756
|
// src/session.ts
|
|
757
|
+
function isSavepointSyntaxError(error) {
|
|
758
|
+
if (!(error instanceof Error) || !error.message) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
return error.message.toLowerCase().includes("savepoint") && error.message.toLowerCase().includes("syntax error");
|
|
762
|
+
}
|
|
763
|
+
function rewriteQuery(mode, query) {
|
|
764
|
+
if (mode === "never") {
|
|
765
|
+
return { sql: query, rewritten: false };
|
|
766
|
+
}
|
|
767
|
+
const rewritten = adaptArrayOperators(query);
|
|
768
|
+
return { sql: rewritten, rewritten: rewritten !== query };
|
|
769
|
+
}
|
|
770
|
+
|
|
532
771
|
class DuckDBPreparedQuery extends PgPreparedQuery {
|
|
533
772
|
client;
|
|
534
773
|
dialect;
|
|
@@ -538,11 +777,12 @@ class DuckDBPreparedQuery extends PgPreparedQuery {
|
|
|
538
777
|
fields;
|
|
539
778
|
_isResponseInArrayMode;
|
|
540
779
|
customResultMapper;
|
|
541
|
-
|
|
780
|
+
rewriteArraysMode;
|
|
542
781
|
rejectStringArrayLiterals;
|
|
782
|
+
prepareCache;
|
|
543
783
|
warnOnStringArrayLiteral;
|
|
544
784
|
static [entityKind] = "DuckDBPreparedQuery";
|
|
545
|
-
constructor(client, dialect, queryString, params, logger, fields, _isResponseInArrayMode, customResultMapper,
|
|
785
|
+
constructor(client, dialect, queryString, params, logger, fields, _isResponseInArrayMode, customResultMapper, rewriteArraysMode, rejectStringArrayLiterals, prepareCache, warnOnStringArrayLiteral) {
|
|
546
786
|
super({ sql: queryString, params });
|
|
547
787
|
this.client = client;
|
|
548
788
|
this.dialect = dialect;
|
|
@@ -552,8 +792,9 @@ class DuckDBPreparedQuery extends PgPreparedQuery {
|
|
|
552
792
|
this.fields = fields;
|
|
553
793
|
this._isResponseInArrayMode = _isResponseInArrayMode;
|
|
554
794
|
this.customResultMapper = customResultMapper;
|
|
555
|
-
this.
|
|
795
|
+
this.rewriteArraysMode = rewriteArraysMode;
|
|
556
796
|
this.rejectStringArrayLiterals = rejectStringArrayLiterals;
|
|
797
|
+
this.prepareCache = prepareCache;
|
|
557
798
|
this.warnOnStringArrayLiteral = warnOnStringArrayLiteral;
|
|
558
799
|
}
|
|
559
800
|
async execute(placeholderValues = {}) {
|
|
@@ -562,18 +803,23 @@ class DuckDBPreparedQuery extends PgPreparedQuery {
|
|
|
562
803
|
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
563
804
|
warnOnStringArrayLiteral: this.warnOnStringArrayLiteral ? () => this.warnOnStringArrayLiteral?.(this.queryString) : undefined
|
|
564
805
|
});
|
|
565
|
-
const rewrittenQuery
|
|
566
|
-
if (
|
|
806
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(this.rewriteArraysMode, this.queryString);
|
|
807
|
+
if (didRewrite) {
|
|
567
808
|
this.logger.logQuery(`[duckdb] original query before array rewrite: ${this.queryString}`, params);
|
|
568
809
|
}
|
|
569
810
|
this.logger.logQuery(rewrittenQuery, params);
|
|
570
811
|
const { fields, joinsNotNullableMap, customResultMapper } = this;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
812
|
+
if (fields) {
|
|
813
|
+
const { rows: rows2 } = await executeArraysOnClient(this.client, rewrittenQuery, params, { prepareCache: this.prepareCache });
|
|
814
|
+
if (rows2.length === 0) {
|
|
815
|
+
return [];
|
|
816
|
+
}
|
|
817
|
+
return customResultMapper ? customResultMapper(rows2) : rows2.map((row) => mapResultRow(fields, row, joinsNotNullableMap));
|
|
574
818
|
}
|
|
575
|
-
const
|
|
576
|
-
|
|
819
|
+
const rows = await executeOnClient(this.client, rewrittenQuery, params, {
|
|
820
|
+
prepareCache: this.prepareCache
|
|
821
|
+
});
|
|
822
|
+
return rows;
|
|
577
823
|
}
|
|
578
824
|
all(placeholderValues = {}) {
|
|
579
825
|
return this.execute(placeholderValues);
|
|
@@ -590,9 +836,11 @@ class DuckDBSession extends PgSession {
|
|
|
590
836
|
static [entityKind] = "DuckDBSession";
|
|
591
837
|
dialect;
|
|
592
838
|
logger;
|
|
593
|
-
|
|
839
|
+
rewriteArraysMode;
|
|
594
840
|
rejectStringArrayLiterals;
|
|
841
|
+
prepareCache;
|
|
595
842
|
hasWarnedArrayLiteral = false;
|
|
843
|
+
rollbackOnly = false;
|
|
596
844
|
constructor(client, dialect, schema, options = {}) {
|
|
597
845
|
super(dialect);
|
|
598
846
|
this.client = client;
|
|
@@ -600,13 +848,27 @@ class DuckDBSession extends PgSession {
|
|
|
600
848
|
this.options = options;
|
|
601
849
|
this.dialect = dialect;
|
|
602
850
|
this.logger = options.logger ?? new NoopLogger;
|
|
603
|
-
this.
|
|
851
|
+
this.rewriteArraysMode = options.rewriteArrays ?? "auto";
|
|
604
852
|
this.rejectStringArrayLiterals = options.rejectStringArrayLiterals ?? false;
|
|
853
|
+
this.prepareCache = options.prepareCache;
|
|
854
|
+
this.options = {
|
|
855
|
+
...options,
|
|
856
|
+
rewriteArrays: this.rewriteArraysMode,
|
|
857
|
+
prepareCache: this.prepareCache
|
|
858
|
+
};
|
|
605
859
|
}
|
|
606
860
|
prepareQuery(query, fields, name, isResponseInArrayMode, customResultMapper) {
|
|
607
|
-
return new DuckDBPreparedQuery(this.client, this.dialect, query.sql, query.params, this.logger, fields, isResponseInArrayMode, customResultMapper, this.
|
|
861
|
+
return new DuckDBPreparedQuery(this.client, this.dialect, query.sql, query.params, this.logger, fields, isResponseInArrayMode, customResultMapper, this.rewriteArraysMode, this.rejectStringArrayLiterals, this.prepareCache, this.rejectStringArrayLiterals ? undefined : this.warnOnStringArrayLiteral);
|
|
608
862
|
}
|
|
609
|
-
|
|
863
|
+
execute(query) {
|
|
864
|
+
this.dialect.resetPgJsonFlag();
|
|
865
|
+
return super.execute(query);
|
|
866
|
+
}
|
|
867
|
+
all(query) {
|
|
868
|
+
this.dialect.resetPgJsonFlag();
|
|
869
|
+
return super.all(query);
|
|
870
|
+
}
|
|
871
|
+
async transaction(transaction, config) {
|
|
610
872
|
let pinnedConnection;
|
|
611
873
|
let pool;
|
|
612
874
|
let clientForTx = this.client;
|
|
@@ -619,8 +881,15 @@ class DuckDBSession extends PgSession {
|
|
|
619
881
|
const tx = new DuckDBTransaction(this.dialect, session, this.schema);
|
|
620
882
|
try {
|
|
621
883
|
await tx.execute(sql`BEGIN TRANSACTION;`);
|
|
884
|
+
if (config) {
|
|
885
|
+
await tx.setTransaction(config);
|
|
886
|
+
}
|
|
622
887
|
try {
|
|
623
888
|
const result = await transaction(tx);
|
|
889
|
+
if (session.isRollbackOnly()) {
|
|
890
|
+
await tx.execute(sql`rollback`);
|
|
891
|
+
throw new TransactionRollbackError;
|
|
892
|
+
}
|
|
624
893
|
await tx.execute(sql`commit`);
|
|
625
894
|
return result;
|
|
626
895
|
} catch (error) {
|
|
@@ -642,31 +911,56 @@ class DuckDBSession extends PgSession {
|
|
|
642
911
|
query: ${query}`, []);
|
|
643
912
|
};
|
|
644
913
|
executeBatches(query, options = {}) {
|
|
914
|
+
this.dialect.resetPgJsonFlag();
|
|
645
915
|
const builtQuery = this.dialect.sqlToQuery(query);
|
|
916
|
+
this.dialect.assertNoPgJsonColumns();
|
|
646
917
|
const params = prepareParams(builtQuery.params, {
|
|
647
918
|
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
648
919
|
warnOnStringArrayLiteral: this.rejectStringArrayLiterals ? undefined : () => this.warnOnStringArrayLiteral(builtQuery.sql)
|
|
649
920
|
});
|
|
650
|
-
const rewrittenQuery = this.
|
|
651
|
-
if (
|
|
921
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(this.rewriteArraysMode, builtQuery.sql);
|
|
922
|
+
if (didRewrite) {
|
|
652
923
|
this.logger.logQuery(`[duckdb] original query before array rewrite: ${builtQuery.sql}`, params);
|
|
653
924
|
}
|
|
654
925
|
this.logger.logQuery(rewrittenQuery, params);
|
|
655
926
|
return executeInBatches(this.client, rewrittenQuery, params, options);
|
|
656
927
|
}
|
|
928
|
+
executeBatchesRaw(query, options = {}) {
|
|
929
|
+
this.dialect.resetPgJsonFlag();
|
|
930
|
+
const builtQuery = this.dialect.sqlToQuery(query);
|
|
931
|
+
this.dialect.assertNoPgJsonColumns();
|
|
932
|
+
const params = prepareParams(builtQuery.params, {
|
|
933
|
+
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
934
|
+
warnOnStringArrayLiteral: this.rejectStringArrayLiterals ? undefined : () => this.warnOnStringArrayLiteral(builtQuery.sql)
|
|
935
|
+
});
|
|
936
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(this.rewriteArraysMode, builtQuery.sql);
|
|
937
|
+
if (didRewrite) {
|
|
938
|
+
this.logger.logQuery(`[duckdb] original query before array rewrite: ${builtQuery.sql}`, params);
|
|
939
|
+
}
|
|
940
|
+
this.logger.logQuery(rewrittenQuery, params);
|
|
941
|
+
return executeInBatchesRaw(this.client, rewrittenQuery, params, options);
|
|
942
|
+
}
|
|
657
943
|
async executeArrow(query) {
|
|
944
|
+
this.dialect.resetPgJsonFlag();
|
|
658
945
|
const builtQuery = this.dialect.sqlToQuery(query);
|
|
946
|
+
this.dialect.assertNoPgJsonColumns();
|
|
659
947
|
const params = prepareParams(builtQuery.params, {
|
|
660
948
|
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
661
949
|
warnOnStringArrayLiteral: this.rejectStringArrayLiterals ? undefined : () => this.warnOnStringArrayLiteral(builtQuery.sql)
|
|
662
950
|
});
|
|
663
|
-
const rewrittenQuery = this.
|
|
664
|
-
if (
|
|
951
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(this.rewriteArraysMode, builtQuery.sql);
|
|
952
|
+
if (didRewrite) {
|
|
665
953
|
this.logger.logQuery(`[duckdb] original query before array rewrite: ${builtQuery.sql}`, params);
|
|
666
954
|
}
|
|
667
955
|
this.logger.logQuery(rewrittenQuery, params);
|
|
668
956
|
return executeArrowOnClient(this.client, rewrittenQuery, params);
|
|
669
957
|
}
|
|
958
|
+
markRollbackOnly() {
|
|
959
|
+
this.rollbackOnly = true;
|
|
960
|
+
}
|
|
961
|
+
isRollbackOnly() {
|
|
962
|
+
return this.rollbackOnly;
|
|
963
|
+
}
|
|
670
964
|
}
|
|
671
965
|
|
|
672
966
|
class DuckDBTransaction extends PgTransaction {
|
|
@@ -693,12 +987,53 @@ class DuckDBTransaction extends PgTransaction {
|
|
|
693
987
|
executeBatches(query, options = {}) {
|
|
694
988
|
return this.session.executeBatches(query, options);
|
|
695
989
|
}
|
|
990
|
+
executeBatchesRaw(query, options = {}) {
|
|
991
|
+
return this.session.executeBatchesRaw(query, options);
|
|
992
|
+
}
|
|
696
993
|
executeArrow(query) {
|
|
697
994
|
return this.session.executeArrow(query);
|
|
698
995
|
}
|
|
699
996
|
async transaction(transaction) {
|
|
700
|
-
const
|
|
701
|
-
|
|
997
|
+
const internals = this;
|
|
998
|
+
const savepoint = `drizzle_savepoint_${this.nestedIndex + 1}`;
|
|
999
|
+
const savepointSql = sql.raw(`savepoint ${savepoint}`);
|
|
1000
|
+
const releaseSql = sql.raw(`release savepoint ${savepoint}`);
|
|
1001
|
+
const rollbackSql = sql.raw(`rollback to savepoint ${savepoint}`);
|
|
1002
|
+
const nestedTx = new DuckDBTransaction(internals.dialect, internals.session, this.schema, this.nestedIndex + 1);
|
|
1003
|
+
if (internals.dialect.areSavepointsUnsupported()) {
|
|
1004
|
+
return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
|
|
1005
|
+
}
|
|
1006
|
+
let createdSavepoint = false;
|
|
1007
|
+
try {
|
|
1008
|
+
await internals.session.execute(savepointSql);
|
|
1009
|
+
internals.dialect.markSavepointsSupported();
|
|
1010
|
+
createdSavepoint = true;
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
if (!isSavepointSyntaxError(error)) {
|
|
1013
|
+
throw error;
|
|
1014
|
+
}
|
|
1015
|
+
internals.dialect.markSavepointsUnsupported();
|
|
1016
|
+
return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
|
|
1017
|
+
}
|
|
1018
|
+
try {
|
|
1019
|
+
const result = await transaction(nestedTx);
|
|
1020
|
+
if (createdSavepoint) {
|
|
1021
|
+
await internals.session.execute(releaseSql);
|
|
1022
|
+
}
|
|
1023
|
+
return result;
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
if (createdSavepoint) {
|
|
1026
|
+
await internals.session.execute(rollbackSql);
|
|
1027
|
+
}
|
|
1028
|
+
internals.session.markRollbackOnly();
|
|
1029
|
+
throw error;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
runNestedWithoutSavepoint(transaction, nestedTx, internals) {
|
|
1033
|
+
return transaction(nestedTx).catch((error) => {
|
|
1034
|
+
internals.session.markRollbackOnly();
|
|
1035
|
+
throw error;
|
|
1036
|
+
});
|
|
702
1037
|
}
|
|
703
1038
|
}
|
|
704
1039
|
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.";
|
|
@@ -720,15 +1055,30 @@ import {
|
|
|
720
1055
|
import {
|
|
721
1056
|
sql as sql2
|
|
722
1057
|
} from "drizzle-orm";
|
|
723
|
-
|
|
724
1058
|
class DuckDBDialect extends PgDialect {
|
|
725
1059
|
static [entityKind2] = "DuckDBPgDialect";
|
|
726
1060
|
hasPgJsonColumn = false;
|
|
1061
|
+
savepointsSupported = 0 /* Unknown */;
|
|
1062
|
+
resetPgJsonFlag() {
|
|
1063
|
+
this.hasPgJsonColumn = false;
|
|
1064
|
+
}
|
|
1065
|
+
markPgJsonDetected() {
|
|
1066
|
+
this.hasPgJsonColumn = true;
|
|
1067
|
+
}
|
|
727
1068
|
assertNoPgJsonColumns() {
|
|
728
1069
|
if (this.hasPgJsonColumn) {
|
|
729
|
-
throw new Error("Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB
|
|
1070
|
+
throw new Error("Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB's native JSON type.");
|
|
730
1071
|
}
|
|
731
1072
|
}
|
|
1073
|
+
areSavepointsUnsupported() {
|
|
1074
|
+
return this.savepointsSupported === 2 /* No */;
|
|
1075
|
+
}
|
|
1076
|
+
markSavepointsSupported() {
|
|
1077
|
+
this.savepointsSupported = 1 /* Yes */;
|
|
1078
|
+
}
|
|
1079
|
+
markSavepointsUnsupported() {
|
|
1080
|
+
this.savepointsSupported = 2 /* No */;
|
|
1081
|
+
}
|
|
732
1082
|
async migrate(migrations, session, config) {
|
|
733
1083
|
const migrationConfig = typeof config === "string" ? { migrationsFolder: config } : config;
|
|
734
1084
|
const migrationsSchema = migrationConfig.migrationsSchema ?? "drizzle";
|
|
@@ -765,8 +1115,8 @@ class DuckDBDialect extends PgDialect {
|
|
|
765
1115
|
}
|
|
766
1116
|
prepareTyping(encoder) {
|
|
767
1117
|
if (is2(encoder, PgJsonb) || is2(encoder, PgJson)) {
|
|
768
|
-
this.
|
|
769
|
-
|
|
1118
|
+
this.markPgJsonDetected();
|
|
1119
|
+
throw new Error("Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB's native JSON type.");
|
|
770
1120
|
} else if (is2(encoder, PgNumeric)) {
|
|
771
1121
|
return "decimal";
|
|
772
1122
|
} else if (is2(encoder, PgTime2)) {
|
|
@@ -900,41 +1250,148 @@ function resolvePoolSize(pool) {
|
|
|
900
1250
|
}
|
|
901
1251
|
function createDuckDBConnectionPool(instance, options = {}) {
|
|
902
1252
|
const size = options.size && options.size > 0 ? options.size : 4;
|
|
1253
|
+
const acquireTimeout = options.acquireTimeout ?? 30000;
|
|
1254
|
+
const maxWaitingRequests = options.maxWaitingRequests ?? 100;
|
|
1255
|
+
const maxLifetimeMs = options.maxLifetimeMs;
|
|
1256
|
+
const idleTimeoutMs = options.idleTimeoutMs;
|
|
1257
|
+
const metadata = new WeakMap;
|
|
903
1258
|
const idle = [];
|
|
904
1259
|
const waiting = [];
|
|
905
1260
|
let total = 0;
|
|
906
1261
|
let closed = false;
|
|
1262
|
+
let pendingAcquires = 0;
|
|
1263
|
+
const shouldRecycle = (conn, now) => {
|
|
1264
|
+
if (maxLifetimeMs !== undefined && now - conn.createdAt >= maxLifetimeMs) {
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
if (idleTimeoutMs !== undefined && now - conn.lastUsedAt >= idleTimeoutMs) {
|
|
1268
|
+
return true;
|
|
1269
|
+
}
|
|
1270
|
+
return false;
|
|
1271
|
+
};
|
|
907
1272
|
const acquire = async () => {
|
|
908
1273
|
if (closed) {
|
|
909
1274
|
throw new Error("DuckDB connection pool is closed");
|
|
910
1275
|
}
|
|
911
|
-
|
|
912
|
-
|
|
1276
|
+
while (idle.length > 0) {
|
|
1277
|
+
const pooled = idle.pop();
|
|
1278
|
+
const now = Date.now();
|
|
1279
|
+
if (shouldRecycle(pooled, now)) {
|
|
1280
|
+
await closeClientConnection(pooled.connection);
|
|
1281
|
+
total = Math.max(0, total - 1);
|
|
1282
|
+
metadata.delete(pooled.connection);
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
pooled.lastUsedAt = now;
|
|
1286
|
+
metadata.set(pooled.connection, {
|
|
1287
|
+
createdAt: pooled.createdAt,
|
|
1288
|
+
lastUsedAt: pooled.lastUsedAt
|
|
1289
|
+
});
|
|
1290
|
+
return pooled.connection;
|
|
913
1291
|
}
|
|
914
1292
|
if (total < size) {
|
|
1293
|
+
pendingAcquires += 1;
|
|
915
1294
|
total += 1;
|
|
916
|
-
|
|
1295
|
+
try {
|
|
1296
|
+
const connection = await DuckDBConnection.create(instance);
|
|
1297
|
+
if (closed) {
|
|
1298
|
+
await closeClientConnection(connection);
|
|
1299
|
+
total -= 1;
|
|
1300
|
+
throw new Error("DuckDB connection pool is closed");
|
|
1301
|
+
}
|
|
1302
|
+
const now = Date.now();
|
|
1303
|
+
metadata.set(connection, { createdAt: now, lastUsedAt: now });
|
|
1304
|
+
return connection;
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
total -= 1;
|
|
1307
|
+
throw error;
|
|
1308
|
+
} finally {
|
|
1309
|
+
pendingAcquires -= 1;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (waiting.length >= maxWaitingRequests) {
|
|
1313
|
+
throw new Error(`DuckDB connection pool queue is full (max ${maxWaitingRequests} waiting requests)`);
|
|
917
1314
|
}
|
|
918
|
-
return await new Promise((resolve) => {
|
|
919
|
-
|
|
1315
|
+
return await new Promise((resolve, reject) => {
|
|
1316
|
+
const timeoutId = setTimeout(() => {
|
|
1317
|
+
const idx = waiting.findIndex((w) => w.timeoutId === timeoutId);
|
|
1318
|
+
if (idx !== -1) {
|
|
1319
|
+
waiting.splice(idx, 1);
|
|
1320
|
+
}
|
|
1321
|
+
reject(new Error(`DuckDB connection pool acquire timeout after ${acquireTimeout}ms`));
|
|
1322
|
+
}, acquireTimeout);
|
|
1323
|
+
waiting.push({ resolve, reject, timeoutId });
|
|
920
1324
|
});
|
|
921
1325
|
};
|
|
922
1326
|
const release = async (connection) => {
|
|
1327
|
+
const waiter = waiting.shift();
|
|
1328
|
+
if (waiter) {
|
|
1329
|
+
clearTimeout(waiter.timeoutId);
|
|
1330
|
+
const now2 = Date.now();
|
|
1331
|
+
const meta = metadata.get(connection) ?? { createdAt: now2, lastUsedAt: now2 };
|
|
1332
|
+
const expired = maxLifetimeMs !== undefined && now2 - meta.createdAt >= maxLifetimeMs;
|
|
1333
|
+
if (closed) {
|
|
1334
|
+
await closeClientConnection(connection);
|
|
1335
|
+
total = Math.max(0, total - 1);
|
|
1336
|
+
metadata.delete(connection);
|
|
1337
|
+
waiter.reject(new Error("DuckDB connection pool is closed"));
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
if (expired) {
|
|
1341
|
+
await closeClientConnection(connection);
|
|
1342
|
+
total = Math.max(0, total - 1);
|
|
1343
|
+
metadata.delete(connection);
|
|
1344
|
+
try {
|
|
1345
|
+
const replacement = await acquire();
|
|
1346
|
+
waiter.resolve(replacement);
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
waiter.reject(error);
|
|
1349
|
+
}
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
meta.lastUsedAt = now2;
|
|
1353
|
+
metadata.set(connection, meta);
|
|
1354
|
+
waiter.resolve(connection);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
923
1357
|
if (closed) {
|
|
924
1358
|
await closeClientConnection(connection);
|
|
1359
|
+
metadata.delete(connection);
|
|
1360
|
+
total = Math.max(0, total - 1);
|
|
925
1361
|
return;
|
|
926
1362
|
}
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
1363
|
+
const now = Date.now();
|
|
1364
|
+
const existingMeta = metadata.get(connection) ?? { createdAt: now, lastUsedAt: now };
|
|
1365
|
+
existingMeta.lastUsedAt = now;
|
|
1366
|
+
metadata.set(connection, existingMeta);
|
|
1367
|
+
if (maxLifetimeMs !== undefined && now - existingMeta.createdAt >= maxLifetimeMs) {
|
|
1368
|
+
await closeClientConnection(connection);
|
|
1369
|
+
total -= 1;
|
|
1370
|
+
metadata.delete(connection);
|
|
930
1371
|
return;
|
|
931
1372
|
}
|
|
932
|
-
idle.push(
|
|
1373
|
+
idle.push({
|
|
1374
|
+
connection,
|
|
1375
|
+
createdAt: existingMeta.createdAt,
|
|
1376
|
+
lastUsedAt: existingMeta.lastUsedAt
|
|
1377
|
+
});
|
|
933
1378
|
};
|
|
934
1379
|
const close = async () => {
|
|
935
1380
|
closed = true;
|
|
1381
|
+
const waiters = waiting.splice(0, waiting.length);
|
|
1382
|
+
for (const waiter of waiters) {
|
|
1383
|
+
clearTimeout(waiter.timeoutId);
|
|
1384
|
+
waiter.reject(new Error("DuckDB connection pool is closed"));
|
|
1385
|
+
}
|
|
936
1386
|
const toClose = idle.splice(0, idle.length);
|
|
937
|
-
await Promise.
|
|
1387
|
+
await Promise.allSettled(toClose.map((item) => closeClientConnection(item.connection)));
|
|
1388
|
+
total = Math.max(0, total - toClose.length);
|
|
1389
|
+
toClose.forEach((item) => metadata.delete(item.connection));
|
|
1390
|
+
const maxWait = 5000;
|
|
1391
|
+
const start = Date.now();
|
|
1392
|
+
while (pendingAcquires > 0 && Date.now() - start < maxWait) {
|
|
1393
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1394
|
+
}
|
|
938
1395
|
};
|
|
939
1396
|
return {
|
|
940
1397
|
acquire,
|
|
@@ -944,6 +1401,32 @@ function createDuckDBConnectionPool(instance, options = {}) {
|
|
|
944
1401
|
};
|
|
945
1402
|
}
|
|
946
1403
|
|
|
1404
|
+
// src/options.ts
|
|
1405
|
+
var DEFAULT_REWRITE_ARRAYS_MODE = "auto";
|
|
1406
|
+
function resolveRewriteArraysOption(value) {
|
|
1407
|
+
if (value === undefined)
|
|
1408
|
+
return DEFAULT_REWRITE_ARRAYS_MODE;
|
|
1409
|
+
if (value === true)
|
|
1410
|
+
return "auto";
|
|
1411
|
+
if (value === false)
|
|
1412
|
+
return "never";
|
|
1413
|
+
return value;
|
|
1414
|
+
}
|
|
1415
|
+
var DEFAULT_PREPARED_CACHE_SIZE = 32;
|
|
1416
|
+
function resolvePrepareCacheOption(option) {
|
|
1417
|
+
if (!option)
|
|
1418
|
+
return;
|
|
1419
|
+
if (option === true) {
|
|
1420
|
+
return { size: DEFAULT_PREPARED_CACHE_SIZE };
|
|
1421
|
+
}
|
|
1422
|
+
if (typeof option === "number") {
|
|
1423
|
+
const size2 = Math.max(1, Math.floor(option));
|
|
1424
|
+
return { size: size2 };
|
|
1425
|
+
}
|
|
1426
|
+
const size = option.size ?? DEFAULT_PREPARED_CACHE_SIZE;
|
|
1427
|
+
return { size: Math.max(1, Math.floor(size)) };
|
|
1428
|
+
}
|
|
1429
|
+
|
|
947
1430
|
// src/driver.ts
|
|
948
1431
|
class DuckDBDriver {
|
|
949
1432
|
client;
|
|
@@ -958,8 +1441,9 @@ class DuckDBDriver {
|
|
|
958
1441
|
createSession(schema) {
|
|
959
1442
|
return new DuckDBSession(this.client, this.dialect, schema, {
|
|
960
1443
|
logger: this.options.logger,
|
|
961
|
-
rewriteArrays: this.options.rewriteArrays,
|
|
962
|
-
rejectStringArrayLiterals: this.options.rejectStringArrayLiterals
|
|
1444
|
+
rewriteArrays: this.options.rewriteArrays ?? "auto",
|
|
1445
|
+
rejectStringArrayLiterals: this.options.rejectStringArrayLiterals,
|
|
1446
|
+
prepareCache: this.options.prepareCache
|
|
963
1447
|
});
|
|
964
1448
|
}
|
|
965
1449
|
}
|
|
@@ -972,6 +1456,8 @@ function isConfigObject(data) {
|
|
|
972
1456
|
}
|
|
973
1457
|
function createFromClient(client, config = {}, instance) {
|
|
974
1458
|
const dialect = new DuckDBDialect;
|
|
1459
|
+
const rewriteArraysMode = resolveRewriteArraysOption(config.rewriteArrays);
|
|
1460
|
+
const prepareCache = resolvePrepareCacheOption(config.prepareCache);
|
|
975
1461
|
const logger = config.logger === true ? new DefaultLogger : config.logger || undefined;
|
|
976
1462
|
let schema;
|
|
977
1463
|
if (config.schema) {
|
|
@@ -984,8 +1470,9 @@ function createFromClient(client, config = {}, instance) {
|
|
|
984
1470
|
}
|
|
985
1471
|
const driver = new DuckDBDriver(client, dialect, {
|
|
986
1472
|
logger,
|
|
987
|
-
rewriteArrays:
|
|
988
|
-
rejectStringArrayLiterals: config.rejectStringArrayLiterals
|
|
1473
|
+
rewriteArrays: rewriteArraysMode,
|
|
1474
|
+
rejectStringArrayLiterals: config.rejectStringArrayLiterals,
|
|
1475
|
+
prepareCache
|
|
989
1476
|
});
|
|
990
1477
|
const session = driver.createSession(schema);
|
|
991
1478
|
const db = new DuckDBDatabase(dialect, session, schema, client, instance);
|
|
@@ -1042,6 +1529,17 @@ class DuckDBDatabase extends PgDatabase {
|
|
|
1042
1529
|
if (isPool(this.$client) && this.$client.close) {
|
|
1043
1530
|
await this.$client.close();
|
|
1044
1531
|
}
|
|
1532
|
+
if (!isPool(this.$client)) {
|
|
1533
|
+
await closeClientConnection(this.$client);
|
|
1534
|
+
}
|
|
1535
|
+
if (this.$instance) {
|
|
1536
|
+
const maybeClosable = this.$instance;
|
|
1537
|
+
if (typeof maybeClosable.close === "function") {
|
|
1538
|
+
await maybeClosable.close();
|
|
1539
|
+
} else if (typeof maybeClosable.closeSync === "function") {
|
|
1540
|
+
maybeClosable.closeSync();
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1045
1543
|
}
|
|
1046
1544
|
select(fields) {
|
|
1047
1545
|
const selectedFields = fields ? aliasFields(fields) : undefined;
|
|
@@ -1054,6 +1552,9 @@ class DuckDBDatabase extends PgDatabase {
|
|
|
1054
1552
|
executeBatches(query, options = {}) {
|
|
1055
1553
|
return this.session.executeBatches(query, options);
|
|
1056
1554
|
}
|
|
1555
|
+
executeBatchesRaw(query, options = {}) {
|
|
1556
|
+
return this.session.executeBatchesRaw(query, options);
|
|
1557
|
+
}
|
|
1057
1558
|
executeArrow(query) {
|
|
1058
1559
|
return this.session.executeArrow(query);
|
|
1059
1560
|
}
|
|
@@ -1445,6 +1946,20 @@ function mapDuckDbType(column, imports, options) {
|
|
|
1445
1946
|
imports.pgCore.add("text");
|
|
1446
1947
|
return { builder: `text(${columnName(column.name)}) /* JSON */` };
|
|
1447
1948
|
}
|
|
1949
|
+
if (upper.startsWith("ENUM")) {
|
|
1950
|
+
imports.pgCore.add("text");
|
|
1951
|
+
const enumLiteral = raw.replace(/^ENUM\s*/i, "").trim();
|
|
1952
|
+
return {
|
|
1953
|
+
builder: `text(${columnName(column.name)}) /* ENUM ${enumLiteral} */`
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
if (upper.startsWith("UNION")) {
|
|
1957
|
+
imports.pgCore.add("text");
|
|
1958
|
+
const unionLiteral = raw.replace(/^UNION\s*/i, "").trim();
|
|
1959
|
+
return {
|
|
1960
|
+
builder: `text(${columnName(column.name)}) /* UNION ${unionLiteral} */`
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1448
1963
|
if (upper === "INET") {
|
|
1449
1964
|
imports.local.add("duckDbInet");
|
|
1450
1965
|
return { builder: `duckDbInet(${columnName(column.name)})` };
|
|
@@ -1510,7 +2025,7 @@ function mapDuckDbType(column, imports, options) {
|
|
|
1510
2025
|
}
|
|
1511
2026
|
imports.pgCore.add("text");
|
|
1512
2027
|
return {
|
|
1513
|
-
builder: `text(${columnName(column.name)}) /*
|
|
2028
|
+
builder: `text(${columnName(column.name)}) /* unsupported DuckDB type: ${upper} */`
|
|
1514
2029
|
};
|
|
1515
2030
|
}
|
|
1516
2031
|
function parseStructFields(inner) {
|
|
@@ -1620,6 +2135,7 @@ function renderImports(imports, importBasePath) {
|
|
|
1620
2135
|
function parseArgs(argv) {
|
|
1621
2136
|
const options = {
|
|
1622
2137
|
outFile: path.resolve(process.cwd(), "drizzle/schema.ts"),
|
|
2138
|
+
outMeta: undefined,
|
|
1623
2139
|
allDatabases: false,
|
|
1624
2140
|
includeViews: false,
|
|
1625
2141
|
useCustomTimeTypes: true
|
|
@@ -1645,6 +2161,11 @@ function parseArgs(argv) {
|
|
|
1645
2161
|
case "--outFile":
|
|
1646
2162
|
options.outFile = path.resolve(process.cwd(), argv[++i] ?? "drizzle/schema.ts");
|
|
1647
2163
|
break;
|
|
2164
|
+
case "--out-json":
|
|
2165
|
+
case "--outJson":
|
|
2166
|
+
case "--json":
|
|
2167
|
+
options.outMeta = path.resolve(process.cwd(), argv[++i] ?? "drizzle/schema.meta.json");
|
|
2168
|
+
break;
|
|
1648
2169
|
case "--include-views":
|
|
1649
2170
|
case "--includeViews":
|
|
1650
2171
|
options.includeViews = true;
|
|
@@ -1679,6 +2200,7 @@ Options:
|
|
|
1679
2200
|
--all-databases Introspect all attached databases (not just current)
|
|
1680
2201
|
--schema Comma separated schema list (defaults to all non-system schemas)
|
|
1681
2202
|
--out Output file (default: ./drizzle/schema.ts)
|
|
2203
|
+
--json Optional JSON metadata output (default: ./drizzle/schema.meta.json)
|
|
1682
2204
|
--include-views Include views in the generated schema
|
|
1683
2205
|
--use-pg-time Use pg-core timestamp/date/time instead of DuckDB custom helpers
|
|
1684
2206
|
--import-base Override import path for duckdb helpers (default: package name)
|
|
@@ -1719,11 +2241,23 @@ async function main() {
|
|
|
1719
2241
|
});
|
|
1720
2242
|
await mkdir(path.dirname(options.outFile), { recursive: true });
|
|
1721
2243
|
await writeFile(options.outFile, result.files.schemaTs, "utf8");
|
|
2244
|
+
if (options.outMeta) {
|
|
2245
|
+
await mkdir(path.dirname(options.outMeta), { recursive: true });
|
|
2246
|
+
await writeFile(options.outMeta, JSON.stringify(result.files.metaJson, null, 2), "utf8");
|
|
2247
|
+
}
|
|
1722
2248
|
console.log(`Wrote schema to ${options.outFile}`);
|
|
2249
|
+
if (options.outMeta) {
|
|
2250
|
+
console.log(`Wrote metadata to ${options.outMeta}`);
|
|
2251
|
+
}
|
|
1723
2252
|
} finally {
|
|
1724
2253
|
if ("closeSync" in connection && typeof connection.closeSync === "function") {
|
|
1725
2254
|
connection.closeSync();
|
|
1726
2255
|
}
|
|
2256
|
+
if ("closeSync" in instance && typeof instance.closeSync === "function") {
|
|
2257
|
+
instance.closeSync();
|
|
2258
|
+
} else if ("close" in instance && typeof instance.close === "function") {
|
|
2259
|
+
await instance.close();
|
|
2260
|
+
}
|
|
1727
2261
|
}
|
|
1728
2262
|
}
|
|
1729
2263
|
main().catch((err) => {
|