diff --git a/src/EFCore.PG/Query/Internal/NonQueryConvertingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NonQueryConvertingExpressionVisitor.cs index 87fdac3ef6..9881a89481 100644 --- a/src/EFCore.PG/Query/Internal/NonQueryConvertingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NonQueryConvertingExpressionVisitor.cs @@ -27,7 +27,8 @@ protected virtual Expression VisitDelete(DeleteExpression deleteExpression) || selectExpression.GroupBy.Count > 0 || selectExpression.Projection.Count > 0) { - throw new InvalidOperationException(RelationalStrings.BulkOperationWithUnsupportedOperatorInSqlGeneration); + throw new InvalidOperationException( + RelationalStrings.BulkOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.BulkDelete))); } var fromItems = new List(); @@ -63,7 +64,9 @@ protected virtual Expression VisitDelete(DeleteExpression deleteExpression) break; default: - throw new InvalidOperationException(RelationalStrings.BulkOperationWithUnsupportedOperatorInSqlGeneration); + throw new InvalidOperationException( + RelationalStrings.BulkOperationWithUnsupportedOperatorInSqlGeneration( + nameof(RelationalQueryableExtensions.BulkDelete))); } } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs index a4ff3aa33b..f98a11f1f9 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs @@ -21,6 +21,7 @@ protected override bool IsValidSelectExpressionForBulkDelete( // Here we extend this to also inner joins to tables, which we generate via the PostgreSQL-specific USING construct. if (selectExpression.Offset == null && selectExpression.Limit == null + // If entity type has primary key then Distinct is no-op && (!selectExpression.IsDistinct || entityShaperExpression.EntityType.FindPrimaryKey() != null) && selectExpression.GroupBy.Count == 0 && selectExpression.Having == null diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesNpgsqlTest.cs index 7549586d2f..5da14ad0a4 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -8,12 +8,71 @@ public class FiltersInheritanceBulkUpdatesNpgsqlTest : FiltersInheritanceBulkUpd public FiltersInheritanceBulkUpdatesNpgsqlTest(FiltersInheritanceQueryNpgsqlFixture fixture) : base(fixture) { + ClearLog(); + } + + public override async Task Delete_where_hierarchy(bool async) + { + await base.Delete_where_hierarchy(async); + + AssertSql( + @"DELETE FROM ""Animals"" AS a +WHERE a.""CountryId"" = 1 AND a.""Name"" = 'Great spotted kiwi'"); + } + + public override async Task Delete_where_hierarchy_derived(bool async) + { + await base.Delete_where_hierarchy_derived(async); + + AssertSql( + @"DELETE FROM ""Animals"" AS a +WHERE a.""Discriminator"" = 'Kiwi' AND a.""CountryId"" = 1 AND a.""Name"" = 'Great spotted kiwi'"); + } + + public override async Task Delete_where_using_hierarchy(bool async) + { + await base.Delete_where_using_hierarchy(async); + + AssertSql( + @"DELETE FROM ""Countries"" AS c +WHERE ( + SELECT count(*)::int + FROM ""Animals"" AS a + WHERE a.""CountryId"" = 1 AND c.""Id"" = a.""CountryId"" AND a.""CountryId"" > 0) > 0"); + } + + public override async Task Delete_where_using_hierarchy_derived(bool async) + { + await base.Delete_where_using_hierarchy_derived(async); + + AssertSql( + @"DELETE FROM ""Countries"" AS c +WHERE ( + SELECT count(*)::int + FROM ""Animals"" AS a + WHERE a.""CountryId"" = 1 AND c.""Id"" = a.""CountryId"" AND a.""Discriminator"" = 'Kiwi' AND a.""CountryId"" > 0) > 0"); + } + + public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Delete_where_keyless_entity_mapped_to_sql_query(async); + + AssertSql(); + } + + public override async Task Delete_where_hierarchy_subquery(bool async) + { + await base.Delete_where_hierarchy_subquery(async); + + AssertSql(); } [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesNpgsqlTest.cs index 31b807f5fe..e4025637ce 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesNpgsqlTest.cs @@ -8,12 +8,71 @@ public class InheritanceBulkUpdatesNpgsqlTest : InheritanceBulkUpdatesTestBase 0) > 0"); + } + + public override async Task Delete_where_using_hierarchy_derived(bool async) + { + await base.Delete_where_using_hierarchy_derived(async); + + AssertSql( + @"DELETE FROM ""Countries"" AS c +WHERE ( + SELECT count(*)::int + FROM ""Animals"" AS a + WHERE c.""Id"" = a.""CountryId"" AND a.""Discriminator"" = 'Kiwi' AND a.""CountryId"" > 0) > 0"); + } + + public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Delete_where_keyless_entity_mapped_to_sql_query(async); + + AssertSql(); + } + + public override async Task Delete_where_hierarchy_subquery(bool async) + { + await base.Delete_where_hierarchy_subquery(async); + + AssertSql(); } [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs index f18d1ecee3..c97cab8bb1 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs @@ -9,7 +9,7 @@ public NorthwindBulkUpdatesNpgsqlTest(NorthwindQueryNpgsqlFixture Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs new file mode 100644 index 0000000000..87e789c18a --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPCFiltersInheritanceBulkUpdatesNpgsqlTest : TPCFiltersInheritanceBulkUpdatesTestBase +{ + public TPCFiltersInheritanceBulkUpdatesNpgsqlTest(TPCFiltersInheritanceQueryNpgsqlFixture fixture) + : base(fixture) + { + ClearLog(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task Delete_where_hierarchy(bool async) + { + await base.Delete_where_hierarchy(async); + + AssertSql(); + } + + public override async Task Delete_where_hierarchy_derived(bool async) + { + await base.Delete_where_hierarchy_derived(async); + + AssertSql( + @"DELETE FROM ""Kiwi"" AS k +WHERE k.""CountryId"" = 1 AND k.""Name"" = 'Great spotted kiwi'"); + } + + public override async Task Delete_where_using_hierarchy(bool async) + { + await base.Delete_where_using_hierarchy(async); + + AssertSql( + @"DELETE FROM ""Countries"" AS c +WHERE ( + SELECT count(*)::int + FROM ( + SELECT e.""Id"", e.""CountryId"", e.""Name"", e.""Species"", e.""EagleId"", e.""IsFlightless"", e.""Group"", NULL AS ""FoundOn"", 'Eagle' AS ""Discriminator"" + FROM ""Eagle"" AS e + UNION ALL + SELECT k.""Id"", k.""CountryId"", k.""Name"", k.""Species"", k.""EagleId"", k.""IsFlightless"", NULL AS ""Group"", k.""FoundOn"", 'Kiwi' AS ""Discriminator"" + FROM ""Kiwi"" AS k + ) AS t + WHERE t.""CountryId"" = 1 AND c.""Id"" = t.""CountryId"" AND t.""CountryId"" > 0) > 0"); + } + + public override async Task Delete_where_using_hierarchy_derived(bool async) + { + await base.Delete_where_using_hierarchy_derived(async); + + AssertSql( + @"DELETE FROM ""Countries"" AS c +WHERE ( + SELECT count(*)::int + FROM ( + SELECT k.""Id"", k.""CountryId"", k.""Name"", k.""Species"", k.""EagleId"", k.""IsFlightless"", NULL AS ""Group"", k.""FoundOn"", 'Kiwi' AS ""Discriminator"" + FROM ""Kiwi"" AS k + ) AS t + WHERE t.""CountryId"" = 1 AND c.""Id"" = t.""CountryId"" AND t.""CountryId"" > 0) > 0"); + } + + public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Delete_where_keyless_entity_mapped_to_sql_query(async); + + AssertSql(); + } + + public override async Task Delete_where_hierarchy_subquery(bool async) + { + await base.Delete_where_hierarchy_subquery(async); + + AssertSql(); + } + + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs new file mode 100644 index 0000000000..ed689045cd --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,86 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPCInheritanceBulkUpdatesNpgsqlTest : TPCInheritanceBulkUpdatesTestBase +{ + public TPCInheritanceBulkUpdatesNpgsqlTest(TPCInheritanceQueryNpgsqlFixture fixture) + : base(fixture) + { + ClearLog(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task Delete_where_hierarchy(bool async) + { + await base.Delete_where_hierarchy(async); + + AssertSql(); + } + + public override async Task Delete_where_hierarchy_derived(bool async) + { + await base.Delete_where_hierarchy_derived(async); + + AssertSql( + @"DELETE FROM ""Kiwi"" AS k +WHERE k.""Name"" = 'Great spotted kiwi'"); + } + + public override async Task Delete_where_using_hierarchy(bool async) + { + await base.Delete_where_using_hierarchy(async); + + AssertSql( + @"DELETE FROM ""Countries"" AS c +WHERE ( + SELECT count(*)::int + FROM ( + SELECT e.""Id"", e.""CountryId"", e.""Name"", e.""Species"", e.""EagleId"", e.""IsFlightless"", e.""Group"", NULL AS ""FoundOn"", 'Eagle' AS ""Discriminator"" + FROM ""Eagle"" AS e + UNION ALL + SELECT k.""Id"", k.""CountryId"", k.""Name"", k.""Species"", k.""EagleId"", k.""IsFlightless"", NULL AS ""Group"", k.""FoundOn"", 'Kiwi' AS ""Discriminator"" + FROM ""Kiwi"" AS k + ) AS t + WHERE c.""Id"" = t.""CountryId"" AND t.""CountryId"" > 0) > 0"); + } + + public override async Task Delete_where_using_hierarchy_derived(bool async) + { + await base.Delete_where_using_hierarchy_derived(async); + + AssertSql( + @"DELETE FROM ""Countries"" AS c +WHERE ( + SELECT count(*)::int + FROM ( + SELECT k.""Id"", k.""CountryId"", k.""Name"", k.""Species"", k.""EagleId"", k.""IsFlightless"", NULL AS ""Group"", k.""FoundOn"", 'Kiwi' AS ""Discriminator"" + FROM ""Kiwi"" AS k + ) AS t + WHERE c.""Id"" = t.""CountryId"" AND t.""CountryId"" > 0) > 0"); + } + + public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Delete_where_keyless_entity_mapped_to_sql_query(async); + + AssertSql(); + } + + public override async Task Delete_where_hierarchy_subquery(bool async) + { + await base.Delete_where_hierarchy_subquery(async); + + AssertSql(); + } + + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} + diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs new file mode 100644 index 0000000000..40fc6ce619 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,76 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPTFiltersInheritanceBulkUpdatesSqlServerTest : TPTFiltersInheritanceBulkUpdatesTestBase +{ + public TPTFiltersInheritanceBulkUpdatesSqlServerTest(TPTFiltersInheritanceQuerySqlServerFixture fixture) + : base(fixture) + { + ClearLog(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task Delete_where_hierarchy(bool async) + { + await base.Delete_where_hierarchy(async); + + AssertSql(); + } + + public override async Task Delete_where_hierarchy_derived(bool async) + { + await base.Delete_where_hierarchy_derived(async); + + AssertSql(); + } + + public override async Task Delete_where_using_hierarchy(bool async) + { + await base.Delete_where_using_hierarchy(async); + + AssertSql( + @"DELETE FROM [c] +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM [Animals] AS [a] + WHERE [a].[CountryId] = 1 AND [c].[Id] = [a].[CountryId] AND [a].[CountryId] > 0) > 0"); + } + + public override async Task Delete_where_using_hierarchy_derived(bool async) + { + await base.Delete_where_using_hierarchy_derived(async); + + AssertSql( + @"DELETE FROM [c] +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM [Animals] AS [a] + WHERE [a].[CountryId] = 1 AND [c].[Id] = [a].[CountryId] AND [a].[Discriminator] = N'Kiwi' AND [a].[CountryId] > 0) > 0"); + } + + public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Delete_where_keyless_entity_mapped_to_sql_query(async); + + AssertSql(); + } + + public override async Task Delete_where_hierarchy_subquery(bool async) + { + await base.Delete_where_hierarchy_subquery(async); + + AssertSql(); + } + + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs new file mode 100644 index 0000000000..5b068f3946 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPTInheritanceBulkUpdatesNpgsqlTest : TPTInheritanceBulkUpdatesTestBase +{ + public TPTInheritanceBulkUpdatesNpgsqlTest(TPTInheritanceQueryNpgsqlFixture fixture) + : base(fixture) + { + ClearLog(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task Delete_where_hierarchy(bool async) + { + await base.Delete_where_hierarchy(async); + + AssertSql(); + } + + public override async Task Delete_where_hierarchy_derived(bool async) + { + await base.Delete_where_hierarchy_derived(async); + + AssertSql(); + } + + public override async Task Delete_where_using_hierarchy(bool async) + { + await base.Delete_where_using_hierarchy(async); + + AssertSql(); + } + + public override async Task Delete_where_using_hierarchy_derived(bool async) + { + await base.Delete_where_using_hierarchy_derived(async); + + AssertSql(); + } + + public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Delete_where_keyless_entity_mapped_to_sql_query(async); + + AssertSql(); + } + + public override async Task Delete_where_hierarchy_subquery(bool async) + { + await base.Delete_where_hierarchy_subquery(async); + + AssertSql(); + } + + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +}