@leonardovida-md/drizzle-neo-duckdb 1.1.4 → 1.2.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/README.md +2 -3
- package/dist/dialect.d.ts +3 -1
- package/dist/driver.d.ts +1 -3
- package/dist/duckdb-introspect.mjs +552 -837
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +560 -832
- package/dist/operators.d.ts +8 -0
- package/dist/options.d.ts +0 -3
- package/dist/session.d.ts +2 -5
- package/dist/sql/ast-transformer.d.ts +31 -0
- package/dist/sql/visitors/array-operators.d.ts +5 -0
- package/dist/sql/visitors/column-qualifier.d.ts +10 -0
- package/dist/utils.d.ts +0 -1
- package/package.json +4 -3
- package/src/dialect.ts +20 -0
- package/src/driver.ts +0 -8
- package/src/index.ts +1 -0
- package/src/operators.ts +27 -0
- package/src/options.ts +0 -15
- package/src/session.ts +10 -96
- package/src/sql/ast-transformer.ts +144 -0
- package/src/sql/visitors/array-operators.ts +214 -0
- package/src/sql/visitors/column-qualifier.ts +565 -0
- package/src/utils.ts +0 -1
- package/dist/sql/query-rewriters.d.ts +0 -15
- package/src/sql/query-rewriters.ts +0 -1161
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DuckDB-native array operators. Generate DuckDB-compatible SQL directly
|
|
3
|
+
* without query rewriting.
|
|
4
|
+
*/
|
|
5
|
+
import { type SQL, type SQLWrapper } from 'drizzle-orm';
|
|
6
|
+
export declare function arrayHasAll<T>(column: SQLWrapper, values: T[] | SQLWrapper): SQL;
|
|
7
|
+
export declare function arrayHasAny<T>(column: SQLWrapper, values: T[] | SQLWrapper): SQL;
|
|
8
|
+
export declare function arrayContainedBy<T>(column: SQLWrapper, values: T[] | SQLWrapper): SQL;
|
package/dist/options.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
export type RewriteArraysMode = 'auto' | 'always' | 'never';
|
|
2
|
-
export type RewriteArraysOption = boolean | RewriteArraysMode;
|
|
3
|
-
export declare function resolveRewriteArraysOption(value?: RewriteArraysOption): RewriteArraysMode;
|
|
4
1
|
export type PrepareCacheOption = boolean | number | {
|
|
5
2
|
size?: number;
|
|
6
3
|
};
|
package/dist/session.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { Assume } from 'drizzle-orm/utils';
|
|
|
10
10
|
import type { DuckDBDialect } from './dialect.ts';
|
|
11
11
|
import type { DuckDBClientLike, RowData } from './client.ts';
|
|
12
12
|
import { type ExecuteBatchesRawChunk, type ExecuteInBatchesOptions } from './client.ts';
|
|
13
|
-
import type { PreparedStatementCacheConfig
|
|
13
|
+
import type { PreparedStatementCacheConfig } from './options.ts';
|
|
14
14
|
export type { DuckDBClientLike, RowData } from './client.ts';
|
|
15
15
|
export declare class DuckDBPreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
|
|
16
16
|
private client;
|
|
@@ -21,19 +21,17 @@ export declare class DuckDBPreparedQuery<T extends PreparedQueryConfig> extends
|
|
|
21
21
|
private fields;
|
|
22
22
|
private _isResponseInArrayMode;
|
|
23
23
|
private customResultMapper;
|
|
24
|
-
private rewriteArraysMode;
|
|
25
24
|
private rejectStringArrayLiterals;
|
|
26
25
|
private prepareCache;
|
|
27
26
|
private warnOnStringArrayLiteral?;
|
|
28
27
|
static readonly [entityKind]: string;
|
|
29
|
-
constructor(client: DuckDBClientLike, dialect: DuckDBDialect, queryString: string, params: unknown[], logger: Logger, fields: SelectedFieldsOrdered | undefined, _isResponseInArrayMode: boolean, customResultMapper: ((rows: unknown[][]) => T['execute']) | undefined,
|
|
28
|
+
constructor(client: DuckDBClientLike, dialect: DuckDBDialect, queryString: string, params: unknown[], logger: Logger, fields: SelectedFieldsOrdered | undefined, _isResponseInArrayMode: boolean, customResultMapper: ((rows: unknown[][]) => T['execute']) | undefined, rejectStringArrayLiterals: boolean, prepareCache: PreparedStatementCacheConfig | undefined, warnOnStringArrayLiteral?: ((sql: string) => void) | undefined);
|
|
30
29
|
execute(placeholderValues?: Record<string, unknown> | undefined): Promise<T['execute']>;
|
|
31
30
|
all(placeholderValues?: Record<string, unknown> | undefined): Promise<T['all']>;
|
|
32
31
|
isResponseInArrayMode(): boolean;
|
|
33
32
|
}
|
|
34
33
|
export interface DuckDBSessionOptions {
|
|
35
34
|
logger?: Logger;
|
|
36
|
-
rewriteArrays?: RewriteArraysMode;
|
|
37
35
|
rejectStringArrayLiterals?: boolean;
|
|
38
36
|
prepareCache?: PreparedStatementCacheConfig;
|
|
39
37
|
}
|
|
@@ -44,7 +42,6 @@ export declare class DuckDBSession<TFullSchema extends Record<string, unknown> =
|
|
|
44
42
|
static readonly [entityKind]: string;
|
|
45
43
|
protected dialect: DuckDBDialect;
|
|
46
44
|
private logger;
|
|
47
|
-
private rewriteArraysMode;
|
|
48
45
|
private rejectStringArrayLiterals;
|
|
49
46
|
private prepareCache;
|
|
50
47
|
private hasWarnedArrayLiteral;
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
export type TransformResult = {
|
|
14
|
+
sql: string;
|
|
15
|
+
transformed: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function transformSQL(query: string): TransformResult;
|
|
18
|
+
/**
|
|
19
|
+
* Clear the transformation cache. Useful for testing or memory management.
|
|
20
|
+
*/
|
|
21
|
+
export declare function clearTransformCache(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Get current cache statistics for monitoring.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getTransformCacheStats(): {
|
|
26
|
+
size: number;
|
|
27
|
+
maxSize: number;
|
|
28
|
+
};
|
|
29
|
+
export declare function needsTransformation(query: string): boolean;
|
|
30
|
+
export { transformArrayOperators } from './visitors/array-operators.ts';
|
|
31
|
+
export { qualifyJoinColumns } from './visitors/column-qualifier.ts';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST visitor to qualify unqualified column references in JOIN ON clauses.
|
|
3
|
+
*
|
|
4
|
+
* Performance optimizations:
|
|
5
|
+
* - Early exit when no unqualified columns found in ON clause
|
|
6
|
+
* - Skip processing if all columns are already qualified
|
|
7
|
+
* - Minimal tree traversal when possible
|
|
8
|
+
*/
|
|
9
|
+
import type { AST } from 'node-sql-parser';
|
|
10
|
+
export declare function qualifyJoinColumns(ast: AST | AST[]): boolean;
|
package/dist/utils.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"module": "./dist/index.mjs",
|
|
4
4
|
"main": "./dist/index.mjs",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
|
-
"version": "1.1
|
|
6
|
+
"version": "1.2.1",
|
|
7
7
|
"description": "A drizzle ORM client for use with DuckDB. Based on drizzle's Postgres client.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"scripts": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"build:declarations": "tsc --emitDeclarationOnly --project tsconfig.types.json",
|
|
12
12
|
"test": "vitest",
|
|
13
13
|
"t": "vitest --watch --ui",
|
|
14
|
-
"bench": "vitest bench --
|
|
14
|
+
"bench": "vitest bench --run test/perf --pool=threads --poolOptions.threads.singleThread=true --no-file-parallelism",
|
|
15
15
|
"perf:run": "bun run scripts/run-perf.ts",
|
|
16
16
|
"perf:compare": "bun run scripts/compare-perf.ts"
|
|
17
17
|
},
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"dist/**/*.d.ts"
|
|
80
80
|
],
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@duckdb/node-bindings-darwin-arm64": "^1.4.2-r.1"
|
|
82
|
+
"@duckdb/node-bindings-darwin-arm64": "^1.4.2-r.1",
|
|
83
|
+
"node-sql-parser": "^5.3.13"
|
|
83
84
|
}
|
|
84
85
|
}
|
package/src/dialect.ts
CHANGED
|
@@ -15,9 +15,13 @@ import {
|
|
|
15
15
|
} from 'drizzle-orm/pg-core';
|
|
16
16
|
import {
|
|
17
17
|
sql,
|
|
18
|
+
SQL,
|
|
18
19
|
type DriverValueEncoder,
|
|
19
20
|
type QueryTypingsValue,
|
|
20
21
|
} from 'drizzle-orm';
|
|
22
|
+
import type { QueryWithTypings } from 'drizzle-orm/sql/sql';
|
|
23
|
+
|
|
24
|
+
import { transformSQL } from './sql/ast-transformer.ts';
|
|
21
25
|
|
|
22
26
|
const enum SavepointSupport {
|
|
23
27
|
Unknown = 0,
|
|
@@ -181,4 +185,20 @@ export class DuckDBDialect extends PgDialect {
|
|
|
181
185
|
return 'none';
|
|
182
186
|
}
|
|
183
187
|
}
|
|
188
|
+
|
|
189
|
+
override sqlToQuery(
|
|
190
|
+
sqlObj: SQL,
|
|
191
|
+
invokeSource?: 'indexes' | undefined
|
|
192
|
+
): QueryWithTypings {
|
|
193
|
+
// First, let the parent generate the SQL string
|
|
194
|
+
const result = super.sqlToQuery(sqlObj, invokeSource);
|
|
195
|
+
|
|
196
|
+
// Apply AST-based transformations for DuckDB compatibility
|
|
197
|
+
const transformed = transformSQL(result.sql);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
...result,
|
|
201
|
+
sql: transformed.sql,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
184
204
|
}
|
package/src/driver.ts
CHANGED
|
@@ -37,16 +37,12 @@ import {
|
|
|
37
37
|
} from './pool.ts';
|
|
38
38
|
import {
|
|
39
39
|
resolvePrepareCacheOption,
|
|
40
|
-
resolveRewriteArraysOption,
|
|
41
40
|
type PreparedStatementCacheConfig,
|
|
42
41
|
type PrepareCacheOption,
|
|
43
|
-
type RewriteArraysMode,
|
|
44
|
-
type RewriteArraysOption,
|
|
45
42
|
} from './options.ts';
|
|
46
43
|
|
|
47
44
|
export interface PgDriverOptions {
|
|
48
45
|
logger?: Logger;
|
|
49
|
-
rewriteArrays?: RewriteArraysMode;
|
|
50
46
|
rejectStringArrayLiterals?: boolean;
|
|
51
47
|
prepareCache?: PreparedStatementCacheConfig;
|
|
52
48
|
}
|
|
@@ -65,7 +61,6 @@ export class DuckDBDriver {
|
|
|
65
61
|
): DuckDBSession<Record<string, unknown>, TablesRelationalConfig> {
|
|
66
62
|
return new DuckDBSession(this.client, this.dialect, schema, {
|
|
67
63
|
logger: this.options.logger,
|
|
68
|
-
rewriteArrays: this.options.rewriteArrays ?? 'auto',
|
|
69
64
|
rejectStringArrayLiterals: this.options.rejectStringArrayLiterals,
|
|
70
65
|
prepareCache: this.options.prepareCache,
|
|
71
66
|
});
|
|
@@ -83,7 +78,6 @@ export interface DuckDBConnectionConfig {
|
|
|
83
78
|
export interface DuckDBDrizzleConfig<
|
|
84
79
|
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
85
80
|
> extends DrizzleConfig<TSchema> {
|
|
86
|
-
rewriteArrays?: RewriteArraysOption;
|
|
87
81
|
rejectStringArrayLiterals?: boolean;
|
|
88
82
|
prepareCache?: PrepareCacheOption;
|
|
89
83
|
/** Pool configuration. Use preset name, size config, or false to disable. */
|
|
@@ -126,7 +120,6 @@ function createFromClient<
|
|
|
126
120
|
instance?: DuckDBInstance
|
|
127
121
|
): DuckDBDatabase<TSchema, ExtractTablesWithRelations<TSchema>> {
|
|
128
122
|
const dialect = new DuckDBDialect();
|
|
129
|
-
const rewriteArraysMode = resolveRewriteArraysOption(config.rewriteArrays);
|
|
130
123
|
const prepareCache = resolvePrepareCacheOption(config.prepareCache);
|
|
131
124
|
|
|
132
125
|
const logger =
|
|
@@ -148,7 +141,6 @@ function createFromClient<
|
|
|
148
141
|
|
|
149
142
|
const driver = new DuckDBDriver(client, dialect, {
|
|
150
143
|
logger,
|
|
151
|
-
rewriteArrays: rewriteArraysMode,
|
|
152
144
|
rejectStringArrayLiterals: config.rejectStringArrayLiterals,
|
|
153
145
|
prepareCache,
|
|
154
146
|
});
|
package/src/index.ts
CHANGED
package/src/operators.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DuckDB-native array operators. Generate DuckDB-compatible SQL directly
|
|
3
|
+
* without query rewriting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sql, type SQL, type SQLWrapper } from 'drizzle-orm';
|
|
7
|
+
|
|
8
|
+
export function arrayHasAll<T>(
|
|
9
|
+
column: SQLWrapper,
|
|
10
|
+
values: T[] | SQLWrapper
|
|
11
|
+
): SQL {
|
|
12
|
+
return sql`array_has_all(${column}, ${values})`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function arrayHasAny<T>(
|
|
16
|
+
column: SQLWrapper,
|
|
17
|
+
values: T[] | SQLWrapper
|
|
18
|
+
): SQL {
|
|
19
|
+
return sql`array_has_any(${column}, ${values})`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function arrayContainedBy<T>(
|
|
23
|
+
column: SQLWrapper,
|
|
24
|
+
values: T[] | SQLWrapper
|
|
25
|
+
): SQL {
|
|
26
|
+
return sql`array_has_all(${values}, ${column})`;
|
|
27
|
+
}
|
package/src/options.ts
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
export type RewriteArraysMode = 'auto' | 'always' | 'never';
|
|
2
|
-
|
|
3
|
-
export type RewriteArraysOption = boolean | RewriteArraysMode;
|
|
4
|
-
|
|
5
|
-
const DEFAULT_REWRITE_ARRAYS_MODE: RewriteArraysMode = 'auto';
|
|
6
|
-
|
|
7
|
-
export function resolveRewriteArraysOption(
|
|
8
|
-
value?: RewriteArraysOption
|
|
9
|
-
): RewriteArraysMode {
|
|
10
|
-
if (value === undefined) return DEFAULT_REWRITE_ARRAYS_MODE;
|
|
11
|
-
if (value === true) return 'auto';
|
|
12
|
-
if (value === false) return 'never';
|
|
13
|
-
return value;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
1
|
export type PrepareCacheOption = boolean | number | { size?: number };
|
|
17
2
|
|
|
18
3
|
export interface PreparedStatementCacheConfig {
|
package/src/session.ts
CHANGED
|
@@ -15,10 +15,6 @@ import type {
|
|
|
15
15
|
} from 'drizzle-orm/relations';
|
|
16
16
|
import { fillPlaceholders, type Query, SQL, sql } from 'drizzle-orm/sql/sql';
|
|
17
17
|
import type { Assume } from 'drizzle-orm/utils';
|
|
18
|
-
import {
|
|
19
|
-
adaptArrayOperators,
|
|
20
|
-
qualifyJoinColumns,
|
|
21
|
-
} from './sql/query-rewriters.ts';
|
|
22
18
|
import { mapResultRow } from './sql/result-mapper.ts';
|
|
23
19
|
import { TransactionRollbackError } from 'drizzle-orm/errors';
|
|
24
20
|
import type { DuckDBDialect } from './dialect.ts';
|
|
@@ -39,10 +35,7 @@ import {
|
|
|
39
35
|
} from './client.ts';
|
|
40
36
|
import { isPool } from './client.ts';
|
|
41
37
|
import type { DuckDBConnection } from '@duckdb/node-api';
|
|
42
|
-
import type {
|
|
43
|
-
PreparedStatementCacheConfig,
|
|
44
|
-
RewriteArraysMode,
|
|
45
|
-
} from './options.ts';
|
|
38
|
+
import type { PreparedStatementCacheConfig } from './options.ts';
|
|
46
39
|
|
|
47
40
|
export type { DuckDBClientLike, RowData } from './client.ts';
|
|
48
41
|
|
|
@@ -56,34 +49,6 @@ function isSavepointSyntaxError(error: unknown): boolean {
|
|
|
56
49
|
);
|
|
57
50
|
}
|
|
58
51
|
|
|
59
|
-
function rewriteQuery(
|
|
60
|
-
mode: RewriteArraysMode,
|
|
61
|
-
query: string
|
|
62
|
-
): { sql: string; rewritten: boolean } {
|
|
63
|
-
if (mode === 'never') {
|
|
64
|
-
return { sql: query, rewritten: false };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let result = query;
|
|
68
|
-
let wasRewritten = false;
|
|
69
|
-
|
|
70
|
-
// Rewrite Postgres array operators to DuckDB functions
|
|
71
|
-
const arrayRewritten = adaptArrayOperators(result);
|
|
72
|
-
if (arrayRewritten !== result) {
|
|
73
|
-
result = arrayRewritten;
|
|
74
|
-
wasRewritten = true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Qualify unqualified column references in JOIN ON clauses
|
|
78
|
-
const joinQualified = qualifyJoinColumns(result);
|
|
79
|
-
if (joinQualified !== result) {
|
|
80
|
-
result = joinQualified;
|
|
81
|
-
wasRewritten = true;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return { sql: result, rewritten: wasRewritten };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
52
|
export class DuckDBPreparedQuery<
|
|
88
53
|
T extends PreparedQueryConfig,
|
|
89
54
|
> extends PgPreparedQuery<T> {
|
|
@@ -100,7 +65,6 @@ export class DuckDBPreparedQuery<
|
|
|
100
65
|
private customResultMapper:
|
|
101
66
|
| ((rows: unknown[][]) => T['execute'])
|
|
102
67
|
| undefined,
|
|
103
|
-
private rewriteArraysMode: RewriteArraysMode,
|
|
104
68
|
private rejectStringArrayLiterals: boolean,
|
|
105
69
|
private prepareCache: PreparedStatementCacheConfig | undefined,
|
|
106
70
|
private warnOnStringArrayLiteral?: (sql: string) => void
|
|
@@ -121,19 +85,7 @@ export class DuckDBPreparedQuery<
|
|
|
121
85
|
: undefined,
|
|
122
86
|
}
|
|
123
87
|
);
|
|
124
|
-
|
|
125
|
-
this.rewriteArraysMode,
|
|
126
|
-
this.queryString
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
if (didRewrite) {
|
|
130
|
-
this.logger.logQuery(
|
|
131
|
-
`[duckdb] original query before array rewrite: ${this.queryString}`,
|
|
132
|
-
params
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
this.logger.logQuery(rewrittenQuery, params);
|
|
88
|
+
this.logger.logQuery(this.queryString, params);
|
|
137
89
|
|
|
138
90
|
const { fields, joinsNotNullableMap, customResultMapper } =
|
|
139
91
|
this as typeof this & { joinsNotNullableMap?: Record<string, boolean> };
|
|
@@ -141,7 +93,7 @@ export class DuckDBPreparedQuery<
|
|
|
141
93
|
if (fields) {
|
|
142
94
|
const { rows } = await executeArraysOnClient(
|
|
143
95
|
this.client,
|
|
144
|
-
|
|
96
|
+
this.queryString,
|
|
145
97
|
params,
|
|
146
98
|
{ prepareCache: this.prepareCache }
|
|
147
99
|
);
|
|
@@ -157,7 +109,7 @@ export class DuckDBPreparedQuery<
|
|
|
157
109
|
);
|
|
158
110
|
}
|
|
159
111
|
|
|
160
|
-
const rows = await executeOnClient(this.client,
|
|
112
|
+
const rows = await executeOnClient(this.client, this.queryString, params, {
|
|
161
113
|
prepareCache: this.prepareCache,
|
|
162
114
|
});
|
|
163
115
|
|
|
@@ -177,7 +129,6 @@ export class DuckDBPreparedQuery<
|
|
|
177
129
|
|
|
178
130
|
export interface DuckDBSessionOptions {
|
|
179
131
|
logger?: Logger;
|
|
180
|
-
rewriteArrays?: RewriteArraysMode;
|
|
181
132
|
rejectStringArrayLiterals?: boolean;
|
|
182
133
|
prepareCache?: PreparedStatementCacheConfig;
|
|
183
134
|
}
|
|
@@ -190,7 +141,6 @@ export class DuckDBSession<
|
|
|
190
141
|
|
|
191
142
|
protected override dialect: DuckDBDialect;
|
|
192
143
|
private logger: Logger;
|
|
193
|
-
private rewriteArraysMode: RewriteArraysMode;
|
|
194
144
|
private rejectStringArrayLiterals: boolean;
|
|
195
145
|
private prepareCache: PreparedStatementCacheConfig | undefined;
|
|
196
146
|
private hasWarnedArrayLiteral = false;
|
|
@@ -205,12 +155,10 @@ export class DuckDBSession<
|
|
|
205
155
|
super(dialect);
|
|
206
156
|
this.dialect = dialect;
|
|
207
157
|
this.logger = options.logger ?? new NoopLogger();
|
|
208
|
-
this.rewriteArraysMode = options.rewriteArrays ?? 'auto';
|
|
209
158
|
this.rejectStringArrayLiterals = options.rejectStringArrayLiterals ?? false;
|
|
210
159
|
this.prepareCache = options.prepareCache;
|
|
211
160
|
this.options = {
|
|
212
161
|
...options,
|
|
213
|
-
rewriteArrays: this.rewriteArraysMode,
|
|
214
162
|
prepareCache: this.prepareCache,
|
|
215
163
|
};
|
|
216
164
|
}
|
|
@@ -232,7 +180,6 @@ export class DuckDBSession<
|
|
|
232
180
|
fields,
|
|
233
181
|
isResponseInArrayMode,
|
|
234
182
|
customResultMapper,
|
|
235
|
-
this.rewriteArraysMode,
|
|
236
183
|
this.rejectStringArrayLiterals,
|
|
237
184
|
this.prepareCache,
|
|
238
185
|
this.rejectStringArrayLiterals ? undefined : this.warnOnStringArrayLiteral
|
|
@@ -326,23 +273,12 @@ export class DuckDBSession<
|
|
|
326
273
|
? undefined
|
|
327
274
|
: () => this.warnOnStringArrayLiteral(builtQuery.sql),
|
|
328
275
|
});
|
|
329
|
-
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
|
|
330
|
-
this.rewriteArraysMode,
|
|
331
|
-
builtQuery.sql
|
|
332
|
-
);
|
|
333
276
|
|
|
334
|
-
|
|
335
|
-
this.logger.logQuery(
|
|
336
|
-
`[duckdb] original query before array rewrite: ${builtQuery.sql}`,
|
|
337
|
-
params
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
this.logger.logQuery(rewrittenQuery, params);
|
|
277
|
+
this.logger.logQuery(builtQuery.sql, params);
|
|
342
278
|
|
|
343
279
|
return executeInBatches(
|
|
344
280
|
this.client,
|
|
345
|
-
|
|
281
|
+
builtQuery.sql,
|
|
346
282
|
params,
|
|
347
283
|
options
|
|
348
284
|
) as AsyncGenerator<GenericRowData<T>[], void, void>;
|
|
@@ -361,21 +297,10 @@ export class DuckDBSession<
|
|
|
361
297
|
? undefined
|
|
362
298
|
: () => this.warnOnStringArrayLiteral(builtQuery.sql),
|
|
363
299
|
});
|
|
364
|
-
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
|
|
365
|
-
this.rewriteArraysMode,
|
|
366
|
-
builtQuery.sql
|
|
367
|
-
);
|
|
368
300
|
|
|
369
|
-
|
|
370
|
-
this.logger.logQuery(
|
|
371
|
-
`[duckdb] original query before array rewrite: ${builtQuery.sql}`,
|
|
372
|
-
params
|
|
373
|
-
);
|
|
374
|
-
}
|
|
301
|
+
this.logger.logQuery(builtQuery.sql, params);
|
|
375
302
|
|
|
376
|
-
this.
|
|
377
|
-
|
|
378
|
-
return executeInBatchesRaw(this.client, rewrittenQuery, params, options);
|
|
303
|
+
return executeInBatchesRaw(this.client, builtQuery.sql, params, options);
|
|
379
304
|
}
|
|
380
305
|
|
|
381
306
|
async executeArrow(query: SQL): Promise<unknown> {
|
|
@@ -388,21 +313,10 @@ export class DuckDBSession<
|
|
|
388
313
|
? undefined
|
|
389
314
|
: () => this.warnOnStringArrayLiteral(builtQuery.sql),
|
|
390
315
|
});
|
|
391
|
-
const { sql: rewrittenQuery, rewritten: didRewrite } = rewriteQuery(
|
|
392
|
-
this.rewriteArraysMode,
|
|
393
|
-
builtQuery.sql
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
if (didRewrite) {
|
|
397
|
-
this.logger.logQuery(
|
|
398
|
-
`[duckdb] original query before array rewrite: ${builtQuery.sql}`,
|
|
399
|
-
params
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
316
|
|
|
403
|
-
this.logger.logQuery(
|
|
317
|
+
this.logger.logQuery(builtQuery.sql, params);
|
|
404
318
|
|
|
405
|
-
return executeArrowOnClient(this.client,
|
|
319
|
+
return executeArrowOnClient(this.client, builtQuery.sql, params);
|
|
406
320
|
}
|
|
407
321
|
|
|
408
322
|
markRollbackOnly(): void {
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
|
|
21
|
+
const parser = new Parser();
|
|
22
|
+
|
|
23
|
+
export type TransformResult = {
|
|
24
|
+
sql: string;
|
|
25
|
+
transformed: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// LRU cache for transformed SQL queries
|
|
29
|
+
// Key: original SQL, Value: transformed result
|
|
30
|
+
const CACHE_SIZE = 500;
|
|
31
|
+
const transformCache = new Map<string, TransformResult>();
|
|
32
|
+
|
|
33
|
+
function getCachedOrTransform(
|
|
34
|
+
query: string,
|
|
35
|
+
transform: () => TransformResult
|
|
36
|
+
): TransformResult {
|
|
37
|
+
const cached = transformCache.get(query);
|
|
38
|
+
if (cached) {
|
|
39
|
+
// Move to end for LRU behavior
|
|
40
|
+
transformCache.delete(query);
|
|
41
|
+
transformCache.set(query, cached);
|
|
42
|
+
return cached;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = transform();
|
|
46
|
+
|
|
47
|
+
// Add to cache with LRU eviction
|
|
48
|
+
if (transformCache.size >= CACHE_SIZE) {
|
|
49
|
+
// Delete oldest entry (first key in Map iteration order)
|
|
50
|
+
const oldestKey = transformCache.keys().next().value;
|
|
51
|
+
if (oldestKey) {
|
|
52
|
+
transformCache.delete(oldestKey);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
transformCache.set(query, result);
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DEBUG_ENV = 'DRIZZLE_DUCKDB_DEBUG_AST';
|
|
61
|
+
|
|
62
|
+
function hasJoin(query: string): boolean {
|
|
63
|
+
return /\bjoin\b/i.test(query);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function debugLog(message: string, payload?: unknown): void {
|
|
67
|
+
if (process?.env?.[DEBUG_ENV]) {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.debug('[duckdb-ast]', message, payload ?? '');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function transformSQL(query: string): TransformResult {
|
|
74
|
+
const needsArrayTransform =
|
|
75
|
+
query.includes('@>') || query.includes('<@') || query.includes('&&');
|
|
76
|
+
const needsJoinTransform =
|
|
77
|
+
hasJoin(query) || /\bupdate\b/i.test(query) || /\bdelete\b/i.test(query);
|
|
78
|
+
|
|
79
|
+
if (!needsArrayTransform && !needsJoinTransform) {
|
|
80
|
+
return { sql: query, transformed: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Use cache for repeated queries
|
|
84
|
+
return getCachedOrTransform(query, () => {
|
|
85
|
+
try {
|
|
86
|
+
const ast = parser.astify(query, { database: 'PostgreSQL' });
|
|
87
|
+
|
|
88
|
+
let transformed = false;
|
|
89
|
+
|
|
90
|
+
if (needsArrayTransform) {
|
|
91
|
+
transformed = transformArrayOperators(ast) || transformed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (needsJoinTransform) {
|
|
95
|
+
transformed = qualifyJoinColumns(ast) || transformed;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!transformed) {
|
|
99
|
+
debugLog('AST parsed but no transformation applied', {
|
|
100
|
+
join: needsJoinTransform,
|
|
101
|
+
});
|
|
102
|
+
return { sql: query, transformed: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const transformedSql = parser.sqlify(ast, { database: 'PostgreSQL' });
|
|
106
|
+
|
|
107
|
+
return { sql: transformedSql, transformed: true };
|
|
108
|
+
} catch (err) {
|
|
109
|
+
debugLog('AST transform failed; returning original SQL', {
|
|
110
|
+
error: (err as Error).message,
|
|
111
|
+
});
|
|
112
|
+
return { sql: query, transformed: false };
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clear the transformation cache. Useful for testing or memory management.
|
|
119
|
+
*/
|
|
120
|
+
export function clearTransformCache(): void {
|
|
121
|
+
transformCache.clear();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get current cache statistics for monitoring.
|
|
126
|
+
*/
|
|
127
|
+
export function getTransformCacheStats(): { size: number; maxSize: number } {
|
|
128
|
+
return { size: transformCache.size, maxSize: CACHE_SIZE };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function needsTransformation(query: string): boolean {
|
|
132
|
+
const lower = query.toLowerCase();
|
|
133
|
+
return (
|
|
134
|
+
query.includes('@>') ||
|
|
135
|
+
query.includes('<@') ||
|
|
136
|
+
query.includes('&&') ||
|
|
137
|
+
lower.includes('join') ||
|
|
138
|
+
lower.includes('update') ||
|
|
139
|
+
lower.includes('delete')
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { transformArrayOperators } from './visitors/array-operators.ts';
|
|
144
|
+
export { qualifyJoinColumns } from './visitors/column-qualifier.ts';
|