diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerObjectToStringTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerObjectToStringTranslator.cs index 67baec12504..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, @@ -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..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, @@ -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( diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index efb32442eaa..86a49c9c431 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -84,13 +84,20 @@ 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( 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 7ca1b124855..0a1f82f4c2d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -3988,7 +3988,26 @@ 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] +"""); + } + + [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] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index fb3d5eb4945..0579ce86f3a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -12638,7 +12638,26 @@ 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] +"""); + } + + [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] """); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 634064306f1..ea8825997c5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -10792,7 +10792,27 @@ 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] +"""); + } + + [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] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index a7b9ce28a68..1dcddbbdf2e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -8264,7 +8264,26 @@ 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] +"""); + } + + [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] """); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index cddc6d96f46..5b3204a837a 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -6023,7 +6023,22 @@ 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" +"""); + } + + 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" """);