@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.
@@ -0,0 +1,565 @@
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
+
10
+ import type {
11
+ AST,
12
+ Binary,
13
+ ColumnRefItem,
14
+ ExpressionValue,
15
+ Select,
16
+ From,
17
+ Join,
18
+ OrderBy,
19
+ Column,
20
+ } from 'node-sql-parser';
21
+
22
+ type TableSource = {
23
+ name: string;
24
+ alias: string | null;
25
+ schema: string | null;
26
+ };
27
+
28
+ type Qualifier = {
29
+ table: string;
30
+ schema: string | null;
31
+ };
32
+
33
+ function getTableSource(from: From): TableSource | null {
34
+ if ('table' in from && from.table) {
35
+ return {
36
+ name: from.table,
37
+ alias: from.as ?? null,
38
+ schema: 'db' in from ? (from.db ?? null) : null,
39
+ };
40
+ }
41
+ if ('expr' in from && from.as) {
42
+ return {
43
+ name: from.as,
44
+ alias: from.as,
45
+ schema: null,
46
+ };
47
+ }
48
+ return null;
49
+ }
50
+
51
+ function getQualifier(source: TableSource): Qualifier {
52
+ return {
53
+ table: source.alias ?? source.name,
54
+ schema: source.schema,
55
+ };
56
+ }
57
+
58
+ function isUnqualifiedColumnRef(expr: ExpressionValue): expr is ColumnRefItem {
59
+ return (
60
+ typeof expr === 'object' &&
61
+ expr !== null &&
62
+ 'type' in expr &&
63
+ expr.type === 'column_ref' &&
64
+ (!('table' in expr) || !expr.table)
65
+ );
66
+ }
67
+
68
+ function isQualifiedColumnRef(expr: ExpressionValue): expr is ColumnRefItem {
69
+ return (
70
+ typeof expr === 'object' &&
71
+ expr !== null &&
72
+ 'type' in expr &&
73
+ expr.type === 'column_ref' &&
74
+ 'table' in expr &&
75
+ !!expr.table
76
+ );
77
+ }
78
+
79
+ function getColumnName(col: ColumnRefItem): string | null {
80
+ if (typeof col.column === 'string') {
81
+ return col.column;
82
+ }
83
+ if (col.column && 'expr' in col.column && col.column.expr?.value) {
84
+ return String(col.column.expr.value);
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function applyQualifier(col: ColumnRefItem, qualifier: Qualifier): void {
90
+ col.table = qualifier.table;
91
+ if (!('schema' in col) || !col.schema) {
92
+ (col as ColumnRefItem & { schema?: string | null }).schema =
93
+ qualifier.schema;
94
+ }
95
+ }
96
+
97
+ function unwrapColumnRef(
98
+ expr: ExpressionValue | undefined
99
+ ): ColumnRefItem | null {
100
+ if (!expr || typeof expr !== 'object') return null;
101
+ if ('type' in expr && expr.type === 'column_ref') {
102
+ return expr as ColumnRefItem;
103
+ }
104
+ if ('expr' in expr && expr.expr) {
105
+ return unwrapColumnRef(expr.expr as ExpressionValue);
106
+ }
107
+ if ('ast' in expr && expr.ast && typeof expr.ast === 'object') {
108
+ return null;
109
+ }
110
+ if ('args' in expr && expr.args) {
111
+ const args = expr.args as {
112
+ value?: ExpressionValue[];
113
+ expr?: ExpressionValue;
114
+ };
115
+ if (args.expr) {
116
+ return unwrapColumnRef(args.expr as ExpressionValue);
117
+ }
118
+ if (args.value && args.value.length === 1) {
119
+ return unwrapColumnRef(args.value[0] as ExpressionValue);
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+
125
+ function isBinaryExpr(
126
+ expr: ExpressionValue | Binary | null | undefined
127
+ ): expr is Binary {
128
+ return (
129
+ !!expr &&
130
+ typeof expr === 'object' &&
131
+ 'type' in expr &&
132
+ (expr as { type?: string }).type === 'binary_expr'
133
+ );
134
+ }
135
+
136
+ function walkOnClause(
137
+ expr: Binary | ExpressionValue | null | undefined,
138
+ leftQualifier: Qualifier,
139
+ rightQualifier: Qualifier,
140
+ ambiguousColumns: Set<string>
141
+ ): boolean {
142
+ if (!expr || typeof expr !== 'object') return false;
143
+
144
+ let transformed = false;
145
+
146
+ if (isBinaryExpr(expr)) {
147
+ const left = expr.left as ExpressionValue;
148
+ const right = expr.right as ExpressionValue;
149
+
150
+ const leftCol = unwrapColumnRef(left);
151
+ const rightCol = unwrapColumnRef(right);
152
+
153
+ const leftUnqualified = leftCol ? isUnqualifiedColumnRef(leftCol) : false;
154
+ const rightUnqualified = rightCol
155
+ ? isUnqualifiedColumnRef(rightCol)
156
+ : false;
157
+ const leftQualified = leftCol ? isQualifiedColumnRef(leftCol) : false;
158
+ const rightQualified = rightCol ? isQualifiedColumnRef(rightCol) : false;
159
+ const leftColName = leftCol ? getColumnName(leftCol) : null;
160
+ const rightColName = rightCol ? getColumnName(rightCol) : null;
161
+
162
+ if (
163
+ expr.operator === '=' &&
164
+ leftColName &&
165
+ rightColName &&
166
+ leftColName === rightColName
167
+ ) {
168
+ if (leftUnqualified && rightUnqualified) {
169
+ applyQualifier(leftCol!, leftQualifier);
170
+ applyQualifier(rightCol!, rightQualifier);
171
+ ambiguousColumns.add(leftColName);
172
+ transformed = true;
173
+ } else if (leftQualified && rightUnqualified) {
174
+ applyQualifier(rightCol!, rightQualifier);
175
+ ambiguousColumns.add(rightColName);
176
+ transformed = true;
177
+ } else if (leftUnqualified && rightQualified) {
178
+ applyQualifier(leftCol!, leftQualifier);
179
+ ambiguousColumns.add(leftColName);
180
+ transformed = true;
181
+ }
182
+ }
183
+
184
+ transformed =
185
+ walkOnClause(
186
+ isBinaryExpr(expr.left as Binary)
187
+ ? (expr.left as Binary)
188
+ : (expr.left as ExpressionValue),
189
+ leftQualifier,
190
+ rightQualifier,
191
+ ambiguousColumns
192
+ ) || transformed;
193
+ transformed =
194
+ walkOnClause(
195
+ isBinaryExpr(expr.right as Binary)
196
+ ? (expr.right as Binary)
197
+ : (expr.right as ExpressionValue),
198
+ leftQualifier,
199
+ rightQualifier,
200
+ ambiguousColumns
201
+ ) || transformed;
202
+ }
203
+
204
+ return transformed;
205
+ }
206
+
207
+ function qualifyAmbiguousInExpression(
208
+ expr: ExpressionValue | null | undefined,
209
+ defaultQualifier: Qualifier,
210
+ ambiguousColumns: Set<string>
211
+ ): boolean {
212
+ if (!expr || typeof expr !== 'object') return false;
213
+
214
+ let transformed = false;
215
+
216
+ if (isUnqualifiedColumnRef(expr)) {
217
+ const colName = getColumnName(expr);
218
+ if (colName && ambiguousColumns.has(colName)) {
219
+ applyQualifier(expr, defaultQualifier);
220
+ transformed = true;
221
+ }
222
+ return transformed;
223
+ }
224
+
225
+ if (isBinaryExpr(expr)) {
226
+ const binary = expr as Binary;
227
+ transformed =
228
+ qualifyAmbiguousInExpression(
229
+ binary.left as ExpressionValue,
230
+ defaultQualifier,
231
+ ambiguousColumns
232
+ ) || transformed;
233
+ transformed =
234
+ qualifyAmbiguousInExpression(
235
+ binary.right as ExpressionValue,
236
+ defaultQualifier,
237
+ ambiguousColumns
238
+ ) || transformed;
239
+ return transformed;
240
+ }
241
+
242
+ if ('args' in expr && expr.args) {
243
+ const args = expr.args as {
244
+ value?: ExpressionValue[];
245
+ expr?: ExpressionValue;
246
+ };
247
+ if (args.value && Array.isArray(args.value)) {
248
+ for (const arg of args.value) {
249
+ transformed =
250
+ qualifyAmbiguousInExpression(
251
+ arg,
252
+ defaultQualifier,
253
+ ambiguousColumns
254
+ ) || transformed;
255
+ }
256
+ }
257
+ if (args.expr) {
258
+ transformed =
259
+ qualifyAmbiguousInExpression(
260
+ args.expr,
261
+ defaultQualifier,
262
+ ambiguousColumns
263
+ ) || transformed;
264
+ }
265
+ }
266
+
267
+ if ('over' in expr && expr.over && typeof expr.over === 'object') {
268
+ const over = expr.over as {
269
+ partition?: ExpressionValue[];
270
+ orderby?: ExpressionValue[];
271
+ };
272
+ if (Array.isArray(over.partition)) {
273
+ for (const part of over.partition) {
274
+ transformed =
275
+ qualifyAmbiguousInExpression(
276
+ part,
277
+ defaultQualifier,
278
+ ambiguousColumns
279
+ ) || transformed;
280
+ }
281
+ }
282
+ if (Array.isArray(over.orderby)) {
283
+ for (const order of over.orderby) {
284
+ transformed =
285
+ qualifyAmbiguousInExpression(
286
+ order,
287
+ defaultQualifier,
288
+ ambiguousColumns
289
+ ) || transformed;
290
+ }
291
+ }
292
+ }
293
+
294
+ return transformed;
295
+ }
296
+
297
+ /**
298
+ * Quick check if an ON clause has any unqualified column references.
299
+ * Used for early exit optimization.
300
+ */
301
+ function hasUnqualifiedColumns(expr: Binary | null | undefined): boolean {
302
+ if (!expr || typeof expr !== 'object') return false;
303
+
304
+ if ('type' in expr && expr.type === 'binary_expr') {
305
+ const left = expr.left as ExpressionValue;
306
+ const right = expr.right as ExpressionValue;
307
+ const leftCol = unwrapColumnRef(left);
308
+ const rightCol = unwrapColumnRef(right);
309
+ if (
310
+ isUnqualifiedColumnRef(left) ||
311
+ isUnqualifiedColumnRef(right) ||
312
+ (leftCol && isUnqualifiedColumnRef(leftCol)) ||
313
+ (rightCol && isUnqualifiedColumnRef(rightCol))
314
+ ) {
315
+ return true;
316
+ }
317
+ if (
318
+ isBinaryExpr(expr.left as Binary) &&
319
+ hasUnqualifiedColumns(expr.left as Binary)
320
+ )
321
+ return true;
322
+ if (
323
+ isBinaryExpr(expr.right as Binary) &&
324
+ hasUnqualifiedColumns(expr.right as Binary)
325
+ )
326
+ return true;
327
+ }
328
+
329
+ if ('args' in expr && expr.args) {
330
+ const args = expr.args as {
331
+ value?: ExpressionValue[];
332
+ expr?: ExpressionValue;
333
+ };
334
+ if (args.expr && isUnqualifiedColumnRef(args.expr as ExpressionValue))
335
+ return true;
336
+ if (args.value) {
337
+ for (const arg of args.value) {
338
+ if (isUnqualifiedColumnRef(arg)) return true;
339
+ }
340
+ }
341
+ }
342
+
343
+ return false;
344
+ }
345
+
346
+ function walkSelect(select: Select): boolean {
347
+ let transformed = false;
348
+ const ambiguousColumns = new Set<string>();
349
+
350
+ if (Array.isArray(select.from) && select.from.length >= 2) {
351
+ const firstSource = getTableSource(select.from[0]);
352
+ const defaultQualifier = firstSource ? getQualifier(firstSource) : null;
353
+ let prevSource = firstSource;
354
+
355
+ let hasAnyUnqualified = false;
356
+ for (const from of select.from) {
357
+ if ('join' in from) {
358
+ const join = from as Join;
359
+ if (join.on && hasUnqualifiedColumns(join.on)) {
360
+ hasAnyUnqualified = true;
361
+ break;
362
+ }
363
+ }
364
+ }
365
+
366
+ if (!hasAnyUnqualified) {
367
+ for (const from of select.from) {
368
+ if ('expr' in from && from.expr && 'ast' in from.expr) {
369
+ transformed = walkSelect(from.expr.ast) || transformed;
370
+ }
371
+ }
372
+ } else {
373
+ for (const from of select.from) {
374
+ if ('join' in from) {
375
+ const join = from as Join;
376
+ const currentSource = getTableSource(join);
377
+
378
+ if (join.on && prevSource && currentSource) {
379
+ const leftQualifier = getQualifier(prevSource);
380
+ const rightQualifier = getQualifier(currentSource);
381
+
382
+ transformed =
383
+ walkOnClause(
384
+ join.on,
385
+ leftQualifier,
386
+ rightQualifier,
387
+ ambiguousColumns
388
+ ) || transformed;
389
+ }
390
+
391
+ if (join.using && prevSource && currentSource) {
392
+ for (const usingCol of join.using) {
393
+ if (typeof usingCol === 'string') {
394
+ ambiguousColumns.add(usingCol);
395
+ } else if ('value' in usingCol) {
396
+ ambiguousColumns.add(
397
+ String((usingCol as { value: unknown }).value)
398
+ );
399
+ }
400
+ }
401
+ }
402
+
403
+ prevSource = currentSource;
404
+ } else {
405
+ const source = getTableSource(from);
406
+ if (source) {
407
+ prevSource = source;
408
+ }
409
+ }
410
+
411
+ if ('expr' in from && from.expr && 'ast' in from.expr) {
412
+ transformed = walkSelect(from.expr.ast) || transformed;
413
+ }
414
+ }
415
+
416
+ if (ambiguousColumns.size > 0 && defaultQualifier) {
417
+ if (Array.isArray(select.columns)) {
418
+ for (const col of select.columns as Column[]) {
419
+ if ('expr' in col) {
420
+ transformed =
421
+ qualifyAmbiguousInExpression(
422
+ col.expr,
423
+ defaultQualifier,
424
+ ambiguousColumns
425
+ ) || transformed;
426
+ }
427
+ }
428
+ }
429
+
430
+ transformed =
431
+ qualifyAmbiguousInExpression(
432
+ select.where,
433
+ defaultQualifier,
434
+ ambiguousColumns
435
+ ) || transformed;
436
+
437
+ if (Array.isArray(select.orderby)) {
438
+ for (const order of select.orderby as OrderBy[]) {
439
+ if (order.expr) {
440
+ transformed =
441
+ qualifyAmbiguousInExpression(
442
+ order.expr,
443
+ defaultQualifier,
444
+ ambiguousColumns
445
+ ) || transformed;
446
+ }
447
+ }
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ if (select.with) {
454
+ for (const cte of select.with) {
455
+ const cteSelect = cte.stmt?.ast ?? cte.stmt;
456
+ if (cteSelect && cteSelect.type === 'select') {
457
+ transformed = walkSelect(cteSelect as Select) || transformed;
458
+ }
459
+ }
460
+ }
461
+
462
+ if (select._next) {
463
+ transformed = walkSelect(select._next) || transformed;
464
+ }
465
+
466
+ return transformed;
467
+ }
468
+
469
+ export function qualifyJoinColumns(ast: AST | AST[]): boolean {
470
+ const statements = Array.isArray(ast) ? ast : [ast];
471
+ let transformed = false;
472
+
473
+ for (const stmt of statements) {
474
+ if (stmt.type === 'select') {
475
+ transformed = walkSelect(stmt as Select) || transformed;
476
+ } else if (stmt.type === 'insert') {
477
+ const insert = stmt as unknown as { values?: unknown };
478
+ if (
479
+ insert.values &&
480
+ typeof insert.values === 'object' &&
481
+ 'type' in insert.values &&
482
+ (insert.values as { type: string }).type === 'select'
483
+ ) {
484
+ transformed =
485
+ walkSelect(insert.values as unknown as Select) || transformed;
486
+ }
487
+ } else if (stmt.type === 'update') {
488
+ const update = stmt as unknown as {
489
+ table?: From[];
490
+ from?: From[];
491
+ where?: ExpressionValue;
492
+ returning?: ExpressionValue | ExpressionValue[];
493
+ };
494
+ const mainSource = update.table?.[0]
495
+ ? getTableSource(update.table[0] as From)
496
+ : null;
497
+ const defaultQualifier = mainSource ? getQualifier(mainSource) : null;
498
+ const fromSources = update.from ?? [];
499
+ const firstFrom = fromSources[0] ? getTableSource(fromSources[0]) : null;
500
+ if (update.where && defaultQualifier && firstFrom) {
501
+ const ambiguous = new Set<string>();
502
+ transformed =
503
+ walkOnClause(
504
+ update.where as Binary,
505
+ defaultQualifier,
506
+ getQualifier(firstFrom),
507
+ ambiguous
508
+ ) || transformed;
509
+ transformed =
510
+ qualifyAmbiguousInExpression(
511
+ update.where,
512
+ defaultQualifier,
513
+ ambiguous
514
+ ) || transformed;
515
+ }
516
+ if (Array.isArray(update.returning) && defaultQualifier) {
517
+ for (const ret of update.returning) {
518
+ transformed =
519
+ qualifyAmbiguousInExpression(
520
+ ret,
521
+ defaultQualifier,
522
+ new Set<string>()
523
+ ) || transformed;
524
+ }
525
+ }
526
+ } else if (stmt.type === 'delete') {
527
+ const del = stmt as unknown as {
528
+ table?: From[];
529
+ from?: From[];
530
+ where?: ExpressionValue;
531
+ };
532
+ const mainSource = del.table?.[0]
533
+ ? getTableSource(del.table[0] as From)
534
+ : null;
535
+ const defaultQualifier = mainSource ? getQualifier(mainSource) : null;
536
+ const fromSources = del.from ?? [];
537
+ const firstFrom = fromSources[0] ? getTableSource(fromSources[0]) : null;
538
+ if (del.where && defaultQualifier && firstFrom) {
539
+ const ambiguous = new Set<string>();
540
+ transformed =
541
+ walkOnClause(
542
+ del.where as Binary,
543
+ defaultQualifier,
544
+ getQualifier(firstFrom),
545
+ ambiguous
546
+ ) || transformed;
547
+ transformed =
548
+ qualifyAmbiguousInExpression(
549
+ del.where,
550
+ defaultQualifier,
551
+ ambiguous
552
+ ) || transformed;
553
+ } else if (del.where && defaultQualifier) {
554
+ transformed =
555
+ qualifyAmbiguousInExpression(
556
+ del.where,
557
+ defaultQualifier,
558
+ new Set<string>()
559
+ ) || transformed;
560
+ }
561
+ }
562
+ }
563
+
564
+ return transformed;
565
+ }
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,15 +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, SELECT, WHERE,
5
- * and ORDER BY clauses.
6
- *
7
- * Transforms patterns like:
8
- * `select "col" from "a" left join "b" on "col" = "col" where "col" in (...)`
9
- * To:
10
- * `select "a"."col" from "a" left join "b" on "a"."col" = "b"."col" where "a"."col" in (...)`
11
- *
12
- * This fixes the issue where drizzle-orm generates unqualified column
13
- * references when joining CTEs with eq().
14
- */
15
- export declare function qualifyJoinColumns(query: string): string;