From b77d2f4f221fb7edd863deeb0dddc962ecb0ace5 Mon Sep 17 00:00:00 2001 From: Andrea Canciani Date: Fri, 31 May 2024 05:21:33 +0200 Subject: [PATCH] Fix comparison of nullable values (#33757) Fix comparison of nullable values In C# an ordered comparison (<, >, <=, >=) between two nullable values always returns a boolean value: if either operand is null, the result is false; otherwise, the result is that of the comparison of the (non-null) values. Fixes #33752 --- .../Query/SqlNullabilityProcessor.cs | 22 +++ .../SqliteStringMethodTranslator.cs | 21 ++- .../NorthwindFunctionsQueryCosmosTest.cs | 38 +++++ .../Query/NullSemanticsQueryTestBase.cs | 5 +- .../Query/NorthwindFunctionsQueryTestBase.cs | 23 +++ .../ComplexNavigationsQuerySqlServerTest.cs | 4 +- ...NavigationsSharedTypeQuerySqlServerTest.cs | 4 +- .../NorthwindFunctionsQuerySqlServerTest.cs | 40 ++++++ .../Query/NullSemanticsQuerySqlServerTest.cs | 55 ++++++- .../Query/FromSqlQuerySqliteTest.cs | 4 +- .../Query/FunkyDataQuerySqliteTest.cs | 2 +- .../Query/GearsOfWarQuerySqliteTest.cs | 23 +-- .../NorthwindFunctionsQuerySqliteTest.cs | 51 ++++++- .../NorthwindMiscellaneousQuerySqliteTest.cs | 2 +- .../Query/NullSemanticsQuerySqliteTest.cs | 135 ++++++++++++++++++ .../Query/SpatialQuerySqliteTest.cs | 5 +- 16 files changed, 396 insertions(+), 38 deletions(-) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index c17c378b096..c371c7622ba 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -1327,6 +1327,28 @@ protected virtual SqlExpression VisitSqlBinary( nullable = leftNullable || rightNullable; var result = sqlBinaryExpression.Update(left, right); + if (nullable && !optimize && result.OperatorType + is ExpressionType.GreaterThan + or ExpressionType.GreaterThanOrEqual + or ExpressionType.LessThan + or ExpressionType.LessThanOrEqual) + { + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types#lifted-operators + // For the comparison operators <, >, <=, and >=, if one or both + // operands are null, the result is false; otherwise, the contained + // values of operands are compared. + + // if either operand is NULL, the SQL comparison would return NULL; + // to match the C# semantics, replace expr with + // CASE WHEN expr THEN TRUE ELSE FALSE + + nullable = false; + return _sqlExpressionFactory.Case( + [new(result, _sqlExpressionFactory.Constant(true, result.TypeMapping))], + _sqlExpressionFactory.Constant(false, result.TypeMapping) + ); + } + return result is SqlBinaryExpression sqlBinaryResult && sqlBinaryExpression.OperatorType is ExpressionType.AndAlso or ExpressionType.OrElse ? SimplifyLogicalSqlBinaryExpression(sqlBinaryResult) diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteStringMethodTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteStringMethodTranslator.cs index 5c79249748b..709122ef33e 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteStringMethodTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteStringMethodTranslator.cs @@ -207,20 +207,15 @@ public SqliteStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) instance = _sqlExpressionFactory.ApplyTypeMapping(instance, stringTypeMapping); pattern = _sqlExpressionFactory.ApplyTypeMapping(pattern, stringTypeMapping); - // Note: we add IS NOT NULL checks here since we don't do null semantics/compensation for comparison (greater-than) return - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(instance), - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(pattern), - _sqlExpressionFactory.GreaterThan( - _sqlExpressionFactory.Function( - "instr", - new[] { instance, pattern }, - nullable: true, - argumentsPropagateNullability: new[] { true, true }, - typeof(int)), - _sqlExpressionFactory.Constant(0)))); + _sqlExpressionFactory.GreaterThan( + _sqlExpressionFactory.Function( + "instr", + new[] { instance, pattern }, + nullable: true, + argumentsPropagateNullability: new[] { true, true }, + typeof(int)), + _sqlExpressionFactory.Constant(0)); } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs index c65d2341d05..424a74feccb 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs @@ -1890,6 +1890,44 @@ FROM root c """); }); + public override Task String_Contains_negated_in_predicate(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.String_Contains_negated_in_predicate(a); + + AssertSql( +""" +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Customer") AND NOT(CONTAINS(c["CompanyName"], c["ContactName"]))) +"""); + }); + + public override Task String_Contains_negated_in_projection(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.String_Contains_negated_in_projection(a); + + AssertSql( +""" +SELECT VALUE {"Id" : c["CustomerID"], "Value" : NOT(CONTAINS(c["CompanyName"], c["ContactName"]))} +FROM root c +WHERE (c["Discriminator"] = "Customer") +"""); + }); + + [ConditionalTheory(Skip = "issue #33858")] + public override Task String_Contains_in_projection(bool async) + => Fixture.NoSyncTest( + async, async a => + { + await base.String_Contains_in_projection(a); + + AssertSql(""); + }); + public override Task String_Join_over_non_nullable_column(bool async) => AssertTranslationFailed(() => base.String_Join_over_non_nullable_column(async)); diff --git a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs index ae9b5725c84..f2101c0b6ec 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs @@ -1066,6 +1066,9 @@ await AssertQueryScalar( e => (e.BoolA ? e.NullableBoolA != e.NullableBoolB : e.BoolC) != e.BoolB ? e.BoolA : e.NullableBoolB == e.NullableBoolC).Select(e => e.Id)); + await AssertQueryScalar( + async, + ss => ss.Set().Select(e => (e.BoolA ? e.NullableIntA : e.IntB) > e.IntC)); } [ConditionalTheory] @@ -1558,7 +1561,7 @@ public virtual async Task Negated_order_comparison_on_non_nullable_arguments_get await AssertQueryScalar(async, ss => ss.Set().Where(e => !(e.IntA <= i)).Select(e => e.Id)); } - [ConditionalTheory(Skip = "issue #9544")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Negated_order_comparison_on_nullable_arguments_doesnt_get_optimized(bool async) { diff --git a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs index b21d0d36be6..40791fdab34 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs @@ -133,6 +133,29 @@ public virtual Task String_Contains_Column(bool async) async, ss => ss.Set().Where(c => c.ContactName.Contains(c.ContactName))); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task String_Contains_in_projection(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(c => new { Id = c.CustomerID, Value = c.CompanyName.Contains(c.ContactName) }), + elementSorter: e => e.Id); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task String_Contains_negated_in_predicate(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => !c.CompanyName.Contains(c.ContactName))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task String_Contains_negated_in_projection(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(c => new { Id = c.CustomerID, Value = !c.CompanyName.Contains(c.ContactName) }), + elementSorter: e => e.Id); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task String_FirstOrDefault_MethodCall(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index fbac77b43ee..76a2eae5416 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -4126,10 +4126,10 @@ INNER JOIN ( FROM [LevelTwo] AS [l0] ) AS [l1] GROUP BY [l1].[Key] -) AS [l2] ON [l].[Id] = [l2].[Key] AND CAST(0 AS bit) = CASE +) AS [l2] ON [l].[Id] = [l2].[Key] AND CASE WHEN [l2].[Sum] <= 10 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) -END +END = CAST(0 AS bit) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index 92936948f55..bc8b3af0b95 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs @@ -974,10 +974,10 @@ WHEN [l2].[OneToOne_Required_PK_Date] IS NOT NULL AND [l2].[Level1_Required_Id] WHERE [l2].[OneToOne_Required_PK_Date] IS NOT NULL AND [l2].[Level1_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse2Id] IS NOT NULL ) AS [s] GROUP BY [s].[Key] -) AS [s1] ON [l].[Id] = [s1].[Key] AND CAST(0 AS bit) = CASE +) AS [s1] ON [l].[Id] = [s1].[Key] AND CASE WHEN [s1].[Sum] <= 10 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) -END +END = CAST(0 AS bit) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs index ae4e0c9cbdd..ec711fabc35 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs @@ -2750,6 +2750,46 @@ ELSE CHARINDEX(CONVERT(varchar(11), [o].[OrderID]), '123') - 1 """); } + public override async Task String_Contains_in_projection(bool async) + { + await base.String_Contains_in_projection(async); + + AssertSql( +""" +SELECT [c].[CustomerID] AS [Id], CASE + WHEN [c].[ContactName] IS NOT NULL AND (CHARINDEX([c].[ContactName], [c].[CompanyName]) > 0 OR [c].[ContactName] LIKE N'') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Value] +FROM [Customers] AS [c] +"""); + } + + public override async Task String_Contains_negated_in_predicate(bool async) + { + await base.String_Contains_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].[ContactName] IS NULL OR (CHARINDEX([c].[ContactName], [c].[CompanyName]) <= 0 AND [c].[ContactName] NOT LIKE N'') +"""); + } + + public override async Task String_Contains_negated_in_projection(bool async) + { + await base.String_Contains_negated_in_projection(async); + + AssertSql( +""" +SELECT [c].[CustomerID] AS [Id], CASE + WHEN [c].[ContactName] IS NULL OR (CHARINDEX([c].[ContactName], [c].[CompanyName]) <= 0 AND [c].[ContactName] NOT LIKE N'') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Value] +FROM [Customers] AS [c] +"""); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task StandardDeviation(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs index 524521ac452..24575897ce7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs @@ -1757,6 +1757,17 @@ ELSE CASE ELSE CAST(0 AS bit) END END = CAST(1 AS bit) +""", + // + """ +SELECT CASE + WHEN CASE + WHEN [e].[BoolA] = CAST(1 AS bit) THEN [e].[NullableIntA] + ELSE [e].[IntB] + END > [e].[IntC] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Entities1] AS [e] """); } @@ -2518,7 +2529,49 @@ public override async Task Negated_order_comparison_on_nullable_arguments_doesnt await base.Negated_order_comparison_on_nullable_arguments_doesnt_get_optimized(async); AssertSql( - @""); + """ +@__i_0='1' (Nullable = true) + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE CASE + WHEN [e].[NullableIntA] > @__i_0 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = CAST(0 AS bit) +""", + // + """ +@__i_0='1' (Nullable = true) + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE CASE + WHEN [e].[NullableIntA] >= @__i_0 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = CAST(0 AS bit) +""", + // + """ +@__i_0='1' (Nullable = true) + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE CASE + WHEN [e].[NullableIntA] < @__i_0 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = CAST(0 AS bit) +""", + // + """ +@__i_0='1' (Nullable = true) + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE CASE + WHEN [e].[NullableIntA] <= @__i_0 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = CAST(0 AS bit) +"""); } public override async Task Nullable_column_info_propagates_inside_binary_AndAlso(bool async) diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/FromSqlQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/FromSqlQuerySqliteTest.cs index 4519fb9c661..309f4bdfd22 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/FromSqlQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/FromSqlQuerySqliteTest.cs @@ -25,7 +25,7 @@ public override async Task FromSqlRaw_queryable_composed(bool async) FROM ( SELECT * FROM "Customers" ) AS "m" -WHERE "m"."ContactName" IS NOT NULL AND instr("m"."ContactName", 'z') > 0 +WHERE instr("m"."ContactName", 'z') > 0 """); } @@ -75,7 +75,7 @@ public override async Task FromSqlRaw_composed_with_common_table_expression(bool ) SELECT * FROM "Customers2" ) AS "m" -WHERE "m"."ContactName" IS NOT NULL AND instr("m"."ContactName", 'z') > 0 +WHERE instr("m"."ContactName", 'z') > 0 """); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/FunkyDataQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/FunkyDataQuerySqliteTest.cs index 94573420c87..579dc6ea03c 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/FunkyDataQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/FunkyDataQuerySqliteTest.cs @@ -25,7 +25,7 @@ public override async Task String_Contains_and_StartsWith_with_same_parameter(bo SELECT "f"."Id", "f"."FirstName", "f"."LastName", "f"."NullableBool" FROM "FunkyCustomers" AS "f" -WHERE ("f"."FirstName" IS NOT NULL AND instr("f"."FirstName", @__s_0) > 0) OR "f"."LastName" LIKE @__s_0_startswith ESCAPE '\' +WHERE instr("f"."FirstName", @__s_0) > 0 OR "f"."LastName" LIKE @__s_0_startswith ESCAPE '\' """); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 84c20f2a955..87817d139b6 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -1178,7 +1178,7 @@ SELECT COALESCE(( FROM ( SELECT DISTINCT "w"."Id", "w"."AmmunitionType", "w"."IsAutomatic", "w"."Name", "w"."OwnerFullName", "w"."SynergyWithId" FROM "Weapons" AS "w" - WHERE "g"."FullName" = "w"."OwnerFullName" AND "w"."Name" IS NOT NULL AND instr("w"."Name", 'Lancer') > 0 + WHERE "g"."FullName" = "w"."OwnerFullName" AND instr("w"."Name", 'Lancer') > 0 ) AS "w0" LIMIT 1), 0) FROM "Gears" AS "g" @@ -2342,7 +2342,7 @@ public override async Task Where_subquery_distinct_singleordefault_boolean2(bool WHERE "g"."HasSoulPatch" AND COALESCE(( SELECT DISTINCT "w"."IsAutomatic" FROM "Weapons" AS "w" - WHERE "g"."FullName" = "w"."OwnerFullName" AND "w"."Name" IS NOT NULL AND instr("w"."Name", 'Lancer') > 0 + WHERE "g"."FullName" = "w"."OwnerFullName" AND instr("w"."Name", 'Lancer') > 0 LIMIT 1), 0) ORDER BY "g"."Nickname" """); @@ -3049,7 +3049,7 @@ public override async Task Where_subquery_distinct_singleordefault_boolean_with_ FROM ( SELECT DISTINCT "w"."Id", "w"."AmmunitionType", "w"."IsAutomatic", "w"."Name", "w"."OwnerFullName", "w"."SynergyWithId" FROM "Weapons" AS "w" - WHERE "g"."FullName" = "w"."OwnerFullName" AND "w"."Name" IS NOT NULL AND instr("w"."Name", 'Lancer') > 0 + WHERE "g"."FullName" = "w"."OwnerFullName" AND instr("w"."Name", 'Lancer') > 0 ) AS "w0" LIMIT 1) ORDER BY "g"."Nickname" @@ -3252,7 +3252,7 @@ public override async Task Where_subquery_distinct_singleordefault_boolean1(bool FROM ( SELECT DISTINCT "w"."Id", "w"."AmmunitionType", "w"."IsAutomatic", "w"."Name", "w"."OwnerFullName", "w"."SynergyWithId" FROM "Weapons" AS "w" - WHERE "g"."FullName" = "w"."OwnerFullName" AND "w"."Name" IS NOT NULL AND instr("w"."Name", 'Lancer') > 0 + WHERE "g"."FullName" = "w"."OwnerFullName" AND instr("w"."Name", 'Lancer') > 0 ) AS "w0" LIMIT 1), 0) ORDER BY "g"."Nickname" @@ -3906,7 +3906,7 @@ public override async Task Non_unicode_string_literals_is_used_for_non_unicode_c """ SELECT "c"."Name", "c"."Location", "c"."Nation" FROM "Cities" AS "c" -WHERE "c"."Location" IS NOT NULL AND instr("c"."Location", 'Jacinto') > 0 +WHERE instr("c"."Location", 'Jacinto') > 0 """); } @@ -4821,7 +4821,7 @@ public override async Task Select_subquery_distinct_singleordefault_boolean2(boo SELECT COALESCE(( SELECT DISTINCT "w"."IsAutomatic" FROM "Weapons" AS "w" - WHERE "g"."FullName" = "w"."OwnerFullName" AND "w"."Name" IS NOT NULL AND instr("w"."Name", 'Lancer') > 0 + WHERE "g"."FullName" = "w"."OwnerFullName" AND instr("w"."Name", 'Lancer') > 0 LIMIT 1), 0) FROM "Gears" AS "g" WHERE "g"."HasSoulPatch" @@ -5047,7 +5047,10 @@ public override async Task Optional_navigation_type_compensation_works_with_bina AssertSql( """ -SELECT "g"."HasSoulPatch" AND "t"."Note" IS NOT NULL AND instr("t"."Note", 'Cole') > 0 +SELECT "g"."HasSoulPatch" AND CASE + WHEN instr("t"."Note", 'Cole') > 0 THEN 1 + ELSE 0 +END FROM "Tags" AS "t" LEFT JOIN "Gears" AS "g" ON "t"."GearNickName" = "g"."Nickname" AND "t"."GearSquadId" = "g"."SquadId" """); @@ -7235,7 +7238,7 @@ public override async Task Optional_navigation_type_compensation_works_with_bina SELECT "t"."Id", "t"."GearNickName", "t"."GearSquadId", "t"."IssueDate", "t"."Note" FROM "Tags" AS "t" LEFT JOIN "Gears" AS "g" ON "t"."GearNickName" = "g"."Nickname" AND "t"."GearSquadId" = "g"."SquadId" -WHERE "g"."HasSoulPatch" OR ("t"."Note" IS NOT NULL AND instr("t"."Note", 'Cole') > 0) +WHERE "g"."HasSoulPatch" OR instr("t"."Note", 'Cole') > 0 """); } @@ -7331,7 +7334,7 @@ public override async Task Select_subquery_distinct_singleordefault_boolean_with FROM ( SELECT DISTINCT "w"."Id", "w"."AmmunitionType", "w"."IsAutomatic", "w"."Name", "w"."OwnerFullName", "w"."SynergyWithId" FROM "Weapons" AS "w" - WHERE "g"."FullName" = "w"."OwnerFullName" AND "w"."Name" IS NOT NULL AND instr("w"."Name", 'Lancer') > 0 + WHERE "g"."FullName" = "w"."OwnerFullName" AND instr("w"."Name", 'Lancer') > 0 ) AS "w0" LIMIT 1) FROM "Gears" AS "g" @@ -8695,7 +8698,7 @@ public override async Task Include_with_complex_order_by(bool async) ORDER BY ( SELECT "w"."Name" FROM "Weapons" AS "w" - WHERE "g"."FullName" = "w"."OwnerFullName" AND "w"."Name" IS NOT NULL AND instr("w"."Name", 'Gnasher') > 0 + WHERE "g"."FullName" = "w"."OwnerFullName" AND instr("w"."Name", 'Gnasher') > 0 LIMIT 1), "g"."Nickname", "g"."SquadId" """); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs index 431a781be27..704235085bd 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs @@ -828,7 +828,7 @@ public override async Task String_Contains_Literal(bool async) """ 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"."ContactName" IS NOT NULL AND instr("c"."ContactName", 'M') > 0 +WHERE instr("c"."ContactName", 'M') > 0 """); } @@ -840,7 +840,7 @@ public override async Task String_Contains_Identity(bool async) """ 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"."ContactName" IS NOT NULL AND instr("c"."ContactName", "c"."ContactName") > 0 +WHERE instr("c"."ContactName", "c"."ContactName") > 0 """); } @@ -852,7 +852,50 @@ public override async Task String_Contains_Column(bool async) """ 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"."ContactName" IS NOT NULL AND instr("c"."ContactName", "c"."ContactName") > 0 +WHERE instr("c"."ContactName", "c"."ContactName") > 0 +"""); + } + + public override async Task String_Contains_in_projection(bool async) + { + await base.String_Contains_in_projection(async); + + AssertSql( + """ +SELECT "c"."CustomerID" AS "Id", CASE + WHEN instr("c"."CompanyName", "c"."ContactName") > 0 THEN 1 + ELSE 0 +END AS "Value" +FROM "Customers" AS "c" +"""); + } + + public override async Task String_Contains_negated_in_predicate(bool async) + { + await base.String_Contains_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 NOT (CASE + WHEN instr("c"."CompanyName", "c"."ContactName") > 0 THEN 1 + ELSE 0 +END) +"""); + } + + public override async Task String_Contains_negated_in_projection(bool async) + { + await base.String_Contains_negated_in_projection(async); + + AssertSql( + """ +SELECT "c"."CustomerID" AS "Id", NOT (CASE + WHEN instr("c"."CompanyName", "c"."ContactName") > 0 THEN 1 + ELSE 0 +END) AS "Value" +FROM "Customers" AS "c" """); } @@ -886,7 +929,7 @@ public override async Task String_Contains_MethodCall(bool async) """ 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"."ContactName" IS NOT NULL AND instr("c"."ContactName", 'M') > 0 +WHERE instr("c"."ContactName", 'M') > 0 """); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs index 12d30d3ccbe..c9351cae83a 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs @@ -28,7 +28,7 @@ public override async Task Query_expression_with_to_string_and_contains(bool asy """ SELECT "o"."CustomerID" FROM "Orders" AS "o" -WHERE "o"."OrderDate" IS NOT NULL AND "o"."EmployeeID" IS NOT NULL AND instr(CAST("o"."EmployeeID" AS TEXT), '7') > 0 +WHERE "o"."OrderDate" IS NOT NULL AND instr(CAST("o"."EmployeeID" AS TEXT), '7') > 0 """); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NullSemanticsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NullSemanticsQuerySqliteTest.cs index 3ef14c2ba98..6036dbdc033 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NullSemanticsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NullSemanticsQuerySqliteTest.cs @@ -41,6 +41,53 @@ public override async Task Join_uses_csharp_semantics_for_anon_objects(bool asyn """); } + public override async Task Null_semantics_conditional(bool async) + { + await base.Null_semantics_conditional(async); + + AssertSql( + """ +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE "e"."BoolA" = CASE + WHEN "e"."BoolB" THEN "e"."NullableBoolB" + ELSE "e"."NullableBoolC" +END +""", + // + """ +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE CASE + WHEN ("e"."NullableBoolA" <> "e"."NullableBoolB" OR "e"."NullableBoolA" IS NULL OR "e"."NullableBoolB" IS NULL) AND ("e"."NullableBoolA" IS NOT NULL OR "e"."NullableBoolB" IS NOT NULL) THEN "e"."BoolB" + ELSE "e"."BoolC" +END = "e"."BoolA" +""", + // + """ +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE CASE + WHEN CASE + WHEN "e"."BoolA" THEN ("e"."NullableBoolA" <> "e"."NullableBoolB" OR "e"."NullableBoolA" IS NULL OR "e"."NullableBoolB" IS NULL) AND ("e"."NullableBoolA" IS NOT NULL OR "e"."NullableBoolB" IS NOT NULL) + ELSE "e"."BoolC" + END <> "e"."BoolB" THEN "e"."BoolA" + ELSE ("e"."NullableBoolB" = "e"."NullableBoolC" AND "e"."NullableBoolB" IS NOT NULL AND "e"."NullableBoolC" IS NOT NULL) OR ("e"."NullableBoolB" IS NULL AND "e"."NullableBoolC" IS NULL) +END +""", + // + """ +SELECT CASE + WHEN CASE + WHEN "e"."BoolA" THEN "e"."NullableIntA" + ELSE "e"."IntB" + END > "e"."IntC" THEN 1 + ELSE 0 +END +FROM "Entities1" AS "e" +"""); + } + public override async Task Null_semantics_contains_non_nullable_item_with_non_nullable_subquery(bool async) { await base.Null_semantics_contains_non_nullable_item_with_non_nullable_subquery(async); @@ -350,6 +397,94 @@ WHERE 0 """); } + public override async Task Negated_order_comparison_on_non_nullable_arguments_gets_optimized(bool async) + { + await base.Negated_order_comparison_on_non_nullable_arguments_gets_optimized(async); + + AssertSql( + """ +@__i_0='1' + +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE "e"."IntA" <= @__i_0 +""", + // + """ +@__i_0='1' + +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE "e"."IntA" < @__i_0 +""", + // + """ +@__i_0='1' + +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE "e"."IntA" >= @__i_0 +""", + // + """ +@__i_0='1' + +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE "e"."IntA" > @__i_0 +"""); + } + + public override async Task Negated_order_comparison_on_nullable_arguments_doesnt_get_optimized(bool async) + { + await base.Negated_order_comparison_on_nullable_arguments_doesnt_get_optimized(async); + + AssertSql( + """ +@__i_0='1' (Nullable = true) + +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE NOT (CASE + WHEN "e"."NullableIntA" > @__i_0 THEN 1 + ELSE 0 +END) +""", + // + """ +@__i_0='1' (Nullable = true) + +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE NOT (CASE + WHEN "e"."NullableIntA" >= @__i_0 THEN 1 + ELSE 0 +END) +""", + // + """ +@__i_0='1' (Nullable = true) + +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE NOT (CASE + WHEN "e"."NullableIntA" < @__i_0 THEN 1 + ELSE 0 +END) +""", + // + """ +@__i_0='1' (Nullable = true) + +SELECT "e"."Id" +FROM "Entities1" AS "e" +WHERE NOT (CASE + WHEN "e"."NullableIntA" <= @__i_0 THEN 1 + ELSE 0 +END) +"""); + } + public override async Task Comparison_compared_to_null_check_on_bool(bool async) { await base.Comparison_compared_to_null_check_on_bool(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs index c55d767ef47..8fd9e52daed 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs @@ -692,7 +692,10 @@ public override async Task IsWithinDistance(bool async) """ @__point_0='0x0001000000000000000000000000000000000000F03F00000000000000000000...' (Size = 60) (DbType = String) -SELECT "p"."Id", Distance("p"."Point", @__point_0) <= 1.0 AS "IsWithinDistance" +SELECT "p"."Id", CASE + WHEN Distance("p"."Point", @__point_0) <= 1.0 THEN 1 + ELSE 0 +END AS "IsWithinDistance" FROM "PointEntity" AS "p" """); }