@leonardovida-md/drizzle-neo-duckdb 1.1.0 → 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.
- package/dist/columns.d.ts +5 -0
- package/dist/dialect.d.ts +21 -0
- package/dist/duckdb-introspect.mjs +414 -92
- package/dist/helpers.mjs +4 -4
- package/dist/index.mjs +416 -96
- package/dist/introspect.d.ts +8 -0
- package/dist/pool.d.ts +8 -0
- package/dist/session.d.ts +7 -1
- 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 +13 -10
- package/src/columns.ts +9 -9
- package/src/dialect.ts +51 -3
- package/src/driver.ts +15 -2
- package/src/introspect.ts +23 -6
- package/src/pool.ts +182 -12
- package/src/session.ts +98 -5
- package/src/sql/query-rewriters.ts +183 -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,
|
|
@@ -36,6 +36,16 @@ import type { DuckDBConnection } from '@duckdb/node-api';
|
|
|
36
36
|
|
|
37
37
|
export type { DuckDBClientLike, RowData } from './client.ts';
|
|
38
38
|
|
|
39
|
+
function isSavepointSyntaxError(error: unknown): boolean {
|
|
40
|
+
if (!(error instanceof Error) || !error.message) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return (
|
|
44
|
+
error.message.toLowerCase().includes('savepoint') &&
|
|
45
|
+
error.message.toLowerCase().includes('syntax error')
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
export class DuckDBPreparedQuery<
|
|
40
50
|
T extends PreparedQueryConfig,
|
|
41
51
|
> extends PgPreparedQuery<T> {
|
|
@@ -131,6 +141,7 @@ export class DuckDBSession<
|
|
|
131
141
|
private rewriteArrays: boolean;
|
|
132
142
|
private rejectStringArrayLiterals: boolean;
|
|
133
143
|
private hasWarnedArrayLiteral = false;
|
|
144
|
+
private rollbackOnly = false;
|
|
134
145
|
|
|
135
146
|
constructor(
|
|
136
147
|
private client: DuckDBClientLike,
|
|
@@ -168,8 +179,19 @@ export class DuckDBSession<
|
|
|
168
179
|
);
|
|
169
180
|
}
|
|
170
181
|
|
|
182
|
+
override execute<T>(query: SQL): Promise<T> {
|
|
183
|
+
this.dialect.resetPgJsonFlag();
|
|
184
|
+
return super.execute(query);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
override all<T = unknown>(query: SQL): Promise<T[]> {
|
|
188
|
+
this.dialect.resetPgJsonFlag();
|
|
189
|
+
return super.all(query);
|
|
190
|
+
}
|
|
191
|
+
|
|
171
192
|
override async transaction<T>(
|
|
172
|
-
transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T
|
|
193
|
+
transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>,
|
|
194
|
+
config?: PgTransactionConfig
|
|
173
195
|
): Promise<T> {
|
|
174
196
|
let pinnedConnection: DuckDBConnection | undefined;
|
|
175
197
|
let pool: DuckDBConnectionPool | undefined;
|
|
@@ -197,8 +219,16 @@ export class DuckDBSession<
|
|
|
197
219
|
try {
|
|
198
220
|
await tx.execute(sql`BEGIN TRANSACTION;`);
|
|
199
221
|
|
|
222
|
+
if (config) {
|
|
223
|
+
await tx.setTransaction(config);
|
|
224
|
+
}
|
|
225
|
+
|
|
200
226
|
try {
|
|
201
227
|
const result = await transaction(tx);
|
|
228
|
+
if (session.isRollbackOnly()) {
|
|
229
|
+
await tx.execute(sql`rollback`);
|
|
230
|
+
throw new TransactionRollbackError();
|
|
231
|
+
}
|
|
202
232
|
await tx.execute(sql`commit`);
|
|
203
233
|
return result;
|
|
204
234
|
} catch (error) {
|
|
@@ -227,7 +257,9 @@ export class DuckDBSession<
|
|
|
227
257
|
query: SQL,
|
|
228
258
|
options: ExecuteInBatchesOptions = {}
|
|
229
259
|
): AsyncGenerator<GenericRowData<T>[], void, void> {
|
|
260
|
+
this.dialect.resetPgJsonFlag();
|
|
230
261
|
const builtQuery = this.dialect.sqlToQuery(query);
|
|
262
|
+
this.dialect.assertNoPgJsonColumns();
|
|
231
263
|
const params = prepareParams(builtQuery.params, {
|
|
232
264
|
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
233
265
|
warnOnStringArrayLiteral: this.rejectStringArrayLiterals
|
|
@@ -256,7 +288,9 @@ export class DuckDBSession<
|
|
|
256
288
|
}
|
|
257
289
|
|
|
258
290
|
async executeArrow(query: SQL): Promise<unknown> {
|
|
291
|
+
this.dialect.resetPgJsonFlag();
|
|
259
292
|
const builtQuery = this.dialect.sqlToQuery(query);
|
|
293
|
+
this.dialect.assertNoPgJsonColumns();
|
|
260
294
|
const params = prepareParams(builtQuery.params, {
|
|
261
295
|
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
262
296
|
warnOnStringArrayLiteral: this.rejectStringArrayLiterals
|
|
@@ -278,6 +312,14 @@ export class DuckDBSession<
|
|
|
278
312
|
|
|
279
313
|
return executeArrowOnClient(this.client, rewrittenQuery, params);
|
|
280
314
|
}
|
|
315
|
+
|
|
316
|
+
markRollbackOnly(): void {
|
|
317
|
+
this.rollbackOnly = true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
isRollbackOnly(): boolean {
|
|
321
|
+
return this.rollbackOnly;
|
|
322
|
+
}
|
|
281
323
|
}
|
|
282
324
|
|
|
283
325
|
type PgTransactionInternals<
|
|
@@ -346,14 +388,65 @@ export class DuckDBTransaction<
|
|
|
346
388
|
): Promise<T> {
|
|
347
389
|
// Cast needed: PgTransaction doesn't expose dialect/session properties in public API
|
|
348
390
|
type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
|
|
391
|
+
const internals = this as unknown as Tx;
|
|
392
|
+
const savepoint = `drizzle_savepoint_${this.nestedIndex + 1}`;
|
|
393
|
+
const savepointSql = sql.raw(`savepoint ${savepoint}`);
|
|
394
|
+
const releaseSql = sql.raw(`release savepoint ${savepoint}`);
|
|
395
|
+
const rollbackSql = sql.raw(`rollback to savepoint ${savepoint}`);
|
|
396
|
+
|
|
349
397
|
const nestedTx = new DuckDBTransaction<TFullSchema, TSchema>(
|
|
350
|
-
|
|
351
|
-
|
|
398
|
+
internals.dialect,
|
|
399
|
+
internals.session,
|
|
352
400
|
this.schema,
|
|
353
401
|
this.nestedIndex + 1
|
|
354
402
|
);
|
|
355
403
|
|
|
356
|
-
|
|
404
|
+
// Check dialect-level savepoint support (per-instance, not global)
|
|
405
|
+
if (internals.dialect.areSavepointsUnsupported()) {
|
|
406
|
+
return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let createdSavepoint = false;
|
|
410
|
+
try {
|
|
411
|
+
await internals.session.execute(savepointSql);
|
|
412
|
+
internals.dialect.markSavepointsSupported();
|
|
413
|
+
createdSavepoint = true;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (!isSavepointSyntaxError(error)) {
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
internals.dialect.markSavepointsUnsupported();
|
|
419
|
+
return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const result = await transaction(nestedTx);
|
|
424
|
+
if (createdSavepoint) {
|
|
425
|
+
await internals.session.execute(releaseSql);
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (createdSavepoint) {
|
|
430
|
+
await internals.session.execute(rollbackSql);
|
|
431
|
+
}
|
|
432
|
+
(
|
|
433
|
+
internals.session as DuckDBSession<TFullSchema, TSchema>
|
|
434
|
+
).markRollbackOnly();
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private runNestedWithoutSavepoint<T>(
|
|
440
|
+
transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>,
|
|
441
|
+
nestedTx: DuckDBTransaction<TFullSchema, TSchema>,
|
|
442
|
+
internals: DuckDBTransactionWithInternals<TFullSchema, TSchema>
|
|
443
|
+
): Promise<T> {
|
|
444
|
+
return transaction(nestedTx).catch((error) => {
|
|
445
|
+
(
|
|
446
|
+
internals.session as DuckDBSession<TFullSchema, TSchema>
|
|
447
|
+
).markRollbackOnly();
|
|
448
|
+
throw error;
|
|
449
|
+
});
|
|
357
450
|
}
|
|
358
451
|
}
|
|
359
452
|
|
|
@@ -1,96 +1,204 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
type ArrayOperator = {
|
|
2
|
+
token: '@>' | '<@' | '&&';
|
|
3
|
+
fn: 'array_has_all' | 'array_has_any';
|
|
4
|
+
swap?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const OPERATORS: ArrayOperator[] = [
|
|
8
|
+
{ token: '@>', fn: 'array_has_all' },
|
|
9
|
+
{ token: '<@', fn: 'array_has_all', swap: true },
|
|
10
|
+
{ token: '&&', fn: 'array_has_any' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const isWhitespace = (char: string | undefined) =>
|
|
14
|
+
char !== undefined && /\s/.test(char);
|
|
15
|
+
|
|
16
|
+
export function scrubForRewrite(query: string): string {
|
|
17
|
+
let scrubbed = '';
|
|
18
|
+
type State = 'code' | 'single' | 'double' | 'lineComment' | 'blockComment';
|
|
19
|
+
let state: State = 'code';
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < query.length; i += 1) {
|
|
22
|
+
const char = query[i]!;
|
|
23
|
+
const next = query[i + 1];
|
|
24
|
+
|
|
25
|
+
if (state === 'code') {
|
|
26
|
+
if (char === "'") {
|
|
27
|
+
scrubbed += "'";
|
|
28
|
+
state = 'single';
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (char === '"') {
|
|
32
|
+
scrubbed += '"';
|
|
33
|
+
state = 'double';
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (char === '-' && next === '-') {
|
|
37
|
+
scrubbed += ' ';
|
|
38
|
+
i += 1;
|
|
39
|
+
state = 'lineComment';
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (char === '/' && next === '*') {
|
|
43
|
+
scrubbed += ' ';
|
|
44
|
+
i += 1;
|
|
45
|
+
state = 'blockComment';
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
scrubbed += char;
|
|
50
|
+
continue;
|
|
21
51
|
}
|
|
22
52
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (ch === "'" && source[idx - 1] !== '\\') {
|
|
29
|
-
inString = !inString;
|
|
53
|
+
if (state === 'single') {
|
|
54
|
+
if (char === "'" && next === "'") {
|
|
55
|
+
scrubbed += "''";
|
|
56
|
+
i += 1;
|
|
57
|
+
continue;
|
|
30
58
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
// Preserve quote for boundary detection but mask inner chars with a
|
|
60
|
+
// non-whitespace placeholder to avoid false positives on operators.
|
|
61
|
+
scrubbed += char === "'" ? "'" : '.';
|
|
62
|
+
if (char === "'") {
|
|
63
|
+
state = 'code';
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (state === 'double') {
|
|
69
|
+
if (char === '"' && next === '"') {
|
|
70
|
+
scrubbed += '""';
|
|
71
|
+
i += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
scrubbed += char === '"' ? '"' : '.';
|
|
75
|
+
if (char === '"') {
|
|
76
|
+
state = 'code';
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (state === 'lineComment') {
|
|
82
|
+
scrubbed += char === '\n' ? '\n' : ' ';
|
|
83
|
+
if (char === '\n') {
|
|
84
|
+
state = 'code';
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (state === 'blockComment') {
|
|
90
|
+
if (char === '*' && next === '/') {
|
|
91
|
+
scrubbed += ' ';
|
|
92
|
+
i += 1;
|
|
93
|
+
state = 'code';
|
|
94
|
+
} else {
|
|
95
|
+
scrubbed += ' ';
|
|
41
96
|
}
|
|
42
97
|
}
|
|
43
|
-
|
|
44
|
-
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return scrubbed;
|
|
101
|
+
}
|
|
45
102
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
103
|
+
function findNextOperator(
|
|
104
|
+
scrubbed: string,
|
|
105
|
+
start: number
|
|
106
|
+
): { index: number; operator: ArrayOperator } | null {
|
|
107
|
+
for (let idx = start; idx < scrubbed.length; idx += 1) {
|
|
108
|
+
for (const operator of OPERATORS) {
|
|
109
|
+
if (scrubbed.startsWith(operator.token, idx)) {
|
|
110
|
+
return { index: idx, operator };
|
|
111
|
+
}
|
|
50
112
|
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
51
116
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
117
|
+
function walkLeft(
|
|
118
|
+
source: string,
|
|
119
|
+
scrubbed: string,
|
|
120
|
+
start: number
|
|
121
|
+
): [number, string] {
|
|
122
|
+
let idx = start;
|
|
123
|
+
while (idx >= 0 && isWhitespace(scrubbed[idx])) {
|
|
124
|
+
idx -= 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let depth = 0;
|
|
128
|
+
for (; idx >= 0; idx -= 1) {
|
|
129
|
+
const ch = scrubbed[idx];
|
|
130
|
+
if (ch === ')' || ch === ']') {
|
|
131
|
+
depth += 1;
|
|
132
|
+
} else if (ch === '(' || ch === '[') {
|
|
133
|
+
if (depth === 0) {
|
|
134
|
+
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
59
135
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
136
|
+
depth = Math.max(0, depth - 1);
|
|
137
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
138
|
+
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return [0, source.slice(0, start + 1)];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function walkRight(
|
|
146
|
+
source: string,
|
|
147
|
+
scrubbed: string,
|
|
148
|
+
start: number
|
|
149
|
+
): [number, string] {
|
|
150
|
+
let idx = start;
|
|
151
|
+
while (idx < scrubbed.length && isWhitespace(scrubbed[idx])) {
|
|
152
|
+
idx += 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let depth = 0;
|
|
156
|
+
for (; idx < scrubbed.length; idx += 1) {
|
|
157
|
+
const ch = scrubbed[idx];
|
|
158
|
+
if (ch === '(' || ch === '[') {
|
|
159
|
+
depth += 1;
|
|
160
|
+
} else if (ch === ')' || ch === ']') {
|
|
161
|
+
if (depth === 0) {
|
|
69
162
|
return [idx, source.slice(start, idx)];
|
|
70
163
|
}
|
|
164
|
+
depth = Math.max(0, depth - 1);
|
|
165
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
166
|
+
return [idx, source.slice(start, idx)];
|
|
71
167
|
}
|
|
72
|
-
|
|
73
|
-
};
|
|
168
|
+
}
|
|
74
169
|
|
|
170
|
+
return [scrubbed.length, source.slice(start)];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function adaptArrayOperators(query: string): string {
|
|
75
174
|
let rewritten = query;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
while (idx !== -1) {
|
|
79
|
-
const [leftStart, leftExpr] = walkLeft(rewritten, idx - 1);
|
|
80
|
-
const [rightEnd, rightExpr] = walkRight(rewritten, idx + token.length);
|
|
175
|
+
let scrubbed = scrubForRewrite(query);
|
|
176
|
+
let searchStart = 0;
|
|
81
177
|
|
|
82
|
-
|
|
83
|
-
|
|
178
|
+
// Re-run after each replacement to keep indexes aligned with the current string
|
|
179
|
+
while (true) {
|
|
180
|
+
const next = findNextOperator(scrubbed, searchStart);
|
|
181
|
+
if (!next) break;
|
|
84
182
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
183
|
+
const { index, operator } = next;
|
|
184
|
+
const [leftStart, leftExpr] = walkLeft(rewritten, scrubbed, index - 1);
|
|
185
|
+
const [rightEnd, rightExpr] = walkRight(
|
|
186
|
+
rewritten,
|
|
187
|
+
scrubbed,
|
|
188
|
+
index + operator.token.length
|
|
189
|
+
);
|
|
88
190
|
|
|
89
|
-
|
|
90
|
-
|
|
191
|
+
const left = leftExpr.trim();
|
|
192
|
+
const right = rightExpr.trim();
|
|
91
193
|
|
|
92
|
-
|
|
93
|
-
|
|
194
|
+
const replacement = `${operator.fn}(${operator.swap ? right : left}, ${
|
|
195
|
+
operator.swap ? left : right
|
|
196
|
+
})`;
|
|
197
|
+
|
|
198
|
+
rewritten =
|
|
199
|
+
rewritten.slice(0, leftStart) + replacement + rewritten.slice(rightEnd);
|
|
200
|
+
scrubbed = scrubForRewrite(rewritten);
|
|
201
|
+
searchStart = leftStart + replacement.length;
|
|
94
202
|
}
|
|
95
203
|
|
|
96
204
|
return rewritten;
|