@duckdbfan/drizzle-duckdb 0.0.7 → 1.3.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 +349 -62
- package/dist/bin/duckdb-introspect.d.ts +2 -0
- package/dist/client.d.ts +42 -0
- package/dist/columns.d.ts +100 -9
- package/dist/dialect.d.ts +27 -2
- package/dist/driver.d.ts +53 -37
- package/dist/duckdb-introspect.mjs +2890 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.mjs +360 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +3015 -228
- package/dist/introspect.d.ts +74 -0
- package/dist/migrator.d.ts +3 -2
- package/dist/olap.d.ts +46 -0
- package/dist/operators.d.ts +8 -0
- package/dist/options.d.ts +7 -0
- package/dist/pool.d.ts +30 -0
- package/dist/select-builder.d.ts +31 -0
- package/dist/session.d.ts +33 -8
- package/dist/sql/ast-transformer.d.ts +33 -0
- package/dist/sql/result-mapper.d.ts +9 -0
- package/dist/sql/selection.d.ts +2 -0
- package/dist/sql/visitors/array-operators.d.ts +5 -0
- package/dist/sql/visitors/column-qualifier.d.ts +10 -0
- package/dist/sql/visitors/generate-series-alias.d.ts +13 -0
- package/dist/sql/visitors/union-with-hoister.d.ts +11 -0
- package/dist/utils.d.ts +2 -5
- package/dist/value-wrappers-core.d.ts +42 -0
- package/dist/value-wrappers.d.ts +8 -0
- package/package.json +53 -16
- package/src/bin/duckdb-introspect.ts +181 -0
- package/src/client.ts +528 -0
- package/src/columns.ts +420 -65
- package/src/dialect.ts +111 -15
- package/src/driver.ts +266 -180
- package/src/helpers.ts +18 -0
- package/src/index.ts +8 -1
- package/src/introspect.ts +935 -0
- package/src/migrator.ts +10 -5
- package/src/olap.ts +190 -0
- package/src/operators.ts +27 -0
- package/src/options.ts +25 -0
- package/src/pool.ts +274 -0
- package/src/select-builder.ts +110 -0
- package/src/session.ts +306 -66
- package/src/sql/ast-transformer.ts +170 -0
- package/src/sql/result-mapper.ts +303 -0
- package/src/sql/selection.ts +60 -0
- package/src/sql/visitors/array-operators.ts +214 -0
- package/src/sql/visitors/column-qualifier.ts +586 -0
- package/src/sql/visitors/generate-series-alias.ts +291 -0
- package/src/sql/visitors/union-with-hoister.ts +106 -0
- package/src/utils.ts +2 -222
- package/src/value-wrappers-core.ts +168 -0
- 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 {
|
|
4
|
-
import
|
|
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 './
|
|
20
|
-
import type { DuckDBDialect } from './dialect';
|
|
18
|
+
import { mapResultRow } from './sql/result-mapper.ts';
|
|
21
19
|
import { TransactionRollbackError } from 'drizzle-orm/errors';
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
112
|
+
const rows = await executeOnClient(this.client, this.queryString, params, {
|
|
113
|
+
prepareCache: this.prepareCache,
|
|
114
|
+
});
|
|
67
115
|
|
|
68
|
-
return
|
|
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
|
|
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:
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
404
|
+
// Cast needed: PgTransaction doesn't expose dialect/session properties in public API
|
|
210
405
|
type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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(
|
|
222
|
-
|
|
438
|
+
const result = await transaction(nestedTx);
|
|
439
|
+
if (createdSavepoint) {
|
|
440
|
+
await internals.session.execute(releaseSql);
|
|
441
|
+
}
|
|
223
442
|
return result;
|
|
224
|
-
} catch (
|
|
225
|
-
|
|
226
|
-
|
|
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';
|