@leonardovida-md/drizzle-neo-duckdb 1.2.0 → 1.2.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 +2 -1
- package/dist/duckdb-introspect.mjs +562 -73
- package/dist/index.mjs +554 -65
- package/dist/sql/ast-transformer.d.ts +18 -0
- package/dist/sql/visitors/column-qualifier.d.ts +5 -0
- package/dist/sql/visitors/generate-series-alias.d.ts +13 -0
- package/dist/sql/visitors/union-with-hoister.d.ts +11 -0
- package/package.json +2 -2
- package/src/sql/ast-transformer.ts +120 -18
- package/src/sql/visitors/column-qualifier.ts +393 -85
- package/src/sql/visitors/generate-series-alias.ts +291 -0
- package/src/sql/visitors/union-with-hoister.ts +106 -0
|
@@ -4,12 +4,30 @@
|
|
|
4
4
|
* Transforms:
|
|
5
5
|
* - Array operators: @>, <@, && -> array_has_all(), array_has_any()
|
|
6
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
|
|
7
12
|
*/
|
|
8
13
|
export type TransformResult = {
|
|
9
14
|
sql: string;
|
|
10
15
|
transformed: boolean;
|
|
11
16
|
};
|
|
12
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
|
+
};
|
|
13
29
|
export declare function needsTransformation(query: string): boolean;
|
|
14
30
|
export { transformArrayOperators } from './visitors/array-operators.ts';
|
|
15
31
|
export { qualifyJoinColumns } from './visitors/column-qualifier.ts';
|
|
32
|
+
export { rewriteGenerateSeriesAliases } from './visitors/generate-series-alias.ts';
|
|
33
|
+
export { hoistUnionWith } from './visitors/union-with-hoister.ts';
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
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
|
|
3
8
|
*/
|
|
4
9
|
import type { AST } from 'node-sql-parser';
|
|
5
10
|
export declare function qualifyJoinColumns(ast: AST | AST[]): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST visitor to rewrite Postgres style generate_series aliases.
|
|
3
|
+
*
|
|
4
|
+
* Postgres lets you reference a generate_series alias as a column:
|
|
5
|
+
* FROM generate_series(...) AS gs
|
|
6
|
+
* SELECT gs::date
|
|
7
|
+
*
|
|
8
|
+
* DuckDB treats gs as a table alias, and the column is generate_series.
|
|
9
|
+
* This visitor rewrites unqualified column refs that match a
|
|
10
|
+
* generate_series alias to gs.generate_series.
|
|
11
|
+
*/
|
|
12
|
+
import type { AST } from 'node-sql-parser';
|
|
13
|
+
export declare function rewriteGenerateSeriesAliases(ast: AST | AST[]): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST visitor to hoist WITH clauses out of UNION and other set operations.
|
|
3
|
+
*
|
|
4
|
+
* Drizzle can emit SQL like:
|
|
5
|
+
* (with a as (...) select ...) union (with b as (...) select ...)
|
|
6
|
+
*
|
|
7
|
+
* DuckDB 1.4.x has an internal binder bug for this pattern.
|
|
8
|
+
* We merge per arm CTEs into a single top level WITH when names do not collide.
|
|
9
|
+
*/
|
|
10
|
+
import type { AST } from 'node-sql-parser';
|
|
11
|
+
export declare function hoistUnionWith(ast: AST | AST[]): boolean;
|
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.2.
|
|
6
|
+
"version": "1.2.2",
|
|
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
|
},
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Transforms:
|
|
5
5
|
* - Array operators: @>, <@, && -> array_has_all(), array_has_any()
|
|
6
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
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
import nodeSqlParser from 'node-sql-parser';
|
|
@@ -12,6 +17,8 @@ import type { AST } from 'node-sql-parser';
|
|
|
12
17
|
|
|
13
18
|
import { transformArrayOperators } from './visitors/array-operators.ts';
|
|
14
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';
|
|
15
22
|
|
|
16
23
|
const parser = new Parser();
|
|
17
24
|
|
|
@@ -20,38 +27,125 @@ export type TransformResult = {
|
|
|
20
27
|
transformed: boolean;
|
|
21
28
|
};
|
|
22
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
|
+
|
|
23
75
|
export function transformSQL(query: string): TransformResult {
|
|
24
76
|
const needsArrayTransform =
|
|
25
77
|
query.includes('@>') || query.includes('<@') || query.includes('&&');
|
|
26
|
-
const needsJoinTransform =
|
|
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);
|
|
27
85
|
|
|
28
|
-
if (
|
|
86
|
+
if (
|
|
87
|
+
!needsArrayTransform &&
|
|
88
|
+
!needsJoinTransform &&
|
|
89
|
+
!needsUnionTransform &&
|
|
90
|
+
!needsGenerateSeriesTransform
|
|
91
|
+
) {
|
|
29
92
|
return { sql: query, transformed: false };
|
|
30
93
|
}
|
|
31
94
|
|
|
32
|
-
|
|
33
|
-
|
|
95
|
+
// Use cache for repeated queries
|
|
96
|
+
return getCachedOrTransform(query, () => {
|
|
97
|
+
try {
|
|
98
|
+
const ast = parser.astify(query, { database: 'PostgreSQL' });
|
|
34
99
|
|
|
35
|
-
|
|
100
|
+
let transformed = false;
|
|
36
101
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
102
|
+
if (needsArrayTransform) {
|
|
103
|
+
transformed = transformArrayOperators(ast) || transformed;
|
|
104
|
+
}
|
|
40
105
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
106
|
+
if (needsJoinTransform) {
|
|
107
|
+
transformed = qualifyJoinColumns(ast) || transformed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (needsGenerateSeriesTransform) {
|
|
111
|
+
transformed = rewriteGenerateSeriesAliases(ast) || transformed;
|
|
112
|
+
}
|
|
44
113
|
|
|
45
|
-
|
|
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
|
+
});
|
|
46
132
|
return { sql: query, transformed: false };
|
|
47
133
|
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
48
136
|
|
|
49
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Clear the transformation cache. Useful for testing or memory management.
|
|
139
|
+
*/
|
|
140
|
+
export function clearTransformCache(): void {
|
|
141
|
+
transformCache.clear();
|
|
142
|
+
}
|
|
50
143
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 };
|
|
55
149
|
}
|
|
56
150
|
|
|
57
151
|
export function needsTransformation(query: string): boolean {
|
|
@@ -60,9 +154,17 @@ export function needsTransformation(query: string): boolean {
|
|
|
60
154
|
query.includes('@>') ||
|
|
61
155
|
query.includes('<@') ||
|
|
62
156
|
query.includes('&&') ||
|
|
63
|
-
lower.includes('join')
|
|
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')
|
|
64
164
|
);
|
|
65
165
|
}
|
|
66
166
|
|
|
67
167
|
export { transformArrayOperators } from './visitors/array-operators.ts';
|
|
68
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';
|