diff --git a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs index 4483d6cbb..0c0e42f6f 100644 --- a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs @@ -105,6 +105,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.PG/Query/Expressions/Internal/PostgresDeleteExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PostgresDeleteExpression.cs new file mode 100644 index 000000000..5b240faac --- /dev/null +++ b/src/EFCore.PG/Query/Expressions/Internal/PostgresDeleteExpression.cs @@ -0,0 +1,85 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; + +public sealed class PostgresDeleteExpression : Expression, IPrintableExpression +{ + /// + /// The tables that rows are to be deleted from. + /// + public TableExpression Table { get; } + + /// + /// Additional tables which can be referenced in the predicate. + /// + public IReadOnlyList FromItems { get; } + + /// + /// The WHERE predicate for the DELETE. + /// + public SqlExpression? Predicate { get; } + + public PostgresDeleteExpression(TableExpression table, IReadOnlyList fromItems, SqlExpression? predicate) + => (Table, FromItems, Predicate) = (table, fromItems, predicate); + + /// + public override Type Type + => typeof(object); + + /// + public override ExpressionType NodeType + => ExpressionType.Extension; + + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Predicate is null + ? this + : Update((SqlExpression?)visitor.Visit(Predicate)); + + public PostgresDeleteExpression Update(SqlExpression? predicate) + => predicate == Predicate + ? this + : new PostgresDeleteExpression(Table, FromItems, predicate); + + public void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.AppendLine($"DELETE FROM {Table.Name} AS {Table.Alias}"); + + if (FromItems.Count > 0) + { + var first = true; + foreach (var fromItem in FromItems) + { + if (first) + { + expressionPrinter.Append("USING "); + first = false; + } + else + { + expressionPrinter.Append(", "); + } + + expressionPrinter.Visit(fromItem); + } + } + + if (Predicate is not null) + { + expressionPrinter.Append("WHERE "); + expressionPrinter.Visit(Predicate); + } + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is PostgresDeleteExpression pgDeleteExpression + && Equals(pgDeleteExpression)); + + private bool Equals(PostgresDeleteExpression pgDeleteExpression) + => Table == pgDeleteExpression.Table + && FromItems.SequenceEqual(pgDeleteExpression.FromItems) + && (Predicate is null ? pgDeleteExpression.Predicate is null : Predicate.Equals(pgDeleteExpression.Predicate)); + + /// + public override int GetHashCode() => Table.GetHashCode(); +} diff --git a/src/EFCore.PG/Query/Internal/NonQueryConvertingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NonQueryConvertingExpressionVisitor.cs new file mode 100644 index 000000000..f14e51eb5 --- /dev/null +++ b/src/EFCore.PG/Query/Internal/NonQueryConvertingExpressionVisitor.cs @@ -0,0 +1,86 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; + +/// +/// Converts the relational into a PG-specific , which +/// precisely models a DELETE statement in PostgreSQL. This is done to handle the PG-specific USING syntax for table joining. +/// +public class NonQueryConvertingExpressionVisitor : ExpressionVisitor +{ + public virtual Expression Process(Expression node) + => node switch + { + DeleteExpression deleteExpression => VisitDelete(deleteExpression), + + _ => node + }; + + protected virtual Expression VisitDelete(DeleteExpression deleteExpression) + { + var selectExpression = deleteExpression.SelectExpression; + + if (selectExpression.Offset != null + || selectExpression.Limit != null + || selectExpression.Having != null + || selectExpression.Orderings.Count > 0 + || selectExpression.GroupBy.Count > 0 + || selectExpression.Projection.Count > 0) + { + throw new InvalidOperationException( + RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration( + nameof(RelationalQueryableExtensions.ExecuteDelete))); + } + + var fromItems = new List(); + SqlExpression? joinPredicates = null; + + // The SelectExpression also contains the target table being modified (same as deleteExpression.Table). + // If it has additional inner joins, use the PostgreSQL-specific USING syntax to express the join. + // Note that the non-join TableExpression isn't necessary the target table - through projection the last table being + // joined may be the one being modified. + foreach (var tableBase in selectExpression.Tables) + { + switch (tableBase) + { + case TableExpression tableExpression: + if (tableExpression != deleteExpression.Table) + { + fromItems.Add(tableExpression); + } + + break; + + case InnerJoinExpression { Table: { } tableExpression } innerJoinExpression: + if (tableExpression != deleteExpression.Table) + { + fromItems.Add(tableExpression); + } + + joinPredicates = joinPredicates is null + ? innerJoinExpression.JoinPredicate + : new SqlBinaryExpression( + ExpressionType.AndAlso, joinPredicates, innerJoinExpression.JoinPredicate, typeof(bool), + innerJoinExpression.JoinPredicate.TypeMapping); + break; + + default: + throw new InvalidOperationException( + RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration( + nameof(RelationalQueryableExtensions.ExecuteDelete))); + } + } + + // Combine the join predicates (if any) before the user-provided predicate + var predicate = (joinPredicates, selectExpression.Predicate) switch + { + (null, not null) => selectExpression.Predicate, + (not null, null) => joinPredicates, + (null, null) => null, + (not null, not null) => new SqlBinaryExpression( + ExpressionType.AndAlso, joinPredicates, selectExpression.Predicate, typeof(bool), joinPredicates.TypeMapping) + }; + + return new PostgresDeleteExpression(deleteExpression.Table, fromItems, predicate); + } +} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs b/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs index dd0827077..884d856f5 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlParameterBasedSqlProcessor.cs @@ -9,6 +9,18 @@ public NpgsqlParameterBasedSqlProcessor( { } + public override Expression Optimize( + Expression queryExpression, + IReadOnlyDictionary parametersValues, + out bool canCache) + { + queryExpression = base.Optimize(queryExpression, parametersValues, out canCache); + + queryExpression = new NonQueryConvertingExpressionVisitor().Process(queryExpression); + + return queryExpression; + } + /// protected override Expression ProcessSqlNullability( Expression selectExpression, IReadOnlyDictionary parametersValues, out bool canCache) diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs index 1581ea14c..662861d16 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs @@ -44,6 +44,7 @@ protected override Expression VisitExtension(Expression extensionExpression) PostgresAnyExpression anyExpression => VisitArrayAny(anyExpression), PostgresArrayIndexExpression arrayIndexExpression => VisitArrayIndex(arrayIndexExpression), PostgresBinaryExpression binaryExpression => VisitPostgresBinary(binaryExpression), + PostgresDeleteExpression deleteExpression => VisitPostgresDelete(deleteExpression), PostgresFunctionExpression functionExpression => VisitPostgresFunction(functionExpression), PostgresILikeExpression iLikeExpression => VisitILike(iLikeExpression), PostgresJsonTraversalExpression jsonTraversalExpression => VisitJsonPathTraversal(jsonTraversalExpression), @@ -206,6 +207,45 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression binary) } } + protected override void GenerateRootCommand(Expression rootExpression) + { + switch (rootExpression) + { + case PostgresDeleteExpression pgDeleteExpression: + VisitPostgresDelete(pgDeleteExpression); + return; + + default: + base.GenerateRootCommand(rootExpression); + return; + } + } + + // NonQueryConvertingExpressionVisitor converts the relational DeleteExpression to PostgresDeleteExpression, so we should never + // get here + protected override Expression VisitDelete(DeleteExpression deleteExpression) + => throw new InvalidOperationException("Inconceivable!"); + + protected virtual Expression VisitPostgresDelete(PostgresDeleteExpression pgDeleteExpression) + { + Sql.Append("DELETE FROM "); + Visit(pgDeleteExpression.Table); + + if (pgDeleteExpression.FromItems.Count > 0) + { + Sql.AppendLine().Append("USING "); + GenerateList(pgDeleteExpression.FromItems, t => Visit(t), sql => sql.Append(", ")); + } + + if (pgDeleteExpression.Predicate != null) + { + Sql.AppendLine().Append("WHERE "); + Visit(pgDeleteExpression.Predicate); + } + + return pgDeleteExpression; + } + protected virtual Expression VisitPostgresNewArray(PostgresNewArrayExpression postgresNewArrayExpression) { Debug.Assert(postgresNewArrayExpression.TypeMapping is not null); @@ -769,4 +809,22 @@ public virtual Expression VisitPostgresFunction(PostgresFunctionExpression e) private static bool RequiresBrackets(SqlExpression expression) => expression is SqlBinaryExpression || expression is LikeExpression || expression is PostgresBinaryExpression; + + private void GenerateList( + IReadOnlyList items, + Action generationAction, + Action? joinAction = null) + { + joinAction ??= (isb => isb.Append(", ")); + + for (var i = 0; i < items.Count; i++) + { + if (i > 0) + { + joinAction(Sql); + } + + generationAction(items[i]); + } + } } \ No newline at end of file diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs new file mode 100644 index 000000000..c0d2405bf --- /dev/null +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitor.cs @@ -0,0 +1,57 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; + +public class NpgsqlQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor +{ + public NpgsqlQueryableMethodTranslatingExpressionVisitor( + QueryableMethodTranslatingExpressionVisitorDependencies dependencies, + RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, + QueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) + { + } + + protected override bool IsValidSelectExpressionForExecuteDelete( + SelectExpression selectExpression, + EntityShaperExpression entityShaperExpression, + [NotNullWhen(true)] out TableExpression? tableExpression) + { + // The default relational behavior is to allow only single-table expressions, and the only permitted feature is a predicate. + // 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 + && selectExpression.Orderings.Count == 0) + { + TableExpressionBase? table = null; + if (selectExpression.Tables.Count == 1) + { + table = selectExpression.Tables[0]; + } + else if (selectExpression.Tables.All(t => t is TableExpression or InnerJoinExpression)) + { + var projectionBindingExpression = (ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression; + var entityProjectionExpression = (EntityProjectionExpression)selectExpression.GetProjection(projectionBindingExpression); + var column = entityProjectionExpression.BindProperty(entityShaperExpression.EntityType.GetProperties().First()); + table = column.Table; + if (table is JoinExpressionBase joinExpressionBase) + { + table = joinExpressionBase.Table; + } + } + + if (table is TableExpression te) + { + tableExpression = te; + return true; + } + } + + tableExpression = null; + return false; + } +} diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitorFactory.cs b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitorFactory.cs new file mode 100644 index 000000000..64ebaa2f0 --- /dev/null +++ b/src/EFCore.PG/Query/Internal/NpgsqlQueryableMethodTranslatingExpressionVisitorFactory.cs @@ -0,0 +1,19 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal; + +public class NpgsqlQueryableMethodTranslatingExpressionVisitorFactory : IQueryableMethodTranslatingExpressionVisitorFactory +{ + public NpgsqlQueryableMethodTranslatingExpressionVisitorFactory( + QueryableMethodTranslatingExpressionVisitorDependencies dependencies, + RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + } + + protected virtual QueryableMethodTranslatingExpressionVisitorDependencies Dependencies { get; } + + protected virtual RelationalQueryableMethodTranslatingExpressionVisitorDependencies RelationalDependencies { get; } + + public virtual QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext) + => new NpgsqlQueryableMethodTranslatingExpressionVisitor(Dependencies, RelationalDependencies, queryCompilationContext); +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 000000000..29b346e4a --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class FiltersInheritanceBulkUpdatesNpgsqlFixture : InheritanceBulkUpdatesNpgsqlFixture +{ + protected override bool EnableFilters + => true; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesNpgsqlTest.cs new file mode 100644 index 000000000..b12e4ebd9 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class FiltersInheritanceBulkUpdatesNpgsqlTest : FiltersInheritanceBulkUpdatesTestBase +{ + public FiltersInheritanceBulkUpdatesNpgsqlTest(FiltersInheritanceBulkUpdatesNpgsqlFixture 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/InheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 000000000..de15c7ffe --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class InheritanceBulkUpdatesNpgsqlFixture : InheritanceBulkUpdatesRelationalFixture +{ + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesNpgsqlTest.cs new file mode 100644 index 000000000..603280534 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class InheritanceBulkUpdatesNpgsqlTest : InheritanceBulkUpdatesTestBase +{ + public InheritanceBulkUpdatesNpgsqlTest(InheritanceBulkUpdatesNpgsqlFixture 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.""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.""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 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 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/NorthwindBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 000000000..ff480b148 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class NorthwindBulkUpdatesNpgsqlFixture : NorthwindBulkUpdatesFixture + where TModelCustomizer: IModelCustomizer, new() +{ + protected override ITestStoreFactory TestStoreFactory + => NpgsqlNorthwindTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity() + .Property(c => c.CustomerID) + .HasColumnType("char(5)"); + + modelBuilder.Entity( + b => + { + b.Property(c => c.EmployeeID).HasColumnType("int"); + b.Property(c => c.ReportsTo).HasColumnType("int"); + }); + + modelBuilder.Entity( + b => + { + b.Property(o => o.EmployeeID).HasColumnType("int"); + b.Property(o => o.OrderDate).HasColumnType("timestamp without time zone"); + }); + + modelBuilder.Entity() + .Property(od => od.UnitPrice) + .HasColumnType("money"); + + modelBuilder.Entity( + b => + { + b.Property(p => p.UnitPrice).HasColumnType("money"); + b.Property(p => p.UnitsInStock).HasColumnType("smallint"); + }); + + modelBuilder.Entity() + .Property(p => p.UnitPrice) + .HasColumnType("money"); + } +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs new file mode 100644 index 000000000..39773d70e --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,506 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class NorthwindBulkUpdatesNpgsqlTest : NorthwindBulkUpdatesTestBase> +{ + public NorthwindBulkUpdatesNpgsqlTest( + NorthwindBulkUpdatesNpgsqlFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + ClearLog(); + // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task Delete_Where(bool async) + { + await base.Delete_Where(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE o.""OrderID"" < 10300"); + } + + public override async Task Delete_Where_parameter(bool async) + { + await base.Delete_Where_parameter(async); + + AssertSql( + @"@__quantity_0='1' (Nullable = true) (DbType = Int16) + +DELETE FROM ""Order Details"" AS o +WHERE o.""Quantity"" = @__quantity_0", + // + @"DELETE FROM ""Order Details"" AS o +WHERE FALSE"); + } + + public override async Task Delete_Where_OrderBy(bool async) + { + await base.Delete_Where_OrderBy(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10300 AND o0.""OrderID"" = o.""OrderID"" AND o0.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Where_OrderBy_Skip(bool async) + { + await base.Delete_Where_OrderBy_Skip(async); + + AssertSql( + @"@__p_0='100' + +DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10300 + ORDER BY o0.""OrderID"" NULLS FIRST + OFFSET @__p_0 + ) AS t + WHERE t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Where_OrderBy_Take(bool async) + { + await base.Delete_Where_OrderBy_Take(async); + + AssertSql( + @"@__p_0='100' + +DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10300 + ORDER BY o0.""OrderID"" NULLS FIRST + LIMIT @__p_0 + ) AS t + WHERE t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Where_OrderBy_Skip_Take(bool async) + { + await base.Delete_Where_OrderBy_Skip_Take(async); + + AssertSql( + @"@__p_0='100' + +DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10300 + ORDER BY o0.""OrderID"" NULLS FIRST + LIMIT @__p_0 OFFSET @__p_0 + ) AS t + WHERE t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Where_Skip(bool async) + { + await base.Delete_Where_Skip(async); + + AssertSql( + @"@__p_0='100' + +DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10300 + OFFSET @__p_0 + ) AS t + WHERE t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Where_Take(bool async) + { + await base.Delete_Where_Take(async); + + AssertSql( + @"@__p_0='100' + +DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10300 + LIMIT @__p_0 + ) AS t + WHERE t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Where_Skip_Take(bool async) + { + await base.Delete_Where_Skip_Take(async); + + AssertSql( + @"@__p_0='100' + +DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10300 + LIMIT @__p_0 OFFSET @__p_0 + ) AS t + WHERE t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Where_predicate_with_group_by_aggregate(bool async) + { + await base.Delete_Where_predicate_with_group_by_aggregate(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE o.""OrderID"" < ( + SELECT ( + SELECT o1.""OrderID"" + FROM ""Orders"" AS o1 + WHERE o0.""CustomerID"" = o1.""CustomerID"" OR ((o0.""CustomerID"" IS NULL) AND (o1.""CustomerID"" IS NULL)) + LIMIT 1) + FROM ""Orders"" AS o0 + GROUP BY o0.""CustomerID"" + HAVING count(*)::int > 11 + LIMIT 1)"); + } + + public override async Task Delete_Where_predicate_with_group_by_aggregate_2(bool async) + { + await base.Delete_Where_predicate_with_group_by_aggregate_2(async); + + AssertSql(); + } + + public override async Task Delete_GroupBy_Where_Select(bool async) + { + await base.Delete_GroupBy_Where_Select(async); + + AssertSql(); + } + + public override async Task Delete_Where_Skip_Take_Skip_Take_causing_subquery(bool async) + { + await base.Delete_Where_Skip_Take_Skip_Take_causing_subquery(async); + + AssertSql( + @"@__p_0='100' +@__p_2='5' +@__p_1='20' + +DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT t.""OrderID"", t.""ProductID"", t.""Discount"", t.""Quantity"", t.""UnitPrice"" + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10300 + LIMIT @__p_0 OFFSET @__p_0 + ) AS t + LIMIT @__p_2 OFFSET @__p_1 + ) AS t0 + WHERE t0.""OrderID"" = o.""OrderID"" AND t0.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Where_Distinct(bool async) + { + await base.Delete_Where_Distinct(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE o.""OrderID"" < 10300"); + } + + public override async Task Delete_SelectMany(bool async) + { + await base.Delete_SelectMany(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o0 +USING ""Orders"" AS o +WHERE o.""OrderID"" = o0.""OrderID"" AND o.""OrderID"" < 10250"); + } + + public override async Task Delete_SelectMany_subquery(bool async) + { + await base.Delete_SelectMany_subquery(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ""Orders"" AS o0 + INNER JOIN ( + SELECT o1.""OrderID"", o1.""ProductID"", o1.""Discount"", o1.""Quantity"", o1.""UnitPrice"" + FROM ""Order Details"" AS o1 + WHERE o1.""ProductID"" > 0 + ) AS t ON o0.""OrderID"" = t.""OrderID"" + WHERE o0.""OrderID"" < 10250 AND t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Where_using_navigation(bool async) + { + await base.Delete_Where_using_navigation(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +USING ""Orders"" AS o0 +WHERE o.""OrderID"" = o0.""OrderID"" AND date_part('year', o0.""OrderDate"")::int = 2000"); + } + + public override async Task Delete_Where_using_navigation_2(bool async) + { + await base.Delete_Where_using_navigation_2(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ""Order Details"" AS o0 + INNER JOIN ""Orders"" AS o1 ON o0.""OrderID"" = o1.""OrderID"" + LEFT JOIN ""Customers"" AS c ON o1.""CustomerID"" = c.""CustomerID"" + WHERE (c.""CustomerID"" IS NOT NULL) AND (c.""CustomerID"" LIKE 'F%') AND o0.""OrderID"" = o.""OrderID"" AND o0.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Union(bool async) + { + await base.Delete_Union(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10250 + UNION + SELECT o1.""OrderID"", o1.""ProductID"", o1.""Discount"", o1.""Quantity"", o1.""UnitPrice"" + FROM ""Order Details"" AS o1 + WHERE o1.""OrderID"" > 11250 + ) AS t + WHERE t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Concat(bool async) + { + await base.Delete_Concat(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10250 + UNION ALL + SELECT o1.""OrderID"", o1.""ProductID"", o1.""Discount"", o1.""Quantity"", o1.""UnitPrice"" + FROM ""Order Details"" AS o1 + WHERE o1.""OrderID"" > 11250 + ) AS t + WHERE t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Intersect(bool async) + { + await base.Delete_Intersect(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10250 + INTERSECT + SELECT o1.""OrderID"", o1.""ProductID"", o1.""Discount"", o1.""Quantity"", o1.""UnitPrice"" + FROM ""Order Details"" AS o1 + WHERE o1.""OrderID"" > 11250 + ) AS t + WHERE t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_Except(bool async) + { + await base.Delete_Except(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT o0.""OrderID"", o0.""ProductID"", o0.""Discount"", o0.""Quantity"", o0.""UnitPrice"" + FROM ""Order Details"" AS o0 + WHERE o0.""OrderID"" < 10250 + EXCEPT + SELECT o1.""OrderID"", o1.""ProductID"", o1.""Discount"", o1.""Quantity"", o1.""UnitPrice"" + FROM ""Order Details"" AS o1 + WHERE o1.""OrderID"" > 11250 + ) AS t + WHERE t.""OrderID"" = o.""OrderID"" AND t.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_non_entity_projection(bool async) + { + await base.Delete_non_entity_projection(async); + + AssertSql(); + } + + public override async Task Delete_non_entity_projection_2(bool async) + { + await base.Delete_non_entity_projection_2(async); + + AssertSql(); + } + + public override async Task Delete_non_entity_projection_3(bool async) + { + await base.Delete_non_entity_projection_3(async); + + AssertSql(); + } + + public override async Task Delete_FromSql_converted_to_subquery(bool async) + { + await base.Delete_FromSql_converted_to_subquery(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ( + SELECT ""OrderID"", ""ProductID"", ""UnitPrice"", ""Quantity"", ""Discount"" + FROM ""Order Details"" + WHERE ""OrderID"" < 10300 + ) AS m + WHERE m.""OrderID"" = o.""OrderID"" AND m.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_with_join(bool async) + { + await base.Delete_with_join(async); + + AssertSql( + @"@__p_1='100' +@__p_0='0' + +DELETE FROM ""Order Details"" AS o +USING ( + SELECT o0.""OrderID"", o0.""CustomerID"", o0.""EmployeeID"", o0.""OrderDate"" + FROM ""Orders"" AS o0 + WHERE o0.""OrderID"" < 10300 + ORDER BY o0.""OrderID"" NULLS FIRST + LIMIT @__p_1 OFFSET @__p_0 +) AS t +WHERE o.""OrderID"" = t.""OrderID"""); + } + + public override async Task Delete_with_left_join(bool async) + { + await base.Delete_with_left_join(async); + + AssertSql( + @"@__p_1='100' +@__p_0='0' + +DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ""Order Details"" AS o0 + LEFT JOIN ( + SELECT o1.""OrderID"", o1.""CustomerID"", o1.""EmployeeID"", o1.""OrderDate"" + FROM ""Orders"" AS o1 + WHERE o1.""OrderID"" < 10300 + ORDER BY o1.""OrderID"" NULLS FIRST + LIMIT @__p_1 OFFSET @__p_0 + ) AS t ON o0.""OrderID"" = t.""OrderID"" + WHERE o0.""OrderID"" < 10276 AND o0.""OrderID"" = o.""OrderID"" AND o0.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_with_cross_join(bool async) + { + await base.Delete_with_cross_join(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ""Order Details"" AS o0 + CROSS JOIN ( + SELECT o1.""OrderID"", o1.""CustomerID"", o1.""EmployeeID"", o1.""OrderDate"" + FROM ""Orders"" AS o1 + WHERE o1.""OrderID"" < 10300 + ORDER BY o1.""OrderID"" NULLS FIRST + LIMIT 100 OFFSET 0 + ) AS t + WHERE o0.""OrderID"" < 10276 AND o0.""OrderID"" = o.""OrderID"" AND o0.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_with_cross_apply(bool async) + { + await base.Delete_with_cross_apply(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ""Order Details"" AS o0 + JOIN LATERAL ( + SELECT o1.""OrderID"", o1.""CustomerID"", o1.""EmployeeID"", o1.""OrderDate"" + FROM ""Orders"" AS o1 + WHERE o1.""OrderID"" < o0.""OrderID"" + ORDER BY o1.""OrderID"" NULLS FIRST + LIMIT 100 OFFSET 0 + ) AS t ON TRUE + WHERE o0.""OrderID"" < 10276 AND o0.""OrderID"" = o.""OrderID"" AND o0.""ProductID"" = o.""ProductID"")"); + } + + public override async Task Delete_with_outer_apply(bool async) + { + await base.Delete_with_outer_apply(async); + + AssertSql( + @"DELETE FROM ""Order Details"" AS o +WHERE EXISTS ( + SELECT 1 + FROM ""Order Details"" AS o0 + LEFT JOIN LATERAL ( + SELECT o1.""OrderID"", o1.""CustomerID"", o1.""EmployeeID"", o1.""OrderDate"" + FROM ""Orders"" AS o1 + WHERE o1.""OrderID"" < o0.""OrderID"" + ORDER BY o1.""OrderID"" NULLS FIRST + LIMIT 100 OFFSET 0 + ) AS t ON TRUE + WHERE o0.""OrderID"" < 10276 AND o0.""OrderID"" = o.""OrderID"" AND o0.""ProductID"" = o.""ProductID"")"); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 000000000..55c39f099 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,6 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPCFiltersInheritanceBulkUpdatesNpgsqlFixture : TPCInheritanceBulkUpdatesNpgsqlFixture +{ + protected override bool EnableFilters => true; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs new file mode 100644 index 000000000..cb081243a --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,87 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPCFiltersInheritanceBulkUpdatesNpgsqlTest + : TPCFiltersInheritanceBulkUpdatesTestBase +{ + public TPCFiltersInheritanceBulkUpdatesNpgsqlTest(TPCFiltersInheritanceBulkUpdatesNpgsqlFixture 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/TPCInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 000000000..bfde64c5e --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPCInheritanceBulkUpdatesNpgsqlFixture : TPCInheritanceBulkUpdatesFixture +{ + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + protected override bool UseGeneratedKeys + => false; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs new file mode 100644 index 000000000..40e05db5c --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,88 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPCInheritanceBulkUpdatesNpgsqlTest + : TPCInheritanceBulkUpdatesTestBase +{ + public TPCInheritanceBulkUpdatesNpgsqlTest(TPCInheritanceBulkUpdatesNpgsqlFixture 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/TPTFiltersInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 000000000..2fe58d64b --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,6 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPTFiltersInheritanceBulkUpdatesNpgsqlFixture : TPTInheritanceBulkUpdatesNpgsqlFixture +{ + protected override bool EnableFilters => true; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs new file mode 100644 index 000000000..ae1a4ee92 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesNpgsqlTest.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPTFiltersInheritanceBulkUpdatesSqlServerTest + : TPTFiltersInheritanceBulkUpdatesTestBase +{ + public TPTFiltersInheritanceBulkUpdatesSqlServerTest(TPTFiltersInheritanceBulkUpdatesNpgsqlFixture 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/TPTInheritanceBulkUpdatesNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlFixture.cs new file mode 100644 index 000000000..06e8d4b9f --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlFixture.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore.BulkUpdates; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.BulkUpdates; + +public class TPTInheritanceBulkUpdatesNpgsqlFixture : TPTInheritanceBulkUpdatesFixture +{ + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; +} diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesNpgsqlTest.cs new file mode 100644 index 000000000..3f331637e --- /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(TPTInheritanceBulkUpdatesNpgsqlFixture 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); +}