@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.
@@ -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
 
5
10
  import type {
@@ -17,6 +22,12 @@ import type {
17
22
  type TableSource = {
18
23
  name: string;
19
24
  alias: string | null;
25
+ schema: string | null;
26
+ };
27
+
28
+ type Qualifier = {
29
+ table: string;
30
+ schema: string | null;
20
31
  };
21
32
 
22
33
  function getTableSource(from: From): TableSource | null {
@@ -24,19 +35,24 @@ function getTableSource(from: From): TableSource | null {
24
35
  return {
25
36
  name: from.table,
26
37
  alias: from.as ?? null,
38
+ schema: 'db' in from ? (from.db ?? null) : null,
27
39
  };
28
40
  }
29
41
  if ('expr' in from && from.as) {
30
42
  return {
31
43
  name: from.as,
32
44
  alias: from.as,
45
+ schema: null,
33
46
  };
34
47
  }
35
48
  return null;
36
49
  }
37
50
 
38
- function getQualifier(source: TableSource): string {
39
- return source.alias ?? source.name;
51
+ function getQualifier(source: TableSource): Qualifier {
52
+ return {
53
+ table: source.alias ?? source.name,
54
+ schema: source.schema,
55
+ };
40
56
  }
41
57
 
42
58
  function isUnqualifiedColumnRef(expr: ExpressionValue): expr is ColumnRefItem {
@@ -45,7 +61,18 @@ function isUnqualifiedColumnRef(expr: ExpressionValue): expr is ColumnRefItem {
45
61
  expr !== null &&
46
62
  'type' in expr &&
47
63
  expr.type === 'column_ref' &&
48
- !('table' in expr && expr.table)
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
49
76
  );
50
77
  }
51
78
 
@@ -59,51 +86,140 @@ function getColumnName(col: ColumnRefItem): string | null {
59
86
  return null;
60
87
  }
61
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
+
62
136
  function walkOnClause(
63
- expr: Binary | null | undefined,
64
- leftSource: string,
65
- rightSource: string,
137
+ expr: Binary | ExpressionValue | null | undefined,
138
+ leftQualifier: Qualifier,
139
+ rightQualifier: Qualifier,
66
140
  ambiguousColumns: Set<string>
67
141
  ): boolean {
68
142
  if (!expr || typeof expr !== 'object') return false;
69
143
 
70
144
  let transformed = false;
71
145
 
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
- }
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;
88
181
  }
89
182
  }
90
183
 
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;
184
+ if (
185
+ expr.operator === '=' &&
186
+ leftCol &&
187
+ rightCol &&
188
+ leftColName &&
189
+ rightColName &&
190
+ leftColName !== rightColName
191
+ ) {
192
+ if (leftQualified && rightUnqualified && !rightColName.includes('.')) {
193
+ applyQualifier(rightCol, rightQualifier);
194
+ transformed = true;
195
+ } else if (
196
+ leftUnqualified &&
197
+ rightQualified &&
198
+ !leftColName.includes('.')
199
+ ) {
200
+ applyQualifier(leftCol, leftQualifier);
201
+ transformed = true;
202
+ }
106
203
  }
204
+
205
+ transformed =
206
+ walkOnClause(
207
+ isBinaryExpr(expr.left as Binary)
208
+ ? (expr.left as Binary)
209
+ : (expr.left as ExpressionValue),
210
+ leftQualifier,
211
+ rightQualifier,
212
+ ambiguousColumns
213
+ ) || transformed;
214
+ transformed =
215
+ walkOnClause(
216
+ isBinaryExpr(expr.right as Binary)
217
+ ? (expr.right as Binary)
218
+ : (expr.right as ExpressionValue),
219
+ leftQualifier,
220
+ rightQualifier,
221
+ ambiguousColumns
222
+ ) || transformed;
107
223
  }
108
224
 
109
225
  return transformed;
@@ -111,7 +227,7 @@ function walkOnClause(
111
227
 
112
228
  function qualifyAmbiguousInExpression(
113
229
  expr: ExpressionValue | null | undefined,
114
- defaultQualifier: string,
230
+ defaultQualifier: Qualifier,
115
231
  ambiguousColumns: Set<string>
116
232
  ): boolean {
117
233
  if (!expr || typeof expr !== 'object') return false;
@@ -121,13 +237,13 @@ function qualifyAmbiguousInExpression(
121
237
  if (isUnqualifiedColumnRef(expr)) {
122
238
  const colName = getColumnName(expr);
123
239
  if (colName && ambiguousColumns.has(colName)) {
124
- expr.table = defaultQualifier;
240
+ applyQualifier(expr, defaultQualifier);
125
241
  transformed = true;
126
242
  }
127
243
  return transformed;
128
244
  }
129
245
 
130
- if ('type' in expr && expr.type === 'binary_expr') {
246
+ if (isBinaryExpr(expr)) {
131
247
  const binary = expr as Binary;
132
248
  transformed =
133
249
  qualifyAmbiguousInExpression(
@@ -169,79 +285,186 @@ function qualifyAmbiguousInExpression(
169
285
  }
170
286
  }
171
287
 
288
+ if ('over' in expr && expr.over && typeof expr.over === 'object') {
289
+ const over = expr.over as {
290
+ partition?: ExpressionValue[];
291
+ orderby?: ExpressionValue[];
292
+ };
293
+ if (Array.isArray(over.partition)) {
294
+ for (const part of over.partition) {
295
+ transformed =
296
+ qualifyAmbiguousInExpression(
297
+ part,
298
+ defaultQualifier,
299
+ ambiguousColumns
300
+ ) || transformed;
301
+ }
302
+ }
303
+ if (Array.isArray(over.orderby)) {
304
+ for (const order of over.orderby) {
305
+ transformed =
306
+ qualifyAmbiguousInExpression(
307
+ order,
308
+ defaultQualifier,
309
+ ambiguousColumns
310
+ ) || transformed;
311
+ }
312
+ }
313
+ }
314
+
172
315
  return transformed;
173
316
  }
174
317
 
318
+ /**
319
+ * Quick check if an ON clause has any unqualified column references.
320
+ * Used for early exit optimization.
321
+ */
322
+ function hasUnqualifiedColumns(expr: Binary | null | undefined): boolean {
323
+ if (!expr || typeof expr !== 'object') return false;
324
+
325
+ if ('type' in expr && expr.type === 'binary_expr') {
326
+ const left = expr.left as ExpressionValue;
327
+ const right = expr.right as ExpressionValue;
328
+ const leftCol = unwrapColumnRef(left);
329
+ const rightCol = unwrapColumnRef(right);
330
+ if (
331
+ isUnqualifiedColumnRef(left) ||
332
+ isUnqualifiedColumnRef(right) ||
333
+ (leftCol && isUnqualifiedColumnRef(leftCol)) ||
334
+ (rightCol && isUnqualifiedColumnRef(rightCol))
335
+ ) {
336
+ return true;
337
+ }
338
+ if (
339
+ isBinaryExpr(expr.left as Binary) &&
340
+ hasUnqualifiedColumns(expr.left as Binary)
341
+ )
342
+ return true;
343
+ if (
344
+ isBinaryExpr(expr.right as Binary) &&
345
+ hasUnqualifiedColumns(expr.right as Binary)
346
+ )
347
+ return true;
348
+ }
349
+
350
+ if ('args' in expr && expr.args) {
351
+ const args = expr.args as {
352
+ value?: ExpressionValue[];
353
+ expr?: ExpressionValue;
354
+ };
355
+ if (args.expr && isUnqualifiedColumnRef(args.expr as ExpressionValue))
356
+ return true;
357
+ if (args.value) {
358
+ for (const arg of args.value) {
359
+ if (isUnqualifiedColumnRef(arg)) return true;
360
+ }
361
+ }
362
+ }
363
+
364
+ return false;
365
+ }
366
+
175
367
  function walkSelect(select: Select): boolean {
176
368
  let transformed = false;
177
369
  const ambiguousColumns = new Set<string>();
178
370
 
179
371
  if (Array.isArray(select.from) && select.from.length >= 2) {
180
372
  const firstSource = getTableSource(select.from[0]);
181
- const defaultQualifier = firstSource ? getQualifier(firstSource) : '';
373
+ const defaultQualifier = firstSource ? getQualifier(firstSource) : null;
182
374
  let prevSource = firstSource;
183
375
 
376
+ let hasAnyUnqualified = false;
184
377
  for (const from of select.from) {
185
378
  if ('join' in from) {
186
379
  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;
380
+ if (join.on && hasUnqualifiedColumns(join.on)) {
381
+ hasAnyUnqualified = true;
382
+ break;
200
383
  }
384
+ }
385
+ }
201
386
 
202
- prevSource = currentSource;
203
- } else {
204
- const source = getTableSource(from);
205
- if (source) {
206
- prevSource = source;
387
+ if (!hasAnyUnqualified) {
388
+ for (const from of select.from) {
389
+ if ('expr' in from && from.expr && 'ast' in from.expr) {
390
+ transformed = walkSelect(from.expr.ast) || transformed;
207
391
  }
208
392
  }
393
+ } else {
394
+ for (const from of select.from) {
395
+ if ('join' in from) {
396
+ const join = from as Join;
397
+ const currentSource = getTableSource(join);
209
398
 
210
- if ('expr' in from && from.expr && 'ast' in from.expr) {
211
- transformed = walkSelect(from.expr.ast) || transformed;
212
- }
213
- }
399
+ if (join.on && prevSource && currentSource) {
400
+ const leftQualifier = getQualifier(prevSource);
401
+ const rightQualifier = getQualifier(currentSource);
214
402
 
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
403
  transformed =
220
- qualifyAmbiguousInExpression(
221
- col.expr,
222
- defaultQualifier,
404
+ walkOnClause(
405
+ join.on,
406
+ leftQualifier,
407
+ rightQualifier,
223
408
  ambiguousColumns
224
409
  ) || transformed;
225
410
  }
411
+
412
+ if (join.using && prevSource && currentSource) {
413
+ for (const usingCol of join.using) {
414
+ if (typeof usingCol === 'string') {
415
+ ambiguousColumns.add(usingCol);
416
+ } else if ('value' in usingCol) {
417
+ ambiguousColumns.add(
418
+ String((usingCol as { value: unknown }).value)
419
+ );
420
+ }
421
+ }
422
+ }
423
+
424
+ prevSource = currentSource;
425
+ } else {
426
+ const source = getTableSource(from);
427
+ if (source) {
428
+ prevSource = source;
429
+ }
430
+ }
431
+
432
+ if ('expr' in from && from.expr && 'ast' in from.expr) {
433
+ transformed = walkSelect(from.expr.ast) || transformed;
226
434
  }
227
435
  }
228
436
 
229
- transformed =
230
- qualifyAmbiguousInExpression(
231
- select.where,
232
- defaultQualifier,
233
- ambiguousColumns
234
- ) || transformed;
437
+ if (ambiguousColumns.size > 0 && defaultQualifier) {
438
+ if (Array.isArray(select.columns)) {
439
+ for (const col of select.columns as Column[]) {
440
+ if ('expr' in col) {
441
+ transformed =
442
+ qualifyAmbiguousInExpression(
443
+ col.expr,
444
+ defaultQualifier,
445
+ ambiguousColumns
446
+ ) || transformed;
447
+ }
448
+ }
449
+ }
235
450
 
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;
451
+ transformed =
452
+ qualifyAmbiguousInExpression(
453
+ select.where,
454
+ defaultQualifier,
455
+ ambiguousColumns
456
+ ) || transformed;
457
+
458
+ if (Array.isArray(select.orderby)) {
459
+ for (const order of select.orderby as OrderBy[]) {
460
+ if (order.expr) {
461
+ transformed =
462
+ qualifyAmbiguousInExpression(
463
+ order.expr,
464
+ defaultQualifier,
465
+ ambiguousColumns
466
+ ) || transformed;
467
+ }
245
468
  }
246
469
  }
247
470
  }
@@ -271,6 +494,91 @@ export function qualifyJoinColumns(ast: AST | AST[]): boolean {
271
494
  for (const stmt of statements) {
272
495
  if (stmt.type === 'select') {
273
496
  transformed = walkSelect(stmt as Select) || transformed;
497
+ } else if (stmt.type === 'insert') {
498
+ const insert = stmt as unknown as { values?: unknown };
499
+ if (
500
+ insert.values &&
501
+ typeof insert.values === 'object' &&
502
+ 'type' in insert.values &&
503
+ (insert.values as { type: string }).type === 'select'
504
+ ) {
505
+ transformed =
506
+ walkSelect(insert.values as unknown as Select) || transformed;
507
+ }
508
+ } else if (stmt.type === 'update') {
509
+ const update = stmt as unknown as {
510
+ table?: From[];
511
+ from?: From[];
512
+ where?: ExpressionValue;
513
+ returning?: ExpressionValue | ExpressionValue[];
514
+ };
515
+ const mainSource = update.table?.[0]
516
+ ? getTableSource(update.table[0] as From)
517
+ : null;
518
+ const defaultQualifier = mainSource ? getQualifier(mainSource) : null;
519
+ const fromSources = update.from ?? [];
520
+ const firstFrom = fromSources[0] ? getTableSource(fromSources[0]) : null;
521
+ if (update.where && defaultQualifier && firstFrom) {
522
+ const ambiguous = new Set<string>();
523
+ transformed =
524
+ walkOnClause(
525
+ update.where as Binary,
526
+ defaultQualifier,
527
+ getQualifier(firstFrom),
528
+ ambiguous
529
+ ) || transformed;
530
+ transformed =
531
+ qualifyAmbiguousInExpression(
532
+ update.where,
533
+ defaultQualifier,
534
+ ambiguous
535
+ ) || transformed;
536
+ }
537
+ if (Array.isArray(update.returning) && defaultQualifier) {
538
+ for (const ret of update.returning) {
539
+ transformed =
540
+ qualifyAmbiguousInExpression(
541
+ ret,
542
+ defaultQualifier,
543
+ new Set<string>()
544
+ ) || transformed;
545
+ }
546
+ }
547
+ } else if (stmt.type === 'delete') {
548
+ const del = stmt as unknown as {
549
+ table?: From[];
550
+ from?: From[];
551
+ where?: ExpressionValue;
552
+ };
553
+ const mainSource = del.table?.[0]
554
+ ? getTableSource(del.table[0] as From)
555
+ : null;
556
+ const defaultQualifier = mainSource ? getQualifier(mainSource) : null;
557
+ const fromSources = del.from ?? [];
558
+ const firstFrom = fromSources[0] ? getTableSource(fromSources[0]) : null;
559
+ if (del.where && defaultQualifier && firstFrom) {
560
+ const ambiguous = new Set<string>();
561
+ transformed =
562
+ walkOnClause(
563
+ del.where as Binary,
564
+ defaultQualifier,
565
+ getQualifier(firstFrom),
566
+ ambiguous
567
+ ) || transformed;
568
+ transformed =
569
+ qualifyAmbiguousInExpression(
570
+ del.where,
571
+ defaultQualifier,
572
+ ambiguous
573
+ ) || transformed;
574
+ } else if (del.where && defaultQualifier) {
575
+ transformed =
576
+ qualifyAmbiguousInExpression(
577
+ del.where,
578
+ defaultQualifier,
579
+ new Set<string>()
580
+ ) || transformed;
581
+ }
274
582
  }
275
583
  }
276
584