@leonardovida-md/drizzle-neo-duckdb 1.2.0 → 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.
@@ -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,119 @@ 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;
106
- }
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;
107
202
  }
108
203
 
109
204
  return transformed;
@@ -111,7 +206,7 @@ function walkOnClause(
111
206
 
112
207
  function qualifyAmbiguousInExpression(
113
208
  expr: ExpressionValue | null | undefined,
114
- defaultQualifier: string,
209
+ defaultQualifier: Qualifier,
115
210
  ambiguousColumns: Set<string>
116
211
  ): boolean {
117
212
  if (!expr || typeof expr !== 'object') return false;
@@ -121,13 +216,13 @@ function qualifyAmbiguousInExpression(
121
216
  if (isUnqualifiedColumnRef(expr)) {
122
217
  const colName = getColumnName(expr);
123
218
  if (colName && ambiguousColumns.has(colName)) {
124
- expr.table = defaultQualifier;
219
+ applyQualifier(expr, defaultQualifier);
125
220
  transformed = true;
126
221
  }
127
222
  return transformed;
128
223
  }
129
224
 
130
- if ('type' in expr && expr.type === 'binary_expr') {
225
+ if (isBinaryExpr(expr)) {
131
226
  const binary = expr as Binary;
132
227
  transformed =
133
228
  qualifyAmbiguousInExpression(
@@ -169,79 +264,186 @@ function qualifyAmbiguousInExpression(
169
264
  }
170
265
  }
171
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
+
172
294
  return transformed;
173
295
  }
174
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
+
175
346
  function walkSelect(select: Select): boolean {
176
347
  let transformed = false;
177
348
  const ambiguousColumns = new Set<string>();
178
349
 
179
350
  if (Array.isArray(select.from) && select.from.length >= 2) {
180
351
  const firstSource = getTableSource(select.from[0]);
181
- const defaultQualifier = firstSource ? getQualifier(firstSource) : '';
352
+ const defaultQualifier = firstSource ? getQualifier(firstSource) : null;
182
353
  let prevSource = firstSource;
183
354
 
355
+ let hasAnyUnqualified = false;
184
356
  for (const from of select.from) {
185
357
  if ('join' in from) {
186
358
  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;
359
+ if (join.on && hasUnqualifiedColumns(join.on)) {
360
+ hasAnyUnqualified = true;
361
+ break;
200
362
  }
363
+ }
364
+ }
201
365
 
202
- prevSource = currentSource;
203
- } else {
204
- const source = getTableSource(from);
205
- if (source) {
206
- prevSource = source;
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;
207
370
  }
208
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);
209
377
 
210
- if ('expr' in from && from.expr && 'ast' in from.expr) {
211
- transformed = walkSelect(from.expr.ast) || transformed;
212
- }
213
- }
378
+ if (join.on && prevSource && currentSource) {
379
+ const leftQualifier = getQualifier(prevSource);
380
+ const rightQualifier = getQualifier(currentSource);
214
381
 
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
382
  transformed =
220
- qualifyAmbiguousInExpression(
221
- col.expr,
222
- defaultQualifier,
383
+ walkOnClause(
384
+ join.on,
385
+ leftQualifier,
386
+ rightQualifier,
223
387
  ambiguousColumns
224
388
  ) || transformed;
225
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;
226
413
  }
227
414
  }
228
415
 
229
- transformed =
230
- qualifyAmbiguousInExpression(
231
- select.where,
232
- defaultQualifier,
233
- ambiguousColumns
234
- ) || transformed;
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
+ }
235
429
 
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;
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
+ }
245
447
  }
246
448
  }
247
449
  }
@@ -271,6 +473,91 @@ export function qualifyJoinColumns(ast: AST | AST[]): boolean {
271
473
  for (const stmt of statements) {
272
474
  if (stmt.type === 'select') {
273
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
+ }
274
561
  }
275
562
  }
276
563