@@ -142,6 +142,8 @@ export interface AnalyzedWhereClause {
142
142
expression : BasicExpression < boolean >
143
143
/** Set of table/source aliases that this WHERE clause touches */
144
144
touchedSources : Set < string >
145
+ /** Whether this clause contains namespace-only references that prevent pushdown */
146
+ hasNamespaceOnlyRef : boolean
145
147
}
146
148
147
149
/**
@@ -486,19 +488,31 @@ function splitAndClausesRecursive(
486
488
* This determines whether a clause can be pushed down to a specific table
487
489
* or must remain in the main query (for multi-source clauses like join conditions).
488
490
*
491
+ * Special handling for namespace-only references in outer joins:
492
+ * WHERE clauses that reference only a table namespace (e.g., isUndefined(special), eq(special, value))
493
+ * rather than specific properties (e.g., isUndefined(special.id), eq(special.id, value)) are treated as
494
+ * multi-source to prevent incorrect predicate pushdown that would change join semantics.
495
+ *
489
496
* @param clause - The WHERE expression to analyze
490
497
* @returns Analysis result with the expression and touched source aliases
491
498
*
492
499
* @example
493
500
* ```typescript
494
- * // eq(users.department_id, 1) -> touches ['users']
495
- * // eq(users.id, posts.user_id) -> touches ['users', 'posts']
501
+ * // eq(users.department_id, 1) -> touches ['users'], hasNamespaceOnlyRef: false
502
+ * // eq(users.id, posts.user_id) -> touches ['users', 'posts'], hasNamespaceOnlyRef: false
503
+ * // isUndefined(special) -> touches ['special'], hasNamespaceOnlyRef: true (prevents pushdown)
504
+ * // eq(special, someValue) -> touches ['special'], hasNamespaceOnlyRef: true (prevents pushdown)
505
+ * // isUndefined(special.id) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown)
506
+ * // eq(special.id, 5) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown)
496
507
* ```
497
508
*/
498
509
function analyzeWhereClause (
499
510
clause : BasicExpression < boolean >
500
511
) : AnalyzedWhereClause {
512
+ // Track which table aliases this WHERE clause touches
501
513
const touchedSources = new Set < string > ( )
514
+ // Track whether this clause contains namespace-only references that prevent pushdown
515
+ let hasNamespaceOnlyRef = false
502
516
503
517
/**
504
518
* Recursively collect all table aliases referenced in an expression
@@ -511,6 +525,13 @@ function analyzeWhereClause(
511
525
const firstElement = expr . path [ 0 ]
512
526
if ( firstElement ) {
513
527
touchedSources . add ( firstElement )
528
+
529
+ // If the path has only one element (just the namespace),
530
+ // this is a namespace-only reference that should not be pushed down
531
+ // This applies to ANY function, not just existence-checking functions
532
+ if ( expr . path . length === 1 ) {
533
+ hasNamespaceOnlyRef = true
534
+ }
514
535
}
515
536
}
516
537
break
@@ -537,6 +558,7 @@ function analyzeWhereClause(
537
558
return {
538
559
expression : clause ,
539
560
touchedSources,
561
+ hasNamespaceOnlyRef,
540
562
}
541
563
}
542
564
@@ -557,15 +579,15 @@ function groupWhereClauses(
557
579
558
580
// Categorize each clause based on how many sources it touches
559
581
for ( const clause of analyzedClauses ) {
560
- if ( clause . touchedSources . size === 1 ) {
561
- // Single source clause - can be optimized
582
+ if ( clause . touchedSources . size === 1 && ! clause . hasNamespaceOnlyRef ) {
583
+ // Single source clause without namespace-only references - can be optimized
562
584
const source = Array . from ( clause . touchedSources ) [ 0 ] !
563
585
if ( ! singleSource . has ( source ) ) {
564
586
singleSource . set ( source , [ ] )
565
587
}
566
588
singleSource . get ( source ) ! . push ( clause . expression )
567
- } else if ( clause . touchedSources . size > 1 ) {
568
- // Multi-source clause - must stay in main query
589
+ } else if ( clause . touchedSources . size > 1 || clause . hasNamespaceOnlyRef ) {
590
+ // Multi-source clause or namespace-only reference - must stay in main query
569
591
multiSource . push ( clause . expression )
570
592
}
571
593
// Skip clauses that touch no sources (constants) - they don't need optimization
0 commit comments