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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -10,9 +10,21 @@ import {
10
10
  type AnyDuckDBValueWrapper,
11
11
  } from './value-wrappers.ts';
12
12
 
13
- export type DuckDBClientLike = DuckDBConnection;
13
+ export type DuckDBClientLike = DuckDBConnection | DuckDBConnectionPool;
14
14
  export type RowData = Record<string, unknown>;
15
15
 
16
+ export interface DuckDBConnectionPool {
17
+ acquire(): Promise<DuckDBConnection>;
18
+ release(connection: DuckDBConnection): void | Promise<void>;
19
+ close?(): Promise<void> | void;
20
+ }
21
+
22
+ export function isPool(
23
+ client: DuckDBClientLike
24
+ ): client is DuckDBConnectionPool {
25
+ return typeof (client as DuckDBConnectionPool).acquire === 'function';
26
+ }
27
+
16
28
  export interface PrepareParamsOptions {
17
29
  rejectStringArrayLiterals?: boolean;
18
30
  warnOnStringArrayLiteral?: () => void;
@@ -32,8 +44,6 @@ function parsePgArrayLiteral(value: string): unknown {
32
44
  }
33
45
  }
34
46
 
35
- let warnedArrayLiteral = false;
36
-
37
47
  export function prepareParams(
38
48
  params: unknown[],
39
49
  options: PrepareParamsOptions = {}
@@ -46,8 +56,7 @@ export function prepareParams(
46
56
  );
47
57
  }
48
58
 
49
- if (!warnedArrayLiteral && options.warnOnStringArrayLiteral) {
50
- warnedArrayLiteral = true;
59
+ if (options.warnOnStringArrayLiteral) {
51
60
  options.warnOnStringArrayLiteral();
52
61
  }
53
62
  return parsePgArrayLiteral(param);
@@ -138,6 +147,15 @@ export async function executeOnClient(
138
147
  query: string,
139
148
  params: unknown[]
140
149
  ): Promise<RowData[]> {
150
+ if (isPool(client)) {
151
+ const connection = await client.acquire();
152
+ try {
153
+ return await executeOnClient(connection, query, params);
154
+ } finally {
155
+ await client.release(connection);
156
+ }
157
+ }
158
+
141
159
  const values =
142
160
  params.length > 0
143
161
  ? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
@@ -165,6 +183,16 @@ export async function* executeInBatches(
165
183
  params: unknown[],
166
184
  options: ExecuteInBatchesOptions = {}
167
185
  ): AsyncGenerator<RowData[], void, void> {
186
+ if (isPool(client)) {
187
+ const connection = await client.acquire();
188
+ try {
189
+ yield* executeInBatches(connection, query, params, options);
190
+ return;
191
+ } finally {
192
+ await client.release(connection);
193
+ }
194
+ }
195
+
168
196
  const rowsPerChunk =
169
197
  options.rowsPerChunk && options.rowsPerChunk > 0
170
198
  ? options.rowsPerChunk
@@ -207,12 +235,22 @@ export async function executeArrowOnClient(
207
235
  query: string,
208
236
  params: unknown[]
209
237
  ): Promise<unknown> {
238
+ if (isPool(client)) {
239
+ const connection = await client.acquire();
240
+ try {
241
+ return await executeArrowOnClient(connection, query, params);
242
+ } finally {
243
+ await client.release(connection);
244
+ }
245
+ }
246
+
210
247
  const values =
211
248
  params.length > 0
212
249
  ? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
213
250
  : undefined;
214
251
  const result = await client.run(query, values);
215
252
 
253
+ // Runtime detection for Arrow API support (optional method, not in base type)
216
254
  const maybeArrow =
217
255
  (result as unknown as { toArrow?: () => Promise<unknown> }).toArrow ??
218
256
  (result as unknown as { getArrowTable?: () => Promise<unknown> })
package/src/columns.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  type MapValueWrapper,
13
13
  type BlobValueWrapper,
14
14
  type JsonValueWrapper,
15
- } from './value-wrappers.ts';
15
+ } from './value-wrappers-core.ts';
16
16
 
17
17
  type IntColType =
18
18
  | 'SMALLINT'
package/src/driver.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DuckDBInstance } from '@duckdb/node-api';
1
2
  import { entityKind } from 'drizzle-orm/entity';
2
3
  import type { Logger } from 'drizzle-orm/logger';
3
4
  import { DefaultLogger } from 'drizzle-orm/logger';
@@ -23,6 +24,13 @@ import { DuckDBDialect } from './dialect.ts';
23
24
  import { DuckDBSelectBuilder } from './select-builder.ts';
24
25
  import { aliasFields } from './sql/selection.ts';
25
26
  import type { ExecuteInBatchesOptions, RowData } from './client.ts';
27
+ import { isPool } from './client.ts';
28
+ import {
29
+ createDuckDBConnectionPool,
30
+ resolvePoolSize,
31
+ type DuckDBPoolConfig,
32
+ type PoolPreset,
33
+ } from './pool.ts';
26
34
 
27
35
  export interface PgDriverOptions {
28
36
  logger?: Logger;
@@ -50,18 +58,57 @@ export class DuckDBDriver {
50
58
  }
51
59
  }
52
60
 
61
+ /** Connection configuration when using path-based connection */
62
+ export interface DuckDBConnectionConfig {
63
+ /** Database path: ':memory:', './file.duckdb', 'md:', 'md:database' */
64
+ path: string;
65
+ /** DuckDB instance options (e.g., motherduck_token) */
66
+ options?: Record<string, string>;
67
+ }
68
+
53
69
  export interface DuckDBDrizzleConfig<
54
70
  TSchema extends Record<string, unknown> = Record<string, never>,
55
71
  > extends DrizzleConfig<TSchema> {
56
72
  rewriteArrays?: boolean;
57
73
  rejectStringArrayLiterals?: boolean;
74
+ /** Pool configuration. Use preset name, size config, or false to disable. */
75
+ pool?: DuckDBPoolConfig | PoolPreset | false;
58
76
  }
59
77
 
60
- export function drizzle<
78
+ export interface DuckDBDrizzleConfigWithConnection<
79
+ TSchema extends Record<string, unknown> = Record<string, never>,
80
+ > extends DuckDBDrizzleConfig<TSchema> {
81
+ /** Connection string or config object */
82
+ connection: string | DuckDBConnectionConfig;
83
+ }
84
+
85
+ export interface DuckDBDrizzleConfigWithClient<
86
+ TSchema extends Record<string, unknown> = Record<string, never>,
87
+ > extends DuckDBDrizzleConfig<TSchema> {
88
+ /** Explicit client (connection or pool) */
89
+ client: DuckDBClientLike;
90
+ }
91
+
92
+ /** Check if a value looks like a config object (not a client) */
93
+ function isConfigObject(data: unknown): data is Record<string, unknown> {
94
+ if (typeof data !== 'object' || data === null) return false;
95
+ if (data.constructor?.name !== 'Object') return false;
96
+ return (
97
+ 'connection' in data ||
98
+ 'client' in data ||
99
+ 'pool' in data ||
100
+ 'schema' in data ||
101
+ 'logger' in data
102
+ );
103
+ }
104
+
105
+ /** Internal: create database from a client (connection or pool) */
106
+ function createFromClient<
61
107
  TSchema extends Record<string, unknown> = Record<string, never>,
62
108
  >(
63
109
  client: DuckDBClientLike,
64
- config: DuckDBDrizzleConfig<TSchema> = {}
110
+ config: DuckDBDrizzleConfig<TSchema> = {},
111
+ instance?: DuckDBInstance
65
112
  ): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>> {
66
113
  const dialect = new DuckDBDialect();
67
114
 
@@ -89,10 +136,125 @@ export function drizzle<
89
136
  });
90
137
  const session = driver.createSession(schema);
91
138
 
92
- return new DuckDBDatabase(dialect, session, schema) as DuckDBDatabase<
93
- TSchema,
94
- ExtractTablesWithRelations<TSchema>
95
- >;
139
+ const db = new DuckDBDatabase(dialect, session, schema, client, instance);
140
+ return db as DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
141
+ }
142
+
143
+ /** Internal: create database from a connection string */
144
+ async function createFromConnectionString<
145
+ TSchema extends Record<string, unknown> = Record<string, never>,
146
+ >(
147
+ path: string,
148
+ instanceOptions: Record<string, string> | undefined,
149
+ config: DuckDBDrizzleConfig<TSchema> = {}
150
+ ): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>> {
151
+ const instance = await DuckDBInstance.create(path, instanceOptions);
152
+ const poolSize = resolvePoolSize(config.pool);
153
+
154
+ if (poolSize === false) {
155
+ const connection = await instance.connect();
156
+ return createFromClient(connection, config, instance);
157
+ }
158
+
159
+ const pool = createDuckDBConnectionPool(instance, { size: poolSize });
160
+ return createFromClient(pool, config, instance);
161
+ }
162
+
163
+ // Overload 1: Connection string (async, auto-pools)
164
+ export function drizzle<
165
+ TSchema extends Record<string, unknown> = Record<string, never>,
166
+ >(
167
+ connectionString: string
168
+ ): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
169
+
170
+ // Overload 2: Connection string + config (async, auto-pools)
171
+ export function drizzle<
172
+ TSchema extends Record<string, unknown> = Record<string, never>,
173
+ >(
174
+ connectionString: string,
175
+ config: DuckDBDrizzleConfig<TSchema>
176
+ ): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
177
+
178
+ // Overload 3: Config with connection (async, auto-pools)
179
+ export function drizzle<
180
+ TSchema extends Record<string, unknown> = Record<string, never>,
181
+ >(
182
+ config: DuckDBDrizzleConfigWithConnection<TSchema>
183
+ ): Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>>;
184
+
185
+ // Overload 4: Config with explicit client (sync)
186
+ export function drizzle<
187
+ TSchema extends Record<string, unknown> = Record<string, never>,
188
+ >(
189
+ config: DuckDBDrizzleConfigWithClient<TSchema>
190
+ ): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
191
+
192
+ // Overload 5: Explicit client (sync, backward compatible)
193
+ export function drizzle<
194
+ TSchema extends Record<string, unknown> = Record<string, never>,
195
+ >(
196
+ client: DuckDBClientLike,
197
+ config?: DuckDBDrizzleConfig<TSchema>
198
+ ): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>;
199
+
200
+ // Implementation
201
+ export function drizzle<
202
+ TSchema extends Record<string, unknown> = Record<string, never>,
203
+ >(
204
+ clientOrConfigOrPath:
205
+ | string
206
+ | DuckDBClientLike
207
+ | DuckDBDrizzleConfigWithConnection<TSchema>
208
+ | DuckDBDrizzleConfigWithClient<TSchema>,
209
+ config?: DuckDBDrizzleConfig<TSchema>
210
+ ):
211
+ | DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>
212
+ | Promise<DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>>> {
213
+ // String path -> async with auto-pool
214
+ if (typeof clientOrConfigOrPath === 'string') {
215
+ return createFromConnectionString(clientOrConfigOrPath, undefined, config);
216
+ }
217
+
218
+ // Config object with connection or client
219
+ if (isConfigObject(clientOrConfigOrPath)) {
220
+ const configObj = clientOrConfigOrPath as
221
+ | DuckDBDrizzleConfigWithConnection<TSchema>
222
+ | DuckDBDrizzleConfigWithClient<TSchema>;
223
+
224
+ if ('connection' in configObj) {
225
+ const connConfig =
226
+ configObj as DuckDBDrizzleConfigWithConnection<TSchema>;
227
+ const { connection, ...restConfig } = connConfig;
228
+ if (typeof connection === 'string') {
229
+ return createFromConnectionString(
230
+ connection,
231
+ undefined,
232
+ restConfig as DuckDBDrizzleConfig<TSchema>
233
+ );
234
+ }
235
+ return createFromConnectionString(
236
+ connection.path,
237
+ connection.options,
238
+ restConfig as DuckDBDrizzleConfig<TSchema>
239
+ );
240
+ }
241
+
242
+ if ('client' in configObj) {
243
+ const clientConfig = configObj as DuckDBDrizzleConfigWithClient<TSchema>;
244
+ const { client: clientValue, ...restConfig } = clientConfig;
245
+ return createFromClient(
246
+ clientValue,
247
+ restConfig as DuckDBDrizzleConfig<TSchema>
248
+ );
249
+ }
250
+
251
+ throw new Error(
252
+ 'Invalid drizzle config: either connection or client must be provided'
253
+ );
254
+ }
255
+
256
+ // Direct client (backward compatible)
257
+ return createFromClient(clientOrConfigOrPath as DuckDBClientLike, config);
96
258
  }
97
259
 
98
260
  export class DuckDBDatabase<
@@ -102,12 +264,33 @@ export class DuckDBDatabase<
102
264
  > extends PgDatabase<DuckDBQueryResultHKT, TFullSchema, TSchema> {
103
265
  static readonly [entityKind]: string = 'DuckDBDatabase';
104
266
 
267
+ /** The underlying connection or pool */
268
+ readonly $client: DuckDBClientLike;
269
+
270
+ /** The DuckDB instance (when created from connection string) */
271
+ readonly $instance?: DuckDBInstance;
272
+
105
273
  constructor(
106
274
  readonly dialect: DuckDBDialect,
107
275
  readonly session: DuckDBSession<TFullSchema, TSchema>,
108
- schema: RelationalSchemaConfig<TSchema> | undefined
276
+ schema: RelationalSchemaConfig<TSchema> | undefined,
277
+ client: DuckDBClientLike,
278
+ instance?: DuckDBInstance
109
279
  ) {
110
280
  super(dialect, session, schema);
281
+ this.$client = client;
282
+ this.$instance = instance;
283
+ }
284
+
285
+ /**
286
+ * Close the database connection pool and instance.
287
+ * Should be called when shutting down the application.
288
+ */
289
+ async close(): Promise<void> {
290
+ if (isPool(this.$client) && this.$client.close) {
291
+ await this.$client.close();
292
+ }
293
+ // Note: DuckDBInstance doesn't have a close method in the current API
111
294
  }
112
295
 
113
296
  select(): DuckDBSelectBuilder<undefined>;
@@ -119,6 +302,7 @@ export class DuckDBDatabase<
119
302
  ): DuckDBSelectBuilder<SelectedFields | undefined> {
120
303
  const selectedFields = fields ? aliasFields(fields) : undefined;
121
304
 
305
+ // Cast needed: DuckDBSession is compatible but types don't align exactly with PgSession
122
306
  return new DuckDBSelectBuilder({
123
307
  fields: selectedFields ?? undefined,
124
308
  session: this.session as unknown as PgSession<DuckDBQueryResultHKT>,
package/src/helpers.ts ADDED
@@ -0,0 +1,18 @@
1
+ // Client-safe entrypoint exposing schema builder utilities without pulling
2
+ // the DuckDB Node API bindings. Intended for generated schemas and browser use.
3
+ export {
4
+ duckDbList,
5
+ duckDbArray,
6
+ duckDbMap,
7
+ duckDbStruct,
8
+ duckDbJson,
9
+ duckDbBlob,
10
+ duckDbInet,
11
+ duckDbInterval,
12
+ duckDbTimestamp,
13
+ duckDbDate,
14
+ duckDbTime,
15
+ duckDbArrayContains,
16
+ duckDbArrayContained,
17
+ duckDbArrayOverlaps,
18
+ } from './columns.ts';
package/src/index.ts CHANGED
@@ -4,5 +4,6 @@ export * from './columns.ts';
4
4
  export * from './migrator.ts';
5
5
  export * from './introspect.ts';
6
6
  export * from './client.ts';
7
+ export * from './pool.ts';
7
8
  export * from './olap.ts';
8
9
  export * from './value-wrappers.ts';
package/src/introspect.ts CHANGED
@@ -108,7 +108,8 @@ type ImportBuckets = {
108
108
  local: Set<string>;
109
109
  };
110
110
 
111
- const DEFAULT_IMPORT_BASE = '@leonardovida-md/drizzle-neo-duckdb';
111
+ export const DEFAULT_IMPORT_BASE =
112
+ '@leonardovida-md/drizzle-neo-duckdb/helpers';
112
113
 
113
114
  export async function introspect(
114
115
  db: DuckDBDatabase,
@@ -649,6 +650,28 @@ function mapDuckDbType(
649
650
  return { builder: `doublePrecision(${columnName(column.name)})` };
650
651
  }
651
652
 
653
+ const arrayMatch = /^(.*)\[(\d+)\]$/.exec(upper);
654
+ if (arrayMatch) {
655
+ imports.local.add('duckDbArray');
656
+ const [, base, length] = arrayMatch;
657
+ return {
658
+ builder: `duckDbArray(${columnName(
659
+ column.name
660
+ )}, ${JSON.stringify(base)}, ${Number(length)})`,
661
+ };
662
+ }
663
+
664
+ const listMatch = /^(.*)\[\]$/.exec(upper);
665
+ if (listMatch) {
666
+ imports.local.add('duckDbList');
667
+ const [, base] = listMatch;
668
+ return {
669
+ builder: `duckDbList(${columnName(
670
+ column.name
671
+ )}, ${JSON.stringify(base)})`,
672
+ };
673
+ }
674
+
652
675
  if (upper.startsWith('CHAR(') || upper === 'CHAR') {
653
676
  imports.pgCore.add('char');
654
677
  const length = column.characterLength;
@@ -699,28 +722,6 @@ function mapDuckDbType(
699
722
  return { builder: `duckDbBlob(${columnName(column.name)})` };
700
723
  }
701
724
 
702
- const arrayMatch = /^(.*)\[(\d+)\]$/.exec(upper);
703
- if (arrayMatch) {
704
- imports.local.add('duckDbArray');
705
- const [, base, length] = arrayMatch;
706
- return {
707
- builder: `duckDbArray(${columnName(
708
- column.name
709
- )}, ${JSON.stringify(base)}, ${Number(length)})`,
710
- };
711
- }
712
-
713
- const listMatch = /^(.*)\[\]$/.exec(upper);
714
- if (listMatch) {
715
- imports.local.add('duckDbList');
716
- const [, base] = listMatch;
717
- return {
718
- builder: `duckDbList(${columnName(
719
- column.name
720
- )}, ${JSON.stringify(base)})`,
721
- };
722
- }
723
-
724
725
  if (upper.startsWith('STRUCT')) {
725
726
  imports.local.add('duckDbStruct');
726
727
  const inner = upper.replace(/^STRUCT\s*\(/i, '').replace(/\)$/, '');
package/src/migrator.ts CHANGED
@@ -14,9 +14,9 @@ export async function migrate<TSchema extends Record<string, unknown>>(
14
14
 
15
15
  const migrations = readMigrationFiles(migrationConfig);
16
16
 
17
+ // Cast needed: Drizzle's internal PgSession type differs from exported type
17
18
  await db.dialect.migrate(
18
19
  migrations,
19
- // Need to work around omitted internal types from drizzle...
20
20
  db.session as unknown as PgSession,
21
21
  migrationConfig
22
22
  );
package/src/olap.ts CHANGED
@@ -169,6 +169,7 @@ export class OlapBuilder {
169
169
 
170
170
  Object.assign(selection, this.measureMap);
171
171
 
172
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Drizzle's query builder types don't allow reassignment after groupBy
172
173
  let query: any = this.db
173
174
  .select(selection as SelectedFields)
174
175
  .from(this.source!)
package/src/pool.ts ADDED
@@ -0,0 +1,104 @@
1
+ import { DuckDBConnection, DuckDBInstance } from '@duckdb/node-api';
2
+ import { closeClientConnection, type DuckDBConnectionPool } from './client.ts';
3
+
4
+ /** Pool size presets for different MotherDuck instance types */
5
+ export type PoolPreset =
6
+ | 'pulse'
7
+ | 'standard'
8
+ | 'jumbo'
9
+ | 'mega'
10
+ | 'giga'
11
+ | 'local'
12
+ | 'memory';
13
+
14
+ /** Pool sizes optimized for each MotherDuck instance type */
15
+ export const POOL_PRESETS: Record<PoolPreset, number> = {
16
+ pulse: 4, // Auto-scaling, ad-hoc analytics
17
+ standard: 6, // Balanced ETL/ELT workloads
18
+ jumbo: 8, // Complex queries, high-volume
19
+ mega: 12, // Large-scale transformations
20
+ giga: 16, // Maximum parallelism
21
+ local: 8, // Local DuckDB file
22
+ memory: 4, // In-memory testing
23
+ };
24
+
25
+ export interface DuckDBPoolConfig {
26
+ /** Maximum concurrent connections. Defaults to 4. */
27
+ size?: number;
28
+ }
29
+
30
+ /**
31
+ * Resolve pool configuration to a concrete size.
32
+ * Returns false if pooling is disabled.
33
+ */
34
+ export function resolvePoolSize(
35
+ pool: DuckDBPoolConfig | PoolPreset | false | undefined
36
+ ): number | false {
37
+ if (pool === false) return false;
38
+ if (pool === undefined) return 4;
39
+ if (typeof pool === 'string') return POOL_PRESETS[pool];
40
+ return pool.size ?? 4;
41
+ }
42
+
43
+ export interface DuckDBConnectionPoolOptions {
44
+ /** Maximum concurrent connections. Defaults to 4. */
45
+ size?: number;
46
+ }
47
+
48
+ export function createDuckDBConnectionPool(
49
+ instance: DuckDBInstance,
50
+ options: DuckDBConnectionPoolOptions = {}
51
+ ): DuckDBConnectionPool & { size: number } {
52
+ const size = options.size && options.size > 0 ? options.size : 4;
53
+ const idle: DuckDBConnection[] = [];
54
+ const waiting: Array<(conn: DuckDBConnection) => void> = [];
55
+ let total = 0;
56
+ let closed = false;
57
+
58
+ const acquire = async (): Promise<DuckDBConnection> => {
59
+ if (closed) {
60
+ throw new Error('DuckDB connection pool is closed');
61
+ }
62
+
63
+ if (idle.length > 0) {
64
+ return idle.pop() as DuckDBConnection;
65
+ }
66
+
67
+ if (total < size) {
68
+ total += 1;
69
+ return await DuckDBConnection.create(instance);
70
+ }
71
+
72
+ return await new Promise((resolve) => {
73
+ waiting.push(resolve);
74
+ });
75
+ };
76
+
77
+ const release = async (connection: DuckDBConnection): Promise<void> => {
78
+ if (closed) {
79
+ await closeClientConnection(connection);
80
+ return;
81
+ }
82
+
83
+ const waiter = waiting.shift();
84
+ if (waiter) {
85
+ waiter(connection);
86
+ return;
87
+ }
88
+
89
+ idle.push(connection);
90
+ };
91
+
92
+ const close = async (): Promise<void> => {
93
+ closed = true;
94
+ const toClose = idle.splice(0, idle.length);
95
+ await Promise.all(toClose.map((conn) => closeClientConnection(conn)));
96
+ };
97
+
98
+ return {
99
+ acquire,
100
+ release,
101
+ close,
102
+ size,
103
+ };
104
+ }
package/src/session.ts CHANGED
@@ -19,7 +19,11 @@ import { adaptArrayOperators } from './sql/query-rewriters.ts';
19
19
  import { mapResultRow } from './sql/result-mapper.ts';
20
20
  import type { DuckDBDialect } from './dialect.ts';
21
21
  import { TransactionRollbackError } from 'drizzle-orm/errors';
22
- import type { DuckDBClientLike, RowData } from './client.ts';
22
+ import type {
23
+ DuckDBClientLike,
24
+ DuckDBConnectionPool,
25
+ RowData,
26
+ } from './client.ts';
23
27
  import {
24
28
  executeArrowOnClient,
25
29
  executeInBatches,
@@ -27,6 +31,8 @@ import {
27
31
  prepareParams,
28
32
  type ExecuteInBatchesOptions,
29
33
  } from './client.ts';
34
+ import { isPool } from './client.ts';
35
+ import type { DuckDBConnection } from '@duckdb/node-api';
30
36
 
31
37
  export type { DuckDBClientLike, RowData } from './client.ts';
32
38
 
@@ -165,8 +171,18 @@ export class DuckDBSession<
165
171
  override async transaction<T>(
166
172
  transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
167
173
  ): Promise<T> {
174
+ let pinnedConnection: DuckDBConnection | undefined;
175
+ let pool: DuckDBConnectionPool | undefined;
176
+
177
+ let clientForTx: DuckDBClientLike = this.client;
178
+ if (isPool(this.client)) {
179
+ pool = this.client;
180
+ pinnedConnection = await pool.acquire();
181
+ clientForTx = pinnedConnection;
182
+ }
183
+
168
184
  const session = new DuckDBSession(
169
- this.client,
185
+ clientForTx,
170
186
  this.dialect,
171
187
  this.schema,
172
188
  this.options
@@ -178,15 +194,21 @@ export class DuckDBSession<
178
194
  this.schema
179
195
  );
180
196
 
181
- await tx.execute(sql`BEGIN TRANSACTION;`);
182
-
183
197
  try {
184
- const result = await transaction(tx);
185
- await tx.execute(sql`commit`);
186
- return result;
187
- } catch (error) {
188
- await tx.execute(sql`rollback`);
189
- throw error;
198
+ await tx.execute(sql`BEGIN TRANSACTION;`);
199
+
200
+ try {
201
+ const result = await transaction(tx);
202
+ await tx.execute(sql`commit`);
203
+ return result;
204
+ } catch (error) {
205
+ await tx.execute(sql`rollback`);
206
+ throw error;
207
+ }
208
+ } finally {
209
+ if (pinnedConnection && pool) {
210
+ await pool.release(pinnedConnection);
211
+ }
190
212
  }
191
213
  }
192
214
 
@@ -297,6 +319,7 @@ export class DuckDBTransaction<
297
319
  }
298
320
 
299
321
  setTransaction(config: PgTransactionConfig): Promise<void> {
322
+ // Cast needed: PgTransaction doesn't expose dialect/session properties in public API
300
323
  type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
301
324
  return (this as unknown as Tx).session.execute(
302
325
  sql`set transaction ${this.getTransactionConfigSQL(config)}`
@@ -307,11 +330,13 @@ export class DuckDBTransaction<
307
330
  query: SQL,
308
331
  options: ExecuteInBatchesOptions = {}
309
332
  ): AsyncGenerator<GenericRowData<T>[], void, void> {
333
+ // Cast needed: PgTransaction doesn't expose session property in public API
310
334
  type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
311
335
  return (this as unknown as Tx).session.executeBatches<T>(query, options);
312
336
  }
313
337
 
314
338
  executeArrow(query: SQL): Promise<unknown> {
339
+ // Cast needed: PgTransaction doesn't expose session property in public API
315
340
  type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
316
341
  return (this as unknown as Tx).session.executeArrow(query);
317
342
  }
@@ -319,6 +344,7 @@ export class DuckDBTransaction<
319
344
  override async transaction<T>(
320
345
  transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
321
346
  ): Promise<T> {
347
+ // Cast needed: PgTransaction doesn't expose dialect/session properties in public API
322
348
  type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
323
349
  const nestedTx = new DuckDBTransaction<TFullSchema, TSchema>(
324
350
  (this as unknown as Tx).dialect,