From 59945a67db5e3d0ce1a0b6e960332606db09cb6e Mon Sep 17 00:00:00 2001 From: Andrea Canciani Date: Tue, 18 Jun 2024 14:10:07 +0200 Subject: [PATCH 1/5] Re-enable `ToString_boolean_property_nullable` test --- .../EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index efb32442eaa..dd0ac089f86 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -84,7 +84,7 @@ public virtual Task ToString_boolean_property_non_nullable(bool async) async, ss => ss.Set().Select(w => w.IsAutomatic.ToString())); - [ConditionalTheory(Skip = "Issue #33941 Nullable.ToString() does not match C#")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task ToString_boolean_property_nullable(bool async) => AssertQuery( From 54571ae96841b31f5c30c95251fdca4b4d2b1186 Mon Sep 17 00:00:00 2001 From: Andrea Canciani Date: Tue, 18 Jun 2024 14:08:39 +0200 Subject: [PATCH 2/5] Fix `bool?` `ToString` conversion Nullable values convert to an empty string. --- .../Internal/Translators/SqlServerObjectToStringTranslator.cs | 2 +- .../Internal/Translators/SqliteObjectToStringTranslator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerObjectToStringTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerObjectToStringTranslator.cs index 67baec12504..c42e864594d 100644 --- a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerObjectToStringTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerObjectToStringTranslator.cs @@ -92,7 +92,7 @@ public SqlServerObjectToStringTranslator(ISqlExpressionFactory sqlExpressionFact _sqlExpressionFactory.Constant(true), _sqlExpressionFactory.Constant(true.ToString())) }, - _sqlExpressionFactory.Constant(null, typeof(string))); + _sqlExpressionFactory.Constant(string.Empty)); } return _sqlExpressionFactory.Case( diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteObjectToStringTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteObjectToStringTranslator.cs index 6a241630b27..3820c42027d 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteObjectToStringTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteObjectToStringTranslator.cs @@ -87,7 +87,7 @@ public SqliteObjectToStringTranslator(ISqlExpressionFactory sqlExpressionFactory _sqlExpressionFactory.Constant(true), _sqlExpressionFactory.Constant(true.ToString())) }, - _sqlExpressionFactory.Constant(null, typeof(string))); + _sqlExpressionFactory.Constant(string.Empty)); } return _sqlExpressionFactory.Case( From 6f10a83e4eb9b214f6cf6274e2c11999ac45c5ec Mon Sep 17 00:00:00 2001 From: Andrea Canciani Date: Tue, 18 Jun 2024 14:08:39 +0200 Subject: [PATCH 3/5] Update tests --- .../Query/GearsOfWarQuerySqlServerTest.cs | 2 +- .../Query/TPCGearsOfWarQuerySqlServerTest.cs | 2 +- .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 2 +- .../Query/TemporalGearsOfWarQuerySqlServerTest.cs | 2 +- .../Query/GearsOfWarQuerySqliteTest.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 7ca1b124855..b908943166e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -3988,7 +3988,7 @@ public override async Task ToString_boolean_property_nullable(bool async) SELECT CASE [f].[Eradicated] WHEN CAST(0 AS bit) THEN N'False' WHEN CAST(1 AS bit) THEN N'True' - ELSE NULL + ELSE N'' END FROM [Factions] AS [f] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index fb3d5eb4945..78d54f833aa 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -12638,7 +12638,7 @@ public override async Task ToString_boolean_property_nullable(bool async) SELECT CASE [l].[Eradicated] WHEN CAST(0 AS bit) THEN N'False' WHEN CAST(1 AS bit) THEN N'True' - ELSE NULL + ELSE N'' END FROM [LocustHordes] AS [l] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 634064306f1..412e5f34091 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -10792,7 +10792,7 @@ public override async Task ToString_boolean_property_nullable(bool async) SELECT CASE [l].[Eradicated] WHEN CAST(0 AS bit) THEN N'False' WHEN CAST(1 AS bit) THEN N'True' - ELSE NULL + ELSE N'' END FROM [Factions] AS [f] INNER JOIN [LocustHordes] AS [l] ON [f].[Id] = [l].[Id] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index a7b9ce28a68..7683a6a38a9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -8264,7 +8264,7 @@ public override async Task ToString_boolean_property_nullable(bool async) SELECT CASE [f].[Eradicated] WHEN CAST(0 AS bit) THEN N'False' WHEN CAST(1 AS bit) THEN N'True' - ELSE NULL + ELSE N'' END FROM [Factions] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [f] """); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index cddc6d96f46..6767dd91377 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -6023,7 +6023,7 @@ public override async Task ToString_boolean_property_nullable(bool async) SELECT CASE "f"."Eradicated" WHEN 0 THEN 'False' WHEN 1 THEN 'True' - ELSE NULL + ELSE '' END FROM "Factions" AS "f" """); From 0afa07661484c83e23455a6fdbcb3edad972313f Mon Sep 17 00:00:00 2001 From: Andrea Canciani Date: Tue, 18 Jun 2024 14:08:40 +0200 Subject: [PATCH 4/5] Add test for conversion from `bool?` to string --- .../Query/GearsOfWarQueryTestBase.cs | 7 +++++++ .../Query/GearsOfWarQuerySqlServerTest.cs | 19 ++++++++++++++++++ .../Query/TPCGearsOfWarQuerySqlServerTest.cs | 19 ++++++++++++++++++ .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 20 +++++++++++++++++++ .../TemporalGearsOfWarQuerySqlServerTest.cs | 19 ++++++++++++++++++ .../Query/GearsOfWarQuerySqliteTest.cs | 15 ++++++++++++++ 6 files changed, 99 insertions(+) diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index dd0ac089f86..86a49c9c431 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -91,6 +91,13 @@ public virtual Task ToString_boolean_property_nullable(bool async) async, ss => ss.Set().Select(lh => lh.Eradicated.ToString())); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task ToString_boolean_computed_nullable(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(lh => (lh.Eradicated | lh.CommanderName == "Unknown").ToString())); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task ToString_enum_property_projection(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index b908943166e..0a1f82f4c2d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -3994,6 +3994,25 @@ FROM [Factions] AS [f] """); } + [ConditionalTheory(Skip = "Issue #34001 SqlServer never returns null for bool?")] + public override async Task ToString_boolean_computed_nullable(bool async) + { + await base.ToString_boolean_computed_nullable(async); + + AssertSql( + """ +SELECT CASE CASE + WHEN NOT ([f].[Eradicated] = CAST(1 AS bit) OR ([f].[CommanderName] = N'Unknown' AND [f].[CommanderName] IS NOT NULL)) THEN CAST(0 AS bit) + WHEN [f].[Eradicated] = CAST(1 AS bit) OR ([f].[CommanderName] = N'Unknown' AND [f].[CommanderName] IS NOT NULL) THEN CAST(1 AS bit) +END + WHEN CAST(0 AS bit) THEN N'False' + WHEN CAST(1 AS bit) THEN N'True' + ELSE N'' +END +FROM [Factions] AS [f] +"""); + } + public override async Task ToString_enum_property_projection(bool async) { await base.ToString_enum_property_projection(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index 78d54f833aa..0579ce86f3a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -12644,6 +12644,25 @@ FROM [LocustHordes] AS [l] """); } + [ConditionalTheory(Skip = "Issue #34001 SqlServer never returns null for bool?")] + public override async Task ToString_boolean_computed_nullable(bool async) + { + await base.ToString_boolean_computed_nullable(async); + + AssertSql( + """ +SELECT CASE CASE + WHEN NOT ([l].[Eradicated] = CAST(1 AS bit) OR ([l].[CommanderName] = N'Unknown' AND [l].[CommanderName] IS NOT NULL)) THEN CAST(0 AS bit) + WHEN [l].[Eradicated] = CAST(1 AS bit) OR ([l].[CommanderName] = N'Unknown' AND [l].[CommanderName] IS NOT NULL) THEN CAST(1 AS bit) +END + WHEN CAST(0 AS bit) THEN N'False' + WHEN CAST(1 AS bit) THEN N'True' + ELSE N'' +END +FROM [LocustHordes] AS [l] +"""); + } + public override async Task Correlated_collection_after_distinct_3_levels(bool async) { await base.Correlated_collection_after_distinct_3_levels(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 412e5f34091..ea8825997c5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -10799,6 +10799,26 @@ FROM [Factions] AS [f] """); } + [ConditionalTheory(Skip = "Issue #34001 SqlServer never returns null for bool?")] + public override async Task ToString_boolean_computed_nullable(bool async) + { + await base.ToString_boolean_computed_nullable(async); + + AssertSql( + """ +SELECT CASE CASE + WHEN NOT ([l].[Eradicated] = CAST(1 AS bit) OR ([l].[CommanderName] = N'Unknown' AND [l].[CommanderName] IS NOT NULL)) THEN CAST(0 AS bit) + WHEN [l].[Eradicated] = CAST(1 AS bit) OR ([l].[CommanderName] = N'Unknown' AND [l].[CommanderName] IS NOT NULL) THEN CAST(1 AS bit) +END + WHEN CAST(0 AS bit) THEN N'False' + WHEN CAST(1 AS bit) THEN N'True' + ELSE N'' +END +FROM [Factions] AS [f] +INNER JOIN [LocustHordes] AS [l] ON [f].[Id] = [l].[Id] +"""); + } + public override async Task Correlated_collection_after_distinct_3_levels(bool async) { await base.Correlated_collection_after_distinct_3_levels(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index 7683a6a38a9..1dcddbbdf2e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -8270,6 +8270,25 @@ ELSE N'' """); } + [ConditionalTheory(Skip = "Issue #34001 SqlServer never returns null for bool?")] + public override async Task ToString_boolean_computed_nullable(bool async) + { + await base.ToString_boolean_computed_nullable(async); + + AssertSql( + """ +SELECT CASE CASE + WHEN NOT ([f].[Eradicated] = CAST(1 AS bit) OR ([f].[CommanderName] = N'Unknown' AND [f].[CommanderName] IS NOT NULL)) THEN CAST(0 AS bit) + WHEN [f].[Eradicated] = CAST(1 AS bit) OR ([f].[CommanderName] = N'Unknown' AND [f].[CommanderName] IS NOT NULL) THEN CAST(1 AS bit) +END + WHEN CAST(0 AS bit) THEN N'False' + WHEN CAST(1 AS bit) THEN N'True' + ELSE N'' +END +FROM [Factions] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [f] +"""); + } + public override async Task OrderBy_same_expression_containing_IsNull_correctly_deduplicates_the_ordering(bool async) { await base.OrderBy_same_expression_containing_IsNull_correctly_deduplicates_the_ordering(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index 6767dd91377..5b3204a837a 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -6029,6 +6029,21 @@ WHEN 1 THEN 'True' """); } + public override async Task ToString_boolean_computed_nullable(bool async) + { + await base.ToString_boolean_computed_nullable(async); + + AssertSql( + """ +SELECT CASE "f"."Eradicated" OR ("f"."CommanderName" = 'Unknown' AND "f"."CommanderName" IS NOT NULL) + WHEN 0 THEN 'False' + WHEN 1 THEN 'True' + ELSE '' +END +FROM "Factions" AS "f" +"""); + } + public override async Task Filtered_collection_projection_with_order_comparison_predicate_converted_to_join3(bool async) { await base.Filtered_collection_projection_with_order_comparison_predicate_converted_to_join3(async); From a2f48ee5996cd98366406abb04d6ba45f4f9d2cf Mon Sep 17 00:00:00 2001 From: Andrea Canciani Date: Tue, 18 Jun 2024 14:08:40 +0200 Subject: [PATCH 5/5] Translate `bool?` expressions as nullable The code was previously only treating nullable `ColumnExpression`s as nullable. --- .../Internal/Translators/SqlServerObjectToStringTranslator.cs | 2 +- .../Internal/Translators/SqliteObjectToStringTranslator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerObjectToStringTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerObjectToStringTranslator.cs index c42e864594d..571840f1752 100644 --- a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerObjectToStringTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerObjectToStringTranslator.cs @@ -79,7 +79,7 @@ public SqlServerObjectToStringTranslator(ISqlExpressionFactory sqlExpressionFact if (instance.Type == typeof(bool)) { - if (instance is ColumnExpression { IsNullable: true }) + if (instance is not ColumnExpression { IsNullable: false }) { return _sqlExpressionFactory.Case( instance, diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteObjectToStringTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteObjectToStringTranslator.cs index 3820c42027d..e55d3d993b4 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteObjectToStringTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteObjectToStringTranslator.cs @@ -74,7 +74,7 @@ public SqliteObjectToStringTranslator(ISqlExpressionFactory sqlExpressionFactory if (instance.Type == typeof(bool)) { - if (instance is ColumnExpression { IsNullable: true }) + if (instance is not ColumnExpression { IsNullable: false }) { return _sqlExpressionFactory.Case( instance,