@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
package/dist/index.mjs
CHANGED
|
@@ -16,83 +16,162 @@ import { PgPreparedQuery, PgSession } from "drizzle-orm/pg-core/session";
|
|
|
16
16
|
import { fillPlaceholders, sql } from "drizzle-orm/sql/sql";
|
|
17
17
|
|
|
18
18
|
// src/sql/query-rewriters.ts
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
break;
|
|
37
|
-
if (ch === "'" && source[idx - 1] !== "\\") {
|
|
38
|
-
inString = !inString;
|
|
19
|
+
var OPERATORS = [
|
|
20
|
+
{ token: "@>", fn: "array_has_all" },
|
|
21
|
+
{ token: "<@", fn: "array_has_all", swap: true },
|
|
22
|
+
{ token: "&&", fn: "array_has_any" }
|
|
23
|
+
];
|
|
24
|
+
var isWhitespace = (char) => char !== undefined && /\s/.test(char);
|
|
25
|
+
function scrubForRewrite(query) {
|
|
26
|
+
let scrubbed = "";
|
|
27
|
+
let state = "code";
|
|
28
|
+
for (let i = 0;i < query.length; i += 1) {
|
|
29
|
+
const char = query[i];
|
|
30
|
+
const next = query[i + 1];
|
|
31
|
+
if (state === "code") {
|
|
32
|
+
if (char === "'") {
|
|
33
|
+
scrubbed += "'";
|
|
34
|
+
state = "single";
|
|
35
|
+
continue;
|
|
39
36
|
}
|
|
40
|
-
if (
|
|
37
|
+
if (char === '"') {
|
|
38
|
+
scrubbed += '"';
|
|
39
|
+
state = "double";
|
|
41
40
|
continue;
|
|
42
|
-
if (ch === ")" || ch === "]") {
|
|
43
|
-
depth++;
|
|
44
|
-
} else if (ch === "(" || ch === "[") {
|
|
45
|
-
depth--;
|
|
46
|
-
if (depth < 0) {
|
|
47
|
-
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
48
|
-
}
|
|
49
|
-
} else if (depth === 0 && isWhitespace(ch)) {
|
|
50
|
-
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
51
41
|
}
|
|
42
|
+
if (char === "-" && next === "-") {
|
|
43
|
+
scrubbed += " ";
|
|
44
|
+
i += 1;
|
|
45
|
+
state = "lineComment";
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (char === "/" && next === "*") {
|
|
49
|
+
scrubbed += " ";
|
|
50
|
+
i += 1;
|
|
51
|
+
state = "blockComment";
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
scrubbed += char;
|
|
55
|
+
continue;
|
|
52
56
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
for (;idx < source.length; idx++) {
|
|
63
|
-
const ch = source[idx];
|
|
64
|
-
if (ch === undefined)
|
|
65
|
-
break;
|
|
66
|
-
if (ch === "'" && source[idx - 1] !== "\\") {
|
|
67
|
-
inString = !inString;
|
|
57
|
+
if (state === "single") {
|
|
58
|
+
if (char === "'" && next === "'") {
|
|
59
|
+
scrubbed += "''";
|
|
60
|
+
i += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
scrubbed += char === "'" ? "'" : ".";
|
|
64
|
+
if (char === "'") {
|
|
65
|
+
state = "code";
|
|
68
66
|
}
|
|
69
|
-
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (state === "double") {
|
|
70
|
+
if (char === '"' && next === '"') {
|
|
71
|
+
scrubbed += '""';
|
|
72
|
+
i += 1;
|
|
70
73
|
continue;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
}
|
|
75
|
+
scrubbed += char === '"' ? '"' : ".";
|
|
76
|
+
if (char === '"') {
|
|
77
|
+
state = "code";
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (state === "lineComment") {
|
|
82
|
+
scrubbed += char === `
|
|
83
|
+
` ? `
|
|
84
|
+
` : " ";
|
|
85
|
+
if (char === `
|
|
86
|
+
`) {
|
|
87
|
+
state = "code";
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (state === "blockComment") {
|
|
92
|
+
if (char === "*" && next === "/") {
|
|
93
|
+
scrubbed += " ";
|
|
94
|
+
i += 1;
|
|
95
|
+
state = "code";
|
|
96
|
+
} else {
|
|
97
|
+
scrubbed += " ";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return scrubbed;
|
|
102
|
+
}
|
|
103
|
+
function findNextOperator(scrubbed, start) {
|
|
104
|
+
for (let idx = start;idx < scrubbed.length; idx += 1) {
|
|
105
|
+
for (const operator of OPERATORS) {
|
|
106
|
+
if (scrubbed.startsWith(operator.token, idx)) {
|
|
107
|
+
return { index: idx, operator };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function walkLeft(source, scrubbed, start) {
|
|
114
|
+
let idx = start;
|
|
115
|
+
while (idx >= 0 && isWhitespace(scrubbed[idx])) {
|
|
116
|
+
idx -= 1;
|
|
117
|
+
}
|
|
118
|
+
let depth = 0;
|
|
119
|
+
for (;idx >= 0; idx -= 1) {
|
|
120
|
+
const ch = scrubbed[idx];
|
|
121
|
+
if (ch === ")" || ch === "]") {
|
|
122
|
+
depth += 1;
|
|
123
|
+
} else if (ch === "(" || ch === "[") {
|
|
124
|
+
if (depth === 0) {
|
|
125
|
+
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
126
|
+
}
|
|
127
|
+
depth = Math.max(0, depth - 1);
|
|
128
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
129
|
+
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return [0, source.slice(0, start + 1)];
|
|
133
|
+
}
|
|
134
|
+
function walkRight(source, scrubbed, start) {
|
|
135
|
+
let idx = start;
|
|
136
|
+
while (idx < scrubbed.length && isWhitespace(scrubbed[idx])) {
|
|
137
|
+
idx += 1;
|
|
138
|
+
}
|
|
139
|
+
let depth = 0;
|
|
140
|
+
for (;idx < scrubbed.length; idx += 1) {
|
|
141
|
+
const ch = scrubbed[idx];
|
|
142
|
+
if (ch === "(" || ch === "[") {
|
|
143
|
+
depth += 1;
|
|
144
|
+
} else if (ch === ")" || ch === "]") {
|
|
145
|
+
if (depth === 0) {
|
|
79
146
|
return [idx, source.slice(start, idx)];
|
|
80
147
|
}
|
|
148
|
+
depth = Math.max(0, depth - 1);
|
|
149
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
150
|
+
return [idx, source.slice(start, idx)];
|
|
81
151
|
}
|
|
82
|
-
|
|
83
|
-
|
|
152
|
+
}
|
|
153
|
+
return [scrubbed.length, source.slice(start)];
|
|
154
|
+
}
|
|
155
|
+
function adaptArrayOperators(query) {
|
|
156
|
+
if (query.indexOf("@>") === -1 && query.indexOf("<@") === -1 && query.indexOf("&&") === -1) {
|
|
157
|
+
return query;
|
|
158
|
+
}
|
|
84
159
|
let rewritten = query;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
160
|
+
let scrubbed = scrubForRewrite(query);
|
|
161
|
+
let searchStart = 0;
|
|
162
|
+
while (true) {
|
|
163
|
+
const next = findNextOperator(scrubbed, searchStart);
|
|
164
|
+
if (!next)
|
|
165
|
+
break;
|
|
166
|
+
const { index, operator } = next;
|
|
167
|
+
const [leftStart, leftExpr] = walkLeft(rewritten, scrubbed, index - 1);
|
|
168
|
+
const [rightEnd, rightExpr] = walkRight(rewritten, scrubbed, index + operator.token.length);
|
|
169
|
+
const left = leftExpr.trim();
|
|
170
|
+
const right = rightExpr.trim();
|
|
171
|
+
const replacement = `${operator.fn}(${operator.swap ? right : left}, ${operator.swap ? left : right})`;
|
|
172
|
+
rewritten = rewritten.slice(0, leftStart) + replacement + rewritten.slice(rightEnd);
|
|
173
|
+
scrubbed = scrubForRewrite(rewritten);
|
|
174
|
+
searchStart = leftStart + replacement.length;
|
|
96
175
|
}
|
|
97
176
|
return rewritten;
|
|
98
177
|
}
|
|
@@ -386,6 +465,12 @@ function dateToMicros(value) {
|
|
|
386
465
|
if (value instanceof Date) {
|
|
387
466
|
return BigInt(value.getTime()) * 1000n;
|
|
388
467
|
}
|
|
468
|
+
if (typeof value === "bigint") {
|
|
469
|
+
return value;
|
|
470
|
+
}
|
|
471
|
+
if (typeof value === "number") {
|
|
472
|
+
return BigInt(Math.trunc(value)) * 1000n;
|
|
473
|
+
}
|
|
389
474
|
let normalized = value;
|
|
390
475
|
if (!value.includes("T") && value.includes(" ")) {
|
|
391
476
|
normalized = value.replace(" ", "T");
|
|
@@ -442,6 +527,7 @@ function wrapperToNodeApiValue(wrapper, toValue) {
|
|
|
442
527
|
function isPool(client) {
|
|
443
528
|
return typeof client.acquire === "function";
|
|
444
529
|
}
|
|
530
|
+
var PREPARED_CACHE = Symbol.for("drizzle-duckdb:prepared-cache");
|
|
445
531
|
function isPgArrayLiteral(value) {
|
|
446
532
|
return value.startsWith("{") && value.endsWith("}");
|
|
447
533
|
}
|
|
@@ -455,14 +541,21 @@ function parsePgArrayLiteral(value) {
|
|
|
455
541
|
}
|
|
456
542
|
function prepareParams(params, options = {}) {
|
|
457
543
|
return params.map((param) => {
|
|
458
|
-
if (typeof param === "string" &&
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
544
|
+
if (typeof param === "string" && param.length > 0) {
|
|
545
|
+
const firstChar = param[0];
|
|
546
|
+
const maybeArrayLiteral = firstChar === "{" || firstChar === "[" || firstChar === " " || firstChar === "\t";
|
|
547
|
+
if (maybeArrayLiteral) {
|
|
548
|
+
const trimmed = firstChar === "{" || firstChar === "[" ? param : param.trim();
|
|
549
|
+
if (trimmed && isPgArrayLiteral(trimmed)) {
|
|
550
|
+
if (options.rejectStringArrayLiterals) {
|
|
551
|
+
throw new Error("Stringified array literals are not supported. Use duckDbList()/duckDbArray() or pass native arrays.");
|
|
552
|
+
}
|
|
553
|
+
if (options.warnOnStringArrayLiteral) {
|
|
554
|
+
options.warnOnStringArrayLiteral();
|
|
555
|
+
}
|
|
556
|
+
return parsePgArrayLiteral(trimmed);
|
|
557
|
+
}
|
|
464
558
|
}
|
|
465
|
-
return parsePgArrayLiteral(param);
|
|
466
559
|
}
|
|
467
560
|
return param;
|
|
468
561
|
});
|
|
@@ -486,13 +579,121 @@ function toNodeApiValue(value) {
|
|
|
486
579
|
return value;
|
|
487
580
|
}
|
|
488
581
|
function deduplicateColumns(columns) {
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
582
|
+
const counts = new Map;
|
|
583
|
+
let hasDuplicates = false;
|
|
584
|
+
for (const column of columns) {
|
|
585
|
+
const next = (counts.get(column) ?? 0) + 1;
|
|
586
|
+
counts.set(column, next);
|
|
587
|
+
if (next > 1) {
|
|
588
|
+
hasDuplicates = true;
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (!hasDuplicates) {
|
|
593
|
+
return columns;
|
|
594
|
+
}
|
|
595
|
+
counts.clear();
|
|
596
|
+
return columns.map((column) => {
|
|
597
|
+
const count = counts.get(column) ?? 0;
|
|
598
|
+
counts.set(column, count + 1);
|
|
599
|
+
return count === 0 ? column : `${column}_${count}`;
|
|
494
600
|
});
|
|
495
601
|
}
|
|
602
|
+
function destroyPreparedStatement(entry) {
|
|
603
|
+
if (!entry)
|
|
604
|
+
return;
|
|
605
|
+
try {
|
|
606
|
+
entry.statement.destroySync();
|
|
607
|
+
} catch {}
|
|
608
|
+
}
|
|
609
|
+
function getPreparedCache(connection, size) {
|
|
610
|
+
const store = connection;
|
|
611
|
+
const existing = store[PREPARED_CACHE];
|
|
612
|
+
if (existing) {
|
|
613
|
+
existing.size = size;
|
|
614
|
+
return existing;
|
|
615
|
+
}
|
|
616
|
+
const cache = { size, entries: new Map };
|
|
617
|
+
store[PREPARED_CACHE] = cache;
|
|
618
|
+
return cache;
|
|
619
|
+
}
|
|
620
|
+
function evictOldest(cache) {
|
|
621
|
+
const oldest = cache.entries.keys().next();
|
|
622
|
+
if (!oldest.done) {
|
|
623
|
+
const key = oldest.value;
|
|
624
|
+
const entry = cache.entries.get(key);
|
|
625
|
+
cache.entries.delete(key);
|
|
626
|
+
destroyPreparedStatement(entry);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function evictCacheEntry(cache, key) {
|
|
630
|
+
const entry = cache.entries.get(key);
|
|
631
|
+
cache.entries.delete(key);
|
|
632
|
+
destroyPreparedStatement(entry);
|
|
633
|
+
}
|
|
634
|
+
async function getOrPrepareStatement(connection, query, cacheConfig) {
|
|
635
|
+
const cache = getPreparedCache(connection, cacheConfig.size);
|
|
636
|
+
const cached = cache.entries.get(query);
|
|
637
|
+
if (cached) {
|
|
638
|
+
cache.entries.delete(query);
|
|
639
|
+
cache.entries.set(query, cached);
|
|
640
|
+
return cached.statement;
|
|
641
|
+
}
|
|
642
|
+
const statement = await connection.prepare(query);
|
|
643
|
+
cache.entries.set(query, { statement });
|
|
644
|
+
while (cache.entries.size > cache.size) {
|
|
645
|
+
evictOldest(cache);
|
|
646
|
+
}
|
|
647
|
+
return statement;
|
|
648
|
+
}
|
|
649
|
+
async function materializeResultRows(result) {
|
|
650
|
+
const rows = await result.getRowsJS() ?? [];
|
|
651
|
+
const baseColumns = typeof result.deduplicatedColumnNames === "function" ? result.deduplicatedColumnNames() : result.columnNames();
|
|
652
|
+
const columns = typeof result.deduplicatedColumnNames === "function" ? baseColumns : deduplicateColumns(baseColumns);
|
|
653
|
+
return { columns, rows };
|
|
654
|
+
}
|
|
655
|
+
async function materializeRows(client, query, params, options = {}) {
|
|
656
|
+
if (isPool(client)) {
|
|
657
|
+
const connection2 = await client.acquire();
|
|
658
|
+
try {
|
|
659
|
+
return await materializeRows(connection2, query, params, options);
|
|
660
|
+
} finally {
|
|
661
|
+
await client.release(connection2);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
|
|
665
|
+
const connection = client;
|
|
666
|
+
if (options.prepareCache && typeof connection.prepare === "function") {
|
|
667
|
+
const cache = getPreparedCache(connection, options.prepareCache.size);
|
|
668
|
+
try {
|
|
669
|
+
const statement = await getOrPrepareStatement(connection, query, options.prepareCache);
|
|
670
|
+
if (values) {
|
|
671
|
+
statement.bind(values);
|
|
672
|
+
} else {
|
|
673
|
+
statement.clearBindings?.();
|
|
674
|
+
}
|
|
675
|
+
const result2 = await statement.run();
|
|
676
|
+
cache.entries.delete(query);
|
|
677
|
+
cache.entries.set(query, { statement });
|
|
678
|
+
return await materializeResultRows(result2);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
evictCacheEntry(cache, query);
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const result = await connection.run(query, values);
|
|
685
|
+
return await materializeResultRows(result);
|
|
686
|
+
}
|
|
687
|
+
function clearPreparedCache(connection) {
|
|
688
|
+
const store = connection;
|
|
689
|
+
const cache = store[PREPARED_CACHE];
|
|
690
|
+
if (!cache)
|
|
691
|
+
return;
|
|
692
|
+
for (const entry of cache.entries.values()) {
|
|
693
|
+
destroyPreparedStatement(entry);
|
|
694
|
+
}
|
|
695
|
+
cache.entries.clear();
|
|
696
|
+
}
|
|
496
697
|
function mapRowsToObjects(columns, rows) {
|
|
497
698
|
return rows.map((vals) => {
|
|
498
699
|
const obj = {};
|
|
@@ -503,6 +704,7 @@ function mapRowsToObjects(columns, rows) {
|
|
|
503
704
|
});
|
|
504
705
|
}
|
|
505
706
|
async function closeClientConnection(connection) {
|
|
707
|
+
clearPreparedCache(connection);
|
|
506
708
|
if ("close" in connection && typeof connection.close === "function") {
|
|
507
709
|
await connection.close();
|
|
508
710
|
return;
|
|
@@ -515,27 +717,51 @@ async function closeClientConnection(connection) {
|
|
|
515
717
|
connection.disconnectSync();
|
|
516
718
|
}
|
|
517
719
|
}
|
|
518
|
-
async function executeOnClient(client, query, params) {
|
|
720
|
+
async function executeOnClient(client, query, params, options = {}) {
|
|
721
|
+
const { columns, rows } = await materializeRows(client, query, params, options);
|
|
722
|
+
if (!rows || rows.length === 0) {
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
return mapRowsToObjects(columns, rows);
|
|
726
|
+
}
|
|
727
|
+
async function executeArraysOnClient(client, query, params, options = {}) {
|
|
728
|
+
return await materializeRows(client, query, params, options);
|
|
729
|
+
}
|
|
730
|
+
async function* executeInBatches(client, query, params, options = {}) {
|
|
519
731
|
if (isPool(client)) {
|
|
520
732
|
const connection = await client.acquire();
|
|
521
733
|
try {
|
|
522
|
-
|
|
734
|
+
yield* executeInBatches(connection, query, params, options);
|
|
735
|
+
return;
|
|
523
736
|
} finally {
|
|
524
737
|
await client.release(connection);
|
|
525
738
|
}
|
|
526
739
|
}
|
|
740
|
+
const rowsPerChunk = options.rowsPerChunk && options.rowsPerChunk > 0 ? options.rowsPerChunk : 1e5;
|
|
527
741
|
const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
|
|
528
|
-
const result = await client.
|
|
529
|
-
const
|
|
530
|
-
const columns = result.deduplicatedColumnNames
|
|
531
|
-
|
|
532
|
-
|
|
742
|
+
const result = await client.stream(query, values);
|
|
743
|
+
const rawColumns = typeof result.deduplicatedColumnNames === "function" ? result.deduplicatedColumnNames() : result.columnNames();
|
|
744
|
+
const columns = typeof result.deduplicatedColumnNames === "function" ? rawColumns : deduplicateColumns(rawColumns);
|
|
745
|
+
let buffer = [];
|
|
746
|
+
for await (const chunk of result.yieldRowsJs()) {
|
|
747
|
+
const objects = mapRowsToObjects(columns, chunk);
|
|
748
|
+
for (const row of objects) {
|
|
749
|
+
buffer.push(row);
|
|
750
|
+
if (buffer.length >= rowsPerChunk) {
|
|
751
|
+
yield buffer;
|
|
752
|
+
buffer = [];
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (buffer.length > 0) {
|
|
757
|
+
yield buffer;
|
|
758
|
+
}
|
|
533
759
|
}
|
|
534
|
-
async function*
|
|
760
|
+
async function* executeInBatchesRaw(client, query, params, options = {}) {
|
|
535
761
|
if (isPool(client)) {
|
|
536
762
|
const connection = await client.acquire();
|
|
537
763
|
try {
|
|
538
|
-
yield*
|
|
764
|
+
yield* executeInBatchesRaw(connection, query, params, options);
|
|
539
765
|
return;
|
|
540
766
|
} finally {
|
|
541
767
|
await client.release(connection);
|
|
@@ -544,21 +770,20 @@ async function* executeInBatches(client, query, params, options = {}) {
|
|
|
544
770
|
const rowsPerChunk = options.rowsPerChunk && options.rowsPerChunk > 0 ? options.rowsPerChunk : 1e5;
|
|
545
771
|
const values = params.length > 0 ? params.map((param) => toNodeApiValue(param)) : undefined;
|
|
546
772
|
const result = await client.stream(query, values);
|
|
547
|
-
const
|
|
548
|
-
const
|
|
773
|
+
const rawColumns = typeof result.deduplicatedColumnNames === "function" ? result.deduplicatedColumnNames() : result.columnNames();
|
|
774
|
+
const columns = typeof result.deduplicatedColumnNames === "function" ? rawColumns : deduplicateColumns(rawColumns);
|
|
549
775
|
let buffer = [];
|
|
550
776
|
for await (const chunk of result.yieldRowsJs()) {
|
|
551
|
-
const
|
|
552
|
-
for (const row of objects) {
|
|
777
|
+
for (const row of chunk) {
|
|
553
778
|
buffer.push(row);
|
|
554
779
|
if (buffer.length >= rowsPerChunk) {
|
|
555
|
-
yield buffer;
|
|
780
|
+
yield { columns, rows: buffer };
|
|
556
781
|
buffer = [];
|
|
557
782
|
}
|
|
558
783
|
}
|
|
559
784
|
}
|
|
560
785
|
if (buffer.length > 0) {
|
|
561
|
-
yield buffer;
|
|
786
|
+
yield { columns, rows: buffer };
|
|
562
787
|
}
|
|
563
788
|
}
|
|
564
789
|
async function executeArrowOnClient(client, query, params) {
|
|
@@ -580,6 +805,20 @@ async function executeArrowOnClient(client, query, params) {
|
|
|
580
805
|
}
|
|
581
806
|
|
|
582
807
|
// src/session.ts
|
|
808
|
+
function isSavepointSyntaxError(error) {
|
|
809
|
+
if (!(error instanceof Error) || !error.message) {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
return error.message.toLowerCase().includes("savepoint") && error.message.toLowerCase().includes("syntax error");
|
|
813
|
+
}
|
|
814
|
+
function rewriteQuery(mode, query) {
|
|
815
|
+
if (mode === "never") {
|
|
816
|
+
return { sql: query, rewritten: false };
|
|
817
|
+
}
|
|
818
|
+
const rewritten = adaptArrayOperators(query);
|
|
819
|
+
return { sql: rewritten, rewritten: rewritten !== query };
|
|
820
|
+
}
|
|
821
|
+
|
|
583
822
|
class DuckDBPreparedQuery extends PgPreparedQuery {
|
|
584
823
|
client;
|
|
585
824
|
dialect;
|
|
@@ -589,11 +828,12 @@ class DuckDBPreparedQuery extends PgPreparedQuery {
|
|
|
589
828
|
fields;
|
|
590
829
|
_isResponseInArrayMode;
|
|
591
830
|
customResultMapper;
|
|
592
|
-
|
|
831
|
+
rewriteArraysMode;
|
|
593
832
|
rejectStringArrayLiterals;
|
|
833
|
+
prepareCache;
|
|
594
834
|
warnOnStringArrayLiteral;
|
|
595
835
|
static [entityKind] = "DuckDBPreparedQuery";
|
|
596
|
-
constructor(client, dialect, queryString, params, logger, fields, _isResponseInArrayMode, customResultMapper,
|
|
836
|
+
constructor(client, dialect, queryString, params, logger, fields, _isResponseInArrayMode, customResultMapper, rewriteArraysMode, rejectStringArrayLiterals, prepareCache, warnOnStringArrayLiteral) {
|
|
597
837
|
super({ sql: queryString, params });
|
|
598
838
|
this.client = client;
|
|
599
839
|
this.dialect = dialect;
|
|
@@ -603,8 +843,9 @@ class DuckDBPreparedQuery extends PgPreparedQuery {
|
|
|
603
843
|
this.fields = fields;
|
|
604
844
|
this._isResponseInArrayMode = _isResponseInArrayMode;
|
|
605
845
|
this.customResultMapper = customResultMapper;
|
|
606
|
-
this.
|
|
846
|
+
this.rewriteArraysMode = rewriteArraysMode;
|
|
607
847
|
this.rejectStringArrayLiterals = rejectStringArrayLiterals;
|
|
848
|
+
this.prepareCache = prepareCache;
|
|
608
849
|
this.warnOnStringArrayLiteral = warnOnStringArrayLiteral;
|
|
609
850
|
}
|
|
610
851
|
async execute(placeholderValues = {}) {
|
|
@@ -613,18 +854,23 @@ class DuckDBPreparedQuery extends PgPreparedQuery {
|
|
|
613
854
|
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
614
855
|
warnOnStringArrayLiteral: this.warnOnStringArrayLiteral ? () => this.warnOnStringArrayLiteral?.(this.queryString) : undefined
|
|
615
856
|
});
|
|
616
|
-
const rewrittenQuery
|
|
617
|
-
if (
|
|
857
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(this.rewriteArraysMode, this.queryString);
|
|
858
|
+
if (didRewrite) {
|
|
618
859
|
this.logger.logQuery(`[duckdb] original query before array rewrite: ${this.queryString}`, params);
|
|
619
860
|
}
|
|
620
861
|
this.logger.logQuery(rewrittenQuery, params);
|
|
621
862
|
const { fields, joinsNotNullableMap, customResultMapper } = this;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
863
|
+
if (fields) {
|
|
864
|
+
const { rows: rows2 } = await executeArraysOnClient(this.client, rewrittenQuery, params, { prepareCache: this.prepareCache });
|
|
865
|
+
if (rows2.length === 0) {
|
|
866
|
+
return [];
|
|
867
|
+
}
|
|
868
|
+
return customResultMapper ? customResultMapper(rows2) : rows2.map((row) => mapResultRow(fields, row, joinsNotNullableMap));
|
|
625
869
|
}
|
|
626
|
-
const
|
|
627
|
-
|
|
870
|
+
const rows = await executeOnClient(this.client, rewrittenQuery, params, {
|
|
871
|
+
prepareCache: this.prepareCache
|
|
872
|
+
});
|
|
873
|
+
return rows;
|
|
628
874
|
}
|
|
629
875
|
all(placeholderValues = {}) {
|
|
630
876
|
return this.execute(placeholderValues);
|
|
@@ -641,9 +887,11 @@ class DuckDBSession extends PgSession {
|
|
|
641
887
|
static [entityKind] = "DuckDBSession";
|
|
642
888
|
dialect;
|
|
643
889
|
logger;
|
|
644
|
-
|
|
890
|
+
rewriteArraysMode;
|
|
645
891
|
rejectStringArrayLiterals;
|
|
892
|
+
prepareCache;
|
|
646
893
|
hasWarnedArrayLiteral = false;
|
|
894
|
+
rollbackOnly = false;
|
|
647
895
|
constructor(client, dialect, schema, options = {}) {
|
|
648
896
|
super(dialect);
|
|
649
897
|
this.client = client;
|
|
@@ -651,13 +899,27 @@ class DuckDBSession extends PgSession {
|
|
|
651
899
|
this.options = options;
|
|
652
900
|
this.dialect = dialect;
|
|
653
901
|
this.logger = options.logger ?? new NoopLogger;
|
|
654
|
-
this.
|
|
902
|
+
this.rewriteArraysMode = options.rewriteArrays ?? "auto";
|
|
655
903
|
this.rejectStringArrayLiterals = options.rejectStringArrayLiterals ?? false;
|
|
904
|
+
this.prepareCache = options.prepareCache;
|
|
905
|
+
this.options = {
|
|
906
|
+
...options,
|
|
907
|
+
rewriteArrays: this.rewriteArraysMode,
|
|
908
|
+
prepareCache: this.prepareCache
|
|
909
|
+
};
|
|
656
910
|
}
|
|
657
911
|
prepareQuery(query, fields, name, isResponseInArrayMode, customResultMapper) {
|
|
658
|
-
return new DuckDBPreparedQuery(this.client, this.dialect, query.sql, query.params, this.logger, fields, isResponseInArrayMode, customResultMapper, this.
|
|
912
|
+
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);
|
|
659
913
|
}
|
|
660
|
-
|
|
914
|
+
execute(query) {
|
|
915
|
+
this.dialect.resetPgJsonFlag();
|
|
916
|
+
return super.execute(query);
|
|
917
|
+
}
|
|
918
|
+
all(query) {
|
|
919
|
+
this.dialect.resetPgJsonFlag();
|
|
920
|
+
return super.all(query);
|
|
921
|
+
}
|
|
922
|
+
async transaction(transaction, config) {
|
|
661
923
|
let pinnedConnection;
|
|
662
924
|
let pool;
|
|
663
925
|
let clientForTx = this.client;
|
|
@@ -670,8 +932,15 @@ class DuckDBSession extends PgSession {
|
|
|
670
932
|
const tx = new DuckDBTransaction(this.dialect, session, this.schema);
|
|
671
933
|
try {
|
|
672
934
|
await tx.execute(sql`BEGIN TRANSACTION;`);
|
|
935
|
+
if (config) {
|
|
936
|
+
await tx.setTransaction(config);
|
|
937
|
+
}
|
|
673
938
|
try {
|
|
674
939
|
const result = await transaction(tx);
|
|
940
|
+
if (session.isRollbackOnly()) {
|
|
941
|
+
await tx.execute(sql`rollback`);
|
|
942
|
+
throw new TransactionRollbackError;
|
|
943
|
+
}
|
|
675
944
|
await tx.execute(sql`commit`);
|
|
676
945
|
return result;
|
|
677
946
|
} catch (error) {
|
|
@@ -693,31 +962,56 @@ class DuckDBSession extends PgSession {
|
|
|
693
962
|
query: ${query}`, []);
|
|
694
963
|
};
|
|
695
964
|
executeBatches(query, options = {}) {
|
|
965
|
+
this.dialect.resetPgJsonFlag();
|
|
696
966
|
const builtQuery = this.dialect.sqlToQuery(query);
|
|
967
|
+
this.dialect.assertNoPgJsonColumns();
|
|
697
968
|
const params = prepareParams(builtQuery.params, {
|
|
698
969
|
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
699
970
|
warnOnStringArrayLiteral: this.rejectStringArrayLiterals ? undefined : () => this.warnOnStringArrayLiteral(builtQuery.sql)
|
|
700
971
|
});
|
|
701
|
-
const rewrittenQuery = this.
|
|
702
|
-
if (
|
|
972
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(this.rewriteArraysMode, builtQuery.sql);
|
|
973
|
+
if (didRewrite) {
|
|
703
974
|
this.logger.logQuery(`[duckdb] original query before array rewrite: ${builtQuery.sql}`, params);
|
|
704
975
|
}
|
|
705
976
|
this.logger.logQuery(rewrittenQuery, params);
|
|
706
977
|
return executeInBatches(this.client, rewrittenQuery, params, options);
|
|
707
978
|
}
|
|
979
|
+
executeBatchesRaw(query, options = {}) {
|
|
980
|
+
this.dialect.resetPgJsonFlag();
|
|
981
|
+
const builtQuery = this.dialect.sqlToQuery(query);
|
|
982
|
+
this.dialect.assertNoPgJsonColumns();
|
|
983
|
+
const params = prepareParams(builtQuery.params, {
|
|
984
|
+
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
985
|
+
warnOnStringArrayLiteral: this.rejectStringArrayLiterals ? undefined : () => this.warnOnStringArrayLiteral(builtQuery.sql)
|
|
986
|
+
});
|
|
987
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(this.rewriteArraysMode, builtQuery.sql);
|
|
988
|
+
if (didRewrite) {
|
|
989
|
+
this.logger.logQuery(`[duckdb] original query before array rewrite: ${builtQuery.sql}`, params);
|
|
990
|
+
}
|
|
991
|
+
this.logger.logQuery(rewrittenQuery, params);
|
|
992
|
+
return executeInBatchesRaw(this.client, rewrittenQuery, params, options);
|
|
993
|
+
}
|
|
708
994
|
async executeArrow(query) {
|
|
995
|
+
this.dialect.resetPgJsonFlag();
|
|
709
996
|
const builtQuery = this.dialect.sqlToQuery(query);
|
|
997
|
+
this.dialect.assertNoPgJsonColumns();
|
|
710
998
|
const params = prepareParams(builtQuery.params, {
|
|
711
999
|
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
712
1000
|
warnOnStringArrayLiteral: this.rejectStringArrayLiterals ? undefined : () => this.warnOnStringArrayLiteral(builtQuery.sql)
|
|
713
1001
|
});
|
|
714
|
-
const rewrittenQuery = this.
|
|
715
|
-
if (
|
|
1002
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(this.rewriteArraysMode, builtQuery.sql);
|
|
1003
|
+
if (didRewrite) {
|
|
716
1004
|
this.logger.logQuery(`[duckdb] original query before array rewrite: ${builtQuery.sql}`, params);
|
|
717
1005
|
}
|
|
718
1006
|
this.logger.logQuery(rewrittenQuery, params);
|
|
719
1007
|
return executeArrowOnClient(this.client, rewrittenQuery, params);
|
|
720
1008
|
}
|
|
1009
|
+
markRollbackOnly() {
|
|
1010
|
+
this.rollbackOnly = true;
|
|
1011
|
+
}
|
|
1012
|
+
isRollbackOnly() {
|
|
1013
|
+
return this.rollbackOnly;
|
|
1014
|
+
}
|
|
721
1015
|
}
|
|
722
1016
|
|
|
723
1017
|
class DuckDBTransaction extends PgTransaction {
|
|
@@ -744,12 +1038,53 @@ class DuckDBTransaction extends PgTransaction {
|
|
|
744
1038
|
executeBatches(query, options = {}) {
|
|
745
1039
|
return this.session.executeBatches(query, options);
|
|
746
1040
|
}
|
|
1041
|
+
executeBatchesRaw(query, options = {}) {
|
|
1042
|
+
return this.session.executeBatchesRaw(query, options);
|
|
1043
|
+
}
|
|
747
1044
|
executeArrow(query) {
|
|
748
1045
|
return this.session.executeArrow(query);
|
|
749
1046
|
}
|
|
750
1047
|
async transaction(transaction) {
|
|
751
|
-
const
|
|
752
|
-
|
|
1048
|
+
const internals = this;
|
|
1049
|
+
const savepoint = `drizzle_savepoint_${this.nestedIndex + 1}`;
|
|
1050
|
+
const savepointSql = sql.raw(`savepoint ${savepoint}`);
|
|
1051
|
+
const releaseSql = sql.raw(`release savepoint ${savepoint}`);
|
|
1052
|
+
const rollbackSql = sql.raw(`rollback to savepoint ${savepoint}`);
|
|
1053
|
+
const nestedTx = new DuckDBTransaction(internals.dialect, internals.session, this.schema, this.nestedIndex + 1);
|
|
1054
|
+
if (internals.dialect.areSavepointsUnsupported()) {
|
|
1055
|
+
return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
|
|
1056
|
+
}
|
|
1057
|
+
let createdSavepoint = false;
|
|
1058
|
+
try {
|
|
1059
|
+
await internals.session.execute(savepointSql);
|
|
1060
|
+
internals.dialect.markSavepointsSupported();
|
|
1061
|
+
createdSavepoint = true;
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
if (!isSavepointSyntaxError(error)) {
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
internals.dialect.markSavepointsUnsupported();
|
|
1067
|
+
return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
|
|
1068
|
+
}
|
|
1069
|
+
try {
|
|
1070
|
+
const result = await transaction(nestedTx);
|
|
1071
|
+
if (createdSavepoint) {
|
|
1072
|
+
await internals.session.execute(releaseSql);
|
|
1073
|
+
}
|
|
1074
|
+
return result;
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
if (createdSavepoint) {
|
|
1077
|
+
await internals.session.execute(rollbackSql);
|
|
1078
|
+
}
|
|
1079
|
+
internals.session.markRollbackOnly();
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
runNestedWithoutSavepoint(transaction, nestedTx, internals) {
|
|
1084
|
+
return transaction(nestedTx).catch((error) => {
|
|
1085
|
+
internals.session.markRollbackOnly();
|
|
1086
|
+
throw error;
|
|
1087
|
+
});
|
|
753
1088
|
}
|
|
754
1089
|
}
|
|
755
1090
|
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.";
|
|
@@ -771,15 +1106,30 @@ import {
|
|
|
771
1106
|
import {
|
|
772
1107
|
sql as sql2
|
|
773
1108
|
} from "drizzle-orm";
|
|
774
|
-
|
|
775
1109
|
class DuckDBDialect extends PgDialect {
|
|
776
1110
|
static [entityKind2] = "DuckDBPgDialect";
|
|
777
1111
|
hasPgJsonColumn = false;
|
|
1112
|
+
savepointsSupported = 0 /* Unknown */;
|
|
1113
|
+
resetPgJsonFlag() {
|
|
1114
|
+
this.hasPgJsonColumn = false;
|
|
1115
|
+
}
|
|
1116
|
+
markPgJsonDetected() {
|
|
1117
|
+
this.hasPgJsonColumn = true;
|
|
1118
|
+
}
|
|
778
1119
|
assertNoPgJsonColumns() {
|
|
779
1120
|
if (this.hasPgJsonColumn) {
|
|
780
|
-
throw new Error("Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB
|
|
1121
|
+
throw new Error("Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB's native JSON type.");
|
|
781
1122
|
}
|
|
782
1123
|
}
|
|
1124
|
+
areSavepointsUnsupported() {
|
|
1125
|
+
return this.savepointsSupported === 2 /* No */;
|
|
1126
|
+
}
|
|
1127
|
+
markSavepointsSupported() {
|
|
1128
|
+
this.savepointsSupported = 1 /* Yes */;
|
|
1129
|
+
}
|
|
1130
|
+
markSavepointsUnsupported() {
|
|
1131
|
+
this.savepointsSupported = 2 /* No */;
|
|
1132
|
+
}
|
|
783
1133
|
async migrate(migrations, session, config) {
|
|
784
1134
|
const migrationConfig = typeof config === "string" ? { migrationsFolder: config } : config;
|
|
785
1135
|
const migrationsSchema = migrationConfig.migrationsSchema ?? "drizzle";
|
|
@@ -816,8 +1166,8 @@ class DuckDBDialect extends PgDialect {
|
|
|
816
1166
|
}
|
|
817
1167
|
prepareTyping(encoder) {
|
|
818
1168
|
if (is2(encoder, PgJsonb) || is2(encoder, PgJson)) {
|
|
819
|
-
this.
|
|
820
|
-
|
|
1169
|
+
this.markPgJsonDetected();
|
|
1170
|
+
throw new Error("Pg JSON/JSONB columns are not supported in DuckDB. Replace them with duckDbJson() to use DuckDB's native JSON type.");
|
|
821
1171
|
} else if (is2(encoder, PgNumeric)) {
|
|
822
1172
|
return "decimal";
|
|
823
1173
|
} else if (is2(encoder, PgTime2)) {
|
|
@@ -951,41 +1301,148 @@ function resolvePoolSize(pool) {
|
|
|
951
1301
|
}
|
|
952
1302
|
function createDuckDBConnectionPool(instance, options = {}) {
|
|
953
1303
|
const size = options.size && options.size > 0 ? options.size : 4;
|
|
1304
|
+
const acquireTimeout = options.acquireTimeout ?? 30000;
|
|
1305
|
+
const maxWaitingRequests = options.maxWaitingRequests ?? 100;
|
|
1306
|
+
const maxLifetimeMs = options.maxLifetimeMs;
|
|
1307
|
+
const idleTimeoutMs = options.idleTimeoutMs;
|
|
1308
|
+
const metadata = new WeakMap;
|
|
954
1309
|
const idle = [];
|
|
955
1310
|
const waiting = [];
|
|
956
1311
|
let total = 0;
|
|
957
1312
|
let closed = false;
|
|
1313
|
+
let pendingAcquires = 0;
|
|
1314
|
+
const shouldRecycle = (conn, now) => {
|
|
1315
|
+
if (maxLifetimeMs !== undefined && now - conn.createdAt >= maxLifetimeMs) {
|
|
1316
|
+
return true;
|
|
1317
|
+
}
|
|
1318
|
+
if (idleTimeoutMs !== undefined && now - conn.lastUsedAt >= idleTimeoutMs) {
|
|
1319
|
+
return true;
|
|
1320
|
+
}
|
|
1321
|
+
return false;
|
|
1322
|
+
};
|
|
958
1323
|
const acquire = async () => {
|
|
959
1324
|
if (closed) {
|
|
960
1325
|
throw new Error("DuckDB connection pool is closed");
|
|
961
1326
|
}
|
|
962
|
-
|
|
963
|
-
|
|
1327
|
+
while (idle.length > 0) {
|
|
1328
|
+
const pooled = idle.pop();
|
|
1329
|
+
const now = Date.now();
|
|
1330
|
+
if (shouldRecycle(pooled, now)) {
|
|
1331
|
+
await closeClientConnection(pooled.connection);
|
|
1332
|
+
total = Math.max(0, total - 1);
|
|
1333
|
+
metadata.delete(pooled.connection);
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
pooled.lastUsedAt = now;
|
|
1337
|
+
metadata.set(pooled.connection, {
|
|
1338
|
+
createdAt: pooled.createdAt,
|
|
1339
|
+
lastUsedAt: pooled.lastUsedAt
|
|
1340
|
+
});
|
|
1341
|
+
return pooled.connection;
|
|
964
1342
|
}
|
|
965
1343
|
if (total < size) {
|
|
1344
|
+
pendingAcquires += 1;
|
|
966
1345
|
total += 1;
|
|
967
|
-
|
|
1346
|
+
try {
|
|
1347
|
+
const connection = await DuckDBConnection.create(instance);
|
|
1348
|
+
if (closed) {
|
|
1349
|
+
await closeClientConnection(connection);
|
|
1350
|
+
total -= 1;
|
|
1351
|
+
throw new Error("DuckDB connection pool is closed");
|
|
1352
|
+
}
|
|
1353
|
+
const now = Date.now();
|
|
1354
|
+
metadata.set(connection, { createdAt: now, lastUsedAt: now });
|
|
1355
|
+
return connection;
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
total -= 1;
|
|
1358
|
+
throw error;
|
|
1359
|
+
} finally {
|
|
1360
|
+
pendingAcquires -= 1;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (waiting.length >= maxWaitingRequests) {
|
|
1364
|
+
throw new Error(`DuckDB connection pool queue is full (max ${maxWaitingRequests} waiting requests)`);
|
|
968
1365
|
}
|
|
969
|
-
return await new Promise((resolve) => {
|
|
970
|
-
|
|
1366
|
+
return await new Promise((resolve, reject) => {
|
|
1367
|
+
const timeoutId = setTimeout(() => {
|
|
1368
|
+
const idx = waiting.findIndex((w) => w.timeoutId === timeoutId);
|
|
1369
|
+
if (idx !== -1) {
|
|
1370
|
+
waiting.splice(idx, 1);
|
|
1371
|
+
}
|
|
1372
|
+
reject(new Error(`DuckDB connection pool acquire timeout after ${acquireTimeout}ms`));
|
|
1373
|
+
}, acquireTimeout);
|
|
1374
|
+
waiting.push({ resolve, reject, timeoutId });
|
|
971
1375
|
});
|
|
972
1376
|
};
|
|
973
1377
|
const release = async (connection) => {
|
|
1378
|
+
const waiter = waiting.shift();
|
|
1379
|
+
if (waiter) {
|
|
1380
|
+
clearTimeout(waiter.timeoutId);
|
|
1381
|
+
const now2 = Date.now();
|
|
1382
|
+
const meta = metadata.get(connection) ?? { createdAt: now2, lastUsedAt: now2 };
|
|
1383
|
+
const expired = maxLifetimeMs !== undefined && now2 - meta.createdAt >= maxLifetimeMs;
|
|
1384
|
+
if (closed) {
|
|
1385
|
+
await closeClientConnection(connection);
|
|
1386
|
+
total = Math.max(0, total - 1);
|
|
1387
|
+
metadata.delete(connection);
|
|
1388
|
+
waiter.reject(new Error("DuckDB connection pool is closed"));
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (expired) {
|
|
1392
|
+
await closeClientConnection(connection);
|
|
1393
|
+
total = Math.max(0, total - 1);
|
|
1394
|
+
metadata.delete(connection);
|
|
1395
|
+
try {
|
|
1396
|
+
const replacement = await acquire();
|
|
1397
|
+
waiter.resolve(replacement);
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
waiter.reject(error);
|
|
1400
|
+
}
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
meta.lastUsedAt = now2;
|
|
1404
|
+
metadata.set(connection, meta);
|
|
1405
|
+
waiter.resolve(connection);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
974
1408
|
if (closed) {
|
|
975
1409
|
await closeClientConnection(connection);
|
|
1410
|
+
metadata.delete(connection);
|
|
1411
|
+
total = Math.max(0, total - 1);
|
|
976
1412
|
return;
|
|
977
1413
|
}
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
1414
|
+
const now = Date.now();
|
|
1415
|
+
const existingMeta = metadata.get(connection) ?? { createdAt: now, lastUsedAt: now };
|
|
1416
|
+
existingMeta.lastUsedAt = now;
|
|
1417
|
+
metadata.set(connection, existingMeta);
|
|
1418
|
+
if (maxLifetimeMs !== undefined && now - existingMeta.createdAt >= maxLifetimeMs) {
|
|
1419
|
+
await closeClientConnection(connection);
|
|
1420
|
+
total -= 1;
|
|
1421
|
+
metadata.delete(connection);
|
|
981
1422
|
return;
|
|
982
1423
|
}
|
|
983
|
-
idle.push(
|
|
1424
|
+
idle.push({
|
|
1425
|
+
connection,
|
|
1426
|
+
createdAt: existingMeta.createdAt,
|
|
1427
|
+
lastUsedAt: existingMeta.lastUsedAt
|
|
1428
|
+
});
|
|
984
1429
|
};
|
|
985
1430
|
const close = async () => {
|
|
986
1431
|
closed = true;
|
|
1432
|
+
const waiters = waiting.splice(0, waiting.length);
|
|
1433
|
+
for (const waiter of waiters) {
|
|
1434
|
+
clearTimeout(waiter.timeoutId);
|
|
1435
|
+
waiter.reject(new Error("DuckDB connection pool is closed"));
|
|
1436
|
+
}
|
|
987
1437
|
const toClose = idle.splice(0, idle.length);
|
|
988
|
-
await Promise.
|
|
1438
|
+
await Promise.allSettled(toClose.map((item) => closeClientConnection(item.connection)));
|
|
1439
|
+
total = Math.max(0, total - toClose.length);
|
|
1440
|
+
toClose.forEach((item) => metadata.delete(item.connection));
|
|
1441
|
+
const maxWait = 5000;
|
|
1442
|
+
const start = Date.now();
|
|
1443
|
+
while (pendingAcquires > 0 && Date.now() - start < maxWait) {
|
|
1444
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1445
|
+
}
|
|
989
1446
|
};
|
|
990
1447
|
return {
|
|
991
1448
|
acquire,
|
|
@@ -995,6 +1452,32 @@ function createDuckDBConnectionPool(instance, options = {}) {
|
|
|
995
1452
|
};
|
|
996
1453
|
}
|
|
997
1454
|
|
|
1455
|
+
// src/options.ts
|
|
1456
|
+
var DEFAULT_REWRITE_ARRAYS_MODE = "auto";
|
|
1457
|
+
function resolveRewriteArraysOption(value) {
|
|
1458
|
+
if (value === undefined)
|
|
1459
|
+
return DEFAULT_REWRITE_ARRAYS_MODE;
|
|
1460
|
+
if (value === true)
|
|
1461
|
+
return "auto";
|
|
1462
|
+
if (value === false)
|
|
1463
|
+
return "never";
|
|
1464
|
+
return value;
|
|
1465
|
+
}
|
|
1466
|
+
var DEFAULT_PREPARED_CACHE_SIZE = 32;
|
|
1467
|
+
function resolvePrepareCacheOption(option) {
|
|
1468
|
+
if (!option)
|
|
1469
|
+
return;
|
|
1470
|
+
if (option === true) {
|
|
1471
|
+
return { size: DEFAULT_PREPARED_CACHE_SIZE };
|
|
1472
|
+
}
|
|
1473
|
+
if (typeof option === "number") {
|
|
1474
|
+
const size2 = Math.max(1, Math.floor(option));
|
|
1475
|
+
return { size: size2 };
|
|
1476
|
+
}
|
|
1477
|
+
const size = option.size ?? DEFAULT_PREPARED_CACHE_SIZE;
|
|
1478
|
+
return { size: Math.max(1, Math.floor(size)) };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
998
1481
|
// src/driver.ts
|
|
999
1482
|
class DuckDBDriver {
|
|
1000
1483
|
client;
|
|
@@ -1009,8 +1492,9 @@ class DuckDBDriver {
|
|
|
1009
1492
|
createSession(schema) {
|
|
1010
1493
|
return new DuckDBSession(this.client, this.dialect, schema, {
|
|
1011
1494
|
logger: this.options.logger,
|
|
1012
|
-
rewriteArrays: this.options.rewriteArrays,
|
|
1013
|
-
rejectStringArrayLiterals: this.options.rejectStringArrayLiterals
|
|
1495
|
+
rewriteArrays: this.options.rewriteArrays ?? "auto",
|
|
1496
|
+
rejectStringArrayLiterals: this.options.rejectStringArrayLiterals,
|
|
1497
|
+
prepareCache: this.options.prepareCache
|
|
1014
1498
|
});
|
|
1015
1499
|
}
|
|
1016
1500
|
}
|
|
@@ -1023,6 +1507,8 @@ function isConfigObject(data) {
|
|
|
1023
1507
|
}
|
|
1024
1508
|
function createFromClient(client, config = {}, instance) {
|
|
1025
1509
|
const dialect = new DuckDBDialect;
|
|
1510
|
+
const rewriteArraysMode = resolveRewriteArraysOption(config.rewriteArrays);
|
|
1511
|
+
const prepareCache = resolvePrepareCacheOption(config.prepareCache);
|
|
1026
1512
|
const logger = config.logger === true ? new DefaultLogger : config.logger || undefined;
|
|
1027
1513
|
let schema;
|
|
1028
1514
|
if (config.schema) {
|
|
@@ -1035,8 +1521,9 @@ function createFromClient(client, config = {}, instance) {
|
|
|
1035
1521
|
}
|
|
1036
1522
|
const driver = new DuckDBDriver(client, dialect, {
|
|
1037
1523
|
logger,
|
|
1038
|
-
rewriteArrays:
|
|
1039
|
-
rejectStringArrayLiterals: config.rejectStringArrayLiterals
|
|
1524
|
+
rewriteArrays: rewriteArraysMode,
|
|
1525
|
+
rejectStringArrayLiterals: config.rejectStringArrayLiterals,
|
|
1526
|
+
prepareCache
|
|
1040
1527
|
});
|
|
1041
1528
|
const session = driver.createSession(schema);
|
|
1042
1529
|
const db = new DuckDBDatabase(dialect, session, schema, client, instance);
|
|
@@ -1093,6 +1580,17 @@ class DuckDBDatabase extends PgDatabase {
|
|
|
1093
1580
|
if (isPool(this.$client) && this.$client.close) {
|
|
1094
1581
|
await this.$client.close();
|
|
1095
1582
|
}
|
|
1583
|
+
if (!isPool(this.$client)) {
|
|
1584
|
+
await closeClientConnection(this.$client);
|
|
1585
|
+
}
|
|
1586
|
+
if (this.$instance) {
|
|
1587
|
+
const maybeClosable = this.$instance;
|
|
1588
|
+
if (typeof maybeClosable.close === "function") {
|
|
1589
|
+
await maybeClosable.close();
|
|
1590
|
+
} else if (typeof maybeClosable.closeSync === "function") {
|
|
1591
|
+
maybeClosable.closeSync();
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1096
1594
|
}
|
|
1097
1595
|
select(fields) {
|
|
1098
1596
|
const selectedFields = fields ? aliasFields(fields) : undefined;
|
|
@@ -1105,6 +1603,9 @@ class DuckDBDatabase extends PgDatabase {
|
|
|
1105
1603
|
executeBatches(query, options = {}) {
|
|
1106
1604
|
return this.session.executeBatches(query, options);
|
|
1107
1605
|
}
|
|
1606
|
+
executeBatchesRaw(query, options = {}) {
|
|
1607
|
+
return this.session.executeBatchesRaw(query, options);
|
|
1608
|
+
}
|
|
1108
1609
|
executeArrow(query) {
|
|
1109
1610
|
return this.session.executeArrow(query);
|
|
1110
1611
|
}
|
|
@@ -1176,6 +1677,13 @@ function buildStructLiteral(value, schema) {
|
|
|
1176
1677
|
});
|
|
1177
1678
|
return sql4`struct_pack(${sql4.join(parts, sql4.raw(", "))})`;
|
|
1178
1679
|
}
|
|
1680
|
+
function buildMapLiteral(value, valueType) {
|
|
1681
|
+
const keys = Object.keys(value);
|
|
1682
|
+
const vals = Object.values(value);
|
|
1683
|
+
const keyList = buildListLiteral(keys, "TEXT");
|
|
1684
|
+
const valList = buildListLiteral(vals, valueType?.endsWith("[]") ? valueType.slice(0, -2) : valueType);
|
|
1685
|
+
return sql4`map(${keyList}, ${valList})`;
|
|
1686
|
+
}
|
|
1179
1687
|
var duckDbList = (name, elementType) => customType({
|
|
1180
1688
|
dataType() {
|
|
1181
1689
|
return `${elementType}[]`;
|
|
@@ -1189,11 +1697,11 @@ var duckDbList = (name, elementType) => customType({
|
|
|
1189
1697
|
}
|
|
1190
1698
|
if (typeof value === "string") {
|
|
1191
1699
|
const parsed = coerceArrayString(value);
|
|
1192
|
-
if (parsed) {
|
|
1700
|
+
if (parsed !== undefined) {
|
|
1193
1701
|
return parsed;
|
|
1194
1702
|
}
|
|
1195
1703
|
}
|
|
1196
|
-
return
|
|
1704
|
+
return value;
|
|
1197
1705
|
}
|
|
1198
1706
|
})(name);
|
|
1199
1707
|
var duckDbArray = (name, elementType, fixedLength) => customType({
|
|
@@ -1209,11 +1717,11 @@ var duckDbArray = (name, elementType, fixedLength) => customType({
|
|
|
1209
1717
|
}
|
|
1210
1718
|
if (typeof value === "string") {
|
|
1211
1719
|
const parsed = coerceArrayString(value);
|
|
1212
|
-
if (parsed) {
|
|
1720
|
+
if (parsed !== undefined) {
|
|
1213
1721
|
return parsed;
|
|
1214
1722
|
}
|
|
1215
1723
|
}
|
|
1216
|
-
return
|
|
1724
|
+
return value;
|
|
1217
1725
|
}
|
|
1218
1726
|
})(name);
|
|
1219
1727
|
var duckDbMap = (name, valueType) => customType({
|
|
@@ -1298,6 +1806,21 @@ var duckDbInterval = (name) => customType({
|
|
|
1298
1806
|
return value;
|
|
1299
1807
|
}
|
|
1300
1808
|
})(name);
|
|
1809
|
+
function shouldBindTimestamp(options) {
|
|
1810
|
+
const bindMode = options.bindMode ?? "auto";
|
|
1811
|
+
if (bindMode === "bind")
|
|
1812
|
+
return true;
|
|
1813
|
+
if (bindMode === "literal")
|
|
1814
|
+
return false;
|
|
1815
|
+
const isBun = typeof process !== "undefined" && typeof process.versions?.bun !== "undefined";
|
|
1816
|
+
if (isBun)
|
|
1817
|
+
return false;
|
|
1818
|
+
const forceLiteral = typeof process !== "undefined" ? process.env.DRIZZLE_DUCKDB_FORCE_LITERAL_TIMESTAMPS : undefined;
|
|
1819
|
+
if (forceLiteral && forceLiteral !== "0") {
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1822
|
+
return true;
|
|
1823
|
+
}
|
|
1301
1824
|
var duckDbTimestamp = (name, options = {}) => customType({
|
|
1302
1825
|
dataType() {
|
|
1303
1826
|
if (options.withTimezone) {
|
|
@@ -1307,12 +1830,19 @@ var duckDbTimestamp = (name, options = {}) => customType({
|
|
|
1307
1830
|
return `TIMESTAMP${precision}`;
|
|
1308
1831
|
},
|
|
1309
1832
|
toDriver(value) {
|
|
1833
|
+
if (shouldBindTimestamp(options)) {
|
|
1834
|
+
return wrapTimestamp(value, options.withTimezone ?? false, options.precision);
|
|
1835
|
+
}
|
|
1310
1836
|
const iso = value instanceof Date ? value.toISOString() : value;
|
|
1311
1837
|
const normalized = iso.replace("T", " ").replace("Z", "+00");
|
|
1312
1838
|
const typeKeyword = options.withTimezone ? "TIMESTAMPTZ" : "TIMESTAMP";
|
|
1313
1839
|
return sql4.raw(`${typeKeyword} '${normalized}'`);
|
|
1314
1840
|
},
|
|
1315
1841
|
fromDriver(value) {
|
|
1842
|
+
if (value && typeof value === "object" && "kind" in value && value.kind === "timestamp") {
|
|
1843
|
+
const wrapped = value;
|
|
1844
|
+
return wrapped.data instanceof Date ? wrapped.data : typeof wrapped.data === "number" || typeof wrapped.data === "bigint" ? new Date(Number(wrapped.data) / 1000) : wrapped.data;
|
|
1845
|
+
}
|
|
1316
1846
|
if (options.mode === "string") {
|
|
1317
1847
|
if (value instanceof Date) {
|
|
1318
1848
|
return value.toISOString().replace("T", " ").replace("Z", "+00");
|
|
@@ -1761,6 +2291,20 @@ function mapDuckDbType(column, imports, options) {
|
|
|
1761
2291
|
imports.pgCore.add("text");
|
|
1762
2292
|
return { builder: `text(${columnName(column.name)}) /* JSON */` };
|
|
1763
2293
|
}
|
|
2294
|
+
if (upper.startsWith("ENUM")) {
|
|
2295
|
+
imports.pgCore.add("text");
|
|
2296
|
+
const enumLiteral = raw.replace(/^ENUM\s*/i, "").trim();
|
|
2297
|
+
return {
|
|
2298
|
+
builder: `text(${columnName(column.name)}) /* ENUM ${enumLiteral} */`
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
if (upper.startsWith("UNION")) {
|
|
2302
|
+
imports.pgCore.add("text");
|
|
2303
|
+
const unionLiteral = raw.replace(/^UNION\s*/i, "").trim();
|
|
2304
|
+
return {
|
|
2305
|
+
builder: `text(${columnName(column.name)}) /* UNION ${unionLiteral} */`
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
1764
2308
|
if (upper === "INET") {
|
|
1765
2309
|
imports.local.add("duckDbInet");
|
|
1766
2310
|
return { builder: `duckDbInet(${columnName(column.name)})` };
|
|
@@ -1826,7 +2370,7 @@ function mapDuckDbType(column, imports, options) {
|
|
|
1826
2370
|
}
|
|
1827
2371
|
imports.pgCore.add("text");
|
|
1828
2372
|
return {
|
|
1829
|
-
builder: `text(${columnName(column.name)}) /*
|
|
2373
|
+
builder: `text(${columnName(column.name)}) /* unsupported DuckDB type: ${upper} */`
|
|
1830
2374
|
};
|
|
1831
2375
|
}
|
|
1832
2376
|
function parseStructFields(inner) {
|
|
@@ -2049,13 +2593,19 @@ export {
|
|
|
2049
2593
|
wrapJson,
|
|
2050
2594
|
wrapBlob,
|
|
2051
2595
|
wrapArray,
|
|
2596
|
+
toIdentifier,
|
|
2052
2597
|
sumN,
|
|
2053
2598
|
sumDistinctN,
|
|
2599
|
+
splitTopLevel,
|
|
2054
2600
|
rowNumber,
|
|
2601
|
+
resolveRewriteArraysOption,
|
|
2602
|
+
resolvePrepareCacheOption,
|
|
2055
2603
|
resolvePoolSize,
|
|
2056
2604
|
rank,
|
|
2057
2605
|
prepareParams,
|
|
2058
2606
|
percentileCont,
|
|
2607
|
+
parseStructFields,
|
|
2608
|
+
parseMapValue,
|
|
2059
2609
|
olap,
|
|
2060
2610
|
migrate,
|
|
2061
2611
|
median,
|
|
@@ -2064,9 +2614,12 @@ export {
|
|
|
2064
2614
|
isPool,
|
|
2065
2615
|
isDuckDBWrapper,
|
|
2066
2616
|
introspect,
|
|
2617
|
+
formatLiteral,
|
|
2067
2618
|
executeOnClient,
|
|
2619
|
+
executeInBatchesRaw,
|
|
2068
2620
|
executeInBatches,
|
|
2069
2621
|
executeArrowOnClient,
|
|
2622
|
+
executeArraysOnClient,
|
|
2070
2623
|
duckDbTimestamp,
|
|
2071
2624
|
duckDbTime,
|
|
2072
2625
|
duckDbStruct,
|
|
@@ -2085,7 +2638,12 @@ export {
|
|
|
2085
2638
|
denseRank,
|
|
2086
2639
|
createDuckDBConnectionPool,
|
|
2087
2640
|
countN,
|
|
2641
|
+
coerceArrayString,
|
|
2088
2642
|
closeClientConnection,
|
|
2643
|
+
buildStructLiteral,
|
|
2644
|
+
buildMapLiteral,
|
|
2645
|
+
buildListLiteral,
|
|
2646
|
+
buildDefault,
|
|
2089
2647
|
avgN,
|
|
2090
2648
|
anyValue,
|
|
2091
2649
|
POOL_PRESETS,
|