@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.
@@ -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.0",
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 --runInBand test/perf",
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 = query.toLowerCase().includes('join');
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 (!needsArrayTransform && !needsJoinTransform) {
86
+ if (
87
+ !needsArrayTransform &&
88
+ !needsJoinTransform &&
89
+ !needsUnionTransform &&
90
+ !needsGenerateSeriesTransform
91
+ ) {
29
92
  return { sql: query, transformed: false };
30
93
  }
31
94
 
32
- try {
33
- const ast = parser.astify(query, { database: 'PostgreSQL' });
95
+ // Use cache for repeated queries
96
+ return getCachedOrTransform(query, () => {
97
+ try {
98
+ const ast = parser.astify(query, { database: 'PostgreSQL' });
34
99
 
35
- let transformed = false;
100
+ let transformed = false;
36
101
 
37
- if (needsArrayTransform) {
38
- transformed = transformArrayOperators(ast) || transformed;
39
- }
102
+ if (needsArrayTransform) {
103
+ transformed = transformArrayOperators(ast) || transformed;
104
+ }
40
105
 
41
- if (needsJoinTransform) {
42
- transformed = qualifyJoinColumns(ast) || transformed;
43
- }
106
+ if (needsJoinTransform) {
107
+ transformed = qualifyJoinColumns(ast) || transformed;
108
+ }
109
+
110
+ if (needsGenerateSeriesTransform) {
111
+ transformed = rewriteGenerateSeriesAliases(ast) || transformed;
112
+ }
44
113
 
45
- if (!transformed) {
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
- const transformedSql = parser.sqlify(ast, { database: 'PostgreSQL' });
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
- return { sql: transformedSql, transformed: true };
52
- } catch {
53
- return { sql: query, transformed: false };
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';