@leonardovida-md/drizzle-neo-duckdb 1.1.3 → 1.1.4
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.
- package/dist/duckdb-introspect.mjs +287 -11
- package/dist/helpers.mjs +10 -0
- package/dist/index.mjs +290 -11
- package/dist/sql/query-rewriters.d.ts +4 -3
- package/package.json +5 -2
- package/src/columns.ts +6 -1
- package/src/sql/query-rewriters.ts +481 -28
|
@@ -233,6 +233,7 @@ type JoinClause = {
|
|
|
233
233
|
/**
|
|
234
234
|
* Extracts the identifier name from a quoted identifier like "foo" -> foo
|
|
235
235
|
* Uses the original query string, not the scrubbed one.
|
|
236
|
+
* Handles escaped quotes: "col""name" -> col""name
|
|
236
237
|
*/
|
|
237
238
|
function extractQuotedIdentifier(
|
|
238
239
|
original: string,
|
|
@@ -243,11 +244,15 @@ function extractQuotedIdentifier(
|
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
let pos = start + 1;
|
|
246
|
-
while (pos < original.length
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
pos
|
|
250
|
-
|
|
247
|
+
while (pos < original.length) {
|
|
248
|
+
if (original[pos] === '"') {
|
|
249
|
+
// Check for escaped quote ""
|
|
250
|
+
if (original[pos + 1] === '"') {
|
|
251
|
+
pos += 2;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
// End of identifier
|
|
255
|
+
break;
|
|
251
256
|
}
|
|
252
257
|
pos++;
|
|
253
258
|
}
|
|
@@ -340,7 +345,8 @@ function parseTableSources(original: string, scrubbed: string): TableSource[] {
|
|
|
340
345
|
|
|
341
346
|
/**
|
|
342
347
|
* Parses a table reference (potentially with alias) starting at the given position.
|
|
343
|
-
* Handles: "table", "table" "alias", "table" as "alias", "schema"."table"
|
|
348
|
+
* Handles: "table", "table" "alias", "table" as "alias", "schema"."table",
|
|
349
|
+
* and subqueries: (SELECT ...) AS "alias"
|
|
344
350
|
*/
|
|
345
351
|
function parseTableRef(
|
|
346
352
|
original: string,
|
|
@@ -351,6 +357,48 @@ function parseTableRef(
|
|
|
351
357
|
while (pos < scrubbed.length && isWhitespace(scrubbed[pos])) {
|
|
352
358
|
pos++;
|
|
353
359
|
}
|
|
360
|
+
|
|
361
|
+
// Handle subquery: (SELECT ...) AS "alias"
|
|
362
|
+
if (scrubbed[pos] === '(') {
|
|
363
|
+
const nameStart = pos;
|
|
364
|
+
// Find matching closing parenthesis
|
|
365
|
+
let depth = 1;
|
|
366
|
+
pos++;
|
|
367
|
+
while (pos < scrubbed.length && depth > 0) {
|
|
368
|
+
if (scrubbed[pos] === '(') depth++;
|
|
369
|
+
else if (scrubbed[pos] === ')') depth--;
|
|
370
|
+
pos++;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Skip whitespace after subquery
|
|
374
|
+
while (pos < scrubbed.length && isWhitespace(scrubbed[pos])) {
|
|
375
|
+
pos++;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Look for AS keyword
|
|
379
|
+
const afterSubquery = scrubbed.slice(pos).toLowerCase();
|
|
380
|
+
if (afterSubquery.startsWith('as ')) {
|
|
381
|
+
pos += 3;
|
|
382
|
+
while (pos < scrubbed.length && isWhitespace(scrubbed[pos])) {
|
|
383
|
+
pos++;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Extract alias
|
|
388
|
+
if (original[pos] === '"') {
|
|
389
|
+
const aliasIdent = extractQuotedIdentifier(original, pos);
|
|
390
|
+
if (aliasIdent) {
|
|
391
|
+
return {
|
|
392
|
+
name: aliasIdent.name,
|
|
393
|
+
alias: aliasIdent.name,
|
|
394
|
+
position: nameStart,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
354
402
|
if (original[pos] !== '"') {
|
|
355
403
|
return null;
|
|
356
404
|
}
|
|
@@ -398,20 +446,21 @@ function parseTableRef(
|
|
|
398
446
|
}
|
|
399
447
|
}
|
|
400
448
|
|
|
449
|
+
// Check what comes after (potentially skipping the AS keyword)
|
|
450
|
+
const afterAlias = scrubbed.slice(aliasPos).toLowerCase();
|
|
401
451
|
if (
|
|
402
452
|
original[aliasPos] === '"' &&
|
|
403
|
-
!
|
|
404
|
-
!
|
|
405
|
-
!
|
|
406
|
-
!
|
|
407
|
-
!
|
|
408
|
-
!
|
|
409
|
-
!
|
|
410
|
-
!
|
|
411
|
-
!
|
|
412
|
-
!
|
|
413
|
-
!
|
|
414
|
-
!afterTable.startsWith('as ')
|
|
453
|
+
!afterAlias.startsWith('on ') &&
|
|
454
|
+
!afterAlias.startsWith('left ') &&
|
|
455
|
+
!afterAlias.startsWith('right ') &&
|
|
456
|
+
!afterAlias.startsWith('inner ') &&
|
|
457
|
+
!afterAlias.startsWith('full ') &&
|
|
458
|
+
!afterAlias.startsWith('cross ') &&
|
|
459
|
+
!afterAlias.startsWith('join ') &&
|
|
460
|
+
!afterAlias.startsWith('where ') &&
|
|
461
|
+
!afterAlias.startsWith('group ') &&
|
|
462
|
+
!afterAlias.startsWith('order ') &&
|
|
463
|
+
!afterAlias.startsWith('limit ')
|
|
415
464
|
) {
|
|
416
465
|
const aliasIdent = extractQuotedIdentifier(original, aliasPos);
|
|
417
466
|
if (aliasIdent) {
|
|
@@ -432,7 +481,8 @@ function parseTableRef(
|
|
|
432
481
|
function findJoinClauses(
|
|
433
482
|
original: string,
|
|
434
483
|
scrubbed: string,
|
|
435
|
-
sources: TableSource[]
|
|
484
|
+
sources: TableSource[],
|
|
485
|
+
fromPos: number
|
|
436
486
|
): JoinClause[] {
|
|
437
487
|
const clauses: JoinClause[] = [];
|
|
438
488
|
const lowerScrubbed = scrubbed.toLowerCase();
|
|
@@ -443,6 +493,9 @@ function findJoinClauses(
|
|
|
443
493
|
const joinPattern =
|
|
444
494
|
/\b(left\s+|right\s+|inner\s+|full\s+|cross\s+)?join\s+"[^"]*"(\s*\.\s*"[^"]*")?(\s+as)?(\s+"[^"]*")?\s+on\s+/gi;
|
|
445
495
|
|
|
496
|
+
// Start searching from the main FROM clause, skipping JOINs inside CTE definitions
|
|
497
|
+
joinPattern.lastIndex = fromPos;
|
|
498
|
+
|
|
446
499
|
let match;
|
|
447
500
|
let sourceIndex = 1; // Start at 1 since index 0 is the FROM table
|
|
448
501
|
|
|
@@ -532,12 +585,261 @@ function findJoinClauses(
|
|
|
532
585
|
}
|
|
533
586
|
|
|
534
587
|
/**
|
|
535
|
-
*
|
|
588
|
+
* Finds the boundaries of the main query's SELECT clause (after CTEs).
|
|
589
|
+
* Returns the start position (after "select ") and end position (before "from ").
|
|
590
|
+
*/
|
|
591
|
+
function findMainSelectClause(
|
|
592
|
+
scrubbed: string
|
|
593
|
+
): { start: number; end: number } | null {
|
|
594
|
+
const lowerScrubbed = scrubbed.toLowerCase();
|
|
595
|
+
|
|
596
|
+
// If there's a WITH clause, find where the CTEs end
|
|
597
|
+
let searchStart = 0;
|
|
598
|
+
const withMatch = /\bwith\s+/i.exec(lowerScrubbed);
|
|
599
|
+
if (withMatch) {
|
|
600
|
+
// Find the main SELECT after the CTEs
|
|
601
|
+
let depth = 0;
|
|
602
|
+
let pos = withMatch.index + withMatch[0].length;
|
|
603
|
+
|
|
604
|
+
while (pos < scrubbed.length) {
|
|
605
|
+
const char = scrubbed[pos];
|
|
606
|
+
if (char === '(') {
|
|
607
|
+
depth++;
|
|
608
|
+
} else if (char === ')') {
|
|
609
|
+
depth--;
|
|
610
|
+
} else if (depth === 0) {
|
|
611
|
+
const remaining = lowerScrubbed.slice(pos);
|
|
612
|
+
if (/^\s*select\s+/i.test(remaining)) {
|
|
613
|
+
searchStart = pos;
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
pos++;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Find SELECT keyword
|
|
622
|
+
const selectPattern = /\bselect\s+/gi;
|
|
623
|
+
selectPattern.lastIndex = searchStart;
|
|
624
|
+
const selectMatch = selectPattern.exec(lowerScrubbed);
|
|
625
|
+
if (!selectMatch) return null;
|
|
626
|
+
|
|
627
|
+
const selectStart = selectMatch.index + selectMatch[0].length;
|
|
628
|
+
|
|
629
|
+
// Find FROM keyword at same depth
|
|
630
|
+
let depth = 0;
|
|
631
|
+
let pos = selectStart;
|
|
632
|
+
while (pos < scrubbed.length) {
|
|
633
|
+
const char = scrubbed[pos];
|
|
634
|
+
if (char === '(') {
|
|
635
|
+
depth++;
|
|
636
|
+
} else if (char === ')') {
|
|
637
|
+
depth--;
|
|
638
|
+
} else if (depth === 0) {
|
|
639
|
+
const remaining = lowerScrubbed.slice(pos);
|
|
640
|
+
if (/^\s*from\s+/i.test(remaining)) {
|
|
641
|
+
return { start: selectStart, end: pos };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
pos++;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Finds WHERE clause boundaries in the main query.
|
|
652
|
+
*/
|
|
653
|
+
function findWhereClause(
|
|
654
|
+
scrubbed: string,
|
|
655
|
+
fromPos: number
|
|
656
|
+
): { start: number; end: number } | null {
|
|
657
|
+
const lowerScrubbed = scrubbed.toLowerCase();
|
|
658
|
+
|
|
659
|
+
// Find WHERE keyword after FROM, at depth 0
|
|
660
|
+
let depth = 0;
|
|
661
|
+
let pos = fromPos;
|
|
662
|
+
let whereStart = -1;
|
|
663
|
+
|
|
664
|
+
while (pos < scrubbed.length) {
|
|
665
|
+
const char = scrubbed[pos];
|
|
666
|
+
if (char === '(') {
|
|
667
|
+
depth++;
|
|
668
|
+
} else if (char === ')') {
|
|
669
|
+
depth--;
|
|
670
|
+
} else if (depth === 0) {
|
|
671
|
+
const remaining = lowerScrubbed.slice(pos);
|
|
672
|
+
if (whereStart === -1 && /^\s*where\s+/i.test(remaining)) {
|
|
673
|
+
const match = /^\s*where\s+/i.exec(remaining);
|
|
674
|
+
if (match) {
|
|
675
|
+
whereStart = pos + match[0].length;
|
|
676
|
+
}
|
|
677
|
+
} else if (
|
|
678
|
+
whereStart !== -1 &&
|
|
679
|
+
/^\s*(group\s+by|order\s+by|limit|having|union|intersect|except|$)/i.test(
|
|
680
|
+
remaining
|
|
681
|
+
)
|
|
682
|
+
) {
|
|
683
|
+
return { start: whereStart, end: pos };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
pos++;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (whereStart !== -1) {
|
|
690
|
+
return { start: whereStart, end: scrubbed.length };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Finds ORDER BY clause boundaries in the main query.
|
|
698
|
+
*/
|
|
699
|
+
function findOrderByClause(
|
|
700
|
+
scrubbed: string,
|
|
701
|
+
fromPos: number
|
|
702
|
+
): { start: number; end: number } | null {
|
|
703
|
+
const lowerScrubbed = scrubbed.toLowerCase();
|
|
704
|
+
|
|
705
|
+
// Find ORDER BY keyword after FROM, at depth 0
|
|
706
|
+
let depth = 0;
|
|
707
|
+
let pos = fromPos;
|
|
708
|
+
let orderStart = -1;
|
|
709
|
+
|
|
710
|
+
while (pos < scrubbed.length) {
|
|
711
|
+
const char = scrubbed[pos];
|
|
712
|
+
if (char === '(') {
|
|
713
|
+
depth++;
|
|
714
|
+
} else if (char === ')') {
|
|
715
|
+
depth--;
|
|
716
|
+
} else if (depth === 0) {
|
|
717
|
+
const remaining = lowerScrubbed.slice(pos);
|
|
718
|
+
if (orderStart === -1 && /^\s*order\s+by\s+/i.test(remaining)) {
|
|
719
|
+
const match = /^\s*order\s+by\s+/i.exec(remaining);
|
|
720
|
+
if (match) {
|
|
721
|
+
orderStart = pos + match[0].length;
|
|
722
|
+
}
|
|
723
|
+
} else if (
|
|
724
|
+
orderStart !== -1 &&
|
|
725
|
+
/^\s*(limit|offset|fetch|for\s+update|$)/i.test(remaining)
|
|
726
|
+
) {
|
|
727
|
+
return { start: orderStart, end: pos };
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
pos++;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (orderStart !== -1) {
|
|
734
|
+
return { start: orderStart, end: scrubbed.length };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Qualifies only specific column references (from the ambiguousColumns set) in a clause.
|
|
742
|
+
* This handles SELECT, WHERE, and ORDER BY clauses where columns from joined tables
|
|
743
|
+
* could be ambiguous.
|
|
744
|
+
*/
|
|
745
|
+
function qualifyClauseColumnsSelective(
|
|
746
|
+
original: string,
|
|
747
|
+
scrubbed: string,
|
|
748
|
+
clauseStart: number,
|
|
749
|
+
clauseEnd: number,
|
|
750
|
+
defaultSource: string,
|
|
751
|
+
ambiguousColumns: Set<string>
|
|
752
|
+
): { result: string; offset: number } {
|
|
753
|
+
const clauseOriginal = original.slice(clauseStart, clauseEnd);
|
|
754
|
+
const clauseScrubbed = scrubbed.slice(clauseStart, clauseEnd);
|
|
755
|
+
|
|
756
|
+
let result = clauseOriginal;
|
|
757
|
+
let offset = 0;
|
|
758
|
+
|
|
759
|
+
// Find all unqualified quoted identifiers
|
|
760
|
+
let pos = 0;
|
|
761
|
+
while (pos < clauseScrubbed.length) {
|
|
762
|
+
// Skip to next quote
|
|
763
|
+
const quotePos = clauseScrubbed.indexOf('"', pos);
|
|
764
|
+
if (quotePos === -1) break;
|
|
765
|
+
|
|
766
|
+
// Check if this is already qualified (preceded by a dot)
|
|
767
|
+
if (quotePos > 0 && clauseScrubbed[quotePos - 1] === '.') {
|
|
768
|
+
// Skip this identifier - it's already qualified
|
|
769
|
+
const ident = extractQuotedIdentifier(clauseOriginal, quotePos);
|
|
770
|
+
pos = ident ? ident.end : quotePos + 1;
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Extract the identifier
|
|
775
|
+
const ident = extractQuotedIdentifier(clauseOriginal, quotePos);
|
|
776
|
+
if (!ident) {
|
|
777
|
+
pos = quotePos + 1;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Check if this identifier is followed by a dot (it's a table qualifier, not a column)
|
|
782
|
+
if (
|
|
783
|
+
ident.end < clauseScrubbed.length &&
|
|
784
|
+
clauseScrubbed[ident.end] === '.'
|
|
785
|
+
) {
|
|
786
|
+
pos = ident.end + 1;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Only qualify if this column is in the ambiguous set
|
|
791
|
+
if (!ambiguousColumns.has(ident.name)) {
|
|
792
|
+
pos = ident.end;
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Check if this looks like a column reference (not a function call or alias definition)
|
|
797
|
+
// Skip if followed by ( which would indicate a function
|
|
798
|
+
let afterIdent = ident.end;
|
|
799
|
+
while (
|
|
800
|
+
afterIdent < clauseScrubbed.length &&
|
|
801
|
+
isWhitespace(clauseScrubbed[afterIdent])
|
|
802
|
+
) {
|
|
803
|
+
afterIdent++;
|
|
804
|
+
}
|
|
805
|
+
if (clauseScrubbed[afterIdent] === '(') {
|
|
806
|
+
pos = ident.end;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Skip if this is an alias definition (preceded by AS or follows a column expression)
|
|
811
|
+
// Look for patterns like: "col" as "alias" or just "col", "alias"
|
|
812
|
+
// We need to be careful here - we only want to qualify column references, not aliases
|
|
813
|
+
const beforeQuote = clauseScrubbed.slice(0, quotePos).toLowerCase();
|
|
814
|
+
if (/\bas\s*$/i.test(beforeQuote)) {
|
|
815
|
+
pos = ident.end;
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Qualify this column reference
|
|
820
|
+
const qualified = `"${defaultSource}"."${ident.name}"`;
|
|
821
|
+
const oldLength = ident.end - quotePos;
|
|
822
|
+
|
|
823
|
+
result =
|
|
824
|
+
result.slice(0, quotePos + offset) +
|
|
825
|
+
qualified +
|
|
826
|
+
result.slice(quotePos + oldLength + offset);
|
|
827
|
+
|
|
828
|
+
offset += qualified.length - oldLength;
|
|
829
|
+
pos = ident.end;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return { result, offset };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Qualifies unqualified column references in JOIN ON clauses, SELECT, WHERE,
|
|
837
|
+
* and ORDER BY clauses.
|
|
536
838
|
*
|
|
537
839
|
* Transforms patterns like:
|
|
538
|
-
* `left join "b" on "col" = "col"`
|
|
840
|
+
* `select "col" from "a" left join "b" on "col" = "col" where "col" in (...)`
|
|
539
841
|
* To:
|
|
540
|
-
* `left join "b" on "a"."col" = "b"."col"`
|
|
842
|
+
* `select "a"."col" from "a" left join "b" on "a"."col" = "b"."col" where "a"."col" in (...)`
|
|
541
843
|
*
|
|
542
844
|
* This fixes the issue where drizzle-orm generates unqualified column
|
|
543
845
|
* references when joining CTEs with eq().
|
|
@@ -549,20 +851,30 @@ export function qualifyJoinColumns(query: string): string {
|
|
|
549
851
|
}
|
|
550
852
|
|
|
551
853
|
const scrubbed = scrubForRewrite(query);
|
|
854
|
+
const fromPos = findMainFromClause(scrubbed);
|
|
855
|
+
if (fromPos < 0) {
|
|
856
|
+
return query;
|
|
857
|
+
}
|
|
858
|
+
|
|
552
859
|
const sources = parseTableSources(query, scrubbed);
|
|
553
860
|
if (sources.length < 2) {
|
|
554
861
|
return query;
|
|
555
862
|
}
|
|
556
863
|
|
|
557
|
-
const joinClauses = findJoinClauses(query, scrubbed, sources);
|
|
864
|
+
const joinClauses = findJoinClauses(query, scrubbed, sources, fromPos);
|
|
558
865
|
|
|
559
866
|
if (joinClauses.length === 0) {
|
|
560
867
|
return query;
|
|
561
868
|
}
|
|
562
869
|
|
|
870
|
+
// Get the first source (FROM table) as the default qualifier
|
|
871
|
+
const firstSource = sources[0]!;
|
|
872
|
+
const defaultQualifier = firstSource.alias || firstSource.name;
|
|
873
|
+
|
|
563
874
|
let result = query;
|
|
564
|
-
let
|
|
875
|
+
let totalOffset = 0;
|
|
565
876
|
|
|
877
|
+
// First, qualify ON clauses (existing logic)
|
|
566
878
|
for (const join of joinClauses) {
|
|
567
879
|
const scrubbedOnClause = scrubbed.slice(join.onStart, join.onEnd);
|
|
568
880
|
const originalOnClause = query.slice(join.onStart, join.onEnd);
|
|
@@ -576,8 +888,19 @@ export function qualifyJoinColumns(query: string): string {
|
|
|
576
888
|
}
|
|
577
889
|
if (scrubbedOnClause[lhsEnd] !== '"') continue;
|
|
578
890
|
|
|
891
|
+
// Find start of identifier, handling escaped quotes ""
|
|
579
892
|
let lhsStartPos = lhsEnd - 1;
|
|
580
|
-
while (lhsStartPos >= 0
|
|
893
|
+
while (lhsStartPos >= 0) {
|
|
894
|
+
if (scrubbedOnClause[lhsStartPos] === '"') {
|
|
895
|
+
// Check if this is an escaped quote ""
|
|
896
|
+
if (lhsStartPos > 0 && scrubbedOnClause[lhsStartPos - 1] === '"') {
|
|
897
|
+
// Skip over the escaped quote pair
|
|
898
|
+
lhsStartPos -= 2;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
// Found the opening quote
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
581
904
|
lhsStartPos--;
|
|
582
905
|
}
|
|
583
906
|
if (lhsStartPos < 0) continue;
|
|
@@ -697,10 +1020,140 @@ export function qualifyJoinColumns(query: string): string {
|
|
|
697
1020
|
|
|
698
1021
|
if (clauseResult !== originalOnClause) {
|
|
699
1022
|
result =
|
|
700
|
-
result.slice(0, join.onStart +
|
|
1023
|
+
result.slice(0, join.onStart + totalOffset) +
|
|
701
1024
|
clauseResult +
|
|
702
|
-
result.slice(join.onEnd +
|
|
703
|
-
|
|
1025
|
+
result.slice(join.onEnd + totalOffset);
|
|
1026
|
+
totalOffset += clauseResult.length - originalOnClause.length;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Collect column names that are known to be ambiguous (appeared in ON clauses with same name on both sides)
|
|
1031
|
+
const ambiguousColumns = new Set<string>();
|
|
1032
|
+
for (const join of joinClauses) {
|
|
1033
|
+
const scrubbedOnClause = scrubbed.slice(join.onStart, join.onEnd);
|
|
1034
|
+
const originalOnClause = query.slice(join.onStart, join.onEnd);
|
|
1035
|
+
|
|
1036
|
+
let eqPos = -1;
|
|
1037
|
+
while ((eqPos = scrubbedOnClause.indexOf('=', eqPos + 1)) !== -1) {
|
|
1038
|
+
let lhsEnd = eqPos - 1;
|
|
1039
|
+
while (lhsEnd >= 0 && isWhitespace(scrubbedOnClause[lhsEnd])) {
|
|
1040
|
+
lhsEnd--;
|
|
1041
|
+
}
|
|
1042
|
+
if (scrubbedOnClause[lhsEnd] !== '"') continue;
|
|
1043
|
+
|
|
1044
|
+
let lhsStartPos = lhsEnd - 1;
|
|
1045
|
+
while (lhsStartPos >= 0) {
|
|
1046
|
+
if (scrubbedOnClause[lhsStartPos] === '"') {
|
|
1047
|
+
if (lhsStartPos > 0 && scrubbedOnClause[lhsStartPos - 1] === '"') {
|
|
1048
|
+
lhsStartPos -= 2;
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
lhsStartPos--;
|
|
1054
|
+
}
|
|
1055
|
+
if (lhsStartPos < 0) continue;
|
|
1056
|
+
|
|
1057
|
+
let rhsStartPos = eqPos + 1;
|
|
1058
|
+
while (
|
|
1059
|
+
rhsStartPos < scrubbedOnClause.length &&
|
|
1060
|
+
isWhitespace(scrubbedOnClause[rhsStartPos])
|
|
1061
|
+
) {
|
|
1062
|
+
rhsStartPos++;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (originalOnClause[rhsStartPos] !== '"') continue;
|
|
1066
|
+
|
|
1067
|
+
const lhsIdent = extractQuotedIdentifier(originalOnClause, lhsStartPos);
|
|
1068
|
+
const rhsIdent = extractQuotedIdentifier(originalOnClause, rhsStartPos);
|
|
1069
|
+
|
|
1070
|
+
if (lhsIdent && rhsIdent && lhsIdent.name === rhsIdent.name) {
|
|
1071
|
+
ambiguousColumns.add(lhsIdent.name);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// If no ambiguous columns were found, we're done
|
|
1077
|
+
if (ambiguousColumns.size === 0) {
|
|
1078
|
+
return result;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Now qualify SELECT, WHERE, and ORDER BY clauses - only for ambiguous columns
|
|
1082
|
+
// We need to re-scrub since the query may have changed
|
|
1083
|
+
const updatedScrubbed = scrubForRewrite(result);
|
|
1084
|
+
|
|
1085
|
+
// Qualify SELECT clause
|
|
1086
|
+
const selectClause = findMainSelectClause(updatedScrubbed);
|
|
1087
|
+
if (selectClause) {
|
|
1088
|
+
const { result: selectResult, offset: selectOffset } =
|
|
1089
|
+
qualifyClauseColumnsSelective(
|
|
1090
|
+
result,
|
|
1091
|
+
updatedScrubbed,
|
|
1092
|
+
selectClause.start,
|
|
1093
|
+
selectClause.end,
|
|
1094
|
+
defaultQualifier,
|
|
1095
|
+
ambiguousColumns
|
|
1096
|
+
);
|
|
1097
|
+
if (selectOffset !== 0) {
|
|
1098
|
+
// Splice the modified clause back into the full query
|
|
1099
|
+
result =
|
|
1100
|
+
result.slice(0, selectClause.start) +
|
|
1101
|
+
selectResult +
|
|
1102
|
+
result.slice(selectClause.end);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Re-scrub after SELECT changes
|
|
1107
|
+
const scrubbed2 = scrubForRewrite(result);
|
|
1108
|
+
const fromPos2 = findMainFromClause(scrubbed2);
|
|
1109
|
+
|
|
1110
|
+
// Qualify WHERE clause
|
|
1111
|
+
if (fromPos2 >= 0) {
|
|
1112
|
+
const whereClause = findWhereClause(scrubbed2, fromPos2);
|
|
1113
|
+
if (whereClause) {
|
|
1114
|
+
const { result: whereResult, offset: whereOffset } =
|
|
1115
|
+
qualifyClauseColumnsSelective(
|
|
1116
|
+
result,
|
|
1117
|
+
scrubbed2,
|
|
1118
|
+
whereClause.start,
|
|
1119
|
+
whereClause.end,
|
|
1120
|
+
defaultQualifier,
|
|
1121
|
+
ambiguousColumns
|
|
1122
|
+
);
|
|
1123
|
+
if (whereOffset !== 0) {
|
|
1124
|
+
// Splice the modified clause back into the full query
|
|
1125
|
+
result =
|
|
1126
|
+
result.slice(0, whereClause.start) +
|
|
1127
|
+
whereResult +
|
|
1128
|
+
result.slice(whereClause.end);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Re-scrub after WHERE changes
|
|
1134
|
+
const scrubbed3 = scrubForRewrite(result);
|
|
1135
|
+
const fromPos3 = findMainFromClause(scrubbed3);
|
|
1136
|
+
|
|
1137
|
+
// Qualify ORDER BY clause
|
|
1138
|
+
if (fromPos3 >= 0) {
|
|
1139
|
+
const orderByClause = findOrderByClause(scrubbed3, fromPos3);
|
|
1140
|
+
if (orderByClause) {
|
|
1141
|
+
const { result: orderResult, offset: orderOffset } =
|
|
1142
|
+
qualifyClauseColumnsSelective(
|
|
1143
|
+
result,
|
|
1144
|
+
scrubbed3,
|
|
1145
|
+
orderByClause.start,
|
|
1146
|
+
orderByClause.end,
|
|
1147
|
+
defaultQualifier,
|
|
1148
|
+
ambiguousColumns
|
|
1149
|
+
);
|
|
1150
|
+
if (orderOffset !== 0) {
|
|
1151
|
+
// Splice the modified clause back into the full query
|
|
1152
|
+
result =
|
|
1153
|
+
result.slice(0, orderByClause.start) +
|
|
1154
|
+
orderResult +
|
|
1155
|
+
result.slice(orderByClause.end);
|
|
1156
|
+
}
|
|
704
1157
|
}
|
|
705
1158
|
}
|
|
706
1159
|
|