@@ -672,6 +672,8 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
672
672
// we want to avoid double visitation
673
673
var subqueryProjection = subquery . Projection . Single ( ) . Expression ;
674
674
675
+ inExpression = inExpression . Update ( item , values : null , subquery ) ;
676
+
675
677
var unwrappedSubqueryProjection = subqueryProjection ;
676
678
while ( unwrappedSubqueryProjection is SqlUnaryExpression
677
679
{
@@ -688,48 +690,90 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
688
690
{
689
691
nullable = itemNullable || projectionNullable ;
690
692
691
- return inExpression . Update ( item , values : null , subquery ) ;
693
+ return inExpression ;
692
694
}
693
695
694
696
nullable = false ;
695
697
696
- // Null item and non-nullable projection - return false immediately
697
- if ( IsNull ( item ) && ! projectionNullable )
698
- {
699
- return _sqlExpressionFactory . Constant ( false , inExpression . TypeMapping ) ;
700
- }
698
+ // SQL IN returns null when the item is null, and when the values (subquery projection) contains NULL and no match was made.
701
699
702
- // SQL IN returns null when the item is null, and when the values contains NULL and no match was made.
703
- // If both sides are non-nullable, IN never returns null, so is safe to use as-is.
704
- // If one side is null but not the other, IN returns null when the item isn't found; this is also safe to use as-is in
705
- // optimized expansion (null interpreted as false).
706
- // Note that we could coalesce NULL to false when we're not in optimized expansion, but that leads to more complex SQL that may
707
- // be worse performance-wise than the EXISTS rewrite below.
708
- if ( ! itemNullable && ! projectionNullable
709
- || allowOptimizedExpansion && ( itemNullable ^ projectionNullable ) )
700
+ switch ( ( itemNullable , projectionNullable ) )
710
701
{
711
- return inExpression . Update ( item , values : null , subquery ) ;
712
- }
702
+ // If both sides are non-nullable, IN never returns null, so is safe to use as-is.
703
+ case ( false , false ) :
704
+ return inExpression ;
705
+
706
+ case ( true , false ) :
707
+ {
708
+ // If the item is actually null (not just nullable) and the projection is non-nullable, just return false immediately:
709
+ // WHERE NULL IN (SELECT NonNullable FROM foo) -> false
710
+ if ( IsNull ( item ) )
711
+ {
712
+ return _sqlExpressionFactory . Constant ( false , inExpression . TypeMapping ) ;
713
+ }
714
+
715
+ // Otherwise, since the projection is non-nullable, NULL will only be returned if the item wasn't found. Use as-is
716
+ // in optimized expansion (NULL is interpreted as false anyway), or compensate for the item being possibly null:
717
+ // WHERE Nullable IN (SELECT NonNullable FROM foo) -> WHERE Nullable IN (SELECT NonNullable FROM foo) AND Nullable IS NOT NULL
718
+ // WHERE Nullable NOT IN (SELECT NonNullable FROM foo) -> WHERE Nullable NOT IN (SELECT NonNullable FROM foo) OR Nullable IS NULL
719
+ return allowOptimizedExpansion
720
+ ? inExpression
721
+ : inExpression . IsNegated
722
+ ? _sqlExpressionFactory . OrElse ( inExpression , _sqlExpressionFactory . IsNull ( item ) )
723
+ : _sqlExpressionFactory . AndAlso ( inExpression , _sqlExpressionFactory . IsNotNull ( item ) ) ;
724
+ }
713
725
714
- // We'll need to mutate the subquery to introduce the predicate inside it, but it might be referenced by other places in the
715
- // tree, so we create a copy to work on.
716
-
717
- // No need for a projection with EXISTS, clear it to get SELECT 1
718
- subquery = subquery . Update (
719
- Array . Empty < ProjectionExpression > ( ) ,
720
- subquery . Tables ,
721
- subquery . Predicate ,
722
- subquery . GroupBy ,
723
- subquery . Having ,
724
- subquery . Orderings ,
725
- subquery . Limit ,
726
- subquery . Offset ) ;
727
-
728
- var predicate = VisitSqlBinary ( _sqlExpressionFactory . Equal ( subqueryProjection , item ) , allowOptimizedExpansion : true , out _ ) ;
729
- subquery . ApplyPredicate ( predicate ) ;
730
- subquery . ClearOrdering ( ) ;
731
-
732
- return _sqlExpressionFactory . Exists ( subquery , inExpression . IsNegated ) ;
726
+ case ( false , true ) :
727
+ {
728
+ // If the item is non-nullable but the projection is nullable, NULL will only be returned if the item wasn't found
729
+ // (as with the above case).
730
+ // Use as-is in optimized expansion (NULL is interpreted as false anyway), or compensate by coalescing NULL to false:
731
+ // WHERE NonNullable IN (SELECT Nullable FROM foo) -> WHERE COALESCE(NonNullable IN (SELECT Nullable FROM foo), false)
732
+ if ( allowOptimizedExpansion )
733
+ {
734
+ return inExpression ;
735
+ }
736
+
737
+ // On SQL Server, EXISTS isn't less efficient than IN, and the additional COALESCE (and CASE/WHEN which it requires)
738
+ // add unneeded clutter (and possibly hurt perf). So allow providers to prefer EXISTS.
739
+ if ( PreferExistsToComplexIn )
740
+ {
741
+ goto TransformToExists ;
742
+ }
743
+
744
+ return inExpression . IsNegated
745
+ ? _sqlExpressionFactory . Not (
746
+ _sqlExpressionFactory . Coalesce ( inExpression . Negate ( ) , _sqlExpressionFactory . Constant ( false ) ) )
747
+ : _sqlExpressionFactory . Coalesce ( inExpression , _sqlExpressionFactory . Constant ( false ) ) ;
748
+ }
749
+
750
+ case ( true , true ) :
751
+ TransformToExists :
752
+ // Worst case: both sides are nullable; there's no way to distinguish the item was found or not.
753
+ // We rewrite to an EXISTS subquery where we can generate a precise predicate to check for what we need. Note that this
754
+ // performs (significantly) worse than an IN expression, since it involves a correlated subquery.
755
+
756
+ // We'll need to mutate the subquery to introduce the predicate inside it, but it might be referenced by other places in
757
+ // the tree, so we create a copy to work on.
758
+
759
+ // No need for a projection with EXISTS, clear it to get SELECT 1
760
+ subquery = subquery . Update (
761
+ Array . Empty < ProjectionExpression > ( ) ,
762
+ subquery . Tables ,
763
+ subquery . Predicate ,
764
+ subquery . GroupBy ,
765
+ subquery . Having ,
766
+ subquery . Orderings ,
767
+ subquery . Limit ,
768
+ subquery . Offset ) ;
769
+
770
+ var predicate = VisitSqlBinary (
771
+ _sqlExpressionFactory . Equal ( subqueryProjection , item ) , allowOptimizedExpansion : true , out _ ) ;
772
+ subquery . ApplyPredicate ( predicate ) ;
773
+ subquery . ClearOrdering ( ) ;
774
+
775
+ return _sqlExpressionFactory . Exists ( subquery , inExpression . IsNegated ) ;
776
+ }
733
777
}
734
778
735
779
// Non-subquery case
@@ -1303,6 +1347,12 @@ protected virtual SqlExpression VisitJsonScalar(
1303
1347
return jsonScalarExpression ;
1304
1348
}
1305
1349
1350
+ /// <summary>
1351
+ /// Determines whether an <see cref="InExpression" /> will be transformed to an <see cref="ExistsExpression" /> when it would
1352
+ /// otherwise require complex compensation for null semantics.
1353
+ /// </summary>
1354
+ protected virtual bool PreferExistsToComplexIn => false ;
1355
+
1306
1356
private static bool ? TryGetBoolConstantValue ( SqlExpression ? expression )
1307
1357
=> expression is SqlConstantExpression { Value : bool boolValue } ? boolValue : null ;
1308
1358
0 commit comments