@leonardovida-md/drizzle-neo-duckdb 1.0.3 → 1.1.1

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