@leonardovida-md/drizzle-neo-duckdb 1.1.3 → 1.2.0

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.
@@ -0,0 +1,214 @@
1
+ /**
2
+ * AST visitor to transform Postgres array operators to DuckDB functions.
3
+ */
4
+
5
+ import type {
6
+ AST,
7
+ Binary,
8
+ ExpressionValue,
9
+ Select,
10
+ From,
11
+ Join,
12
+ } from 'node-sql-parser';
13
+
14
+ const OPERATOR_MAP: Record<string, { fn: string; swap?: boolean }> = {
15
+ '@>': { fn: 'array_has_all' },
16
+ '<@': { fn: 'array_has_all', swap: true },
17
+ '&&': { fn: 'array_has_any' },
18
+ };
19
+
20
+ function walkExpression(
21
+ expr: ExpressionValue | null | undefined,
22
+ parent?: object,
23
+ key?: string
24
+ ): boolean {
25
+ if (!expr || typeof expr !== 'object') return false;
26
+
27
+ let transformed = false;
28
+ const exprObj = expr as Record<string, unknown>;
29
+
30
+ if ('type' in expr && exprObj.type === 'binary_expr') {
31
+ const binary = expr as Binary;
32
+ const mapping = OPERATOR_MAP[binary.operator];
33
+
34
+ if (mapping) {
35
+ const fnExpr = {
36
+ type: 'function' as const,
37
+ name: { name: [{ type: 'default', value: mapping.fn }] },
38
+ args: {
39
+ type: 'expr_list' as const,
40
+ value: mapping.swap
41
+ ? [binary.right, binary.left]
42
+ : [binary.left, binary.right],
43
+ },
44
+ };
45
+
46
+ if (parent && key) {
47
+ (parent as Record<string, unknown>)[key] = fnExpr;
48
+ }
49
+ transformed = true;
50
+ } else {
51
+ transformed =
52
+ walkExpression(binary.left as ExpressionValue, binary, 'left') ||
53
+ transformed;
54
+ transformed =
55
+ walkExpression(binary.right as ExpressionValue, binary, 'right') ||
56
+ transformed;
57
+ }
58
+ }
59
+
60
+ if ('type' in expr && exprObj.type === 'unary_expr') {
61
+ if ('expr' in exprObj) {
62
+ transformed =
63
+ walkExpression(exprObj.expr as ExpressionValue, exprObj, 'expr') ||
64
+ transformed;
65
+ }
66
+ }
67
+
68
+ if ('type' in expr && exprObj.type === 'case') {
69
+ if ('expr' in exprObj && exprObj.expr) {
70
+ transformed =
71
+ walkExpression(exprObj.expr as ExpressionValue, exprObj, 'expr') ||
72
+ transformed;
73
+ }
74
+ if ('args' in exprObj && Array.isArray(exprObj.args)) {
75
+ for (let i = 0; i < exprObj.args.length; i++) {
76
+ const whenClause = exprObj.args[i] as Record<string, unknown>;
77
+ if (whenClause.cond) {
78
+ transformed =
79
+ walkExpression(
80
+ whenClause.cond as ExpressionValue,
81
+ whenClause,
82
+ 'cond'
83
+ ) || transformed;
84
+ }
85
+ if (whenClause.result) {
86
+ transformed =
87
+ walkExpression(
88
+ whenClause.result as ExpressionValue,
89
+ whenClause,
90
+ 'result'
91
+ ) || transformed;
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ if ('args' in expr && exprObj.args) {
98
+ const args = exprObj.args as Record<string, unknown>;
99
+ if ('value' in args && Array.isArray(args.value)) {
100
+ for (let i = 0; i < args.value.length; i++) {
101
+ transformed =
102
+ walkExpression(
103
+ args.value[i] as ExpressionValue,
104
+ args.value,
105
+ String(i)
106
+ ) || transformed;
107
+ }
108
+ } else if ('expr' in args) {
109
+ transformed =
110
+ walkExpression(args.expr as ExpressionValue, args, 'expr') ||
111
+ transformed;
112
+ }
113
+ }
114
+
115
+ if ('ast' in exprObj && exprObj.ast) {
116
+ const subAst = exprObj.ast as Select;
117
+ if (subAst.type === 'select') {
118
+ transformed = walkSelectImpl(subAst) || transformed;
119
+ }
120
+ }
121
+
122
+ if ('type' in expr && exprObj.type === 'expr_list') {
123
+ if ('value' in exprObj && Array.isArray(exprObj.value)) {
124
+ for (let i = 0; i < exprObj.value.length; i++) {
125
+ transformed =
126
+ walkExpression(
127
+ exprObj.value[i] as ExpressionValue,
128
+ exprObj.value,
129
+ String(i)
130
+ ) || transformed;
131
+ }
132
+ }
133
+ }
134
+
135
+ return transformed;
136
+ }
137
+
138
+ function walkFrom(from: From[] | null | undefined): boolean {
139
+ if (!from || !Array.isArray(from)) return false;
140
+
141
+ let transformed = false;
142
+
143
+ for (const f of from) {
144
+ if ('join' in f) {
145
+ const join = f as Join;
146
+ transformed = walkExpression(join.on, join, 'on') || transformed;
147
+ }
148
+ if ('expr' in f && f.expr && 'ast' in f.expr) {
149
+ transformed = walkSelectImpl(f.expr.ast) || transformed;
150
+ }
151
+ }
152
+
153
+ return transformed;
154
+ }
155
+
156
+ function walkSelectImpl(select: Select): boolean {
157
+ let transformed = false;
158
+
159
+ if (select.with) {
160
+ for (const cte of select.with) {
161
+ const cteSelect = cte.stmt?.ast ?? cte.stmt;
162
+ if (cteSelect && cteSelect.type === 'select') {
163
+ transformed = walkSelectImpl(cteSelect as Select) || transformed;
164
+ }
165
+ }
166
+ }
167
+
168
+ if (Array.isArray(select.from)) {
169
+ transformed = walkFrom(select.from) || transformed;
170
+ }
171
+
172
+ transformed = walkExpression(select.where, select, 'where') || transformed;
173
+
174
+ if (select.having) {
175
+ if (Array.isArray(select.having)) {
176
+ for (let i = 0; i < select.having.length; i++) {
177
+ transformed =
178
+ walkExpression(select.having[i], select.having, String(i)) ||
179
+ transformed;
180
+ }
181
+ } else {
182
+ transformed =
183
+ walkExpression(select.having as ExpressionValue, select, 'having') ||
184
+ transformed;
185
+ }
186
+ }
187
+
188
+ if (Array.isArray(select.columns)) {
189
+ for (const col of select.columns) {
190
+ if ('expr' in col) {
191
+ transformed = walkExpression(col.expr, col, 'expr') || transformed;
192
+ }
193
+ }
194
+ }
195
+
196
+ if (select._next) {
197
+ transformed = walkSelectImpl(select._next) || transformed;
198
+ }
199
+
200
+ return transformed;
201
+ }
202
+
203
+ export function transformArrayOperators(ast: AST | AST[]): boolean {
204
+ const statements = Array.isArray(ast) ? ast : [ast];
205
+ let transformed = false;
206
+
207
+ for (const stmt of statements) {
208
+ if (stmt.type === 'select') {
209
+ transformed = walkSelectImpl(stmt as Select) || transformed;
210
+ }
211
+ }
212
+
213
+ return transformed;
214
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * AST visitor to qualify unqualified column references in JOIN ON clauses.
3
+ */
4
+
5
+ import type {
6
+ AST,
7
+ Binary,
8
+ ColumnRefItem,
9
+ ExpressionValue,
10
+ Select,
11
+ From,
12
+ Join,
13
+ OrderBy,
14
+ Column,
15
+ } from 'node-sql-parser';
16
+
17
+ type TableSource = {
18
+ name: string;
19
+ alias: string | null;
20
+ };
21
+
22
+ function getTableSource(from: From): TableSource | null {
23
+ if ('table' in from && from.table) {
24
+ return {
25
+ name: from.table,
26
+ alias: from.as ?? null,
27
+ };
28
+ }
29
+ if ('expr' in from && from.as) {
30
+ return {
31
+ name: from.as,
32
+ alias: from.as,
33
+ };
34
+ }
35
+ return null;
36
+ }
37
+
38
+ function getQualifier(source: TableSource): string {
39
+ return source.alias ?? source.name;
40
+ }
41
+
42
+ function isUnqualifiedColumnRef(expr: ExpressionValue): expr is ColumnRefItem {
43
+ return (
44
+ typeof expr === 'object' &&
45
+ expr !== null &&
46
+ 'type' in expr &&
47
+ expr.type === 'column_ref' &&
48
+ !('table' in expr && expr.table)
49
+ );
50
+ }
51
+
52
+ function getColumnName(col: ColumnRefItem): string | null {
53
+ if (typeof col.column === 'string') {
54
+ return col.column;
55
+ }
56
+ if (col.column && 'expr' in col.column && col.column.expr?.value) {
57
+ return String(col.column.expr.value);
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function walkOnClause(
63
+ expr: Binary | null | undefined,
64
+ leftSource: string,
65
+ rightSource: string,
66
+ ambiguousColumns: Set<string>
67
+ ): boolean {
68
+ if (!expr || typeof expr !== 'object') return false;
69
+
70
+ let transformed = false;
71
+
72
+ if (expr.type === 'binary_expr') {
73
+ if (expr.operator === '=') {
74
+ const left = expr.left as ExpressionValue;
75
+ const right = expr.right as ExpressionValue;
76
+
77
+ if (isUnqualifiedColumnRef(left) && isUnqualifiedColumnRef(right)) {
78
+ const leftColName = getColumnName(left);
79
+ const rightColName = getColumnName(right);
80
+
81
+ if (leftColName && rightColName && leftColName === rightColName) {
82
+ left.table = leftSource;
83
+ right.table = rightSource;
84
+
85
+ ambiguousColumns.add(leftColName);
86
+ transformed = true;
87
+ }
88
+ }
89
+ }
90
+
91
+ if (expr.operator === 'AND' || expr.operator === 'OR') {
92
+ transformed =
93
+ walkOnClause(
94
+ expr.left as Binary,
95
+ leftSource,
96
+ rightSource,
97
+ ambiguousColumns
98
+ ) || transformed;
99
+ transformed =
100
+ walkOnClause(
101
+ expr.right as Binary,
102
+ leftSource,
103
+ rightSource,
104
+ ambiguousColumns
105
+ ) || transformed;
106
+ }
107
+ }
108
+
109
+ return transformed;
110
+ }
111
+
112
+ function qualifyAmbiguousInExpression(
113
+ expr: ExpressionValue | null | undefined,
114
+ defaultQualifier: string,
115
+ ambiguousColumns: Set<string>
116
+ ): boolean {
117
+ if (!expr || typeof expr !== 'object') return false;
118
+
119
+ let transformed = false;
120
+
121
+ if (isUnqualifiedColumnRef(expr)) {
122
+ const colName = getColumnName(expr);
123
+ if (colName && ambiguousColumns.has(colName)) {
124
+ expr.table = defaultQualifier;
125
+ transformed = true;
126
+ }
127
+ return transformed;
128
+ }
129
+
130
+ if ('type' in expr && expr.type === 'binary_expr') {
131
+ const binary = expr as Binary;
132
+ transformed =
133
+ qualifyAmbiguousInExpression(
134
+ binary.left as ExpressionValue,
135
+ defaultQualifier,
136
+ ambiguousColumns
137
+ ) || transformed;
138
+ transformed =
139
+ qualifyAmbiguousInExpression(
140
+ binary.right as ExpressionValue,
141
+ defaultQualifier,
142
+ ambiguousColumns
143
+ ) || transformed;
144
+ return transformed;
145
+ }
146
+
147
+ if ('args' in expr && expr.args) {
148
+ const args = expr.args as {
149
+ value?: ExpressionValue[];
150
+ expr?: ExpressionValue;
151
+ };
152
+ if (args.value && Array.isArray(args.value)) {
153
+ for (const arg of args.value) {
154
+ transformed =
155
+ qualifyAmbiguousInExpression(
156
+ arg,
157
+ defaultQualifier,
158
+ ambiguousColumns
159
+ ) || transformed;
160
+ }
161
+ }
162
+ if (args.expr) {
163
+ transformed =
164
+ qualifyAmbiguousInExpression(
165
+ args.expr,
166
+ defaultQualifier,
167
+ ambiguousColumns
168
+ ) || transformed;
169
+ }
170
+ }
171
+
172
+ return transformed;
173
+ }
174
+
175
+ function walkSelect(select: Select): boolean {
176
+ let transformed = false;
177
+ const ambiguousColumns = new Set<string>();
178
+
179
+ if (Array.isArray(select.from) && select.from.length >= 2) {
180
+ const firstSource = getTableSource(select.from[0]);
181
+ const defaultQualifier = firstSource ? getQualifier(firstSource) : '';
182
+ let prevSource = firstSource;
183
+
184
+ for (const from of select.from) {
185
+ if ('join' in from) {
186
+ const join = from as Join;
187
+ const currentSource = getTableSource(join);
188
+
189
+ if (join.on && prevSource && currentSource) {
190
+ const leftQualifier = getQualifier(prevSource);
191
+ const rightQualifier = getQualifier(currentSource);
192
+
193
+ transformed =
194
+ walkOnClause(
195
+ join.on,
196
+ leftQualifier,
197
+ rightQualifier,
198
+ ambiguousColumns
199
+ ) || transformed;
200
+ }
201
+
202
+ prevSource = currentSource;
203
+ } else {
204
+ const source = getTableSource(from);
205
+ if (source) {
206
+ prevSource = source;
207
+ }
208
+ }
209
+
210
+ if ('expr' in from && from.expr && 'ast' in from.expr) {
211
+ transformed = walkSelect(from.expr.ast) || transformed;
212
+ }
213
+ }
214
+
215
+ if (ambiguousColumns.size > 0 && defaultQualifier) {
216
+ if (Array.isArray(select.columns)) {
217
+ for (const col of select.columns as Column[]) {
218
+ if ('expr' in col) {
219
+ transformed =
220
+ qualifyAmbiguousInExpression(
221
+ col.expr,
222
+ defaultQualifier,
223
+ ambiguousColumns
224
+ ) || transformed;
225
+ }
226
+ }
227
+ }
228
+
229
+ transformed =
230
+ qualifyAmbiguousInExpression(
231
+ select.where,
232
+ defaultQualifier,
233
+ ambiguousColumns
234
+ ) || transformed;
235
+
236
+ if (Array.isArray(select.orderby)) {
237
+ for (const order of select.orderby as OrderBy[]) {
238
+ if (order.expr) {
239
+ transformed =
240
+ qualifyAmbiguousInExpression(
241
+ order.expr,
242
+ defaultQualifier,
243
+ ambiguousColumns
244
+ ) || transformed;
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+
251
+ if (select.with) {
252
+ for (const cte of select.with) {
253
+ const cteSelect = cte.stmt?.ast ?? cte.stmt;
254
+ if (cteSelect && cteSelect.type === 'select') {
255
+ transformed = walkSelect(cteSelect as Select) || transformed;
256
+ }
257
+ }
258
+ }
259
+
260
+ if (select._next) {
261
+ transformed = walkSelect(select._next) || transformed;
262
+ }
263
+
264
+ return transformed;
265
+ }
266
+
267
+ export function qualifyJoinColumns(ast: AST | AST[]): boolean {
268
+ const statements = Array.isArray(ast) ? ast : [ast];
269
+ let transformed = false;
270
+
271
+ for (const stmt of statements) {
272
+ if (stmt.type === 'select') {
273
+ transformed = walkSelect(stmt as Select) || transformed;
274
+ }
275
+ }
276
+
277
+ return transformed;
278
+ }
package/src/utils.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  export { aliasFields } from './sql/selection.ts';
2
- export { adaptArrayOperators } from './sql/query-rewriters.ts';
3
2
  export { mapResultRow } from './sql/result-mapper.ts';
@@ -1,14 +0,0 @@
1
- export declare function scrubForRewrite(query: string): string;
2
- export declare function adaptArrayOperators(query: string): string;
3
- /**
4
- * Qualifies unqualified column references in JOIN ON clauses.
5
- *
6
- * Transforms patterns like:
7
- * `left join "b" on "col" = "col"`
8
- * To:
9
- * `left join "b" on "a"."col" = "b"."col"`
10
- *
11
- * This fixes the issue where drizzle-orm generates unqualified column
12
- * references when joining CTEs with eq().
13
- */
14
- export declare function qualifyJoinColumns(query: string): string;