@leonardovida-md/drizzle-neo-duckdb 1.1.2 → 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 +611 -2
- package/dist/helpers.mjs +10 -0
- package/dist/index.mjs +614 -2
- package/dist/sql/query-rewriters.d.ts +13 -0
- package/package.json +5 -2
- package/src/columns.ts +6 -1
- package/src/session.ts +22 -3
- package/src/sql/query-rewriters.ts +948 -0
|
@@ -211,3 +211,951 @@ export function adaptArrayOperators(query: string): string {
|
|
|
211
211
|
|
|
212
212
|
return rewritten;
|
|
213
213
|
}
|
|
214
|
+
|
|
215
|
+
// Join column qualification types and helpers
|
|
216
|
+
|
|
217
|
+
type TableSource = {
|
|
218
|
+
name: string; // The table/CTE name (without quotes)
|
|
219
|
+
alias?: string; // Optional alias
|
|
220
|
+
position: number; // Position in the query where this source was introduced
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
type JoinClause = {
|
|
224
|
+
joinType: string; // 'left', 'right', 'inner', 'full', 'cross', ''
|
|
225
|
+
tableName: string; // The joined table name
|
|
226
|
+
tableAlias?: string; // Optional alias
|
|
227
|
+
onStart: number; // Start of ON clause content (after "on ")
|
|
228
|
+
onEnd: number; // End of ON clause content
|
|
229
|
+
leftSource: string; // The table/alias on the left side of this join
|
|
230
|
+
rightSource: string; // The table/alias for the joined table
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Extracts the identifier name from a quoted identifier like "foo" -> foo
|
|
235
|
+
* Uses the original query string, not the scrubbed one.
|
|
236
|
+
* Handles escaped quotes: "col""name" -> col""name
|
|
237
|
+
*/
|
|
238
|
+
function extractQuotedIdentifier(
|
|
239
|
+
original: string,
|
|
240
|
+
start: number
|
|
241
|
+
): { name: string; end: number } | null {
|
|
242
|
+
if (original[start] !== '"') {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let pos = start + 1;
|
|
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;
|
|
256
|
+
}
|
|
257
|
+
pos++;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (pos >= original.length) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
name: original.slice(start + 1, pos),
|
|
266
|
+
end: pos + 1,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Finds the main SELECT's FROM clause, skipping any FROM inside CTEs.
|
|
272
|
+
*/
|
|
273
|
+
function findMainFromClause(scrubbed: string): number {
|
|
274
|
+
const lowerScrubbed = scrubbed.toLowerCase();
|
|
275
|
+
|
|
276
|
+
// If there's a WITH clause, find where the CTEs end
|
|
277
|
+
let searchStart = 0;
|
|
278
|
+
const withMatch = /\bwith\s+/i.exec(lowerScrubbed);
|
|
279
|
+
if (withMatch) {
|
|
280
|
+
// Find the main SELECT after the CTEs
|
|
281
|
+
// CTEs are separated by commas and end with the main SELECT
|
|
282
|
+
let depth = 0;
|
|
283
|
+
let pos = withMatch.index + withMatch[0].length;
|
|
284
|
+
|
|
285
|
+
while (pos < scrubbed.length) {
|
|
286
|
+
const char = scrubbed[pos];
|
|
287
|
+
if (char === '(') {
|
|
288
|
+
depth++;
|
|
289
|
+
} else if (char === ')') {
|
|
290
|
+
depth--;
|
|
291
|
+
} else if (depth === 0) {
|
|
292
|
+
// Check if we're at "select" keyword (main query)
|
|
293
|
+
const remaining = lowerScrubbed.slice(pos);
|
|
294
|
+
if (/^\s*select\s+/i.test(remaining)) {
|
|
295
|
+
searchStart = pos;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
pos++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Find FROM after the main SELECT
|
|
304
|
+
const fromPattern = /\bfrom\s+/gi;
|
|
305
|
+
fromPattern.lastIndex = searchStart;
|
|
306
|
+
const fromMatch = fromPattern.exec(lowerScrubbed);
|
|
307
|
+
|
|
308
|
+
return fromMatch ? fromMatch.index + fromMatch[0].length : -1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Parses table sources from FROM and JOIN clauses.
|
|
313
|
+
* Returns an array of table sources in order of appearance.
|
|
314
|
+
*/
|
|
315
|
+
function parseTableSources(original: string, scrubbed: string): TableSource[] {
|
|
316
|
+
const sources: TableSource[] = [];
|
|
317
|
+
const lowerScrubbed = scrubbed.toLowerCase();
|
|
318
|
+
|
|
319
|
+
// Find the main FROM clause (after CTEs if present)
|
|
320
|
+
const fromPos = findMainFromClause(scrubbed);
|
|
321
|
+
if (fromPos < 0) {
|
|
322
|
+
return sources;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const fromTable = parseTableRef(original, scrubbed, fromPos);
|
|
326
|
+
if (fromTable) {
|
|
327
|
+
sources.push(fromTable);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const joinPattern =
|
|
331
|
+
/\b(left\s+|right\s+|inner\s+|full\s+|cross\s+)?join\s+/gi;
|
|
332
|
+
joinPattern.lastIndex = fromPos;
|
|
333
|
+
|
|
334
|
+
let joinMatch;
|
|
335
|
+
while ((joinMatch = joinPattern.exec(lowerScrubbed)) !== null) {
|
|
336
|
+
const tableStart = joinMatch.index + joinMatch[0].length;
|
|
337
|
+
const joinTable = parseTableRef(original, scrubbed, tableStart);
|
|
338
|
+
if (joinTable) {
|
|
339
|
+
sources.push(joinTable);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return sources;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Parses a table reference (potentially with alias) starting at the given position.
|
|
348
|
+
* Handles: "table", "table" "alias", "table" as "alias", "schema"."table",
|
|
349
|
+
* and subqueries: (SELECT ...) AS "alias"
|
|
350
|
+
*/
|
|
351
|
+
function parseTableRef(
|
|
352
|
+
original: string,
|
|
353
|
+
scrubbed: string,
|
|
354
|
+
start: number
|
|
355
|
+
): TableSource | null {
|
|
356
|
+
let pos = start;
|
|
357
|
+
while (pos < scrubbed.length && isWhitespace(scrubbed[pos])) {
|
|
358
|
+
pos++;
|
|
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
|
+
|
|
402
|
+
if (original[pos] !== '"') {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const nameStart = pos;
|
|
407
|
+
const firstIdent = extractQuotedIdentifier(original, pos);
|
|
408
|
+
if (!firstIdent) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let name = firstIdent.name;
|
|
413
|
+
pos = firstIdent.end;
|
|
414
|
+
|
|
415
|
+
// Check for schema.table pattern
|
|
416
|
+
let afterName = pos;
|
|
417
|
+
while (afterName < scrubbed.length && isWhitespace(scrubbed[afterName])) {
|
|
418
|
+
afterName++;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (scrubbed[afterName] === '.') {
|
|
422
|
+
afterName++;
|
|
423
|
+
while (afterName < scrubbed.length && isWhitespace(scrubbed[afterName])) {
|
|
424
|
+
afterName++;
|
|
425
|
+
}
|
|
426
|
+
if (original[afterName] === '"') {
|
|
427
|
+
const tableIdent = extractQuotedIdentifier(original, afterName);
|
|
428
|
+
if (tableIdent) {
|
|
429
|
+
name = tableIdent.name;
|
|
430
|
+
pos = tableIdent.end;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
let alias: string | undefined;
|
|
436
|
+
let aliasPos = pos;
|
|
437
|
+
while (aliasPos < scrubbed.length && isWhitespace(scrubbed[aliasPos])) {
|
|
438
|
+
aliasPos++;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const afterTable = scrubbed.slice(aliasPos).toLowerCase();
|
|
442
|
+
if (afterTable.startsWith('as ')) {
|
|
443
|
+
aliasPos += 3;
|
|
444
|
+
while (aliasPos < scrubbed.length && isWhitespace(scrubbed[aliasPos])) {
|
|
445
|
+
aliasPos++;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Check what comes after (potentially skipping the AS keyword)
|
|
450
|
+
const afterAlias = scrubbed.slice(aliasPos).toLowerCase();
|
|
451
|
+
if (
|
|
452
|
+
original[aliasPos] === '"' &&
|
|
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 ')
|
|
464
|
+
) {
|
|
465
|
+
const aliasIdent = extractQuotedIdentifier(original, aliasPos);
|
|
466
|
+
if (aliasIdent) {
|
|
467
|
+
alias = aliasIdent.name;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
name,
|
|
473
|
+
alias,
|
|
474
|
+
position: nameStart,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Finds all JOIN clauses with their ON clause boundaries.
|
|
480
|
+
*/
|
|
481
|
+
function findJoinClauses(
|
|
482
|
+
original: string,
|
|
483
|
+
scrubbed: string,
|
|
484
|
+
sources: TableSource[],
|
|
485
|
+
fromPos: number
|
|
486
|
+
): JoinClause[] {
|
|
487
|
+
const clauses: JoinClause[] = [];
|
|
488
|
+
const lowerScrubbed = scrubbed.toLowerCase();
|
|
489
|
+
|
|
490
|
+
// Pattern to find JOINs with ON clauses
|
|
491
|
+
// Use scrubbed for matching, original for extracting values
|
|
492
|
+
// Handle optional schema prefix: "schema"."table" or just "table"
|
|
493
|
+
const joinPattern =
|
|
494
|
+
/\b(left\s+|right\s+|inner\s+|full\s+|cross\s+)?join\s+"[^"]*"(\s*\.\s*"[^"]*")?(\s+as)?(\s+"[^"]*")?\s+on\s+/gi;
|
|
495
|
+
|
|
496
|
+
// Start searching from the main FROM clause, skipping JOINs inside CTE definitions
|
|
497
|
+
joinPattern.lastIndex = fromPos;
|
|
498
|
+
|
|
499
|
+
let match;
|
|
500
|
+
let sourceIndex = 1; // Start at 1 since index 0 is the FROM table
|
|
501
|
+
|
|
502
|
+
while ((match = joinPattern.exec(lowerScrubbed)) !== null) {
|
|
503
|
+
const joinType = (match[1] || '').trim().toLowerCase();
|
|
504
|
+
const joinKeywordEnd =
|
|
505
|
+
match.index + (match[1] || '').length + 'join'.length;
|
|
506
|
+
let tableStart = joinKeywordEnd;
|
|
507
|
+
while (tableStart < original.length && isWhitespace(original[tableStart])) {
|
|
508
|
+
tableStart++;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const tableIdent = extractQuotedIdentifier(original, tableStart);
|
|
512
|
+
if (!tableIdent) continue;
|
|
513
|
+
|
|
514
|
+
let tableName = tableIdent.name;
|
|
515
|
+
let afterTable = tableIdent.end;
|
|
516
|
+
let checkPos = afterTable;
|
|
517
|
+
while (checkPos < scrubbed.length && isWhitespace(scrubbed[checkPos])) {
|
|
518
|
+
checkPos++;
|
|
519
|
+
}
|
|
520
|
+
if (scrubbed[checkPos] === '.') {
|
|
521
|
+
checkPos++;
|
|
522
|
+
while (checkPos < scrubbed.length && isWhitespace(scrubbed[checkPos])) {
|
|
523
|
+
checkPos++;
|
|
524
|
+
}
|
|
525
|
+
const realTableIdent = extractQuotedIdentifier(original, checkPos);
|
|
526
|
+
if (realTableIdent) {
|
|
527
|
+
tableName = realTableIdent.name;
|
|
528
|
+
afterTable = realTableIdent.end;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let tableAlias: string | undefined;
|
|
533
|
+
let aliasPos = afterTable;
|
|
534
|
+
while (aliasPos < scrubbed.length && isWhitespace(scrubbed[aliasPos])) {
|
|
535
|
+
aliasPos++;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const afterTableStr = scrubbed.slice(aliasPos).toLowerCase();
|
|
539
|
+
if (afterTableStr.startsWith('as ')) {
|
|
540
|
+
aliasPos += 3;
|
|
541
|
+
while (aliasPos < scrubbed.length && isWhitespace(scrubbed[aliasPos])) {
|
|
542
|
+
aliasPos++;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (original[aliasPos] === '"' && !afterTableStr.startsWith('on ')) {
|
|
547
|
+
const aliasIdent = extractQuotedIdentifier(original, aliasPos);
|
|
548
|
+
if (aliasIdent) {
|
|
549
|
+
tableAlias = aliasIdent.name;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const onStart = match.index + match[0].length;
|
|
554
|
+
|
|
555
|
+
// Find the end of the ON clause (next JOIN, WHERE, GROUP, ORDER, LIMIT, or end)
|
|
556
|
+
const endPattern =
|
|
557
|
+
/\b(left\s+join|right\s+join|inner\s+join|full\s+join|cross\s+join|join|where|group\s+by|order\s+by|limit|$)/i;
|
|
558
|
+
const remaining = lowerScrubbed.slice(onStart);
|
|
559
|
+
const endMatch = endPattern.exec(remaining);
|
|
560
|
+
const onEnd = endMatch ? onStart + endMatch.index : scrubbed.length;
|
|
561
|
+
|
|
562
|
+
// Determine the left source (previous table/CTE)
|
|
563
|
+
let leftSource = '';
|
|
564
|
+
if (sourceIndex > 0 && sourceIndex <= sources.length) {
|
|
565
|
+
const prev = sources[sourceIndex - 1];
|
|
566
|
+
leftSource = prev?.alias || prev?.name || '';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const rightSource = tableAlias || tableName;
|
|
570
|
+
|
|
571
|
+
clauses.push({
|
|
572
|
+
joinType,
|
|
573
|
+
tableName,
|
|
574
|
+
tableAlias,
|
|
575
|
+
onStart,
|
|
576
|
+
onEnd,
|
|
577
|
+
leftSource,
|
|
578
|
+
rightSource,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
sourceIndex++;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return clauses;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
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.
|
|
838
|
+
*
|
|
839
|
+
* Transforms patterns like:
|
|
840
|
+
* `select "col" from "a" left join "b" on "col" = "col" where "col" in (...)`
|
|
841
|
+
* To:
|
|
842
|
+
* `select "a"."col" from "a" left join "b" on "a"."col" = "b"."col" where "a"."col" in (...)`
|
|
843
|
+
*
|
|
844
|
+
* This fixes the issue where drizzle-orm generates unqualified column
|
|
845
|
+
* references when joining CTEs with eq().
|
|
846
|
+
*/
|
|
847
|
+
export function qualifyJoinColumns(query: string): string {
|
|
848
|
+
const lowerQuery = query.toLowerCase();
|
|
849
|
+
if (!lowerQuery.includes('join')) {
|
|
850
|
+
return query;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const scrubbed = scrubForRewrite(query);
|
|
854
|
+
const fromPos = findMainFromClause(scrubbed);
|
|
855
|
+
if (fromPos < 0) {
|
|
856
|
+
return query;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const sources = parseTableSources(query, scrubbed);
|
|
860
|
+
if (sources.length < 2) {
|
|
861
|
+
return query;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const joinClauses = findJoinClauses(query, scrubbed, sources, fromPos);
|
|
865
|
+
|
|
866
|
+
if (joinClauses.length === 0) {
|
|
867
|
+
return query;
|
|
868
|
+
}
|
|
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
|
+
|
|
874
|
+
let result = query;
|
|
875
|
+
let totalOffset = 0;
|
|
876
|
+
|
|
877
|
+
// First, qualify ON clauses (existing logic)
|
|
878
|
+
for (const join of joinClauses) {
|
|
879
|
+
const scrubbedOnClause = scrubbed.slice(join.onStart, join.onEnd);
|
|
880
|
+
const originalOnClause = query.slice(join.onStart, join.onEnd);
|
|
881
|
+
let clauseResult = originalOnClause;
|
|
882
|
+
let clauseOffset = 0;
|
|
883
|
+
let eqPos = -1;
|
|
884
|
+
while ((eqPos = scrubbedOnClause.indexOf('=', eqPos + 1)) !== -1) {
|
|
885
|
+
let lhsEnd = eqPos - 1;
|
|
886
|
+
while (lhsEnd >= 0 && isWhitespace(scrubbedOnClause[lhsEnd])) {
|
|
887
|
+
lhsEnd--;
|
|
888
|
+
}
|
|
889
|
+
if (scrubbedOnClause[lhsEnd] !== '"') continue;
|
|
890
|
+
|
|
891
|
+
// Find start of identifier, handling escaped quotes ""
|
|
892
|
+
let lhsStartPos = lhsEnd - 1;
|
|
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
|
+
}
|
|
904
|
+
lhsStartPos--;
|
|
905
|
+
}
|
|
906
|
+
if (lhsStartPos < 0) continue;
|
|
907
|
+
|
|
908
|
+
const lhsIsQualified =
|
|
909
|
+
lhsStartPos > 0 && scrubbedOnClause[lhsStartPos - 1] === '.';
|
|
910
|
+
let rhsStartPos = eqPos + 1;
|
|
911
|
+
while (
|
|
912
|
+
rhsStartPos < scrubbedOnClause.length &&
|
|
913
|
+
isWhitespace(scrubbedOnClause[rhsStartPos])
|
|
914
|
+
) {
|
|
915
|
+
rhsStartPos++;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const rhsChar = originalOnClause[rhsStartPos];
|
|
919
|
+
const rhsIsParam = rhsChar === '$';
|
|
920
|
+
const rhsIsStringLiteral = rhsChar === "'";
|
|
921
|
+
const rhsIsColumn = rhsChar === '"';
|
|
922
|
+
|
|
923
|
+
if (!rhsIsParam && !rhsIsStringLiteral && !rhsIsColumn) continue;
|
|
924
|
+
|
|
925
|
+
const rhsIsQualified =
|
|
926
|
+
!rhsIsColumn ||
|
|
927
|
+
(rhsStartPos > 0 && scrubbedOnClause[rhsStartPos - 1] === '.');
|
|
928
|
+
if (lhsIsQualified || rhsIsQualified) continue;
|
|
929
|
+
|
|
930
|
+
const lhsIdent = extractQuotedIdentifier(originalOnClause, lhsStartPos);
|
|
931
|
+
if (!lhsIdent) continue;
|
|
932
|
+
|
|
933
|
+
let rhsIdent: { name: string; end: number } | null = null;
|
|
934
|
+
let rhsValue = '';
|
|
935
|
+
let rhsEnd = rhsStartPos;
|
|
936
|
+
|
|
937
|
+
if (rhsIsParam) {
|
|
938
|
+
let paramEnd = rhsStartPos + 1;
|
|
939
|
+
while (
|
|
940
|
+
paramEnd < originalOnClause.length &&
|
|
941
|
+
/\d/.test(originalOnClause[paramEnd]!)
|
|
942
|
+
) {
|
|
943
|
+
paramEnd++;
|
|
944
|
+
}
|
|
945
|
+
rhsValue = originalOnClause.slice(rhsStartPos, paramEnd);
|
|
946
|
+
rhsEnd = paramEnd;
|
|
947
|
+
} else if (rhsIsStringLiteral) {
|
|
948
|
+
let literalEnd = rhsStartPos + 1;
|
|
949
|
+
while (literalEnd < originalOnClause.length) {
|
|
950
|
+
if (originalOnClause[literalEnd] === "'") {
|
|
951
|
+
if (originalOnClause[literalEnd + 1] === "'") {
|
|
952
|
+
literalEnd += 2;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
literalEnd++;
|
|
958
|
+
}
|
|
959
|
+
rhsValue = originalOnClause.slice(rhsStartPos, literalEnd + 1);
|
|
960
|
+
rhsEnd = literalEnd + 1;
|
|
961
|
+
} else if (rhsIsColumn) {
|
|
962
|
+
rhsIdent = extractQuotedIdentifier(originalOnClause, rhsStartPos);
|
|
963
|
+
if (rhsIdent) {
|
|
964
|
+
// Check if this identifier is followed by a dot (meaning it's a table prefix, not the column)
|
|
965
|
+
if (
|
|
966
|
+
rhsIdent.end < scrubbedOnClause.length &&
|
|
967
|
+
scrubbedOnClause[rhsIdent.end] === '.'
|
|
968
|
+
) {
|
|
969
|
+
// This is a qualified reference "table"."column" - skip, it's already qualified
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
rhsValue = `"${rhsIdent.name}"`;
|
|
973
|
+
rhsEnd = rhsIdent.end;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (!rhsValue) continue;
|
|
978
|
+
|
|
979
|
+
// Only qualify when both sides are columns with the same name.
|
|
980
|
+
// Only same-named columns cause "Ambiguous reference" errors in DuckDB.
|
|
981
|
+
if (!rhsIsColumn || !rhsIdent || lhsIdent.name !== rhsIdent.name) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const lhsOriginal = `"${lhsIdent.name}"`;
|
|
986
|
+
let newLhs = lhsOriginal;
|
|
987
|
+
let newRhs = rhsValue;
|
|
988
|
+
|
|
989
|
+
if (!lhsIsQualified && join.leftSource) {
|
|
990
|
+
newLhs = `"${join.leftSource}"."${lhsIdent.name}"`;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (!rhsIsQualified && rhsIsColumn && rhsIdent && join.rightSource) {
|
|
994
|
+
newRhs = `"${join.rightSource}"."${rhsIdent.name}"`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (newLhs !== lhsOriginal || newRhs !== rhsValue) {
|
|
998
|
+
const opStart = lhsIdent.end;
|
|
999
|
+
let opEnd = opStart;
|
|
1000
|
+
while (
|
|
1001
|
+
opEnd < rhsEnd &&
|
|
1002
|
+
(isWhitespace(originalOnClause[opEnd]) ||
|
|
1003
|
+
originalOnClause[opEnd] === '=')
|
|
1004
|
+
) {
|
|
1005
|
+
opEnd++;
|
|
1006
|
+
}
|
|
1007
|
+
const operator = originalOnClause.slice(opStart, opEnd);
|
|
1008
|
+
|
|
1009
|
+
const newExpr = `${newLhs}${operator}${newRhs}`;
|
|
1010
|
+
const oldExprLength = rhsEnd - lhsStartPos;
|
|
1011
|
+
|
|
1012
|
+
clauseResult =
|
|
1013
|
+
clauseResult.slice(0, lhsStartPos + clauseOffset) +
|
|
1014
|
+
newExpr +
|
|
1015
|
+
clauseResult.slice(lhsStartPos + oldExprLength + clauseOffset);
|
|
1016
|
+
|
|
1017
|
+
clauseOffset += newExpr.length - oldExprLength;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (clauseResult !== originalOnClause) {
|
|
1022
|
+
result =
|
|
1023
|
+
result.slice(0, join.onStart + totalOffset) +
|
|
1024
|
+
clauseResult +
|
|
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
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return result;
|
|
1161
|
+
}
|