diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 010f6f80e75..381a8e1b791 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -27,6 +27,7 @@ namespace Microsoft.EntityFrameworkCore.Query public class SqlNullabilityProcessor { private readonly List _nonNullableColumns; + private readonly List _nullValueColumns; private readonly ISqlExpressionFactory _sqlExpressionFactory; private bool _canCache; @@ -46,6 +47,7 @@ public SqlNullabilityProcessor( _sqlExpressionFactory = dependencies.SqlExpressionFactory; UseRelationalNulls = useRelationalNulls; _nonNullableColumns = new List(); + _nullValueColumns = new List(); ParameterValues = null!; } @@ -81,6 +83,7 @@ public virtual SelectExpression Process( _canCache = true; _nonNullableColumns.Clear(); + _nullValueColumns.Clear(); ParameterValues = parameterValues; var result = Visit(selectExpression); @@ -342,13 +345,13 @@ protected virtual SelectExpression Visit(SelectExpression selectExpression) /// An optimized sql expression. [return: NotNullIfNotNull("sqlExpression")] protected virtual SqlExpression? Visit(SqlExpression? sqlExpression, bool allowOptimizedExpansion, out bool nullable) - => Visit(sqlExpression, allowOptimizedExpansion, preserveNonNullableColumns: false, out nullable); + => Visit(sqlExpression, allowOptimizedExpansion, preserveColumnNullabilityInformation: false, out nullable); [return: NotNullIfNotNull("sqlExpression")] private SqlExpression? Visit( SqlExpression? sqlExpression, bool allowOptimizedExpansion, - bool preserveNonNullableColumns, + bool preserveColumnNullabilityInformation, out bool nullable) { if (sqlExpression == null) @@ -358,6 +361,7 @@ protected virtual SelectExpression Visit(SelectExpression selectExpression) } var nonNullableColumnsCount = _nonNullableColumns.Count; + var nullValueColumnsCount = _nullValueColumns.Count; var result = sqlExpression switch { CaseExpression caseExpression @@ -393,9 +397,10 @@ SqlUnaryExpression sqlUnaryExpression _ => VisitCustomSqlExpression(sqlExpression, allowOptimizedExpansion, out nullable) }; - if (!preserveNonNullableColumns) + if (!preserveColumnNullabilityInformation) { RestoreNonNullableColumnsList(nonNullableColumnsCount); + RestoreNullValueColumnsList(nullValueColumnsCount); } return result; @@ -430,6 +435,7 @@ protected virtual SqlExpression VisitCase(CaseExpression caseExpression, bool al // otherwise the result is nullable if any of the WhenClause results OR ElseResult is nullable nullable = caseExpression.ElseResult == null; var currentNonNullableColumnsCount = _nonNullableColumns.Count; + var currentNullValueColumnsCount = _nullValueColumns.Count; var operand = Visit(caseExpression.Operand, out _); var whenClauses = new List(); @@ -438,8 +444,8 @@ protected virtual SqlExpression VisitCase(CaseExpression caseExpression, bool al var testEvaluatesToTrue = false; foreach (var whenClause in caseExpression.WhenClauses) { - // we can use non-nullable column information we got from visiting Test, in the Result - var test = Visit(whenClause.Test, allowOptimizedExpansion: testIsCondition, preserveNonNullableColumns: true, out _); + // we can use column nullability information we got from visiting Test, in the Result + var test = Visit(whenClause.Test, allowOptimizedExpansion: testIsCondition, preserveColumnNullabilityInformation: true, out _); if (TryGetBoolConstantValue(test) is bool testConstantBool) { @@ -451,6 +457,7 @@ protected virtual SqlExpression VisitCase(CaseExpression caseExpression, bool al { // if test evaluates to 'false' we can remove the WhenClause RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + RestoreNullValueColumnsList(currentNullValueColumnsCount); continue; } @@ -461,6 +468,7 @@ protected virtual SqlExpression VisitCase(CaseExpression caseExpression, bool al nullable |= resultNullable; whenClauses.Add(new CaseWhenClause(test, newResult)); RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + RestoreNullValueColumnsList(currentNonNullableColumnsCount); // if test evaluates to 'true' we can remove every condition that comes after, including ElseResult if (testEvaluatesToTrue) @@ -476,6 +484,9 @@ protected virtual SqlExpression VisitCase(CaseExpression caseExpression, bool al nullable |= elseResultNullable; } + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + RestoreNullValueColumnsList(currentNullValueColumnsCount); + // if there are no whenClauses left (e.g. their tests evaluated to false): // - if there is Else block, return it // - if there is no Else block, return null @@ -830,16 +841,30 @@ protected virtual SqlExpression VisitSqlBinary( || sqlBinaryExpression.OperatorType == ExpressionType.OrElse); var currentNonNullableColumnsCount = _nonNullableColumns.Count; + var currentNullValueColumnsCount = _nullValueColumns.Count; - var left = Visit(sqlBinaryExpression.Left, allowOptimizedExpansion, preserveNonNullableColumns: true, out var leftNullable); + var left = Visit(sqlBinaryExpression.Left, allowOptimizedExpansion, preserveColumnNullabilityInformation: true, out var leftNullable); var leftNonNullableColumns = _nonNullableColumns.Skip(currentNonNullableColumnsCount).ToList(); + var leftNullValueColumns = _nullValueColumns.Skip(currentNullValueColumnsCount).ToList(); if (sqlBinaryExpression.OperatorType != ExpressionType.AndAlso) { RestoreNonNullableColumnsList(currentNonNullableColumnsCount); } - var right = Visit(sqlBinaryExpression.Right, allowOptimizedExpansion, preserveNonNullableColumns: true, out var rightNullable); + if (sqlBinaryExpression.OperatorType == ExpressionType.OrElse) + { + // in case of OrElse, we can assume all null value columns on the left side can be treated as non-nullable on the right + // e.g. (a == null || b == null) || f(a, b) + // f(a, b) will only be executed if a != null and b != null + _nonNullableColumns.AddRange(_nullValueColumns.Skip(currentNullValueColumnsCount).ToList()); + } + else + { + RestoreNullValueColumnsList(currentNullValueColumnsCount); + } + + var right = Visit(sqlBinaryExpression.Right, allowOptimizedExpansion, preserveColumnNullabilityInformation: true, out var rightNullable); if (sqlBinaryExpression.OperatorType == ExpressionType.OrElse) { @@ -853,6 +878,17 @@ protected virtual SqlExpression VisitSqlBinary( RestoreNonNullableColumnsList(currentNonNullableColumnsCount); } + if (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso) + { + var intersect = leftNullValueColumns.Intersect(_nullValueColumns.Skip(currentNullValueColumnsCount)).ToList(); + RestoreNullValueColumnsList(currentNullValueColumnsCount); + _nullValueColumns.AddRange(intersect); + } + else if (sqlBinaryExpression.OperatorType != ExpressionType.OrElse) + { + RestoreNullValueColumnsList(currentNullValueColumnsCount); + } + // nullableStringColumn + a -> COALESCE(nullableStringColumn, "") + a if (sqlBinaryExpression.OperatorType == ExpressionType.Add && sqlBinaryExpression.Type == typeof(string)) @@ -886,10 +922,16 @@ protected virtual SqlExpression VisitSqlBinary( out nullable); if (optimized is SqlUnaryExpression optimizedUnary - && optimizedUnary.OperatorType == ExpressionType.NotEqual && optimizedUnary.Operand is ColumnExpression optimizedUnaryColumnOperand) { - _nonNullableColumns.Add(optimizedUnaryColumnOperand); + if (optimizedUnary.OperatorType == ExpressionType.NotEqual) + { + _nonNullableColumns.Add(optimizedUnaryColumnOperand); + } + else if (optimizedUnary.OperatorType == ExpressionType.Equal) + { + _nullValueColumns.Add(optimizedUnaryColumnOperand); + } } // we assume that NullSemantics rewrite is only needed (on the current level) @@ -1069,10 +1111,16 @@ protected virtual SqlExpression VisitSqlUnary( nullable = false; if (result is SqlUnaryExpression resultUnary - && resultUnary.OperatorType == ExpressionType.NotEqual && resultUnary.Operand is ColumnExpression resultColumnOperand) { - _nonNullableColumns.Add(resultColumnOperand); + if (resultUnary.OperatorType == ExpressionType.NotEqual) + { + _nonNullableColumns.Add(resultColumnOperand); + } + else if (resultUnary.OperatorType == ExpressionType.Equal) + { + _nullValueColumns.Add(resultColumnOperand); + } } return result; @@ -1099,6 +1147,14 @@ private void RestoreNonNullableColumnsList(int counter) } } + private void RestoreNullValueColumnsList(int counter) + { + if (counter < _nullValueColumns.Count) + { + _nullValueColumns.RemoveRange(counter, _nullValueColumns.Count - counter); + } + } + private SqlExpression ProcessJoinPredicate(SqlExpression predicate) { if (predicate is SqlBinaryExpression sqlBinaryExpression) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs index 60f6ba46599..8277b093d62 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs @@ -1087,15 +1087,23 @@ FROM root c WHERE (c[""Discriminator""] = ""Customer"")"); } + [ConditionalTheory(Skip = "Issue #17246")] + public override async Task IsNullOrEmpty_negated_in_predicate(bool async) + { + await base.IsNullOrEmpty_negated_in_predicate(async); + + AssertSql(@""); + } + [ConditionalTheory(Skip = "Issue #17246")] public override Task IsNullOrWhiteSpace_in_predicate_on_non_nullable_column(bool async) { return base.IsNullOrWhiteSpace_in_predicate_on_non_nullable_column(async); } - public override void IsNullOrEmpty_in_projection() + public override async Task IsNullOrEmpty_in_projection(bool async) { - base.IsNullOrEmpty_in_projection(); + await base.IsNullOrEmpty_in_projection(async); AssertSql( @"SELECT c[""CustomerID""], c[""Region""] @@ -1103,9 +1111,9 @@ FROM root c WHERE (c[""Discriminator""] = ""Customer"")"); } - public override void IsNullOrEmpty_negated_in_projection() + public override async Task IsNullOrEmpty_negated_in_projection(bool async) { - base.IsNullOrEmpty_negated_in_projection(); + await base.IsNullOrEmpty_negated_in_projection(async); AssertSql( @"SELECT c[""CustomerID""], c[""Region""] diff --git a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs index 0d6e703259c..36f31baf7f3 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs @@ -1756,6 +1756,104 @@ public virtual async Task Negated_contains_with_comparison_without_null_get_comb Assert.Equal(expected.Count, result.Count); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_simple(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(x => !(x.NullableStringA == null || x.NullableStringA != "Foo"))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_negative(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(x => !(x.NullableStringA == null && x.NullableStringA != "Foo"))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_nested(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(x => x.NullableStringA == null + || x.NullableStringB == null + || x.NullableStringA != x.NullableStringB)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_intersection(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(x => (x.NullableStringA == null + && (x.StringA == "Foo" || x.NullableStringA == null || x.NullableStringB == null)) + || x.NullableStringA != x.NullableStringB)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(x => x.NullableStringA == null + ? x.NullableStringA != x.NullableStringB + : x.NullableStringA != x.NullableStringC)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_multiple(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(x => x.NullableStringA == null || x.NullableStringB == null + ? x.NullableStringA == x.NullableStringB + : x.NullableStringA != x.NullableStringB)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_negative(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(x => (x.NullableStringA == null || x.NullableStringB == null) && x.NullableBoolC == null + ? x.NullableStringA == x.NullableStringB + : x.NullableStringA != x.NullableStringB)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_with_setup(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(x => x.NullableBoolA == null + || (x.NullableBoolB == null + ? x.NullableBoolB != x.NullableBoolA + : x.NullableBoolA != x.NullableBoolB))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_nested(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(x => x.NullableBoolA == null + ? x.BoolA == x.BoolB + : (x.NullableBoolC == null + ? x.NullableBoolA != x.NullableBoolC + : x.NullableBoolC != x.NullableBoolA))); + } + private string NormalizeDelimitersInRawString(string sql) => Fixture.TestStore.NormalizeDelimitersInRawString(sql); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs index 037c81e9708..c581f85d460 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs @@ -1648,28 +1648,34 @@ public virtual Task IsNullOrEmpty_in_predicate(bool async) entryCount: 60); } - [ConditionalFact] - public virtual void IsNullOrEmpty_in_projection() + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task IsNullOrEmpty_in_projection(bool async) { - using var context = CreateContext(); - var query = context.Set() - .Select( - c => new { Id = c.CustomerID, Value = string.IsNullOrEmpty(c.Region) }) - .ToList(); - - Assert.Equal(91, query.Count); + return AssertQuery( + async, + ss => ss.Set().Select(c => new { Id = c.CustomerID, Value = string.IsNullOrEmpty(c.Region) }), + elementSorter: e => e.Id); } - [ConditionalFact] - public virtual void IsNullOrEmpty_negated_in_projection() + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task IsNullOrEmpty_negated_in_predicate(bool async) { - using var context = CreateContext(); - var query = context.Set() - .Select( - c => new { Id = c.CustomerID, Value = !string.IsNullOrEmpty(c.Region) }) - .ToList(); + return AssertQuery( + async, + ss => ss.Set().Where(c => !string.IsNullOrEmpty(c.Region)), + entryCount: 31); + } - Assert.Equal(91, query.Count); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task IsNullOrEmpty_negated_in_projection(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Select(c => new { Id = c.CustomerID, Value = !string.IsNullOrEmpty(c.Region) }), + elementSorter: e => e.Id); } [ConditionalTheory] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs index 680cf207440..83f8d5ff2b4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs @@ -1626,9 +1626,9 @@ FROM [Customers] AS [c] WHERE [c].[Region] IS NULL OR ([c].[Region] LIKE N'')"); } - public override void IsNullOrEmpty_in_projection() + public override async Task IsNullOrEmpty_in_projection(bool async) { - base.IsNullOrEmpty_in_projection(); + await base.IsNullOrEmpty_in_projection(async); AssertSql( @"SELECT [c].[CustomerID] AS [Id], CASE @@ -1638,13 +1638,23 @@ END AS [Value] FROM [Customers] AS [c]"); } - public override void IsNullOrEmpty_negated_in_projection() + public override async Task IsNullOrEmpty_negated_in_predicate(bool async) { - base.IsNullOrEmpty_negated_in_projection(); + await base.IsNullOrEmpty_negated_in_predicate(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[Region] IS NOT NULL AND NOT ([c].[Region] LIKE N'')"); + } + + public override async Task IsNullOrEmpty_negated_in_projection(bool async) + { + await base.IsNullOrEmpty_negated_in_projection(async); AssertSql( @"SELECT [c].[CustomerID] AS [Id], CASE - WHEN NOT ([c].[Region] IS NULL OR ([c].[Region] LIKE N'')) THEN CAST(1 AS bit) + WHEN [c].[Region] IS NOT NULL AND NOT ([c].[Region] LIKE N'') THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END AS [Value] FROM [Customers] AS [c]"); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs index f6e48ec4d1b..086621ff24e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs @@ -2120,6 +2120,149 @@ ELSE CAST(0 AS bit) END"); } + public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_simple(bool async) + { + await base.Is_null_on_column_followed_by_OrElse_optimizes_nullability_simple(async); + + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE [e].[NullableStringA] IS NOT NULL AND ([e].[NullableStringA] = N'Foo')"); + } + + public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_negative(bool async) + { + await base.Is_null_on_column_followed_by_OrElse_optimizes_nullability_negative(async); + + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE [e].[NullableStringA] IS NOT NULL OR (([e].[NullableStringA] = N'Foo') AND [e].[NullableStringA] IS NOT NULL)"); + } + + public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_nested(bool async) + { + await base.Is_null_on_column_followed_by_OrElse_optimizes_nullability_nested(async); + + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE ([e].[NullableStringA] IS NULL OR [e].[NullableStringB] IS NULL) OR ([e].[NullableStringA] <> [e].[NullableStringB])"); + } + + public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_intersection(bool async) + { + await base.Is_null_on_column_followed_by_OrElse_optimizes_nullability_intersection(async); + + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE ([e].[NullableStringA] IS NULL AND ((([e].[StringA] = N'Foo') OR [e].[NullableStringA] IS NULL) OR [e].[NullableStringB] IS NULL)) OR (([e].[NullableStringA] <> [e].[NullableStringB]) OR [e].[NullableStringB] IS NULL)"); + } + + public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional(bool async) + { + await base.Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional(async); + + // issue #25977 + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE CASE + WHEN [e].[NullableStringA] IS NULL THEN CASE + WHEN (([e].[NullableStringA] <> [e].[NullableStringB]) OR ([e].[NullableStringA] IS NULL OR [e].[NullableStringB] IS NULL)) AND ([e].[NullableStringA] IS NOT NULL OR [e].[NullableStringB] IS NOT NULL) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END + ELSE CASE + WHEN (([e].[NullableStringA] <> [e].[NullableStringC]) OR ([e].[NullableStringA] IS NULL OR [e].[NullableStringC] IS NULL)) AND ([e].[NullableStringA] IS NOT NULL OR [e].[NullableStringC] IS NOT NULL) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END +END = CAST(1 AS bit)"); + } + + public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_multiple(bool async) + { + await base.Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_multiple(async); + + // issue #25977 + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE CASE + WHEN [e].[NullableStringA] IS NULL OR [e].[NullableStringB] IS NULL THEN CASE + WHEN (([e].[NullableStringA] = [e].[NullableStringB]) AND ([e].[NullableStringA] IS NOT NULL AND [e].[NullableStringB] IS NOT NULL)) OR ([e].[NullableStringA] IS NULL AND [e].[NullableStringB] IS NULL) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END + ELSE CASE + WHEN (([e].[NullableStringA] <> [e].[NullableStringB]) OR ([e].[NullableStringA] IS NULL OR [e].[NullableStringB] IS NULL)) AND ([e].[NullableStringA] IS NOT NULL OR [e].[NullableStringB] IS NOT NULL) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END +END = CAST(1 AS bit)"); + } + + public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_negative(bool async) + { + await base.Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_negative(async); + + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE CASE + WHEN ([e].[NullableStringA] IS NULL OR [e].[NullableStringB] IS NULL) AND ([e].[NullableBoolC] IS NULL) THEN CASE + WHEN (([e].[NullableStringA] = [e].[NullableStringB]) AND ([e].[NullableStringA] IS NOT NULL AND [e].[NullableStringB] IS NOT NULL)) OR ([e].[NullableStringA] IS NULL AND [e].[NullableStringB] IS NULL) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END + ELSE CASE + WHEN (([e].[NullableStringA] <> [e].[NullableStringB]) OR ([e].[NullableStringA] IS NULL OR [e].[NullableStringB] IS NULL)) AND ([e].[NullableStringA] IS NOT NULL OR [e].[NullableStringB] IS NOT NULL) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END +END = CAST(1 AS bit)"); + } + + public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_with_setup(bool async) + { + await base.Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_with_setup(async); + + // issue #25977 + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE ([e].[NullableBoolA] IS NULL) OR (CASE + WHEN [e].[NullableBoolB] IS NULL THEN CASE + WHEN ([e].[NullableBoolB] <> [e].[NullableBoolA]) OR ([e].[NullableBoolB] IS NULL) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END + ELSE CASE + WHEN ([e].[NullableBoolA] <> [e].[NullableBoolB]) OR ([e].[NullableBoolB] IS NULL) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END +END = CAST(1 AS bit))"); + } + + public override async Task Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_nested(bool async) + { + await base.Is_null_on_column_followed_by_OrElse_optimizes_nullability_conditional_nested(async); + + // issue #25977 + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE CASE + WHEN [e].[NullableBoolA] IS NULL THEN CASE + WHEN [e].[BoolA] = [e].[BoolB] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END + WHEN [e].[NullableBoolC] IS NULL THEN CASE + WHEN (([e].[NullableBoolA] <> [e].[NullableBoolC]) OR (([e].[NullableBoolA] IS NULL) OR ([e].[NullableBoolC] IS NULL))) AND (([e].[NullableBoolA] IS NOT NULL) OR ([e].[NullableBoolC] IS NOT NULL)) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END + ELSE CASE + WHEN (([e].[NullableBoolC] <> [e].[NullableBoolA]) OR (([e].[NullableBoolC] IS NULL) OR ([e].[NullableBoolA] IS NULL))) AND (([e].[NullableBoolC] IS NOT NULL) OR ([e].[NullableBoolA] IS NOT NULL)) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END +END = CAST(1 AS bit)"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs index c81ec422dd8..54579cafef4 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs @@ -617,6 +617,35 @@ public override async Task Regex_IsMatch_MethodCall_constant_input(bool async) WHERE regexp(""c"".""CustomerID"", 'ALFKI')"); } + public override async Task IsNullOrEmpty_in_predicate(bool async) + { + await base.IsNullOrEmpty_in_predicate(async); + + AssertSql( + @"SELECT ""c"".""CustomerID"", ""c"".""Address"", ""c"".""City"", ""c"".""CompanyName"", ""c"".""ContactName"", ""c"".""ContactTitle"", ""c"".""Country"", ""c"".""Fax"", ""c"".""Phone"", ""c"".""PostalCode"", ""c"".""Region"" +FROM ""Customers"" AS ""c"" +WHERE ""c"".""Region"" IS NULL OR (""c"".""Region"" = '')"); + } + + public override async Task IsNullOrEmpty_in_projection(bool async) + { + await base.IsNullOrEmpty_in_projection(async); + + AssertSql( + @"SELECT ""c"".""CustomerID"" AS ""Id"", ""c"".""Region"" IS NULL OR (""c"".""Region"" = '') AS ""Value"" +FROM ""Customers"" AS ""c"""); + } + + public override async Task IsNullOrEmpty_negated_in_predicate(bool async) + { + await base.IsNullOrEmpty_negated_in_predicate(async); + + AssertSql( + @"SELECT ""c"".""CustomerID"", ""c"".""Address"", ""c"".""City"", ""c"".""CompanyName"", ""c"".""ContactName"", ""c"".""ContactTitle"", ""c"".""Country"", ""c"".""Fax"", ""c"".""Phone"", ""c"".""PostalCode"", ""c"".""Region"" +FROM ""Customers"" AS ""c"" +WHERE ""c"".""Region"" IS NOT NULL AND (""c"".""Region"" <> '')"); + } + public override Task Datetime_subtraction_TotalDays(bool async) { return AssertTranslationFailed(() => base.Datetime_subtraction_TotalDays(async));