@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/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
|
|
54
|
-
const
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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 =
|
|
76
|
-
|
|
77
|
-
|
|
105
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
|
|
106
|
+
this.rewriteArraysMode,
|
|
107
|
+
this.queryString
|
|
108
|
+
);
|
|
78
109
|
|
|
79
|
-
if (
|
|
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
|
-
|
|
122
|
+
if (fields) {
|
|
123
|
+
const { rows } = await executeArraysOnClient(
|
|
124
|
+
this.client,
|
|
125
|
+
rewrittenQuery,
|
|
126
|
+
params,
|
|
127
|
+
{ prepareCache: this.prepareCache }
|
|
128
|
+
);
|
|
92
129
|
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
141
|
+
const rows = await executeOnClient(this.client, rewrittenQuery, params, {
|
|
142
|
+
prepareCache: this.prepareCache,
|
|
143
|
+
});
|
|
98
144
|
|
|
99
|
-
return
|
|
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?:
|
|
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
|
|
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.
|
|
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.
|
|
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 =
|
|
238
|
-
|
|
239
|
-
|
|
310
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
|
|
311
|
+
this.rewriteArraysMode,
|
|
312
|
+
builtQuery.sql
|
|
313
|
+
);
|
|
240
314
|
|
|
241
|
-
if (
|
|
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 =
|
|
267
|
-
|
|
268
|
-
|
|
372
|
+
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
|
|
373
|
+
this.rewriteArraysMode,
|
|
374
|
+
builtQuery.sql
|
|
375
|
+
);
|
|
269
376
|
|
|
270
|
-
if (
|
|
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
|
-
|
|
351
|
-
|
|
480
|
+
internals.dialect,
|
|
481
|
+
internals.session,
|
|
352
482
|
this.schema,
|
|
353
483
|
this.nestedIndex + 1
|
|
354
484
|
);
|
|
355
485
|
|
|
356
|
-
|
|
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
|
|