@duckdbfan/drizzle-duckdb 0.0.6 → 1.3.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.
Files changed (55) hide show
  1. package/README.md +344 -62
  2. package/dist/bin/duckdb-introspect.d.ts +2 -0
  3. package/dist/client.d.ts +42 -0
  4. package/dist/columns.d.ts +142 -0
  5. package/dist/dialect.d.ts +27 -2
  6. package/dist/driver.d.ts +53 -37
  7. package/dist/duckdb-introspect.mjs +2890 -0
  8. package/dist/helpers.d.ts +1 -0
  9. package/dist/helpers.mjs +360 -0
  10. package/dist/index.d.ts +7 -0
  11. package/dist/index.mjs +3071 -209
  12. package/dist/introspect.d.ts +74 -0
  13. package/dist/migrator.d.ts +3 -2
  14. package/dist/olap.d.ts +46 -0
  15. package/dist/operators.d.ts +8 -0
  16. package/dist/options.d.ts +7 -0
  17. package/dist/pool.d.ts +30 -0
  18. package/dist/select-builder.d.ts +31 -0
  19. package/dist/session.d.ts +33 -8
  20. package/dist/sql/ast-transformer.d.ts +33 -0
  21. package/dist/sql/result-mapper.d.ts +9 -0
  22. package/dist/sql/selection.d.ts +2 -0
  23. package/dist/sql/visitors/array-operators.d.ts +5 -0
  24. package/dist/sql/visitors/column-qualifier.d.ts +10 -0
  25. package/dist/sql/visitors/generate-series-alias.d.ts +13 -0
  26. package/dist/sql/visitors/union-with-hoister.d.ts +11 -0
  27. package/dist/utils.d.ts +2 -5
  28. package/dist/value-wrappers-core.d.ts +42 -0
  29. package/dist/value-wrappers.d.ts +8 -0
  30. package/package.json +53 -16
  31. package/src/bin/duckdb-introspect.ts +181 -0
  32. package/src/client.ts +528 -0
  33. package/src/columns.ts +510 -1
  34. package/src/dialect.ts +111 -15
  35. package/src/driver.ts +266 -180
  36. package/src/helpers.ts +18 -0
  37. package/src/index.ts +8 -1
  38. package/src/introspect.ts +935 -0
  39. package/src/migrator.ts +10 -5
  40. package/src/olap.ts +190 -0
  41. package/src/operators.ts +27 -0
  42. package/src/options.ts +25 -0
  43. package/src/pool.ts +274 -0
  44. package/src/select-builder.ts +110 -0
  45. package/src/session.ts +306 -66
  46. package/src/sql/ast-transformer.ts +170 -0
  47. package/src/sql/result-mapper.ts +303 -0
  48. package/src/sql/selection.ts +60 -0
  49. package/src/sql/visitors/array-operators.ts +214 -0
  50. package/src/sql/visitors/column-qualifier.ts +586 -0
  51. package/src/sql/visitors/generate-series-alias.ts +291 -0
  52. package/src/sql/visitors/union-with-hoister.ts +106 -0
  53. package/src/utils.ts +2 -216
  54. package/src/value-wrappers-core.ts +168 -0
  55. package/src/value-wrappers.ts +165 -0
package/src/session.ts CHANGED
@@ -1,7 +1,6 @@
1
- import type { Connection, Database, RowData } from 'duckdb-async';
2
1
  import { entityKind } from 'drizzle-orm/entity';
3
- import { type Logger, NoopLogger } from 'drizzle-orm/logger';
4
- import type { PgDialect } from 'drizzle-orm/pg-core/dialect';
2
+ import type { Logger } from 'drizzle-orm/logger';
3
+ import { NoopLogger } from 'drizzle-orm/logger';
5
4
  import { PgTransaction } from 'drizzle-orm/pg-core';
6
5
  import type { SelectedFieldsOrdered } from 'drizzle-orm/pg-core/query-builders/select.types';
7
6
  import type {
@@ -16,28 +15,59 @@ import type {
16
15
  } from 'drizzle-orm/relations';
17
16
  import { fillPlaceholders, type Query, SQL, sql } from 'drizzle-orm/sql/sql';
18
17
  import type { Assume } from 'drizzle-orm/utils';
19
- import { mapResultRow } from './utils';
20
- import type { DuckDBDialect } from './dialect';
18
+ import { mapResultRow } from './sql/result-mapper.ts';
21
19
  import { TransactionRollbackError } from 'drizzle-orm/errors';
22
-
23
- export type DuckDBClient = Database;
20
+ import type { DuckDBDialect } from './dialect.ts';
21
+ import type {
22
+ DuckDBClientLike,
23
+ DuckDBConnectionPool,
24
+ RowData,
25
+ } from './client.ts';
26
+ import {
27
+ executeArrowOnClient,
28
+ executeArraysOnClient,
29
+ executeInBatches,
30
+ executeInBatchesRaw,
31
+ executeOnClient,
32
+ prepareParams,
33
+ type ExecuteBatchesRawChunk,
34
+ type ExecuteInBatchesOptions,
35
+ } from './client.ts';
36
+ import { isPool } from './client.ts';
37
+ import type { DuckDBConnection } from '@duckdb/node-api';
38
+ import type { PreparedStatementCacheConfig } from './options.ts';
39
+
40
+ export type { DuckDBClientLike, RowData } from './client.ts';
41
+
42
+ function isSavepointSyntaxError(error: unknown): boolean {
43
+ if (!(error instanceof Error) || !error.message) {
44
+ return false;
45
+ }
46
+ return (
47
+ error.message.toLowerCase().includes('savepoint') &&
48
+ error.message.toLowerCase().includes('syntax error')
49
+ );
50
+ }
24
51
 
25
52
  export class DuckDBPreparedQuery<
26
- T extends PreparedQueryConfig
53
+ T extends PreparedQueryConfig,
27
54
  > extends PgPreparedQuery<T> {
28
55
  static readonly [entityKind]: string = 'DuckDBPreparedQuery';
29
56
 
30
- // private rawQueryConfig: QueryOptions;
31
- // private queryConfig: QueryOptions;
32
-
33
57
  constructor(
34
- private client: DuckDBClient | Connection,
58
+ private client: DuckDBClientLike,
59
+ private dialect: DuckDBDialect,
35
60
  private queryString: string,
36
61
  private params: unknown[],
37
62
  private logger: Logger,
38
63
  private fields: SelectedFieldsOrdered | undefined,
39
64
  private _isResponseInArrayMode: boolean,
40
- private customResultMapper?: (rows: unknown[][]) => T['execute']
65
+ private customResultMapper:
66
+ | ((rows: unknown[][]) => T['execute'])
67
+ | undefined,
68
+ private rejectStringArrayLiterals: boolean,
69
+ private prepareCache: PreparedStatementCacheConfig | undefined,
70
+ private warnOnStringArrayLiteral?: (sql: string) => void
41
71
  ) {
42
72
  super({ sql: queryString, params });
43
73
  }
@@ -45,31 +75,45 @@ export class DuckDBPreparedQuery<
45
75
  async execute(
46
76
  placeholderValues: Record<string, unknown> | undefined = {}
47
77
  ): Promise<T['execute']> {
48
- const params = fillPlaceholders(this.params, placeholderValues);
49
-
78
+ this.dialect.assertNoPgJsonColumns();
79
+ const params = prepareParams(
80
+ fillPlaceholders(this.params, placeholderValues),
81
+ {
82
+ rejectStringArrayLiterals: this.rejectStringArrayLiterals,
83
+ warnOnStringArrayLiteral: this.warnOnStringArrayLiteral
84
+ ? () => this.warnOnStringArrayLiteral?.(this.queryString)
85
+ : undefined,
86
+ }
87
+ );
50
88
  this.logger.logQuery(this.queryString, params);
51
89
 
52
- const {
53
- fields,
54
- client,
55
- joinsNotNullableMap,
56
- customResultMapper,
57
- queryString,
58
- } = this as typeof this & { joinsNotNullableMap?: Record<string, boolean> };
59
-
60
- const rows = (await client.all(queryString, ...params)) ?? [];
61
-
62
- if (rows.length === 0 || !fields) {
63
- return rows;
90
+ const { fields, joinsNotNullableMap, customResultMapper } =
91
+ this as typeof this & { joinsNotNullableMap?: Record<string, boolean> };
92
+
93
+ if (fields) {
94
+ const { rows } = await executeArraysOnClient(
95
+ this.client,
96
+ this.queryString,
97
+ params,
98
+ { prepareCache: this.prepareCache }
99
+ );
100
+
101
+ if (rows.length === 0) {
102
+ return [] as T['execute'];
103
+ }
104
+
105
+ return customResultMapper
106
+ ? customResultMapper(rows)
107
+ : rows.map((row) =>
108
+ mapResultRow<T['execute']>(fields, row, joinsNotNullableMap)
109
+ );
64
110
  }
65
111
 
66
- const rowValues = rows.map((row) => Object.values(row));
112
+ const rows = await executeOnClient(this.client, this.queryString, params, {
113
+ prepareCache: this.prepareCache,
114
+ });
67
115
 
68
- return customResultMapper
69
- ? customResultMapper(rowValues)
70
- : rowValues.map((row) =>
71
- mapResultRow<T['execute']>(fields!, row, joinsNotNullableMap)
72
- );
116
+ return rows as T['execute'];
73
117
  }
74
118
 
75
119
  all(
@@ -79,30 +123,44 @@ export class DuckDBPreparedQuery<
79
123
  }
80
124
 
81
125
  isResponseInArrayMode(): boolean {
82
- return false;
126
+ return this._isResponseInArrayMode;
83
127
  }
84
128
  }
85
129
 
86
130
  export interface DuckDBSessionOptions {
87
131
  logger?: Logger;
132
+ rejectStringArrayLiterals?: boolean;
133
+ prepareCache?: PreparedStatementCacheConfig;
88
134
  }
89
135
 
90
136
  export class DuckDBSession<
91
137
  TFullSchema extends Record<string, unknown> = Record<string, never>,
92
- TSchema extends TablesRelationalConfig = Record<string, never>
138
+ TSchema extends TablesRelationalConfig = Record<string, never>,
93
139
  > extends PgSession<DuckDBQueryResultHKT, TFullSchema, TSchema> {
94
140
  static readonly [entityKind]: string = 'DuckDBSession';
95
141
 
142
+ protected override dialect: DuckDBDialect;
96
143
  private logger: Logger;
144
+ private rejectStringArrayLiterals: boolean;
145
+ private prepareCache: PreparedStatementCacheConfig | undefined;
146
+ private hasWarnedArrayLiteral = false;
147
+ private rollbackOnly = false;
97
148
 
98
149
  constructor(
99
- private client: DuckDBClient | Connection,
150
+ private client: DuckDBClientLike,
100
151
  dialect: DuckDBDialect,
101
152
  private schema: RelationalSchemaConfig<TSchema> | undefined,
102
153
  private options: DuckDBSessionOptions = {}
103
154
  ) {
104
155
  super(dialect);
156
+ this.dialect = dialect;
105
157
  this.logger = options.logger ?? new NoopLogger();
158
+ this.rejectStringArrayLiterals = options.rejectStringArrayLiterals ?? false;
159
+ this.prepareCache = options.prepareCache;
160
+ this.options = {
161
+ ...options,
162
+ prepareCache: this.prepareCache,
163
+ };
106
164
  }
107
165
 
108
166
  prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
@@ -112,25 +170,48 @@ export class DuckDBSession<
112
170
  isResponseInArrayMode: boolean,
113
171
  customResultMapper?: (rows: unknown[][]) => T['execute']
114
172
  ): PgPreparedQuery<T> {
173
+ void name; // DuckDB doesn't support prepared statement names but the signature must match.
115
174
  return new DuckDBPreparedQuery(
116
175
  this.client,
176
+ this.dialect,
117
177
  query.sql,
118
178
  query.params,
119
179
  this.logger,
120
180
  fields,
121
181
  isResponseInArrayMode,
122
- customResultMapper
182
+ customResultMapper,
183
+ this.rejectStringArrayLiterals,
184
+ this.prepareCache,
185
+ this.rejectStringArrayLiterals ? undefined : this.warnOnStringArrayLiteral
123
186
  );
124
187
  }
125
188
 
189
+ override execute<T>(query: SQL): Promise<T> {
190
+ this.dialect.resetPgJsonFlag();
191
+ return super.execute(query);
192
+ }
193
+
194
+ override all<T = unknown>(query: SQL): Promise<T[]> {
195
+ this.dialect.resetPgJsonFlag();
196
+ return super.all(query);
197
+ }
198
+
126
199
  override async transaction<T>(
127
- transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
200
+ transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>,
201
+ config?: PgTransactionConfig
128
202
  ): Promise<T> {
129
- const connection =
130
- 'connect' in this.client ? await this.client.connect() : this.client;
203
+ let pinnedConnection: DuckDBConnection | undefined;
204
+ let pool: DuckDBConnectionPool | undefined;
205
+
206
+ let clientForTx: DuckDBClientLike = this.client;
207
+ if (isPool(this.client)) {
208
+ pool = this.client;
209
+ pinnedConnection = await pool.acquire();
210
+ clientForTx = pinnedConnection;
211
+ }
131
212
 
132
213
  const session = new DuckDBSession(
133
- connection,
214
+ clientForTx,
134
215
  this.dialect,
135
216
  this.schema,
136
217
  this.options
@@ -142,24 +223,114 @@ export class DuckDBSession<
142
223
  this.schema
143
224
  );
144
225
 
145
- await tx.execute(sql`BEGIN TRANSACTION;`);
146
-
147
226
  try {
148
- const result = await transaction(tx);
149
- await tx.execute(sql`commit`);
150
- return result;
151
- } catch (error) {
152
- await tx.execute(sql`rollback`);
153
- throw error;
227
+ await tx.execute(sql`BEGIN TRANSACTION;`);
228
+
229
+ if (config) {
230
+ await tx.setTransaction(config);
231
+ }
232
+
233
+ try {
234
+ const result = await transaction(tx);
235
+ if (session.isRollbackOnly()) {
236
+ await tx.execute(sql`rollback`);
237
+ throw new TransactionRollbackError();
238
+ }
239
+ await tx.execute(sql`commit`);
240
+ return result;
241
+ } catch (error) {
242
+ await tx.execute(sql`rollback`);
243
+ throw error;
244
+ }
154
245
  } finally {
155
- await connection.close();
246
+ if (pinnedConnection && pool) {
247
+ await pool.release(pinnedConnection);
248
+ }
156
249
  }
157
250
  }
251
+
252
+ private warnOnStringArrayLiteral = (query: string) => {
253
+ if (this.hasWarnedArrayLiteral) {
254
+ return;
255
+ }
256
+ this.hasWarnedArrayLiteral = true;
257
+ this.logger.logQuery(
258
+ `[duckdb] ${arrayLiteralWarning}\nquery: ${query}`,
259
+ []
260
+ );
261
+ };
262
+
263
+ executeBatches<T extends RowData = RowData>(
264
+ query: SQL,
265
+ options: ExecuteInBatchesOptions = {}
266
+ ): AsyncGenerator<GenericRowData<T>[], void, void> {
267
+ this.dialect.resetPgJsonFlag();
268
+ const builtQuery = this.dialect.sqlToQuery(query);
269
+ this.dialect.assertNoPgJsonColumns();
270
+ const params = prepareParams(builtQuery.params, {
271
+ rejectStringArrayLiterals: this.rejectStringArrayLiterals,
272
+ warnOnStringArrayLiteral: this.rejectStringArrayLiterals
273
+ ? undefined
274
+ : () => this.warnOnStringArrayLiteral(builtQuery.sql),
275
+ });
276
+
277
+ this.logger.logQuery(builtQuery.sql, params);
278
+
279
+ return executeInBatches(
280
+ this.client,
281
+ builtQuery.sql,
282
+ params,
283
+ options
284
+ ) as AsyncGenerator<GenericRowData<T>[], void, void>;
285
+ }
286
+
287
+ executeBatchesRaw(
288
+ query: SQL,
289
+ options: ExecuteInBatchesOptions = {}
290
+ ): AsyncGenerator<ExecuteBatchesRawChunk, void, void> {
291
+ this.dialect.resetPgJsonFlag();
292
+ const builtQuery = this.dialect.sqlToQuery(query);
293
+ this.dialect.assertNoPgJsonColumns();
294
+ const params = prepareParams(builtQuery.params, {
295
+ rejectStringArrayLiterals: this.rejectStringArrayLiterals,
296
+ warnOnStringArrayLiteral: this.rejectStringArrayLiterals
297
+ ? undefined
298
+ : () => this.warnOnStringArrayLiteral(builtQuery.sql),
299
+ });
300
+
301
+ this.logger.logQuery(builtQuery.sql, params);
302
+
303
+ return executeInBatchesRaw(this.client, builtQuery.sql, params, options);
304
+ }
305
+
306
+ async executeArrow(query: SQL): Promise<unknown> {
307
+ this.dialect.resetPgJsonFlag();
308
+ const builtQuery = this.dialect.sqlToQuery(query);
309
+ this.dialect.assertNoPgJsonColumns();
310
+ const params = prepareParams(builtQuery.params, {
311
+ rejectStringArrayLiterals: this.rejectStringArrayLiterals,
312
+ warnOnStringArrayLiteral: this.rejectStringArrayLiterals
313
+ ? undefined
314
+ : () => this.warnOnStringArrayLiteral(builtQuery.sql),
315
+ });
316
+
317
+ this.logger.logQuery(builtQuery.sql, params);
318
+
319
+ return executeArrowOnClient(this.client, builtQuery.sql, params);
320
+ }
321
+
322
+ markRollbackOnly(): void {
323
+ this.rollbackOnly = true;
324
+ }
325
+
326
+ isRollbackOnly(): boolean {
327
+ return this.rollbackOnly;
328
+ }
158
329
  }
159
330
 
160
331
  type PgTransactionInternals<
161
332
  TFullSchema extends Record<string, unknown> = Record<string, never>,
162
- TSchema extends TablesRelationalConfig = Record<string, never>
333
+ TSchema extends TablesRelationalConfig = Record<string, never>,
163
334
  > = {
164
335
  dialect: DuckDBDialect;
165
336
  session: DuckDBSession<TFullSchema, TSchema>;
@@ -167,13 +338,13 @@ type PgTransactionInternals<
167
338
 
168
339
  type DuckDBTransactionWithInternals<
169
340
  TFullSchema extends Record<string, unknown> = Record<string, never>,
170
- TSchema extends TablesRelationalConfig = Record<string, never>
341
+ TSchema extends TablesRelationalConfig = Record<string, never>,
171
342
  > = PgTransactionInternals<TFullSchema, TSchema> &
172
343
  DuckDBTransaction<TFullSchema, TSchema>;
173
344
 
174
345
  export class DuckDBTransaction<
175
346
  TFullSchema extends Record<string, unknown>,
176
- TSchema extends TablesRelationalConfig
347
+ TSchema extends TablesRelationalConfig,
177
348
  > extends PgTransaction<DuckDBQueryResultHKT, TFullSchema, TSchema> {
178
349
  static readonly [entityKind]: string = 'DuckDBTransaction';
179
350
 
@@ -196,42 +367,111 @@ export class DuckDBTransaction<
196
367
  }
197
368
 
198
369
  setTransaction(config: PgTransactionConfig): Promise<void> {
199
- // Need to work around omitted internal types from drizzle...
370
+ // Cast needed: PgTransaction doesn't expose dialect/session properties in public API
200
371
  type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
201
372
  return (this as unknown as Tx).session.execute(
202
373
  sql`set transaction ${this.getTransactionConfigSQL(config)}`
203
374
  );
204
375
  }
205
376
 
377
+ executeBatches<T extends RowData = RowData>(
378
+ query: SQL,
379
+ options: ExecuteInBatchesOptions = {}
380
+ ): AsyncGenerator<GenericRowData<T>[], void, void> {
381
+ // Cast needed: PgTransaction doesn't expose session property in public API
382
+ type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
383
+ return (this as unknown as Tx).session.executeBatches<T>(query, options);
384
+ }
385
+
386
+ executeBatchesRaw(
387
+ query: SQL,
388
+ options: ExecuteInBatchesOptions = {}
389
+ ): AsyncGenerator<ExecuteBatchesRawChunk, void, void> {
390
+ // Cast needed: PgTransaction doesn't expose session property in public API
391
+ type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
392
+ return (this as unknown as Tx).session.executeBatchesRaw(query, options);
393
+ }
394
+
395
+ executeArrow(query: SQL): Promise<unknown> {
396
+ // Cast needed: PgTransaction doesn't expose session property in public API
397
+ type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
398
+ return (this as unknown as Tx).session.executeArrow(query);
399
+ }
400
+
206
401
  override async transaction<T>(
207
402
  transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
208
403
  ): Promise<T> {
209
- // Need to work around omitted internal types from drizzle...
404
+ // Cast needed: PgTransaction doesn't expose dialect/session properties in public API
210
405
  type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
211
-
212
- const savepointName = `sp${this.nestedIndex + 1}`;
213
- const tx = new DuckDBTransaction<TFullSchema, TSchema>(
214
- (this as unknown as Tx).dialect,
215
- (this as unknown as Tx).session,
406
+ const internals = this as unknown as Tx;
407
+ const savepoint = `drizzle_savepoint_${this.nestedIndex + 1}`;
408
+ const savepointSql = sql.raw(`savepoint ${savepoint}`);
409
+ const releaseSql = sql.raw(`release savepoint ${savepoint}`);
410
+ const rollbackSql = sql.raw(`rollback to savepoint ${savepoint}`);
411
+
412
+ const nestedTx = new DuckDBTransaction<TFullSchema, TSchema>(
413
+ internals.dialect,
414
+ internals.session,
216
415
  this.schema,
217
416
  this.nestedIndex + 1
218
417
  );
219
- await tx.execute(sql.raw(`savepoint ${savepointName}`));
418
+
419
+ // Check dialect-level savepoint support (per-instance, not global)
420
+ if (internals.dialect.areSavepointsUnsupported()) {
421
+ return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
422
+ }
423
+
424
+ let createdSavepoint = false;
425
+ try {
426
+ await internals.session.execute(savepointSql);
427
+ internals.dialect.markSavepointsSupported();
428
+ createdSavepoint = true;
429
+ } catch (error) {
430
+ if (!isSavepointSyntaxError(error)) {
431
+ throw error;
432
+ }
433
+ internals.dialect.markSavepointsUnsupported();
434
+ return this.runNestedWithoutSavepoint(transaction, nestedTx, internals);
435
+ }
436
+
220
437
  try {
221
- const result = await transaction(tx);
222
- await tx.execute(sql.raw(`release savepoint ${savepointName}`));
438
+ const result = await transaction(nestedTx);
439
+ if (createdSavepoint) {
440
+ await internals.session.execute(releaseSql);
441
+ }
223
442
  return result;
224
- } catch (err) {
225
- await tx.execute(sql.raw(`rollback to savepoint ${savepointName}`));
226
- throw err;
443
+ } catch (error) {
444
+ if (createdSavepoint) {
445
+ await internals.session.execute(rollbackSql);
446
+ }
447
+ (
448
+ internals.session as DuckDBSession<TFullSchema, TSchema>
449
+ ).markRollbackOnly();
450
+ throw error;
227
451
  }
228
452
  }
453
+
454
+ private runNestedWithoutSavepoint<T>(
455
+ transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>,
456
+ nestedTx: DuckDBTransaction<TFullSchema, TSchema>,
457
+ internals: DuckDBTransactionWithInternals<TFullSchema, TSchema>
458
+ ): Promise<T> {
459
+ return transaction(nestedTx).catch((error) => {
460
+ (
461
+ internals.session as DuckDBSession<TFullSchema, TSchema>
462
+ ).markRollbackOnly();
463
+ throw error;
464
+ });
465
+ }
229
466
  }
230
467
 
231
468
  export type GenericRowData<T extends RowData = RowData> = T;
232
469
 
233
470
  export type GenericTableData<T = RowData> = T[];
234
471
 
472
+ const arrayLiteralWarning =
473
+ 'Received a stringified Postgres-style array literal. Use duckDbList()/duckDbArray() or pass native arrays instead. You can also set rejectStringArrayLiterals=true to throw.';
474
+
235
475
  export interface DuckDBQueryResultHKT extends PgQueryResultHKT {
236
476
  type: GenericTableData<Assume<this['row'], RowData>>;
237
477
  }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * AST-based SQL transformer for DuckDB compatibility.
3
+ *
4
+ * Transforms:
5
+ * - Array operators: @>, <@, && -> array_has_all(), array_has_any()
6
+ * - JOIN column qualification: "col" = "col" -> "left"."col" = "right"."col"
7
+ *
8
+ * Performance optimizations:
9
+ * - LRU cache for transformed queries (avoids re-parsing identical queries)
10
+ * - Smart heuristics to skip JOIN qualification when not needed
11
+ * - Early exit when no transformation is required
12
+ */
13
+
14
+ import nodeSqlParser from 'node-sql-parser';
15
+ const { Parser } = nodeSqlParser;
16
+ import type { AST } from 'node-sql-parser';
17
+
18
+ import { transformArrayOperators } from './visitors/array-operators.ts';
19
+ import { qualifyJoinColumns } from './visitors/column-qualifier.ts';
20
+ import { rewriteGenerateSeriesAliases } from './visitors/generate-series-alias.ts';
21
+ import { hoistUnionWith } from './visitors/union-with-hoister.ts';
22
+
23
+ const parser = new Parser();
24
+
25
+ export type TransformResult = {
26
+ sql: string;
27
+ transformed: boolean;
28
+ };
29
+
30
+ // LRU cache for transformed SQL queries
31
+ // Key: original SQL, Value: transformed result
32
+ const CACHE_SIZE = 500;
33
+ const transformCache = new Map<string, TransformResult>();
34
+
35
+ function getCachedOrTransform(
36
+ query: string,
37
+ transform: () => TransformResult
38
+ ): TransformResult {
39
+ const cached = transformCache.get(query);
40
+ if (cached) {
41
+ // Move to end for LRU behavior
42
+ transformCache.delete(query);
43
+ transformCache.set(query, cached);
44
+ return cached;
45
+ }
46
+
47
+ const result = transform();
48
+
49
+ // Add to cache with LRU eviction
50
+ if (transformCache.size >= CACHE_SIZE) {
51
+ // Delete oldest entry (first key in Map iteration order)
52
+ const oldestKey = transformCache.keys().next().value;
53
+ if (oldestKey) {
54
+ transformCache.delete(oldestKey);
55
+ }
56
+ }
57
+ transformCache.set(query, result);
58
+
59
+ return result;
60
+ }
61
+
62
+ const DEBUG_ENV = 'DRIZZLE_DUCKDB_DEBUG_AST';
63
+
64
+ function hasJoin(query: string): boolean {
65
+ return /\bjoin\b/i.test(query);
66
+ }
67
+
68
+ function debugLog(message: string, payload?: unknown): void {
69
+ if (process?.env?.[DEBUG_ENV]) {
70
+ // eslint-disable-next-line no-console
71
+ console.debug('[duckdb-ast]', message, payload ?? '');
72
+ }
73
+ }
74
+
75
+ export function transformSQL(query: string): TransformResult {
76
+ const needsArrayTransform =
77
+ query.includes('@>') || query.includes('<@') || query.includes('&&');
78
+ const needsJoinTransform =
79
+ hasJoin(query) || /\bupdate\b/i.test(query) || /\bdelete\b/i.test(query);
80
+ const needsUnionTransform =
81
+ /\bunion\b/i.test(query) ||
82
+ /\bintersect\b/i.test(query) ||
83
+ /\bexcept\b/i.test(query);
84
+ const needsGenerateSeriesTransform = /\bgenerate_series\b/i.test(query);
85
+
86
+ if (
87
+ !needsArrayTransform &&
88
+ !needsJoinTransform &&
89
+ !needsUnionTransform &&
90
+ !needsGenerateSeriesTransform
91
+ ) {
92
+ return { sql: query, transformed: false };
93
+ }
94
+
95
+ // Use cache for repeated queries
96
+ return getCachedOrTransform(query, () => {
97
+ try {
98
+ const ast = parser.astify(query, { database: 'PostgreSQL' });
99
+
100
+ let transformed = false;
101
+
102
+ if (needsArrayTransform) {
103
+ transformed = transformArrayOperators(ast) || transformed;
104
+ }
105
+
106
+ if (needsJoinTransform) {
107
+ transformed = qualifyJoinColumns(ast) || transformed;
108
+ }
109
+
110
+ if (needsGenerateSeriesTransform) {
111
+ transformed = rewriteGenerateSeriesAliases(ast) || transformed;
112
+ }
113
+
114
+ if (needsUnionTransform) {
115
+ transformed = hoistUnionWith(ast) || transformed;
116
+ }
117
+
118
+ if (!transformed) {
119
+ debugLog('AST parsed but no transformation applied', {
120
+ join: needsJoinTransform,
121
+ });
122
+ return { sql: query, transformed: false };
123
+ }
124
+
125
+ const transformedSql = parser.sqlify(ast, { database: 'PostgreSQL' });
126
+
127
+ return { sql: transformedSql, transformed: true };
128
+ } catch (err) {
129
+ debugLog('AST transform failed; returning original SQL', {
130
+ error: (err as Error).message,
131
+ });
132
+ return { sql: query, transformed: false };
133
+ }
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Clear the transformation cache. Useful for testing or memory management.
139
+ */
140
+ export function clearTransformCache(): void {
141
+ transformCache.clear();
142
+ }
143
+
144
+ /**
145
+ * Get current cache statistics for monitoring.
146
+ */
147
+ export function getTransformCacheStats(): { size: number; maxSize: number } {
148
+ return { size: transformCache.size, maxSize: CACHE_SIZE };
149
+ }
150
+
151
+ export function needsTransformation(query: string): boolean {
152
+ const lower = query.toLowerCase();
153
+ return (
154
+ query.includes('@>') ||
155
+ query.includes('<@') ||
156
+ query.includes('&&') ||
157
+ lower.includes('join') ||
158
+ lower.includes('union') ||
159
+ lower.includes('intersect') ||
160
+ lower.includes('except') ||
161
+ lower.includes('generate_series') ||
162
+ lower.includes('update') ||
163
+ lower.includes('delete')
164
+ );
165
+ }
166
+
167
+ export { transformArrayOperators } from './visitors/array-operators.ts';
168
+ export { qualifyJoinColumns } from './visitors/column-qualifier.ts';
169
+ export { rewriteGenerateSeriesAliases } from './visitors/generate-series-alias.ts';
170
+ export { hoistUnionWith } from './visitors/union-with-hoister.ts';