@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/src/pool.ts CHANGED
@@ -43,6 +43,14 @@ export function resolvePoolSize(
43
43
  export interface DuckDBConnectionPoolOptions {
44
44
  /** Maximum concurrent connections. Defaults to 4. */
45
45
  size?: number;
46
+ /** Timeout in milliseconds to wait for a connection. Defaults to 30000 (30s). */
47
+ acquireTimeout?: number;
48
+ /** Maximum number of requests waiting for a connection. Defaults to 100. */
49
+ maxWaitingRequests?: number;
50
+ /** Max time (ms) a connection may live before being recycled. */
51
+ maxLifetimeMs?: number;
52
+ /** Max idle time (ms) before an idle connection is discarded. */
53
+ idleTimeoutMs?: number;
46
54
  }
47
55
 
48
56
  export function createDuckDBConnectionPool(
@@ -50,49 +58,211 @@ export function createDuckDBConnectionPool(
50
58
  options: DuckDBConnectionPoolOptions = {}
51
59
  ): DuckDBConnectionPool & { size: number } {
52
60
  const size = options.size && options.size > 0 ? options.size : 4;
53
- const idle: DuckDBConnection[] = [];
54
- const waiting: Array<(conn: DuckDBConnection) => void> = [];
61
+ const acquireTimeout = options.acquireTimeout ?? 30_000;
62
+ const maxWaitingRequests = options.maxWaitingRequests ?? 100;
63
+ const maxLifetimeMs = options.maxLifetimeMs;
64
+ const idleTimeoutMs = options.idleTimeoutMs;
65
+ const metadata = new WeakMap<
66
+ DuckDBConnection,
67
+ { createdAt: number; lastUsedAt: number }
68
+ >();
69
+
70
+ type PooledConnection = {
71
+ connection: DuckDBConnection;
72
+ createdAt: number;
73
+ lastUsedAt: number;
74
+ };
75
+
76
+ const idle: PooledConnection[] = [];
77
+ const waiting: Array<{
78
+ resolve: (conn: DuckDBConnection) => void;
79
+ reject: (error: Error) => void;
80
+ timeoutId: ReturnType<typeof setTimeout>;
81
+ }> = [];
55
82
  let total = 0;
56
83
  let closed = false;
84
+ // Track pending acquires to handle race conditions during close
85
+ let pendingAcquires = 0;
86
+
87
+ const shouldRecycle = (conn: PooledConnection, now: number): boolean => {
88
+ if (maxLifetimeMs !== undefined && now - conn.createdAt >= maxLifetimeMs) {
89
+ return true;
90
+ }
91
+ if (idleTimeoutMs !== undefined && now - conn.lastUsedAt >= idleTimeoutMs) {
92
+ return true;
93
+ }
94
+ return false;
95
+ };
57
96
 
58
97
  const acquire = async (): Promise<DuckDBConnection> => {
59
98
  if (closed) {
60
99
  throw new Error('DuckDB connection pool is closed');
61
100
  }
62
101
 
63
- if (idle.length > 0) {
64
- return idle.pop() as DuckDBConnection;
102
+ while (idle.length > 0) {
103
+ const pooled = idle.pop() as PooledConnection;
104
+ const now = Date.now();
105
+ if (shouldRecycle(pooled, now)) {
106
+ await closeClientConnection(pooled.connection);
107
+ total = Math.max(0, total - 1);
108
+ metadata.delete(pooled.connection);
109
+ continue;
110
+ }
111
+ pooled.lastUsedAt = now;
112
+ metadata.set(pooled.connection, {
113
+ createdAt: pooled.createdAt,
114
+ lastUsedAt: pooled.lastUsedAt,
115
+ });
116
+ return pooled.connection;
65
117
  }
66
118
 
67
119
  if (total < size) {
120
+ pendingAcquires += 1;
68
121
  total += 1;
69
- return await DuckDBConnection.create(instance);
122
+ try {
123
+ const connection = await DuckDBConnection.create(instance);
124
+ // Check if pool was closed during async connection creation
125
+ if (closed) {
126
+ await closeClientConnection(connection);
127
+ total -= 1;
128
+ throw new Error('DuckDB connection pool is closed');
129
+ }
130
+ const now = Date.now();
131
+ metadata.set(connection, { createdAt: now, lastUsedAt: now });
132
+ return connection;
133
+ } catch (error) {
134
+ total -= 1;
135
+ throw error;
136
+ } finally {
137
+ pendingAcquires -= 1;
138
+ }
70
139
  }
71
140
 
72
- return await new Promise((resolve) => {
73
- waiting.push(resolve);
141
+ // Check queue limit before waiting
142
+ if (waiting.length >= maxWaitingRequests) {
143
+ throw new Error(
144
+ `DuckDB connection pool queue is full (max ${maxWaitingRequests} waiting requests)`
145
+ );
146
+ }
147
+
148
+ return await new Promise((resolve, reject) => {
149
+ const timeoutId = setTimeout(() => {
150
+ // Remove this waiter from the queue
151
+ const idx = waiting.findIndex((w) => w.timeoutId === timeoutId);
152
+ if (idx !== -1) {
153
+ waiting.splice(idx, 1);
154
+ }
155
+ reject(
156
+ new Error(
157
+ `DuckDB connection pool acquire timeout after ${acquireTimeout}ms`
158
+ )
159
+ );
160
+ }, acquireTimeout);
161
+
162
+ waiting.push({ resolve, reject, timeoutId });
74
163
  });
75
164
  };
76
165
 
77
166
  const release = async (connection: DuckDBConnection): Promise<void> => {
167
+ const waiter = waiting.shift();
168
+ if (waiter) {
169
+ clearTimeout(waiter.timeoutId);
170
+ const now = Date.now();
171
+ const meta =
172
+ metadata.get(connection) ??
173
+ ({ createdAt: now, lastUsedAt: now } as {
174
+ createdAt: number;
175
+ lastUsedAt: number;
176
+ });
177
+
178
+ const expired =
179
+ maxLifetimeMs !== undefined && now - meta.createdAt >= maxLifetimeMs;
180
+
181
+ if (closed) {
182
+ await closeClientConnection(connection);
183
+ total = Math.max(0, total - 1);
184
+ metadata.delete(connection);
185
+ waiter.reject(new Error('DuckDB connection pool is closed'));
186
+ return;
187
+ }
188
+
189
+ if (expired) {
190
+ await closeClientConnection(connection);
191
+ total = Math.max(0, total - 1);
192
+ metadata.delete(connection);
193
+ try {
194
+ const replacement = await acquire();
195
+ waiter.resolve(replacement);
196
+ } catch (error) {
197
+ waiter.reject(error as Error);
198
+ }
199
+ return;
200
+ }
201
+
202
+ meta.lastUsedAt = now;
203
+ metadata.set(connection, meta);
204
+ waiter.resolve(connection);
205
+ return;
206
+ }
207
+
78
208
  if (closed) {
79
209
  await closeClientConnection(connection);
210
+ metadata.delete(connection);
211
+ total = Math.max(0, total - 1);
80
212
  return;
81
213
  }
82
214
 
83
- const waiter = waiting.shift();
84
- if (waiter) {
85
- waiter(connection);
215
+ const now = Date.now();
216
+ const existingMeta =
217
+ metadata.get(connection) ??
218
+ ({ createdAt: now, lastUsedAt: now } as {
219
+ createdAt: number;
220
+ lastUsedAt: number;
221
+ });
222
+ existingMeta.lastUsedAt = now;
223
+ metadata.set(connection, existingMeta);
224
+
225
+ if (
226
+ maxLifetimeMs !== undefined &&
227
+ now - existingMeta.createdAt >= maxLifetimeMs
228
+ ) {
229
+ await closeClientConnection(connection);
230
+ total -= 1;
231
+ metadata.delete(connection);
86
232
  return;
87
233
  }
88
234
 
89
- idle.push(connection);
235
+ idle.push({
236
+ connection,
237
+ createdAt: existingMeta.createdAt,
238
+ lastUsedAt: existingMeta.lastUsedAt,
239
+ });
90
240
  };
91
241
 
92
242
  const close = async (): Promise<void> => {
93
243
  closed = true;
244
+
245
+ // Clear all waiting requests with their timeouts
246
+ const waiters = waiting.splice(0, waiting.length);
247
+ for (const waiter of waiters) {
248
+ clearTimeout(waiter.timeoutId);
249
+ waiter.reject(new Error('DuckDB connection pool is closed'));
250
+ }
251
+
252
+ // Close all idle connections (use allSettled to ensure all are attempted)
94
253
  const toClose = idle.splice(0, idle.length);
95
- await Promise.all(toClose.map((conn) => closeClientConnection(conn)));
254
+ await Promise.allSettled(
255
+ toClose.map((item) => closeClientConnection(item.connection))
256
+ );
257
+ total = Math.max(0, total - toClose.length);
258
+ toClose.forEach((item) => metadata.delete(item.connection));
259
+
260
+ // Wait for pending acquires to complete (with a reasonable timeout)
261
+ const maxWait = 5000;
262
+ const start = Date.now();
263
+ while (pendingAcquires > 0 && Date.now() - start < maxWait) {
264
+ await new Promise((r) => setTimeout(r, 10));
265
+ }
96
266
  };
97
267
 
98
268
  return {
package/src/session.ts CHANGED
@@ -17,8 +17,8 @@ import { fillPlaceholders, type Query, SQL, sql } from 'drizzle-orm/sql/sql';
17
17
  import type { Assume } from 'drizzle-orm/utils';
18
18
  import { adaptArrayOperators } from './sql/query-rewriters.ts';
19
19
  import { mapResultRow } from './sql/result-mapper.ts';
20
- import type { DuckDBDialect } from './dialect.ts';
21
20
  import { TransactionRollbackError } from 'drizzle-orm/errors';
21
+ import type { DuckDBDialect } from './dialect.ts';
22
22
  import type {
23
23
  DuckDBClientLike,
24
24
  DuckDBConnectionPool,
@@ -26,16 +26,45 @@ import type {
26
26
  } from './client.ts';
27
27
  import {
28
28
  executeArrowOnClient,
29
+ executeArraysOnClient,
29
30
  executeInBatches,
31
+ executeInBatchesRaw,
30
32
  executeOnClient,
31
33
  prepareParams,
34
+ type ExecuteBatchesRawChunk,
32
35
  type ExecuteInBatchesOptions,
33
36
  } from './client.ts';
34
37
  import { isPool } from './client.ts';
35
38
  import type { DuckDBConnection } from '@duckdb/node-api';
39
+ import type {
40
+ PreparedStatementCacheConfig,
41
+ RewriteArraysMode,
42
+ } from './options.ts';
36
43
 
37
44
  export type { DuckDBClientLike, RowData } from './client.ts';
38
45
 
46
+ function isSavepointSyntaxError(error: unknown): boolean {
47
+ if (!(error instanceof Error) || !error.message) {
48
+ return false;
49
+ }
50
+ return (
51
+ error.message.toLowerCase().includes('savepoint') &&
52
+ error.message.toLowerCase().includes('syntax error')
53
+ );
54
+ }
55
+
56
+ function rewriteQuery(
57
+ mode: RewriteArraysMode,
58
+ query: string
59
+ ): { sql: string; rewritten: boolean } {
60
+ if (mode === 'never') {
61
+ return { sql: query, rewritten: false };
62
+ }
63
+
64
+ const rewritten = adaptArrayOperators(query);
65
+ return { sql: rewritten, rewritten: rewritten !== query };
66
+ }
67
+
39
68
  export class DuckDBPreparedQuery<
40
69
  T extends PreparedQueryConfig,
41
70
  > extends PgPreparedQuery<T> {
@@ -52,8 +81,9 @@ export class DuckDBPreparedQuery<
52
81
  private customResultMapper:
53
82
  | ((rows: unknown[][]) => T['execute'])
54
83
  | undefined,
55
- private rewriteArrays: boolean,
84
+ private rewriteArraysMode: RewriteArraysMode,
56
85
  private rejectStringArrayLiterals: boolean,
86
+ private prepareCache: PreparedStatementCacheConfig | undefined,
57
87
  private warnOnStringArrayLiteral?: (sql: string) => void
58
88
  ) {
59
89
  super({ sql: queryString, params });
@@ -72,11 +102,12 @@ export class DuckDBPreparedQuery<
72
102
  : undefined,
73
103
  }
74
104
  );
75
- const rewrittenQuery = this.rewriteArrays
76
- ? adaptArrayOperators(this.queryString)
77
- : this.queryString;
105
+ const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
106
+ this.rewriteArraysMode,
107
+ this.queryString
108
+ );
78
109
 
79
- if (this.rewriteArrays && rewrittenQuery !== this.queryString) {
110
+ if (didRewrite) {
80
111
  this.logger.logQuery(
81
112
  `[duckdb] original query before array rewrite: ${this.queryString}`,
82
113
  params
@@ -88,19 +119,30 @@ export class DuckDBPreparedQuery<
88
119
  const { fields, joinsNotNullableMap, customResultMapper } =
89
120
  this as typeof this & { joinsNotNullableMap?: Record<string, boolean> };
90
121
 
91
- const rows = await executeOnClient(this.client, rewrittenQuery, params);
122
+ if (fields) {
123
+ const { rows } = await executeArraysOnClient(
124
+ this.client,
125
+ rewrittenQuery,
126
+ params,
127
+ { prepareCache: this.prepareCache }
128
+ );
92
129
 
93
- if (rows.length === 0 || !fields) {
94
- return rows as T['execute'];
130
+ if (rows.length === 0) {
131
+ return [] as T['execute'];
132
+ }
133
+
134
+ return customResultMapper
135
+ ? customResultMapper(rows)
136
+ : rows.map((row) =>
137
+ mapResultRow<T['execute']>(fields, row, joinsNotNullableMap)
138
+ );
95
139
  }
96
140
 
97
- const rowValues = rows.map((row) => Object.values(row));
141
+ const rows = await executeOnClient(this.client, rewrittenQuery, params, {
142
+ prepareCache: this.prepareCache,
143
+ });
98
144
 
99
- return customResultMapper
100
- ? customResultMapper(rowValues)
101
- : rowValues.map((row) =>
102
- mapResultRow<T['execute']>(fields, row, joinsNotNullableMap)
103
- );
145
+ return rows as T['execute'];
104
146
  }
105
147
 
106
148
  all(
@@ -116,8 +158,9 @@ export class DuckDBPreparedQuery<
116
158
 
117
159
  export interface DuckDBSessionOptions {
118
160
  logger?: Logger;
119
- rewriteArrays?: boolean;
161
+ rewriteArrays?: RewriteArraysMode;
120
162
  rejectStringArrayLiterals?: boolean;
163
+ prepareCache?: PreparedStatementCacheConfig;
121
164
  }
122
165
 
123
166
  export class DuckDBSession<
@@ -128,9 +171,11 @@ export class DuckDBSession<
128
171
 
129
172
  protected override dialect: DuckDBDialect;
130
173
  private logger: Logger;
131
- private rewriteArrays: boolean;
174
+ private rewriteArraysMode: RewriteArraysMode;
132
175
  private rejectStringArrayLiterals: boolean;
176
+ private prepareCache: PreparedStatementCacheConfig | undefined;
133
177
  private hasWarnedArrayLiteral = false;
178
+ private rollbackOnly = false;
134
179
 
135
180
  constructor(
136
181
  private client: DuckDBClientLike,
@@ -141,8 +186,14 @@ export class DuckDBSession<
141
186
  super(dialect);
142
187
  this.dialect = dialect;
143
188
  this.logger = options.logger ?? new NoopLogger();
144
- this.rewriteArrays = options.rewriteArrays ?? true;
189
+ this.rewriteArraysMode = options.rewriteArrays ?? 'auto';
145
190
  this.rejectStringArrayLiterals = options.rejectStringArrayLiterals ?? false;
191
+ this.prepareCache = options.prepareCache;
192
+ this.options = {
193
+ ...options,
194
+ rewriteArrays: this.rewriteArraysMode,
195
+ prepareCache: this.prepareCache,
196
+ };
146
197
  }
147
198
 
148
199
  prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
@@ -162,14 +213,26 @@ export class DuckDBSession<
162
213
  fields,
163
214
  isResponseInArrayMode,
164
215
  customResultMapper,
165
- this.rewriteArrays,
216
+ this.rewriteArraysMode,
166
217
  this.rejectStringArrayLiterals,
218
+ this.prepareCache,
167
219
  this.rejectStringArrayLiterals ? undefined : this.warnOnStringArrayLiteral
168
220
  );
169
221
  }
170
222
 
223
+ override execute<T>(query: SQL): Promise<T> {
224
+ this.dialect.resetPgJsonFlag();
225
+ return super.execute(query);
226
+ }
227
+
228
+ override all<T = unknown>(query: SQL): Promise<T[]> {
229
+ this.dialect.resetPgJsonFlag();
230
+ return super.all(query);
231
+ }
232
+
171
233
  override async transaction<T>(
172
- transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
234
+ transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>,
235
+ config?: PgTransactionConfig
173
236
  ): Promise<T> {
174
237
  let pinnedConnection: DuckDBConnection | undefined;
175
238
  let pool: DuckDBConnectionPool | undefined;
@@ -197,8 +260,16 @@ export class DuckDBSession<
197
260
  try {
198
261
  await tx.execute(sql`BEGIN TRANSACTION;`);
199
262
 
263
+ if (config) {
264
+ await tx.setTransaction(config);
265
+ }
266
+
200
267
  try {
201
268
  const result = await transaction(tx);
269
+ if (session.isRollbackOnly()) {
270
+ await tx.execute(sql`rollback`);
271
+ throw new TransactionRollbackError();
272
+ }
202
273
  await tx.execute(sql`commit`);
203
274
  return result;
204
275
  } catch (error) {
@@ -227,18 +298,21 @@ export class DuckDBSession<
227
298
  query: SQL,
228
299
  options: ExecuteInBatchesOptions = {}
229
300
  ): AsyncGenerator<GenericRowData<T>[], void, void> {
301
+ this.dialect.resetPgJsonFlag();
230
302
  const builtQuery = this.dialect.sqlToQuery(query);
303
+ this.dialect.assertNoPgJsonColumns();
231
304
  const params = prepareParams(builtQuery.params, {
232
305
  rejectStringArrayLiterals: this.rejectStringArrayLiterals,
233
306
  warnOnStringArrayLiteral: this.rejectStringArrayLiterals
234
307
  ? undefined
235
308
  : () => this.warnOnStringArrayLiteral(builtQuery.sql),
236
309
  });
237
- const rewrittenQuery = this.rewriteArrays
238
- ? adaptArrayOperators(builtQuery.sql)
239
- : builtQuery.sql;
310
+ const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
311
+ this.rewriteArraysMode,
312
+ builtQuery.sql
313
+ );
240
314
 
241
- if (this.rewriteArrays && rewrittenQuery !== builtQuery.sql) {
315
+ if (didRewrite) {
242
316
  this.logger.logQuery(
243
317
  `[duckdb] original query before array rewrite: ${builtQuery.sql}`,
244
318
  params
@@ -255,19 +329,52 @@ export class DuckDBSession<
255
329
  ) as AsyncGenerator<GenericRowData<T>[], void, void>;
256
330
  }
257
331
 
332
+ executeBatchesRaw(
333
+ query: SQL,
334
+ options: ExecuteInBatchesOptions = {}
335
+ ): AsyncGenerator<ExecuteBatchesRawChunk, void, void> {
336
+ this.dialect.resetPgJsonFlag();
337
+ const builtQuery = this.dialect.sqlToQuery(query);
338
+ this.dialect.assertNoPgJsonColumns();
339
+ const params = prepareParams(builtQuery.params, {
340
+ rejectStringArrayLiterals: this.rejectStringArrayLiterals,
341
+ warnOnStringArrayLiteral: this.rejectStringArrayLiterals
342
+ ? undefined
343
+ : () => this.warnOnStringArrayLiteral(builtQuery.sql),
344
+ });
345
+ const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
346
+ this.rewriteArraysMode,
347
+ builtQuery.sql
348
+ );
349
+
350
+ if (didRewrite) {
351
+ this.logger.logQuery(
352
+ `[duckdb] original query before array rewrite: ${builtQuery.sql}`,
353
+ params
354
+ );
355
+ }
356
+
357
+ this.logger.logQuery(rewrittenQuery, params);
358
+
359
+ return executeInBatchesRaw(this.client, rewrittenQuery, params, options);
360
+ }
361
+
258
362
  async executeArrow(query: SQL): Promise<unknown> {
363
+ this.dialect.resetPgJsonFlag();
259
364
  const builtQuery = this.dialect.sqlToQuery(query);
365
+ this.dialect.assertNoPgJsonColumns();
260
366
  const params = prepareParams(builtQuery.params, {
261
367
  rejectStringArrayLiterals: this.rejectStringArrayLiterals,
262
368
  warnOnStringArrayLiteral: this.rejectStringArrayLiterals
263
369
  ? undefined
264
370
  : () => this.warnOnStringArrayLiteral(builtQuery.sql),
265
371
  });
266
- const rewrittenQuery = this.rewriteArrays
267
- ? adaptArrayOperators(builtQuery.sql)
268
- : builtQuery.sql;
372
+ const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
373
+ this.rewriteArraysMode,
374
+ builtQuery.sql
375
+ );
269
376
 
270
- if (this.rewriteArrays && rewrittenQuery !== builtQuery.sql) {
377
+ if (didRewrite) {
271
378
  this.logger.logQuery(
272
379
  `[duckdb] original query before array rewrite: ${builtQuery.sql}`,
273
380
  params
@@ -278,6 +385,14 @@ export class DuckDBSession<
278
385
 
279
386
  return executeArrowOnClient(this.client, rewrittenQuery, params);
280
387
  }
388
+
389
+ markRollbackOnly(): void {
390
+ this.rollbackOnly = true;
391
+ }
392
+
393
+ isRollbackOnly(): boolean {
394
+ return this.rollbackOnly;
395
+ }
281
396
  }
282
397
 
283
398
  type PgTransactionInternals<
@@ -335,6 +450,15 @@ export class DuckDBTransaction<
335
450
  return (this as unknown as Tx).session.executeBatches<T>(query, options);
336
451
  }
337
452
 
453
+ executeBatchesRaw(
454
+ query: SQL,
455
+ options: ExecuteInBatchesOptions = {}
456
+ ): AsyncGenerator<ExecuteBatchesRawChunk, void, void> {
457
+ // Cast needed: PgTransaction doesn't expose session property in public API
458
+ type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
459
+ return (this as unknown as Tx).session.executeBatchesRaw(query, options);
460
+ }
461
+
338
462
  executeArrow(query: SQL): Promise<unknown> {
339
463
  // Cast needed: PgTransaction doesn't expose session property in public API
340
464
  type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
@@ -346,14 +470,65 @@ export class DuckDBTransaction<
346
470
  ): Promise<T> {
347
471
  // Cast needed: PgTransaction doesn't expose dialect/session properties in public API
348
472
  type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
473
+ const internals = this as unknown as Tx;
474
+ const savepoint = `drizzle_savepoint_${this.nestedIndex + 1}`;
475
+ const savepointSql = sql.raw(`savepoint ${savepoint}`);
476
+ const releaseSql = sql.raw(`release savepoint ${savepoint}`);
477
+ const rollbackSql = sql.raw(`rollback to savepoint ${savepoint}`);
478
+
349
479
  const nestedTx = new DuckDBTransaction<TFullSchema, TSchema>(
350
- (this as unknown as Tx).dialect,
351
- (this as unknown as Tx).session,
480
+ internals.dialect,
481
+ internals.session,
352
482
  this.schema,
353
483
  this.nestedIndex + 1
354
484
  );
355
485
 
356
- return transaction(nestedTx);
486
+ // Check dialect-level savepoint support (per-instance, not global)
487
+ if (internals.dialect.areSavepointsUnsupported()) {
488
+ return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
489
+ }
490
+
491
+ let createdSavepoint = false;
492
+ try {
493
+ await internals.session.execute(savepointSql);
494
+ internals.dialect.markSavepointsSupported();
495
+ createdSavepoint = true;
496
+ } catch (error) {
497
+ if (!isSavepointSyntaxError(error)) {
498
+ throw error;
499
+ }
500
+ internals.dialect.markSavepointsUnsupported();
501
+ return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
502
+ }
503
+
504
+ try {
505
+ const result = await transaction(nestedTx);
506
+ if (createdSavepoint) {
507
+ await internals.session.execute(releaseSql);
508
+ }
509
+ return result;
510
+ } catch (error) {
511
+ if (createdSavepoint) {
512
+ await internals.session.execute(rollbackSql);
513
+ }
514
+ (
515
+ internals.session as DuckDBSession<TFullSchema, TSchema>
516
+ ).markRollbackOnly();
517
+ throw error;
518
+ }
519
+ }
520
+
521
+ private runNestedWithoutSavepoint<T>(
522
+ transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>,
523
+ nestedTx: DuckDBTransaction<TFullSchema, TSchema>,
524
+ internals: DuckDBTransactionWithInternals<TFullSchema, TSchema>
525
+ ): Promise<T> {
526
+ return transaction(nestedTx).catch((error) => {
527
+ (
528
+ internals.session as DuckDBSession<TFullSchema, TSchema>
529
+ ).markRollbackOnly();
530
+ throw error;
531
+ });
357
532
  }
358
533
  }
359
534