From 8d7ac1460bdc5b89458178eb61249f8027b2a222 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Thu, 4 Aug 2022 17:31:49 -0700 Subject: [PATCH] Implement ExecuteUpdate Resolves #795 --- .../RelationalQueryableExtensions.cs | 68 ++++- .../Properties/RelationalStrings.Designer.cs | 254 ++++++++++-------- .../Properties/RelationalStrings.resx | 102 ++++--- .../Query/EntityProjectionExpression.cs | 1 - ...electExpressionPruningExpressionVisitor.cs | 5 + .../Query/NonQueryExpression.cs | 68 ++++- .../Query/QuerySqlGenerator.cs | 51 +++- ...RelationalQueryTranslationPostprocessor.cs | 2 +- ...yableMethodTranslatingExpressionVisitor.cs | 254 +++++++++++++++++- ...alShapedQueryCompilingExpressionVisitor.cs | 2 +- .../Query/SetPropertyStatements.cs | 36 +++ .../Query/SqlExpressionVisitor.cs | 145 +++------- .../Query/SqlExpressions/CaseWhenClause.cs | 2 +- .../Query/SqlExpressions/DeleteExpression.cs | 27 +- .../Query/SqlExpressions/SetColumnValue.cs | 52 ++++ .../Query/SqlExpressions/UpdateExpression.cs | 147 ++++++++++ .../Query/SqlNullabilityProcessor.cs | 31 ++- ...rchConditionConvertingExpressionVisitor.cs | 34 +++ .../Internal/SqlServerQuerySqlGenerator.cs | 52 ++++ src/EFCore/Properties/CoreStrings.resx | 2 +- src/EFCore/Query/ShapedQueryExpression.cs | 2 +- .../BulkUpdates/BulkUpdatesTestBase.cs | 10 + .../FiltersInheritanceBulkUpdatesTestBase.cs | 63 +++++ .../InheritanceBulkUpdatesTestBase.cs | 63 +++++ .../NorthwindBulkUpdatesTestBase.cs | 164 +++++++++++ ...PCFiltersInheritanceBulkUpdatesTestBase.cs | 8 + .../TPCInheritanceBulkUpdatesTestBase.cs | 8 + ...PTFiltersInheritanceBulkUpdatesTestBase.cs | 13 + .../TPTInheritanceBulkUpdatesTestBase.cs | 13 + .../EntitySplittingTestBase.cs | 30 +++ .../TPTTableSplittingTestBase.cs | 8 + .../TableSplittingTestBase.cs | 29 ++ .../TestUtilities/BulkUpdatesAsserter.cs | 59 ++++ .../TestUtilities/TestSqlLoggerFactory.cs | 30 ++- ...tersInheritanceBulkUpdatesSqlServerTest.cs | 67 +++++ .../InheritanceBulkUpdatesSqlServerTest.cs | 67 +++++ .../NorthwindBulkUpdatesSqlServerTest.cs | 133 +++++++++ ...tersInheritanceBulkUpdatesSqlServerTest.cs | 72 +++++ .../TPCInheritanceBulkUpdatesSqlServerTest.cs | 72 +++++ ...tersInheritanceBulkUpdatesSqlServerTest.cs | 65 +++++ .../TPTInheritanceBulkUpdatesSqlServerTest.cs | 65 +++++ .../TableSplittingSqlServerTest.cs | 18 ++ ...FiltersInheritanceBulkUpdatesSqliteTest.cs | 67 +++++ .../InheritanceBulkUpdatesSqliteTest.cs | 67 +++++ .../NorthwindBulkUpdatesSqliteTest.cs | 133 +++++++++ ...FiltersInheritanceBulkUpdatesSqliteTest.cs | 72 +++++ .../TPCInheritanceBulkUpdatesSqliteTest.cs | 72 +++++ ...FiltersInheritanceBulkUpdatesSqliteTest.cs | 65 +++++ .../TPTInheritanceBulkUpdatesSqliteTest.cs | 65 +++++ .../TableSplittingSqliteTest.cs | 14 + 50 files changed, 2648 insertions(+), 301 deletions(-) create mode 100644 src/EFCore.Relational/Query/SetPropertyStatements.cs create mode 100644 src/EFCore.Relational/Query/SqlExpressions/SetColumnValue.cs create mode 100644 src/EFCore.Relational/Query/SqlExpressions/UpdateExpression.cs diff --git a/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs b/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs index dd76c0da430..9f959fe51d6 100644 --- a/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs @@ -236,7 +236,7 @@ internal static readonly MethodInfo AsSplitQueryMethodInfo #region ExecuteDelete /// - /// Deletes all entity instances which match the LINQ query from the database. + /// Deletes all database rows for the entity instances which match the LINQ query from the database. /// /// /// @@ -251,12 +251,12 @@ internal static readonly MethodInfo AsSplitQueryMethodInfo /// /// /// The source query. - /// The total number of entity instances deleted from the database. + /// The total number of rows deleted in the database. public static int ExecuteDelete(this IQueryable source) => source.Provider.Execute(Expression.Call(ExecuteDeleteMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression)); /// - /// Asynchronously deletes all entity instances which match the LINQ query from the database. + /// Asynchronously deletes database rows for the entity instances which match the LINQ query from the database. /// /// /// @@ -272,7 +272,7 @@ public static int ExecuteDelete(this IQueryable source) /// /// The source query. /// A to observe while waiting for the task to complete. - /// The total number of entity instances deleted from the database. + /// The total number of rows deleted in the database. public static Task ExecuteDeleteAsync(this IQueryable source, CancellationToken cancellationToken = default) => source.Provider is IAsyncQueryProvider provider ? provider.ExecuteAsync>( @@ -283,4 +283,64 @@ internal static readonly MethodInfo ExecuteDeleteMethodInfo = typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(ExecuteDelete))!; #endregion + + #region ExecuteUpdate + + /// + /// Updates all database rows for the entity instances which match the LINQ query from the database. + /// + /// + /// + /// This operation executes immediately against the database, rather than being deferred until + /// is called. It also does not interact with the EF change tracker in any way: + /// entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated + /// to reflect the changes. + /// + /// + /// See Executing bulk operations with EF Core + /// for more information and examples. + /// + /// + /// The source query. + /// A collection of set property statements specifying properties to update. + /// The total number of rows updated in the database. + public static int ExecuteUpdate( + this IQueryable source, + Expression, SetPropertyStatements>> setPropertyStatements) + => source.Provider.Execute( + Expression.Call(ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression, setPropertyStatements)); + + /// + /// Asynchronously updates database rows for the entity instances which match the LINQ query from the database. + /// + /// + /// + /// This operation executes immediately against the database, rather than being deferred until + /// is called. It also does not interact with the EF change tracker in any way: + /// entity instances which happen to be tracked when this operation is invoked aren't taken into account, and aren't updated + /// to reflect the changes. + /// + /// + /// See Executing bulk operations with EF Core + /// for more information and examples. + /// + /// + /// The source query. + /// A collection of set property statements specifying properties to update. + /// A to observe while waiting for the task to complete. + /// The total number of rows updated in the database. + public static Task ExecuteUpdateAsync( + this IQueryable source, + Expression, SetPropertyStatements>> setPropertyStatements, + CancellationToken cancellationToken = default) + => source.Provider is IAsyncQueryProvider provider + ? provider.ExecuteAsync>( + Expression.Call( + ExecuteUpdateMethodInfo.MakeGenericMethod(typeof(TSource)), source.Expression, setPropertyStatements), cancellationToken) + : throw new InvalidOperationException(CoreStrings.IQueryableProviderNotAsync); + + internal static readonly MethodInfo ExecuteUpdateMethodInfo + = typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(ExecuteUpdate))!; + + #endregion } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 662861998b6..3da92d26d0c 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -879,6 +879,12 @@ public static string InsertDataOperationValuesCountMismatch(object? valuesCount, public static string InsufficientInformationToIdentifyElementOfCollectionJoin => GetString("InsufficientInformationToIdentifyElementOfCollectionJoin"); + /// + /// The 'setPropertyStatements' argument to 'ExecuteUpdate' may only contain a chain of 'SetProperty' expressing the properties to be updated. + /// + public static string InvalidArgumentToExecuteUpdate + => GetString("InvalidArgumentToExecuteUpdate"); + /// /// The specified 'CommandTimeout' value '{value}' is not valid. It must be a positive number. /// @@ -967,6 +973,114 @@ public static string InvalidMinBatchSize(object? value) GetString("InvalidMinBatchSize", nameof(value)), value); + /// + /// The following lambda argument to 'SetProperty' does not represent a valid property to be set: '{propertyExpression}'. + /// + public static string InvalidPropertyInSetProperty(object? propertyExpression) + => string.Format( + GetString("InvalidPropertyInSetProperty", nameof(propertyExpression)), + propertyExpression); + + /// + /// Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner. + /// + public static string JsonEntityMappedToDifferentViewThanOwner(object? jsonType, object? viewName, object? ownerType, object? ownerViewName) + => string.Format( + GetString("JsonEntityMappedToDifferentViewThanOwner", nameof(jsonType), nameof(viewName), nameof(ownerType), nameof(ownerViewName)), + jsonType, viewName, ownerType, ownerViewName); + + /// + /// Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column. + /// + public static string JsonEntityMultipleRootsMappedToTheSameJsonColumn(object? column, object? table) + => string.Format( + GetString("JsonEntityMultipleRootsMappedToTheSameJsonColumn", nameof(column), nameof(table)), + column, table); + + /// + /// Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves. + /// + public static string JsonEntityOwnedByNonJsonOwnedType(object? nonJsonType, object? table) + => string.Format( + GetString("JsonEntityOwnedByNonJsonOwnedType", nameof(nonJsonType), nameof(table)), + nonJsonType, table); + + /// + /// Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner. + /// + public static string JsonEntityReferencingRegularEntity(object? jsonEntity) + => string.Format( + GetString("JsonEntityReferencingRegularEntity", nameof(jsonEntity)), + jsonEntity); + + /// + /// Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'. + /// + public static string JsonEntityWithDefaultValueSetOnItsProperty(object? jsonEntity, object? property) + => string.Format( + GetString("JsonEntityWithDefaultValueSetOnItsProperty", nameof(jsonEntity), nameof(property)), + jsonEntity, property); + + /// + /// Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly. + /// + public static string JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey(object? keyProperty, object? jsonEntity) + => string.Format( + GetString("JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey", nameof(keyProperty), nameof(jsonEntity)), + keyProperty, jsonEntity); + + /// + /// Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported. + /// + public static string JsonEntityWithExplicitlyConfiguredOrdinalKey(object? jsonEntity) + => string.Format( + GetString("JsonEntityWithExplicitlyConfiguredOrdinalKey", nameof(jsonEntity)), + jsonEntity); + + /// + /// Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}. + /// + public static string JsonEntityWithIncorrectNumberOfKeyProperties(object? jsonEntity, object? expectedCount, object? actualCount) + => string.Format( + GetString("JsonEntityWithIncorrectNumberOfKeyProperties", nameof(jsonEntity), nameof(expectedCount), nameof(actualCount)), + jsonEntity, expectedCount, actualCount); + + /// + /// Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property. + /// + public static string JsonEntityWithMultiplePropertiesMappedToSameJsonProperty(object? jsonEntity, object? property) + => string.Format( + GetString("JsonEntityWithMultiplePropertiesMappedToSameJsonProperty", nameof(jsonEntity), nameof(property)), + jsonEntity, property); + + /// + /// Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities. + /// + public static string JsonEntityWithNonTphInheritanceOnOwner(object? rootType, object? tph) + => string.Format( + GetString("JsonEntityWithNonTphInheritanceOnOwner", nameof(rootType), nameof(tph)), + rootType, tph); + + /// + /// Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported. + /// + public static string JsonEntityWithOwnerNotMappedToTableOrView(object? entity) + => string.Format( + GetString("JsonEntityWithOwnerNotMappedToTableOrView", nameof(entity)), + entity); + + /// + /// Table splitting is not supported for entities containing entities mapped to JSON. + /// + public static string JsonEntityWithTableSplittingIsNotSupported + => GetString("JsonEntityWithTableSplittingIsNotSupported"); + + /// + /// JSON property name should only be configured on nested owned navigations. + /// + public static string JsonPropertyNameShouldBeConfiguredOnNestedNavigation + => GetString("JsonPropertyNameShouldBeConfiguredOnNestedNavigation"); + /// /// The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. /// @@ -997,6 +1111,12 @@ public static string MappedFunctionNotFound(object? entityType, object? function public static string MappingFragmentMissingName => GetString("MappingFragmentMissingName"); + /// + /// This method needs to be implemented in the provider. + /// + public static string MethodNeedsToBeImplementedInTheProvider + => GetString("MethodNeedsToBeImplementedInTheProvider"); + /// /// Using '{methodName}' on DbSet of '{entityType}' is not supported since '{entityType}' is part of hierarchy and does not contain a discriminator property. /// @@ -1077,6 +1197,14 @@ public static string ModificationCommandInvalidEntityStateSensitive(object? enti GetString("ModificationCommandInvalidEntityStateSensitive", nameof(entityType), nameof(keyValues), nameof(entityState)), entityType, keyValues, entityState); + /// + /// Multiple 'SetProperty' invocations refer to properties on different entity types ('{entityType1}' and '{entityType2}'). A single 'ExecuteUpdate' call can only update the properties of a single entity type. + /// + public static string MultipleEntityPropertiesInSetProperty(object? entityType1, object? entityType2) + => string.Format( + GetString("MultipleEntityPropertiesInSetProperty", nameof(entityType1), nameof(entityType2)), + entityType1, entityType2); + /// /// Multiple relational database provider configurations found. A context can only be configured to use a single database provider. /// @@ -1185,6 +1313,12 @@ public static string NonTphViewClash(object? entityType, object? otherEntityType public static string NoProviderConfigured => GetString("NoProviderConfigured"); + /// + /// An 'ExecuteUpdate' call must specify at least one 'SetProperty' invocation, to indicate the properties to be updated. + /// + public static string NoSetPropertyInvocation + => GetString("NoSetPropertyInvocation"); + /// /// Unable to modify a row in table '{table}' because its key column '{keyColumn}' is null. /// @@ -1279,6 +1413,12 @@ public static string SetOperationsNotAllowedAfterClientEvaluation public static string SetOperationsOnDifferentStoreTypes => GetString("SetOperationsOnDifferentStoreTypes"); + /// + /// The SetProperty<TProperty> method can only be used within 'ExecuteUpdate' method. + /// + public static string SetPropertyMethodInvoked + => GetString("SetPropertyMethodInvoked"); + /// /// This LINQ query is being executed in split-query mode, and the SQL shown is for the first query to be executed. Additional queries may also be executed depending on the results of the first query. /// @@ -1647,6 +1787,14 @@ public static string UnableToBindMemberToEntityProjection(object? memberType, ob GetString("UnableToBindMemberToEntityProjection", nameof(memberType), nameof(member), nameof(entityType)), memberType, member, entityType); + /// + /// The following 'SetProperty' failed to translate: 'SetProperty({property}, {value})'. {details} + /// + public static string UnableToTranslateSetProperty(object? property, object? value, object? details) + => string.Format( + GetString("UnableToTranslateSetProperty", nameof(property), nameof(value), nameof(details)), + property, value, details); + /// /// Unhandled annotatable type '{annotatableType}'. /// @@ -1787,112 +1935,6 @@ public static string ViewOverrideMismatch(object? propertySpecification, object? public static string VisitChildrenMustBeOverridden => GetString("VisitChildrenMustBeOverridden"); - /// - /// Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves. - /// - public static string JsonEntityOwnedByNonJsonOwnedType(object? nonJsonType, object? table) - => string.Format( - GetString("JsonEntityOwnedByNonJsonOwnedType", nameof(nonJsonType), nameof(table)), - nonJsonType, table); - - /// - /// Table splitting is not supported for entities containing entities mapped to JSON. - /// - public static string JsonEntityWithTableSplittingIsNotSupported - => GetString("JsonEntityWithTableSplittingIsNotSupported"); - - /// - /// Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column. - /// - public static string JsonEntityMultipleRootsMappedToTheSameJsonColumn(object? column, object? table) - => string.Format( - GetString("JsonEntityMultipleRootsMappedToTheSameJsonColumn", nameof(column), nameof(table)), - column, table); - - /// - /// Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported. - /// - public static string JsonEntityWithOwnerNotMappedToTableOrView(object? entity) - => string.Format( - GetString("JsonEntityWithOwnerNotMappedToTableOrView", nameof(entity)), - entity); - - /// - /// Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner. - /// - public static string JsonEntityMappedToDifferentViewThanOwner(object? jsonType, object? viewName, object? ownerType, object? ownerViewName) - => string.Format( - GetString("JsonEntityMappedToDifferentViewThanOwner", nameof(jsonType), nameof(viewName), nameof(ownerType), nameof(ownerViewName)), - jsonType, viewName, ownerType, ownerViewName); - - /// - /// Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities. - /// - public static string JsonEntityWithNonTphInheritanceOnOwner(object? rootType, object? tph) - => string.Format( - GetString("JsonEntityWithNonTphInheritanceOnOwner", nameof(rootType), nameof(tph)), - rootType, tph); - - /// - /// Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner. - /// - public static string JsonEntityReferencingRegularEntity(object? jsonEntity) - => string.Format( - GetString("JsonEntityReferencingRegularEntity", nameof(jsonEntity)), - jsonEntity); - - /// - /// Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly. - /// - public static string JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey(object? keyProperty, object? jsonEntity) - => string.Format( - GetString("JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey", nameof(keyProperty), nameof(jsonEntity)), - keyProperty, jsonEntity); - - /// - /// Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported. - /// - public static string JsonEntityWithExplicitlyConfiguredOrdinalKey(object? jsonEntity) - => string.Format( - GetString("JsonEntityWithExplicitlyConfiguredOrdinalKey", nameof(jsonEntity)), - jsonEntity); - - /// - /// Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}. - /// - public static string JsonEntityWithIncorrectNumberOfKeyProperties(object? jsonEntity, object? expectedCount, object? actualCount) - => string.Format( - GetString("JsonEntityWithIncorrectNumberOfKeyProperties", nameof(jsonEntity), nameof(expectedCount), nameof(actualCount)), - jsonEntity, expectedCount, actualCount); - - /// - /// Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'. - /// - public static string JsonEntityWithDefaultValueSetOnItsProperty(object? jsonEntity, object? property) - => string.Format( - GetString("JsonEntityWithDefaultValueSetOnItsProperty", nameof(jsonEntity), nameof(property)), - jsonEntity, property); - - /// - /// Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property. - /// - public static string JsonEntityWithMultiplePropertiesMappedToSameJsonProperty(object? jsonEntity, object? property) - => string.Format( - GetString("JsonEntityWithMultiplePropertiesMappedToSameJsonProperty", nameof(jsonEntity), nameof(property)), - jsonEntity, property); - - /// - /// JSON property name should only be configured on nested owned navigations. - /// - public static string JsonPropertyNameShouldBeConfiguredOnNestedNavigation - => GetString("JsonPropertyNameShouldBeConfiguredOnNestedNavigation"); - - /// - /// This method needs to be implemented in the provider. - /// - public static string MethodNeedsToBeImplementedInTheProvider - => GetString("MethodNeedsToBeImplementedInTheProvider"); - private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name)!; diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index aab279ce662..0668a0ba3c5 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -445,6 +445,9 @@ Unable to translate a collection subquery in a projection since either parent or the subquery doesn't project necessary information required to uniquely identify it and correctly generate results on the client side. This can happen when trying to correlate on keyless entity type. This can also happen for some cases of projection before 'Distinct' or some shapes of grouping key in case of 'GroupBy'. These should either contain all key properties of the entity that the operation is applied on, or only contain simple property access expressions. + + The 'setPropertyStatements' argument to 'ExecuteUpdate' may only contain a chain of 'SetProperty' expressing the properties to be updated. + The specified 'CommandTimeout' value '{value}' is not valid. It must be a positive number. @@ -478,6 +481,48 @@ The specified 'MinBatchSize' value '{value}' is not valid. It must be a positive number. + + The following lambda argument to 'SetProperty' does not represent a valid property to be set: '{propertyExpression}'. + + + Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner. + + + Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column. + + + Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves. + + + Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner. + + + Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'. + + + Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly. + + + Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported. + + + Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}. + + + Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property. + + + Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities. + + + Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported. + + + Table splitting is not supported for entities containing entities mapped to JSON. + + + JSON property name should only be configured on nested owned navigations. + The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. @@ -774,6 +819,9 @@ Table name must be specified to configure a table-specific property mapping. + + This method needs to be implemented in the provider. + Using '{methodName}' on DbSet of '{entityType}' is not supported since '{entityType}' is part of hierarchy and does not contain a discriminator property. @@ -807,6 +855,9 @@ Cannot save changes for an entity of type '{entityType}' with primary key values {keyValues} in state '{entityState}'. This may indicate a bug in Entity Framework, please open an issue at https://go.microsoft.com/fwlink/?linkid=2142044. + + Multiple 'SetProperty' invocations refer to properties on different entity types ('{entityType1}' and '{entityType2}'). A single 'ExecuteUpdate' call can only update the properties of a single entity type. + Multiple relational database provider configurations found. A context can only be configured to use a single database provider. @@ -852,6 +903,9 @@ No relational database providers are configured. Configure a database provider using 'OnConfiguring' or by creating an ImmutableDbContextOptions with a configured database provider and passing it to the context. + + An 'ExecuteUpdate' call must specify at least one 'SetProperty' invocation, to indicate the properties to be updated. + Unable to modify a row in table '{table}' because its key column '{keyColumn}' is null. @@ -891,6 +945,9 @@ Unable to translate set operation when matching columns on both sides have different store types. + + The SetProperty<TProperty> method can only be used within 'ExecuteUpdate' method. + This LINQ query is being executed in split-query mode, and the SQL shown is for the first query to be executed. Additional queries may also be executed depending on the results of the first query. @@ -1032,6 +1089,9 @@ Unable to bind '{memberType}.{member}' to an entity projection of '{entityType}'. + + The following 'SetProperty' failed to translate: 'SetProperty({property}, {value})'. {details} + Unhandled annotatable type '{annotatableType}'. @@ -1086,46 +1146,4 @@ 'VisitChildren' must be overridden in the class deriving from 'SqlExpression'. - - Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves. - - - Table splitting is not supported for entities containing entities mapped to JSON. - - - Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column. - - - Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported. - - - Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner. - - - Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities. - - - Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner. - - - Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly. - - - Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported. - - - Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}. - - - Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'. - - - Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property. - - - JSON property name should only be configured on nested owned navigations. - - - This method needs to be implemented in the provider. - \ No newline at end of file diff --git a/src/EFCore.Relational/Query/EntityProjectionExpression.cs b/src/EFCore.Relational/Query/EntityProjectionExpression.cs index fb95ea0f1eb..302e7c5b0a9 100644 --- a/src/EFCore.Relational/Query/EntityProjectionExpression.cs +++ b/src/EFCore.Relational/Query/EntityProjectionExpression.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace Microsoft.EntityFrameworkCore.Query; diff --git a/src/EFCore.Relational/Query/Internal/SelectExpressionPruningExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/SelectExpressionPruningExpressionVisitor.cs index 5c4cf10de5d..2d40657fd4b 100644 --- a/src/EFCore.Relational/Query/Internal/SelectExpressionPruningExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/SelectExpressionPruningExpressionVisitor.cs @@ -40,6 +40,11 @@ public class SelectExpressionPruningExpressionVisitor : ExpressionVisitor case DeleteExpression deleteExpression: return deleteExpression.Update(deleteExpression.SelectExpression.Prune()); + case UpdateExpression updateExpression: + return updateExpression.Update( + updateExpression.SelectExpression.Prune(), + updateExpression.SetColumnValues.Select(e => new SetColumnValue(e.Column, (SqlExpression)Visit(e.Value))).ToList()); + default: return base.Visit(expression); } diff --git a/src/EFCore.Relational/Query/NonQueryExpression.cs b/src/EFCore.Relational/Query/NonQueryExpression.cs index f90adc658ab..ccf2d1fa49d 100644 --- a/src/EFCore.Relational/Query/NonQueryExpression.cs +++ b/src/EFCore.Relational/Query/NonQueryExpression.cs @@ -5,22 +5,57 @@ namespace Microsoft.EntityFrameworkCore.Query; -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +/// +/// +/// An expression that contains a non-query expression. The result of a non-query expression is typically the number of rows affected. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally not used in application code. +/// +/// +/// +/// See Implementation of database providers and extensions +/// and How EF Core queries work for more information and examples. +/// public class NonQueryExpression : Expression, IPrintableExpression { + /// + /// Creates a new instance of the class with associated query expression and command source. + /// + /// The expression to affect rows on the server. + /// The command source to use for this non-query operation. + public NonQueryExpression(Expression expression, CommandSource commandSource) + { + Expression = expression; + CommandSource = commandSource; + } + + /// + /// Creates a new instance of the class with associated delete expression. + /// + /// The delete expression to delete rows on the server. public NonQueryExpression(DeleteExpression deleteExpression) : this(deleteExpression, CommandSource.ExecuteDelete) { } - public NonQueryExpression(DeleteExpression expression, CommandSource commandSource) + /// + /// Creates a new instance of the class with associated update expression. + /// + /// The update expression to update rows on the server. + public NonQueryExpression(UpdateExpression updateExpression) + : this(updateExpression, CommandSource.ExecuteUpdate) { - DeleteExpression = expression; - CommandSource = commandSource; } - public virtual DeleteExpression DeleteExpression { get; } + /// + /// An expression representing the non-query operation to be run against server. + /// + public virtual Expression Expression { get; } + /// + /// The command source to use for this non-query operation. + /// public virtual CommandSource CommandSource { get; } /// @@ -29,23 +64,30 @@ public NonQueryExpression(DeleteExpression expression, CommandSource commandSour /// public sealed override ExpressionType NodeType => ExpressionType.Extension; + /// protected override Expression VisitChildren(ExpressionVisitor visitor) { - var deleteExpression = (DeleteExpression)visitor.Visit(DeleteExpression); + var expression = visitor.Visit(Expression); - return Update(deleteExpression); + return Update(expression); } - public virtual NonQueryExpression Update(DeleteExpression deleteExpression) - => deleteExpression != DeleteExpression - ? new NonQueryExpression(deleteExpression) + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual NonQueryExpression Update(Expression expression) + => expression != Expression + ? new NonQueryExpression(expression, CommandSource) : this; /// public virtual void Print(ExpressionPrinter expressionPrinter) { expressionPrinter.Append($"({nameof(NonQueryExpression)}: "); - expressionPrinter.Visit(DeleteExpression); + expressionPrinter.Visit(Expression); } /// @@ -56,8 +98,8 @@ public override bool Equals(object? obj) && Equals(nonQueryExpression)); private bool Equals(NonQueryExpression nonQueryExpression) - => DeleteExpression == nonQueryExpression.DeleteExpression; + => Expression == nonQueryExpression.Expression; /// - public override int GetHashCode() => DeleteExpression.GetHashCode(); + public override int GetHashCode() => Expression.GetHashCode(); } diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 647bb0afcf1..5dd0a830c74 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -95,12 +95,9 @@ protected virtual void GenerateRootCommand(Expression queryExpression) } break; - case DeleteExpression deleteExpression: - VisitDelete(deleteExpression); - break; - default: - throw new InvalidOperationException(); + base.Visit(queryExpression); + break; } } @@ -1229,4 +1226,48 @@ protected override Expression VisitUnion(UnionExpression unionExpression) return unionExpression; } + + /// + protected override Expression VisitUpdate(UpdateExpression updateExpression) + { + var selectExpression = updateExpression.SelectExpression; + + if (selectExpression.Offset == null + && selectExpression.Limit == null + && selectExpression.Having == null + && selectExpression.Orderings.Count == 0 + && selectExpression.GroupBy.Count == 0 + && selectExpression.Tables.Count == 1 + && selectExpression.Tables[0] == updateExpression.Table + && selectExpression.Projection.Count == 0) + { + _relationalCommandBuilder.Append("UPDATE "); + Visit(updateExpression.Table); + _relationalCommandBuilder.AppendLine(); + using (_relationalCommandBuilder.Indent()) + { + _relationalCommandBuilder.Append("SET "); + GenerateList(updateExpression.SetColumnValues, + e => + { + _relationalCommandBuilder.Append($"{_sqlGenerationHelper.DelimitIdentifier(e.Column.Name)} = "); + Visit(e.Value); + + }, + joinAction: e => e.AppendLine(",")); + _relationalCommandBuilder.AppendLine(); + } + + if (selectExpression.Predicate != null) + { + _relationalCommandBuilder.AppendLine().Append("WHERE "); + Visit(selectExpression.Predicate); + } + + return updateExpression; + } + + throw new InvalidOperationException( + RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate))); + } } diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs index fe29f5a2393..837180ceabb 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs @@ -99,7 +99,7 @@ private sealed class TableAliasVerifyingExpressionVisitor : ExpressionVisitor return relationalSplitCollectionShaperExpression; case NonQueryExpression nonQueryExpression: - VerifyUniqueAliasInExpression(nonQueryExpression.DeleteExpression); + VerifyUniqueAliasInExpression(nonQueryExpression.Expression); return nonQueryExpression; default: diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 82f2f74a278..4cb6e3af477 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -172,7 +172,15 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp when genericMethod == RelationalQueryableExtensions.ExecuteDeleteMethodInfo: return TranslateExecuteDelete(shapedQueryExpression) ?? throw new InvalidOperationException( - RelationalStrings.NonQueryTranslationFailedWithDetails(methodCallExpression.Print(), TranslationErrorDetails)); + RelationalStrings.NonQueryTranslationFailedWithDetails( + methodCallExpression.Print(), TranslationErrorDetails)); + + case nameof(RelationalQueryableExtensions.ExecuteUpdate) + when genericMethod == RelationalQueryableExtensions.ExecuteUpdateMethodInfo: + return TranslateExecuteUpdate(shapedQueryExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()) + ?? throw new InvalidOperationException( + RelationalStrings.NonQueryTranslationFailedWithDetails( + methodCallExpression.Print(), TranslationErrorDetails)); } } } @@ -971,7 +979,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } /// - /// Translates method + /// Translates method /// over the given source. /// /// The shaped query on which the operator is applied. @@ -1056,7 +1064,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp QueryableMethods.AnyWithPredicate.MakeGenericMethod(clrType), source, Expression.Quote(Expression.Lambda( - EntityFrameworkCore.Infrastructure.ExpressionExtensions.BuildEqualsExpression(innerParameter, entityParameter), + Infrastructure.ExpressionExtensions.BuildEqualsExpression(innerParameter, entityParameter), innerParameter))); } @@ -1069,11 +1077,199 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } /// - /// Validates if the current select expression can be used for execute delete operation or it requires to be pushed into a subquery. + /// Translates method + /// over the given source. + /// + /// The shaped query on which the operator is applied. + /// The lambda expression containing statements. + /// The non query after translation. + protected virtual NonQueryExpression? TranslateExecuteUpdate( + ShapedQueryExpression source, + LambdaExpression setPropertyStatements) + { + var propertyValueLambdaExpressions = new List<(LambdaExpression, LambdaExpression)>(); + PopulateSetPropertyStatements(setPropertyStatements.Body, propertyValueLambdaExpressions, setPropertyStatements.Parameters[0]); + if (TranslationErrorDetails != null) + { + return null; + } + + if (propertyValueLambdaExpressions.Count == 0) + { + AddTranslationErrorDetails(RelationalStrings.NoSetPropertyInvocation); + return null; + } + + EntityShaperExpression? entityShaperExpression = null; + var setColumnValues = new List(); + foreach (var (propertyExpression, valueExpression) in propertyValueLambdaExpressions) + { + var left = RemapLambdaBody(source, propertyExpression); + if (!IsValidPropertyAccess(left, out var ese)) + { + AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertyExpression.Print())); + return null; + } + + if (entityShaperExpression is null) + { + entityShaperExpression = ese; + } + else if (!ReferenceEquals(ese, entityShaperExpression)) + { + AddTranslationErrorDetails(RelationalStrings.MultipleEntityPropertiesInSetProperty( + entityShaperExpression.EntityType.DisplayName(), ese.EntityType.DisplayName())); + return null; + } + + var right = RemapLambdaBody(source, valueExpression); + // We generate equality between property = value while translating sothat value infer tye type mapping from property correctly. + // Later we decompose it back into left/right components so that the equality is not in the tree which can get affected by + // null semantics or other visitor. + var setter = Infrastructure.ExpressionExtensions.BuildEqualsExpression(left, right); + var translation = _sqlTranslator.Translate(setter); + if (translation is SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: ColumnExpression column } sqlBinaryExpression) + { + setColumnValues.Add(new SetColumnValue(column, sqlBinaryExpression.Right)); + } + else + { + // We would reach here only if the property is unmapped or value fails to translate. + AddTranslationErrorDetails(RelationalStrings.UnableToTranslateSetProperty( + propertyExpression.Print(), valueExpression.Print(), _sqlTranslator.TranslationErrorDetails)); + return null; + } + } + + Check.DebugAssert(entityShaperExpression != null, "EntityShaperExpression should have a value."); + + var entityType = entityShaperExpression.EntityType; + var mappingStrategy = entityType.GetMappingStrategy(); + if (mappingStrategy == RelationalAnnotationNames.TptMappingStrategy) + { + AddTranslationErrorDetails( + RelationalStrings.ExecuteOperationOnTPT(nameof(RelationalQueryableExtensions.ExecuteUpdate), entityType.DisplayName())); + return null; + } + + if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy + && entityType.GetDirectlyDerivedTypes().Any()) + { + // We allow TPC is it is leaf type + AddTranslationErrorDetails( + RelationalStrings.ExecuteOperationOnTPC(nameof(RelationalQueryableExtensions.ExecuteUpdate), entityType.DisplayName())); + return null; + } + + if (entityType.GetViewOrTableMappings().Count() != 1) + { + AddTranslationErrorDetails( + RelationalStrings.ExecuteOperationOnEntitySplitting( + nameof(RelationalQueryableExtensions.ExecuteUpdate), entityType.DisplayName())); + return null; + } + + var selectExpression = (SelectExpression)source.QueryExpression; + if (IsValidSelectExpressionForExecuteUpdate(selectExpression, entityShaperExpression, out var tableExpression)) + { + selectExpression.ReplaceProjection(new List()); + selectExpression.ApplyProjection(); + + return new NonQueryExpression(new UpdateExpression(tableExpression, selectExpression, setColumnValues)); + } + + // We need to convert to join with original query using PK + var pk = entityType.FindPrimaryKey(); + if (pk == null) + { + AddTranslationErrorDetails( + RelationalStrings.ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator( + nameof(RelationalQueryableExtensions.ExecuteUpdate), + entityType.DisplayName())); + return null; + } + + //var clrType = entityType.ClrType; + //var entityParameter = Expression.Parameter(clrType); + //Expression predicateBody; + //if (pk.Properties.Count == 1) + //{ + // predicateBody = Expression.Call( + // QueryableMethods.Contains.MakeGenericMethod(clrType), source, entityParameter); + //} + //else + //{ + // var innerParameter = Expression.Parameter(clrType); + // predicateBody = Expression.Call( + // QueryableMethods.AnyWithPredicate.MakeGenericMethod(clrType), + // source, + // Expression.Quote(Expression.Lambda(Expression.Equal(innerParameter, entityParameter), innerParameter))); + //} + + //var newSource = Expression.Call( + // QueryableMethods.Where.MakeGenericMethod(clrType), + // new EntityQueryRootExpression(entityType), + // Expression.Quote(Expression.Lambda(predicateBody, entityParameter))); + + //return TranslateExecuteDelete((ShapedQueryExpression)Visit(newSource)); + + return null; + + void PopulateSetPropertyStatements( + Expression expression, List<(LambdaExpression, LambdaExpression)> list, ParameterExpression parameter) + { + switch (expression) + { + case ParameterExpression p + when parameter == p: + break; + + case MethodCallExpression methodCallExpression + when methodCallExpression.Method.IsGenericMethod + && methodCallExpression.Method.Name == nameof(SetPropertyStatements.SetProperty) + && methodCallExpression.Method.DeclaringType!.IsGenericType + && methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(SetPropertyStatements<>): + + list.Add((methodCallExpression.Arguments[0].UnwrapLambdaFromQuote(), + methodCallExpression.Arguments[1].UnwrapLambdaFromQuote())); + PopulateSetPropertyStatements(methodCallExpression.Object!, list, parameter); + + break; + + default: + AddTranslationErrorDetails(RelationalStrings.InvalidArgumentToExecuteUpdate); + break; + } + } + + static bool IsValidPropertyAccess(Expression expression, [NotNullWhen(true)] out EntityShaperExpression? entityShaperExpression) + { + if (expression is MemberExpression { Expression: EntityShaperExpression ese }) + { + entityShaperExpression = ese; + return true; + } + + if (expression is MethodCallExpression mce + && mce.TryGetEFPropertyArguments(out var source, out _) + && source is EntityShaperExpression ese1) + { + entityShaperExpression = ese1; + return true; + } + + entityShaperExpression = null; + return false; + } + } + + /// + /// Checks weather the current select expression can be used as-is for execute a delete operation, + /// or whether it must be pushed down into a subquery. /// /// /// - /// By default, only single-table select expressions are supported, and only with a predicate. + /// By default, only single-table select expressions are supported, and optionally with a predicate. /// /// /// Providers can override this to allow more select expression features to be supported without pushing down into a subquery. @@ -1082,7 +1278,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp /// /// /// The select expression to validate. - /// The entity shaper expression on which delete operation is being applied. + /// The entity shaper expression on which the delete operation is being applied. /// The table expression from which rows are being deleted. /// das protected virtual bool IsValidSelectExpressionForExecuteDelete( @@ -1098,9 +1294,51 @@ protected virtual bool IsValidSelectExpressionForExecuteDelete( && selectExpression.Having == null && selectExpression.Orderings.Count == 0 && selectExpression.Tables.Count == 1 - && selectExpression.Tables[0] is TableExpression) + && selectExpression.Tables[0] is TableExpression expression) + { + tableExpression = expression; + + return true; + } + + tableExpression = null; + return false; + } + + // TODO: Update this documentation. + /// + /// Validates if the current select expression can be used for execute update operation or it requires to be pushed into a subquery. + /// + /// + /// + /// By default, only single-table select expressions are supported, and optionally with a predicate. + /// + /// + /// Providers can override this to allow more select expression features to be supported without pushing down into a subquery. + /// When doing this, VisitUpdate must also be overridden in the provider's QuerySqlGenerator to add SQL generation support for + /// the feature. + /// + /// + /// The select expression to validate. + /// The entity shaper expression on which the update operation is being applied. + /// The table expression from which rows are being deleted. + /// das + protected virtual bool IsValidSelectExpressionForExecuteUpdate( + SelectExpression selectExpression, + EntityShaperExpression entityShaperExpression, + [NotNullWhen(true)] out TableExpression? tableExpression) + { + 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 + && selectExpression.Tables.Count == 1 + && selectExpression.Tables[0] is TableExpression expression) { - tableExpression = (TableExpression)selectExpression.Tables[0]; + tableExpression = expression; return true; } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index 7c3a5ffe687..e6c37bd1a48 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -60,7 +60,7 @@ protected virtual Expression VisitNonQuery(NonQueryExpression nonQueryExpression Dependencies.MemoryCache, RelationalDependencies.QuerySqlGeneratorFactory, RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, - nonQueryExpression.DeleteExpression, + nonQueryExpression.Expression, _useRelationalNulls); return Expression.Call( diff --git a/src/EFCore.Relational/Query/SetPropertyStatements.cs b/src/EFCore.Relational/Query/SetPropertyStatements.cs new file mode 100644 index 00000000000..3972a42d536 --- /dev/null +++ b/src/EFCore.Relational/Query/SetPropertyStatements.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// +/// Supports specifying property and value to be set in ExecuteUpdate method with chaining multiple calls for updating +/// multiple columns. +/// +/// +/// This type does not have any constructor or implementation since it is used inside LINQ query solely for the purpose of +/// creating expression tree. +/// +/// +/// +/// See Implementation of database providers and extensions +/// and How EF Core queries work for more information and examples. +/// +/// The type of source element on which ExecuteUpdate operation is being applied. +public sealed class SetPropertyStatements +{ + /// + /// Specifies a property and corresponding value it should be updated to in ExecuteUpdate method. + /// + /// The type of property. + /// A property access expression. + /// A value expression. + /// The same instance so that multiple calls to can be chained. + public SetPropertyStatements SetProperty( + Expression> propertyExpression, + Expression> valueExpression) + { + throw new InvalidOperationException(RelationalStrings.SetPropertyMethodInvoked); + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index 2aa4c1fe8f8..e337001b143 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -18,108 +18,44 @@ public abstract class SqlExpressionVisitor : ExpressionVisitor { /// protected override Expression VisitExtension(Expression extensionExpression) + => extensionExpression switch { - switch (extensionExpression) - { - case ShapedQueryExpression shapedQueryExpression: - return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)); - - case AtTimeZoneExpression atTimeZoneExpression: - return VisitAtTimeZone(atTimeZoneExpression); - - case CaseExpression caseExpression: - return VisitCase(caseExpression); - - case CollateExpression collateExpression: - return VisitCollate(collateExpression); - - case ColumnExpression columnExpression: - return VisitColumn(columnExpression); - - case CrossApplyExpression crossApplyExpression: - return VisitCrossApply(crossApplyExpression); - - case CrossJoinExpression crossJoinExpression: - return VisitCrossJoin(crossJoinExpression); - - case DeleteExpression deleteExpression: - return VisitDelete(deleteExpression); - - case DistinctExpression distinctExpression: - return VisitDistinct(distinctExpression); - - case ExceptExpression exceptExpression: - return VisitExcept(exceptExpression); - - case ExistsExpression existsExpression: - return VisitExists(existsExpression); - - case FromSqlExpression fromSqlExpression: - return VisitFromSql(fromSqlExpression); - - case InExpression inExpression: - return VisitIn(inExpression); - - case IntersectExpression intersectExpression: - return VisitIntersect(intersectExpression); - - case InnerJoinExpression innerJoinExpression: - return VisitInnerJoin(innerJoinExpression); - - case LeftJoinExpression leftJoinExpression: - return VisitLeftJoin(leftJoinExpression); - - case LikeExpression likeExpression: - return VisitLike(likeExpression); - - case OrderingExpression orderingExpression: - return VisitOrdering(orderingExpression); - - case OuterApplyExpression outerApplyExpression: - return VisitOuterApply(outerApplyExpression); - - case ProjectionExpression projectionExpression: - return VisitProjection(projectionExpression); - - case TableValuedFunctionExpression tableValuedFunctionExpression: - return VisitTableValuedFunction(tableValuedFunctionExpression); - - case RowNumberExpression rowNumberExpression: - return VisitRowNumber(rowNumberExpression); - - case ScalarSubqueryExpression scalarSubqueryExpression: - return VisitScalarSubquery(scalarSubqueryExpression); - - case SelectExpression selectExpression: - return VisitSelect(selectExpression); - - case SqlBinaryExpression sqlBinaryExpression: - return VisitSqlBinary(sqlBinaryExpression); - - case SqlConstantExpression sqlConstantExpression: - return VisitSqlConstant(sqlConstantExpression); - - case SqlFragmentExpression sqlFragmentExpression: - return VisitSqlFragment(sqlFragmentExpression); - - case SqlFunctionExpression sqlFunctionExpression: - return VisitSqlFunction(sqlFunctionExpression); - - case SqlParameterExpression sqlParameterExpression: - return VisitSqlParameter(sqlParameterExpression); - - case SqlUnaryExpression sqlUnaryExpression: - return VisitSqlUnary(sqlUnaryExpression); - - case TableExpression tableExpression: - return VisitTable(tableExpression); - - case UnionExpression unionExpression: - return VisitUnion(unionExpression); - } - - return base.VisitExtension(extensionExpression); - } + ShapedQueryExpression shapedQueryExpression + => shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)), + AtTimeZoneExpression atTimeZoneExpression => VisitAtTimeZone(atTimeZoneExpression), + CaseExpression caseExpression => VisitCase(caseExpression), + CollateExpression collateExpression => VisitCollate(collateExpression), + ColumnExpression columnExpression => VisitColumn(columnExpression), + CrossApplyExpression crossApplyExpression => VisitCrossApply(crossApplyExpression), + CrossJoinExpression crossJoinExpression => VisitCrossJoin(crossJoinExpression), + DeleteExpression deleteExpression => VisitDelete(deleteExpression), + DistinctExpression distinctExpression => VisitDistinct(distinctExpression), + ExceptExpression exceptExpression => VisitExcept(exceptExpression), + ExistsExpression existsExpression => VisitExists(existsExpression), + FromSqlExpression fromSqlExpression => VisitFromSql(fromSqlExpression), + InExpression inExpression => VisitIn(inExpression), + IntersectExpression intersectExpression => VisitIntersect(intersectExpression), + InnerJoinExpression innerJoinExpression => VisitInnerJoin(innerJoinExpression), + LeftJoinExpression leftJoinExpression => VisitLeftJoin(leftJoinExpression), + LikeExpression likeExpression => VisitLike(likeExpression), + OrderingExpression orderingExpression => VisitOrdering(orderingExpression), + OuterApplyExpression outerApplyExpression => VisitOuterApply(outerApplyExpression), + ProjectionExpression projectionExpression => VisitProjection(projectionExpression), + TableValuedFunctionExpression tableValuedFunctionExpression => VisitTableValuedFunction(tableValuedFunctionExpression), + RowNumberExpression rowNumberExpression => VisitRowNumber(rowNumberExpression), + ScalarSubqueryExpression scalarSubqueryExpression => VisitScalarSubquery(scalarSubqueryExpression), + SelectExpression selectExpression => VisitSelect(selectExpression), + SqlBinaryExpression sqlBinaryExpression => VisitSqlBinary(sqlBinaryExpression), + SqlConstantExpression sqlConstantExpression => VisitSqlConstant(sqlConstantExpression), + SqlFragmentExpression sqlFragmentExpression => VisitSqlFragment(sqlFragmentExpression), + SqlFunctionExpression sqlFunctionExpression => VisitSqlFunction(sqlFunctionExpression), + SqlParameterExpression sqlParameterExpression => VisitSqlParameter(sqlParameterExpression), + SqlUnaryExpression sqlUnaryExpression => VisitSqlUnary(sqlUnaryExpression), + TableExpression tableExpression => VisitTable(tableExpression), + UnionExpression unionExpression => VisitUnion(unionExpression), + UpdateExpression updateExpression => VisitUpdate(updateExpression), + _ => base.VisitExtension(extensionExpression), + }; /// @@ -338,4 +274,11 @@ protected override Expression VisitExtension(Expression extensionExpression) /// The expression to visit. /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. protected abstract Expression VisitUnion(UnionExpression unionExpression); + + /// + /// Visits the children of the update expression. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + protected abstract Expression VisitUpdate(UpdateExpression updateExpression); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/CaseWhenClause.cs b/src/EFCore.Relational/Query/SqlExpressions/CaseWhenClause.cs index 609a357ca66..29a7bda05aa 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/CaseWhenClause.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/CaseWhenClause.cs @@ -5,7 +5,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; /// /// -/// An expression that represents a WHEN...THEN... construct in a SQL tree. +/// An object that represents a WHEN...THEN... construct in a SQL tree. /// /// /// This type is typically used by database providers (and other extensions). It is generally diff --git a/src/EFCore.Relational/Query/SqlExpressions/DeleteExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/DeleteExpression.cs index a7c616389bf..1af829df28e 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/DeleteExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/DeleteExpression.cs @@ -3,17 +3,35 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +/// +/// +/// An expression that represents a DELETE operation in a SQL tree. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally not used in application code. +/// +/// public sealed class DeleteExpression : Expression, IPrintableExpression { + /// + /// Creates a new instance of the class. + /// + /// A table on which the delete operation is being applied. + /// A select expression which is used to determine which rows to delete. public DeleteExpression(TableExpression table, SelectExpression selectExpression) { Table = table; SelectExpression = selectExpression; } + /// + /// The table on which the delete operation is being applied. + /// public TableExpression Table { get; } + /// + /// The select expression which is used to determine which rows to delete. + /// public SelectExpression SelectExpression { get; } /// @@ -24,6 +42,7 @@ public override Type Type public sealed override ExpressionType NodeType => ExpressionType.Extension; + /// protected override Expression VisitChildren(ExpressionVisitor visitor) { var selectExpression = (SelectExpression)visitor.Visit(SelectExpression); @@ -31,6 +50,12 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) return Update(selectExpression); } + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. public DeleteExpression Update(SelectExpression selectExpression) => selectExpression != SelectExpression ? new DeleteExpression(Table, selectExpression) diff --git a/src/EFCore.Relational/Query/SqlExpressions/SetColumnValue.cs b/src/EFCore.Relational/Query/SqlExpressions/SetColumnValue.cs new file mode 100644 index 00000000000..72bae3169ab --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/SetColumnValue.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +/// +/// +/// An object that represents a column = value construct in a SET clause of UPDATE command in SQL tree. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class SetColumnValue +{ + /// + /// Creates a new instance of the class. + /// + /// A column to be updated. + /// A value to be assigned to the column. + public SetColumnValue(ColumnExpression column, SqlExpression value) + { + Column = column; + Value = value; + } + + /// + /// The column to update value of. + /// + public virtual ColumnExpression Column { get; } + + /// + /// The value to be assigned to the column. + /// + public virtual SqlExpression Value { get; } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SetColumnValue setColumnValue + && Equals(setColumnValue)); + + private bool Equals(SetColumnValue setColumnValue) + => Column == setColumnValue.Column + && Value == setColumnValue.Value; + + /// + public override int GetHashCode() => HashCode.Combine(Column, Value); +} + diff --git a/src/EFCore.Relational/Query/SqlExpressions/UpdateExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/UpdateExpression.cs new file mode 100644 index 00000000000..326ccca3fef --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/UpdateExpression.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +/// +/// +/// An expression that represents an UPDATE operation in a SQL tree. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public sealed class UpdateExpression : Expression, IPrintableExpression +{ + /// + /// Creates a new instance of the class. + /// + /// A table on which the update operation is being applied. + /// A select expression which is used to determine which rows to update and to get data from additional tables. + /// A list of which specifies columns and their corresponding values to update. + public UpdateExpression(TableExpression table, SelectExpression selectExpression, IReadOnlyList setColumnValues) + { + Table = table; + SelectExpression = selectExpression; + SetColumnValues = setColumnValues; + } + + /// + /// The table on which the update operation is being applied. + /// + public TableExpression Table { get; } + + /// + /// The select expression which is used to determine which rows to update and to get data from additional tables. + /// + public SelectExpression SelectExpression { get; } + + /// + /// The list of which specifies columns and their corresponding values to update. + /// + public IReadOnlyList SetColumnValues { get; } + + /// + public override Type Type + => typeof(object); + + /// + public sealed override ExpressionType NodeType + => ExpressionType.Extension; + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var selectExpression = (SelectExpression)visitor.Visit(SelectExpression); + List? setColumnValues = null; + for (var (i, n) = (0, SetColumnValues.Count); i < n; i++) + { + var setColumnValue = SetColumnValues[i]; + var newValue = (SqlExpression)visitor.Visit(setColumnValue.Value); + if (setColumnValues != null) + { + setColumnValues.Add(new SetColumnValue(setColumnValue.Column, newValue)); + } + else if (!ReferenceEquals(newValue, setColumnValue.Value)) + { + setColumnValues = new(); + for (var j = 0; j < i; j++) + { + setColumnValues.Add(SetColumnValues[j]); + } + setColumnValues.Add(new SetColumnValue(setColumnValue.Column, newValue)); + } + } + + return selectExpression != SelectExpression + || setColumnValues != null + ? new UpdateExpression(Table, selectExpression, setColumnValues ?? SetColumnValues) + : this; + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public UpdateExpression Update(SelectExpression selectExpression, IReadOnlyList setColumnValues) + => selectExpression != SelectExpression || !SetColumnValues.SequenceEqual(setColumnValues) + ? new UpdateExpression(Table, selectExpression, setColumnValues) + : this; + + /// + public void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.AppendLine($"UPDATE {Table.Name} AS {Table.Alias}"); + expressionPrinter.AppendLine("SET"); + using (expressionPrinter.Indent()) + { + var first = true; + foreach (var setColumnValue in SetColumnValues) + { + if (first) + { + first = false; + } + else + { + expressionPrinter.AppendLine(","); + } + expressionPrinter.Visit(setColumnValue.Column); + expressionPrinter.Append(" = "); + expressionPrinter.Visit(setColumnValue.Value); + } + } + expressionPrinter.Visit(SelectExpression); + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is UpdateExpression updateExpression + && Equals(updateExpression)); + + private bool Equals(UpdateExpression updateExpression) + => Table == updateExpression.Table + && SelectExpression == updateExpression.SelectExpression + && SetColumnValues.SequenceEqual(updateExpression.SetColumnValues); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(Table); + hash.Add(SelectExpression); + foreach (var item in SetColumnValues) + { + hash.Add(item); + } + + return hash.ToHashCode(); + } +} + diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 7531af3a256..c72928423c2 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace Microsoft.EntityFrameworkCore.Query; @@ -79,6 +78,7 @@ public virtual Expression Process( { SelectExpression selectExpression => (Expression)Visit(selectExpression), DeleteExpression deleteExpression => deleteExpression.Update(Visit(deleteExpression.SelectExpression)), + UpdateExpression updateExpression => VisitUpdate(updateExpression), _ => throw new InvalidOperationException(), }; @@ -87,6 +87,35 @@ public virtual Expression Process( return result; } + private UpdateExpression VisitUpdate(UpdateExpression updateExpression) + { + var selectExpression = Visit(updateExpression.SelectExpression); + List? setColumnValues = null; + for (var (i, n) = (0, updateExpression.SetColumnValues.Count); i < n; i++) + { + var setColumnValue = updateExpression.SetColumnValues[i]; + var newValue = Visit(setColumnValue.Value, out _); + if (setColumnValues != null) + { + setColumnValues.Add(new SetColumnValue(setColumnValue.Column, newValue)); + } + else if (!ReferenceEquals(newValue, setColumnValue.Value)) + { + setColumnValues = new(); + for (var j = 0; j < i; j++) + { + setColumnValues.Add(updateExpression.SetColumnValues[j]); + } + setColumnValues.Add(new SetColumnValue(setColumnValue.Column, newValue)); + } + } + + return selectExpression != updateExpression.SelectExpression + || setColumnValues != null + ? new UpdateExpression(updateExpression.Table, selectExpression, setColumnValues ?? updateExpression.SetColumnValues) + : updateExpression; + } + /// /// Marks the select expression being processed as cannot be cached. /// diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index f45b7632d43..f5ec4af665a 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -720,4 +720,38 @@ protected override Expression VisitUnion(UnionExpression unionExpression) return unionExpression.Update(source1, source2); } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitUpdate(UpdateExpression updateExpression) + { + var selectExpression = (SelectExpression)Visit(updateExpression.SelectExpression); + var parentSearchCondition = _isSearchCondition; + _isSearchCondition = false; + List? setColumnValues = null; + for (var (i, n) = (0, updateExpression.SetColumnValues.Count); i < n; i++) + { + var setColumnValue = updateExpression.SetColumnValues[i]; + var newValue = (SqlExpression)Visit(setColumnValue.Value); + if (setColumnValues != null) + { + setColumnValues.Add(new SetColumnValue(setColumnValue.Column, newValue)); + } + else if (!ReferenceEquals(newValue, setColumnValue.Value)) + { + setColumnValues = new(); + for (var j = 0; j < i; j++) + { + setColumnValues.Add(updateExpression.SetColumnValues[j]); + } + setColumnValues.Add(new SetColumnValue(setColumnValue.Column, newValue)); + } + } + _isSearchCondition = parentSearchCondition; + return updateExpression.Update(selectExpression, setColumnValues ?? updateExpression.SetColumnValues); + } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index 945262affac..85d4bd799dc 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -69,6 +69,58 @@ protected override Expression VisitDelete(DeleteExpression deleteExpression) RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteDelete))); } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitUpdate(UpdateExpression updateExpression) + { + var selectExpression = updateExpression.SelectExpression; + + if (selectExpression.Offset == null + && selectExpression.Limit == null + && selectExpression.Having == null + && selectExpression.Orderings.Count == 0 + && selectExpression.GroupBy.Count == 0 + && selectExpression.Tables.Count == 1 + && selectExpression.Tables[0] == updateExpression.Table + && selectExpression.Projection.Count == 0) + { + Sql.Append("UPDATE "); + Sql.AppendLine($"{Dependencies.SqlGenerationHelper.DelimitIdentifier(updateExpression.Table.Alias)}"); + using (Sql.Indent()) + { + Sql.Append("SET "); + GenerateList(updateExpression.SetColumnValues, + e => + { + Visit(e.Column); + Sql.Append(" = "); + Visit(e.Value); + + }, + joinAction: e => e.AppendLine(",")); + Sql.AppendLine(); + } + + Sql.Append("FROM "); + GenerateList(selectExpression.Tables, e => Visit(e), sql => sql.AppendLine()); + + if (selectExpression.Predicate != null) + { + Sql.AppendLine().Append("WHERE "); + Visit(selectExpression.Predicate); + } + + return updateExpression; + } + + throw new InvalidOperationException( + RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate))); + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index d04564b4f36..e0f892b9eec 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1490,4 +1490,4 @@ Cannot start tracking the entry for entity type '{entityType}' because it was created by a different StateManager instance. - + \ No newline at end of file diff --git a/src/EFCore/Query/ShapedQueryExpression.cs b/src/EFCore/Query/ShapedQueryExpression.cs index 5f60f9b36b3..33797672f88 100644 --- a/src/EFCore/Query/ShapedQueryExpression.cs +++ b/src/EFCore/Query/ShapedQueryExpression.cs @@ -19,7 +19,7 @@ namespace Microsoft.EntityFrameworkCore.Query; public class ShapedQueryExpression : Expression, IPrintableExpression { /// - /// Creates a new instance of the class with associated query provider. + /// Creates a new instance of the class with associated query and shaper expressions. /// /// The query expression to get results from server. /// The shaper expression to create result objects from server results. diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/BulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/BulkUpdatesTestBase.cs index 95698547f6c..963c21ae6f5 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/BulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/BulkUpdatesTestBase.cs @@ -27,6 +27,16 @@ public Task AssertDelete( int rowsAffectedCount) => BulkUpdatesAsserter.AssertDelete(async, query, rowsAffectedCount); + public Task AssertUpdate( + bool async, + Func> query, + Expression> entitySelector, + Expression, SetPropertyStatements>> setPropertyStatements, + int rowsAffectedCount, + Action, IReadOnlyList> asserter = null) + where TResult : class + => BulkUpdatesAsserter.AssertUpdate(async, query, entitySelector, setPropertyStatements, rowsAffectedCount, asserter); + protected static async Task AssertTranslationFailed(string details, Func query) => Assert.Contains( RelationalStrings.NonQueryTranslationFailedWithDetails("", details)[21..], diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/FiltersInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/FiltersInheritanceBulkUpdatesTestBase.cs index f915cc4dd38..5e85a43743c 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/FiltersInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/FiltersInheritanceBulkUpdatesTestBase.cs @@ -63,5 +63,68 @@ public virtual Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) ss => ss.Set().Where(e => e.CountryId > 0), rowsAffectedCount: 1)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_hierarchy(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(e => e.Name == "Great spotted kiwi"), + e => e, + s => s.SetProperty(e => e.Name, e => "Animal"), + rowsAffectedCount: 1, + (b, a) => a.ForEach(e => Assert.Equal("Animal", e.Name))); + + [ConditionalTheory(Skip = "InnerJoin")] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_hierarchy_subquery(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(e => e.Name == "Great spotted kiwi").OrderBy(e => e.Name).Skip(0).Take(3), + e => e, + s => s.SetProperty(e => e.Name, e => "Animal"), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_hierarchy_derived(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(e => e.Name == "Great spotted kiwi"), + e => e, + s => s.SetProperty(e => e.Name, e => "Kiwi"), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_using_hierarchy(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(e => e.Animals.Where(a => a.CountryId > 0).Count() > 0), + e => e, + s => s.SetProperty(e => e.Name, e => "Monovia"), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_using_hierarchy_derived(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(e => e.Animals.OfType().Where(a => a.CountryId > 0).Count() > 0), + e => e, + s => s.SetProperty(e => e.Name, e => "Monovia"), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + => AssertTranslationFailed( + RelationalStrings.ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator("ExecuteUpdate", "EagleQuery"), + () => AssertUpdate( + async, + ss => ss.Set().Where(e => e.CountryId > 0), + e => e, + s => s.SetProperty(e => e.Name, e => "Eagle"), + rowsAffectedCount: 1)); + protected abstract void ClearLog(); } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/InheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/InheritanceBulkUpdatesTestBase.cs index 3788dd9a1d8..4b491e425d7 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/InheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/InheritanceBulkUpdatesTestBase.cs @@ -63,5 +63,68 @@ public virtual Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) ss => ss.Set().Where(e => e.CountryId > 0), rowsAffectedCount: 1)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_hierarchy(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(e => e.Name == "Great spotted kiwi"), + e => e, + s => s.SetProperty(e => e.Name, e => "Animal"), + rowsAffectedCount: 1, + (b, a) => a.ForEach(e => Assert.Equal("Animal", e.Name))); + + [ConditionalTheory(Skip = "InnerJoin")] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_hierarchy_subquery(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(e => e.Name == "Great spotted kiwi").OrderBy(e => e.Name).Skip(0).Take(3), + e => e, + s => s.SetProperty(e => e.Name, e => "Animal"), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_hierarchy_derived(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(e => e.Name == "Great spotted kiwi"), + e => e, + s => s.SetProperty(e => e.Name, e => "Kiwi"), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_using_hierarchy(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(e => e.Animals.Where(a => a.CountryId > 0).Count() > 0), + e => e, + s => s.SetProperty(e => e.Name, e => "Monovia"), + rowsAffectedCount: 2); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_using_hierarchy_derived(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(e => e.Animals.OfType().Where(a => a.CountryId > 0).Count() > 0), + e => e, + s => s.SetProperty(e => e.Name, e => "Monovia"), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + => AssertTranslationFailed( + RelationalStrings.ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator("ExecuteUpdate", "EagleQuery"), + () => AssertUpdate( + async, + ss => ss.Set().Where(e => e.CountryId > 0), + e => e, + s => s.SetProperty(e => e.Name, e => "Eagle"), + rowsAffectedCount: 1)); + protected abstract void ClearLog(); } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs index 0124f7ae5bd..a68e4cf351b 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Microsoft.VisualBasic; namespace Microsoft.EntityFrameworkCore.BulkUpdates; @@ -331,6 +332,169 @@ from o in ss.Set().Where(o => o.OrderID < od.OrderID).OrderBy(e => e.Orde select od, rowsAffectedCount: 74); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_constant(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => c.ContactName, c => "Updated"), + rowsAffectedCount: 8, + (b, a) => a.ForEach(c => Assert.Equal("Updated", c.ContactName))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_parameter(bool async) + { + var value = "Abc"; + return AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => c.ContactName, c => value), + rowsAffectedCount: 8, + (b, a) => a.ForEach(c => Assert.Equal("Abc", c.ContactName))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_using_property_plus_constant(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => c.ContactName, c => c.ContactName + "Abc"), + rowsAffectedCount: 8, + (b, a) => b.Zip(a).ForEach(e => Assert.Equal(e.First.ContactName + "Abc", e.Second.ContactName))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_using_property_plus_parameter(bool async) + { + var value = "Abc"; + return AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => c.ContactName, c => c.ContactName + value), + rowsAffectedCount: 8, + (b, a) => b.Zip(a).ForEach(e => Assert.Equal(e.First.ContactName + "Abc", e.Second.ContactName))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_using_property_plus_property(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => c.ContactName, c => c.ContactName + c.CustomerID), + rowsAffectedCount: 8, + (b, a) => b.Zip(a).ForEach(e => Assert.Equal(e.First.ContactName + e.First.CustomerID, e.Second.ContactName))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_constant_using_ef_property(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => EF.Property(c, "ContactName"), c => "Updated"), + rowsAffectedCount: 8, + (b, a) => a.ForEach(c => Assert.Equal("Updated", c.ContactName))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_null(bool async) + => AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => c.ContactName, c => null), + rowsAffectedCount: 8, + (b, a) => a.ForEach(c => Assert.Null(c.ContactName))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_without_property_to_set_throws(bool async) + => AssertTranslationFailed( + RelationalStrings.NoSetPropertyInvocation, + () => AssertUpdate( + async, + ss => ss.Set().Where(od => od.OrderID < 10250), + e => e, + s => s, + rowsAffectedCount: 0)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_with_invalid_lambda_throws(bool async) + => AssertTranslationFailed( + RelationalStrings.InvalidArgumentToExecuteUpdate, + () => AssertUpdate( + async, + ss => ss.Set().Where(od => od.OrderID < 10250), + e => e, + s => s.Maybe(e => e), + rowsAffectedCount: 0)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_where_multi_property_update(bool async) + { + var value = "Abc"; + return AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => c.ContactName, c => value).SetProperty(c => c.City, c => "Seattle"), + rowsAffectedCount: 8, + (b, a) => a.ForEach(c => + { + Assert.Equal("Abc", c.ContactName); + Assert.Equal("Seattle", c.City); + })); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_with_invalid_lambda_in_set_property_throws(bool async) + => AssertTranslationFailed( + RelationalStrings.InvalidPropertyInSetProperty(new ExpressionPrinter().Print((OrderDetail e) => e.MaybeScalar(e => e.OrderID))), + () => AssertUpdate( + async, + ss => ss.Set().Where(od => od.OrderID < 10250), + e => e, + s => s.SetProperty(e => e.MaybeScalar(e => e.OrderID), e => 10300), + rowsAffectedCount: 0)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_multiple_entity_update(bool async) + => AssertTranslationFailed( + RelationalStrings.MultipleEntityPropertiesInSetProperty("Order", "Customer"), + () => AssertUpdate( + async, + ss => ss.Set().Where(o => o.CustomerID.StartsWith("F")) + .Select(e => new { e, Customer = e.Customer }), + e => e.Customer, + s => s.SetProperty(c => c.Customer.ContactName, c => "Name").SetProperty(c => c.e.OrderDate, e => new DateTime(2020, 1, 1)), + rowsAffectedCount: 0)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_unmapped_property(bool async) + => AssertTranslationFailed( + RelationalStrings.UnableToTranslateSetProperty("c => c.IsLondon", "c => True", + CoreStrings.QueryUnableToTranslateMember("IsLondon", "Customer")), + () => AssertUpdate( + async, + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), + e => e, + s => s.SetProperty(c => c.IsLondon, c => true), + rowsAffectedCount: 0)); + protected string NormalizeDelimitersInRawString(string sql) => Fixture.TestStore.NormalizeDelimitersInRawString(sql); diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesTestBase.cs index 405a4cf642f..cbab0c39ede 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesTestBase.cs @@ -18,4 +18,12 @@ public override Task Delete_where_hierarchy(bool async) => AssertTranslationFailed( RelationalStrings.ExecuteOperationOnTPC("ExecuteDelete", "Animal"), () => base.Delete_where_hierarchy(async)); + + // Keyless entities are mapped as TPH only + public override Task Update_where_keyless_entity_mapped_to_sql_query(bool async) => Task.CompletedTask; + + public override Task Update_where_hierarchy(bool async) + => AssertTranslationFailed( + RelationalStrings.ExecuteOperationOnTPC("ExecuteUpdate", "Animal"), + () => base.Update_where_hierarchy(async)); } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCInheritanceBulkUpdatesTestBase.cs index b208572abed..1091866fa79 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCInheritanceBulkUpdatesTestBase.cs @@ -18,4 +18,12 @@ public override Task Delete_where_hierarchy(bool async) => AssertTranslationFailed( RelationalStrings.ExecuteOperationOnTPC("ExecuteDelete", "Animal"), () => base.Delete_where_hierarchy(async)); + + // Keyless entities are mapped as TPH only + public override Task Update_where_keyless_entity_mapped_to_sql_query(bool async) => Task.CompletedTask; + + public override Task Update_where_hierarchy(bool async) + => AssertTranslationFailed( + RelationalStrings.ExecuteOperationOnTPC("ExecuteUpdate", "Animal"), + () => base.Update_where_hierarchy(async)); } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs index f1d84ea8b36..9e144a3b6a1 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs @@ -35,4 +35,17 @@ public override Task Delete_where_using_hierarchy_derived(bool async) { return base.Delete_where_using_hierarchy_derived(async); } + + // Keyless entities are mapped as TPH only + public override Task Update_where_keyless_entity_mapped_to_sql_query(bool async) => Task.CompletedTask; + + public override Task Update_where_hierarchy(bool async) + => AssertTranslationFailed( + RelationalStrings.ExecuteOperationOnTPT("ExecuteUpdate", "Animal"), + () => base.Update_where_hierarchy(async)); + + public override Task Update_where_hierarchy_derived(bool async) + => AssertTranslationFailed( + RelationalStrings.ExecuteOperationOnTPT("ExecuteUpdate", "Kiwi"), + () => base.Update_where_hierarchy_derived(async)); } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs index d2c2d6a9a89..3d97ec4d2e4 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs @@ -35,4 +35,17 @@ public override Task Delete_where_using_hierarchy_derived(bool async) { return base.Delete_where_using_hierarchy_derived(async); } + + // Keyless entities are mapped as TPH only + public override Task Update_where_keyless_entity_mapped_to_sql_query(bool async) => Task.CompletedTask; + + public override Task Update_where_hierarchy(bool async) + => AssertTranslationFailed( + RelationalStrings.ExecuteOperationOnTPT("ExecuteUpdate", "Animal"), + () => base.Update_where_hierarchy(async)); + + public override Task Update_where_hierarchy_derived(bool async) + => AssertTranslationFailed( + RelationalStrings.ExecuteOperationOnTPT("ExecuteUpdate", "Kiwi"), + () => base.Update_where_hierarchy_derived(async)); } diff --git a/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs b/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs index 24100a2955b..3cb84e24c1d 100644 --- a/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs @@ -66,6 +66,36 @@ await TestHelpers.ExecuteWithStrategyInTransactionAsync( } } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task ExecuteUpdate_throws_for_entity_splitting(bool async) + { + await InitializeAsync(OnModelCreating, sensitiveLogEnabled: true); + + if (async) + { + await TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => Assert.Contains( + RelationalStrings.NonQueryTranslationFailedWithDetails( + "", RelationalStrings.ExecuteOperationOnEntitySplitting("ExecuteUpdate", "MeterReading"))[21..], + (await Assert.ThrowsAsync( + () => context.MeterReadings.ExecuteUpdateAsync(s => s.SetProperty(m => m.CurrentRead, m => "Value")))).Message)); + } + else + { + TestHelpers.ExecuteWithStrategyInTransaction( + CreateContext, + UseTransaction, + context => Assert.Contains( + RelationalStrings.NonQueryTranslationFailedWithDetails( + "", RelationalStrings.ExecuteOperationOnEntitySplitting("ExecuteUpdate", "MeterReading"))[21..], + Assert.Throws( + () => context.MeterReadings.ExecuteUpdate(s => s.SetProperty(m => m.CurrentRead, m => "Value"))).Message)); + } + } + public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); diff --git a/test/EFCore.Relational.Specification.Tests/TPTTableSplittingTestBase.cs b/test/EFCore.Relational.Specification.Tests/TPTTableSplittingTestBase.cs index ab35341fddf..75d0ef40563 100644 --- a/test/EFCore.Relational.Specification.Tests/TPTTableSplittingTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/TPTTableSplittingTestBase.cs @@ -21,6 +21,14 @@ public override Task Can_use_optional_dependents_with_shared_concurrency_tokens( public override Task ExecuteDelete_throws_for_table_sharing(bool async) => Task.CompletedTask; + public override async Task ExecuteUpdate_works_for_table_sharing(bool async) + { + Assert.Contains( + RelationalStrings.NonQueryTranslationFailedWithDetails( + "", RelationalStrings.ExecuteOperationOnTPT("ExecuteUpdate", "Vehicle"))[21..], + (await Assert.ThrowsAsync(() => base.ExecuteUpdate_works_for_table_sharing(async))).Message); + } + protected override string StoreName => "TPTTableSplittingTest"; diff --git a/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs b/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs index e754c8f0ed0..db77bd0fcad 100644 --- a/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs @@ -646,6 +646,35 @@ await TestHelpers.ExecuteWithStrategyInTransactionAsync( } } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task ExecuteUpdate_works_for_table_sharing(bool async) + { + await InitializeAsync(OnModelCreating); + + if (async) + { + await TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => await context.Set().ExecuteUpdateAsync(s => s.SetProperty(e => e.SeatingCapacity, e => 1)), + context => + { + Assert.True(context.Set().All(e => e.SeatingCapacity == 1)); + + return Task.CompletedTask; + }); + } + else + { + TestHelpers.ExecuteWithStrategyInTransaction( + CreateContext, + UseTransaction, + context => context.Set().ExecuteUpdate(s => s.SetProperty(e => e.SeatingCapacity, e => 1)), + context => Assert.True(context.Set().All(e => e.SeatingCapacity == 1))); + } + } + public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs index fdef4890680..351dc0b37b7 100644 --- a/test/EFCore.Relational.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/BulkUpdatesAsserter.cs @@ -11,6 +11,7 @@ public class BulkUpdatesAsserter private readonly Action _useTransaction; private readonly Func _setSourceCreator; private readonly Func _rewriteServerQueryExpression; + private readonly IReadOnlyDictionary _entitySorters; public BulkUpdatesAsserter(IBulkUpdatesFixtureBase queryFixture, Func rewriteServerQueryExpression) { @@ -18,6 +19,7 @@ public BulkUpdatesAsserter(IBulkUpdatesFixtureBase queryFixture, Func(); } public async Task AssertDelete( @@ -53,6 +55,63 @@ await TestHelpers.ExecuteWithStrategyInTransactionAsync( } } + public async Task AssertUpdate( + bool async, + Func> query, + Expression> entitySelector, + Expression, SetPropertyStatements>> setPropertyStatements, + int rowsAffectedCount, + Action, IReadOnlyList> asserter) + where TResult : class + { + _entitySorters.TryGetValue(typeof(TEntity), out var sorter); + var elementSorter = (Func)sorter; + if (async) + { + await TestHelpers.ExecuteWithStrategyInTransactionAsync( + _contextCreator, _useTransaction, + async context => + { + var processedQuery = RewriteServerQuery(query(_setSourceCreator(context))); + + var before = processedQuery.AsNoTracking().Select(entitySelector).OrderBy(elementSorter).ToList(); + + var result = await processedQuery.ExecuteUpdateAsync(setPropertyStatements); + + Assert.Equal(rowsAffectedCount, result); + + var after = processedQuery.AsNoTracking().Select(entitySelector).OrderBy(elementSorter).ToList(); + + if (asserter != null) + { + asserter(before, after); + } + }); + } + else + { + TestHelpers.ExecuteWithStrategyInTransaction( + _contextCreator, _useTransaction, + context => + { + var processedQuery = RewriteServerQuery(query(_setSourceCreator(context))); + + var before = processedQuery.AsNoTracking().Select(entitySelector).OrderBy(elementSorter).ToList(); + + var result = processedQuery.ExecuteUpdate(setPropertyStatements); + + Assert.Equal(rowsAffectedCount, result); + + var after = processedQuery.AsNoTracking().Select(entitySelector).OrderBy(elementSorter).ToList(); + + if (asserter != null) + { + asserter(before, after); + } + }); + } + } + private IQueryable RewriteServerQuery(IQueryable query) => query.Provider.CreateQuery(_rewriteServerQueryExpression(query.Expression)); } diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs index 82923d7d274..58b511e666f 100644 --- a/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs @@ -41,23 +41,25 @@ public IReadOnlyList Parameters public string Sql => string.Join(_eol + _eol, SqlStatements); - public void AssertBaseline(string[] expected, bool assertOrder = true) + public void AssertBaseline(string[] expected, bool assertOrder = true, bool forUpdate = false) { if (_proceduralQueryGeneration) { return; } + var offset = forUpdate ? 1 : 0; + var count = SqlStatements.Count - offset - offset; try { if (assertOrder) { for (var i = 0; i < expected.Length; i++) { - Assert.Equal(expected[i], SqlStatements[i], ignoreLineEndingDifferences: true); + Assert.Equal(expected[i], SqlStatements[i + offset], ignoreLineEndingDifferences: true); } - Assert.Empty(SqlStatements.Skip(expected.Length)); + Assert.Empty(SqlStatements.Skip(expected.Length + offset + offset)); } else { @@ -100,10 +102,10 @@ public void AssertBaseline(string[] expected, bool assertOrder = true) } var sql = string.Join( - "," + indent + "//" + indent, SqlStatements.Take(9).Select(sql => "@\"" + sql.Replace("\"", "\"\"") + "\"")); + "," + indent + "//" + indent, SqlStatements.Skip(offset).Take(count).Select(sql => "@\"" + sql.Replace("\"", "\"\"") + "\"")); - var newBaseLine = $@" AssertSql( - {string.Join("," + indent + "//" + indent, SqlStatements.Take(20).Select(sql => "@\"" + sql.Replace("\"", "\"\"") + "\""))}); + var newBaseLine = $@" Assert{(forUpdate ? "ExecuteUpdate" : "")}Sql( + {string.Join("," + indent + "//" + indent, SqlStatements.Skip(offset).Take(count).Select(sql => "@\"" + sql.Replace("\"", "\"\"") + "\""))}); "; @@ -131,7 +133,7 @@ public void AssertBaseline(string[] expected, bool assertOrder = true) {{ await base.{methodName}(async); - AssertSql({manipulatedSql}); + Assert{(forUpdate ? "ExecuteUpdate" : "")}Sql({manipulatedSql}); }} " @@ -139,7 +141,7 @@ public void AssertBaseline(string[] expected, bool assertOrder = true) {{ base.{methodName}(); - AssertSql({manipulatedSql}); + Assert{(forUpdate ? "ExecuteUpdate" : "")}Sql({manipulatedSql}); }} "; @@ -273,8 +275,8 @@ void RewriteSourceWithNewBaseline(string fileName, int lineNumber) indentBuilder.Append(" "); var indent = indentBuilder.ToString(); - var newBaseLine = $@"AssertSql( -{indent}{string.Join("," + Environment.NewLine + indent + "//" + Environment.NewLine + indent, SqlStatements.Select(sql => "@\"" + sql.Replace("\"", "\"\"") + "\""))})"; + var newBaseLine = $@"Assert{(forUpdate ? "ExecuteUpdate" : "")}Sql( +{indent}{string.Join("," + Environment.NewLine + indent + "//" + Environment.NewLine + indent, SqlStatements.Skip(offset).Take(count).Select(sql => "@\"" + sql.Replace("\"", "\"\"") + "\""))})"; var numNewlinesInRewritten = newBaseLine.Count(c => c is '\n' or '\r'); writer.Write(newBaseLine); @@ -288,10 +290,10 @@ void RewriteSourceWithNewBaseline(string fileName, int lineNumber) } // Copy the rest of the file contents as-is - int count; - while ((count = reader.ReadBlock(tempBuf, 0, 1024)) > 0) + int c; + while ((c = reader.ReadBlock(tempBuf, 0, 1024)) > 0) { - writer.Write(tempBuf, 0, count); + writer.Write(tempBuf, 0, c); } } } @@ -401,7 +403,7 @@ protected override void UnsafeLog( private struct QueryBaselineRewritingFileInfo { - public QueryBaselineRewritingFileInfo() {} + public QueryBaselineRewritingFileInfo() { } public object Lock { get; set; } = new(); diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqlServerTest.cs index fbe24ed18a6..b5dc9ff2ce6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqlServerTest.cs @@ -75,8 +75,75 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE [a] + SET [a].[Name] = N'Animal' +FROM [Animals] AS [a] +WHERE [a].[CountryId] = 1 AND [a].[Name] = N'Great spotted kiwi'"); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE [a] + SET [a].[Name] = N'Kiwi' +FROM [Animals] AS [a] +WHERE [a].[Discriminator] = N'Kiwi' AND [a].[CountryId] = 1 AND [a].[Name] = N'Great spotted kiwi'"); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +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 Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +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 Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqlServerTest.cs index 0d98333fb89..19086fa182a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqlServerTest.cs @@ -75,8 +75,75 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE [a] + SET [a].[Name] = N'Animal' +FROM [Animals] AS [a] +WHERE [a].[Name] = N'Great spotted kiwi'"); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE [a] + SET [a].[Name] = N'Kiwi' +FROM [Animals] AS [a] +WHERE [a].[Discriminator] = N'Kiwi' AND [a].[Name] = N'Great spotted kiwi'"); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM [Animals] AS [a] + WHERE [c].[Id] = [a].[CountryId] AND [a].[CountryId] > 0) > 0"); + } + + public override async Task Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM [Animals] AS [a] + WHERE [c].[Id] = [a].[CountryId] AND [a].[Discriminator] = N'Kiwi' AND [a].[CountryId] > 0) > 0"); + } + + public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs index 8c969d9d89c..3893c12c948 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs @@ -502,6 +502,139 @@ OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY WHERE [o].[OrderID] < 10276"); } + public override async Task Update_where_constant(bool async) + { + await base.Update_where_constant(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[ContactName] = N'Updated' +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'F%'"); + } + + public override async Task Update_where_parameter(bool async) + { + await base.Update_where_parameter(async); + + AssertExecuteUpdateSql( + @"@__value_0='Abc' (Size = 4000) + +UPDATE [c] + SET [c].[ContactName] = @__value_0 +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'F%'"); + } + + public override async Task Update_where_using_property_plus_constant(bool async) + { + await base.Update_where_using_property_plus_constant(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[ContactName] = COALESCE([c].[ContactName], N'') + N'Abc' +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'F%'"); + } + + public override async Task Update_where_using_property_plus_parameter(bool async) + { + await base.Update_where_using_property_plus_parameter(async); + + AssertExecuteUpdateSql( + @"@__value_0='Abc' (Size = 4000) + +UPDATE [c] + SET [c].[ContactName] = COALESCE([c].[ContactName], N'') + @__value_0 +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'F%'"); + } + + public override async Task Update_where_using_property_plus_property(bool async) + { + await base.Update_where_using_property_plus_property(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[ContactName] = COALESCE([c].[ContactName], N'') + [c].[CustomerID] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'F%'"); + } + + public override async Task Update_where_constant_using_ef_property(bool async) + { + await base.Update_where_constant_using_ef_property(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[ContactName] = N'Updated' +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'F%'"); + } + + public override async Task Update_where_null(bool async) + { + await base.Update_where_null(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[ContactName] = NULL +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'F%'"); + } + + public override async Task Update_without_property_to_set_throws(bool async) + { + await base.Update_without_property_to_set_throws(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_with_invalid_lambda_throws(bool async) + { + await base.Update_with_invalid_lambda_throws(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_multi_property_update(bool async) + { + await base.Update_where_multi_property_update(async); + + AssertExecuteUpdateSql( + @"@__value_0='Abc' (Size = 4000) + +UPDATE [c] + SET [c].[City] = N'Seattle', + [c].[ContactName] = @__value_0 +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'F%'"); + } + + public override async Task Update_with_invalid_lambda_in_set_property_throws(bool async) + { + await base.Update_with_invalid_lambda_in_set_property_throws(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_multiple_entity_update(bool async) + { + await base.Update_multiple_entity_update(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_unmapped_property(bool async) + { + await base.Update_unmapped_property(async); + + AssertExecuteUpdateSql(); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs index 7c004511b8a..1aff954c2aa 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs @@ -81,8 +81,80 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE [k] + SET [k].[Name] = N'Kiwi' +FROM [Kiwi] AS [k] +WHERE [k].[CountryId] = 1 AND [k].[Name] = N'Great spotted kiwi'"); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT [e].[Id], [e].[CountryId], [e].[Name], [e].[Species], [e].[EagleId], [e].[IsFlightless], [e].[Group], NULL AS [FoundOn], N'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], N'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 Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT [k].[Id], [k].[CountryId], [k].[Name], [k].[Species], [k].[EagleId], [k].[IsFlightless], NULL AS [Group], [k].[FoundOn], N'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 Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs index 83318defb3a..75e7110f4c4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs @@ -81,8 +81,80 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE [k] + SET [k].[Name] = N'Kiwi' +FROM [Kiwi] AS [k] +WHERE [k].[Name] = N'Great spotted kiwi'"); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT [e].[Id], [e].[CountryId], [e].[Name], [e].[Species], [e].[EagleId], [e].[IsFlightless], [e].[Group], NULL AS [FoundOn], N'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], N'Kiwi' AS [Discriminator] + FROM [Kiwi] AS [k] + ) AS [t] + WHERE [c].[Id] = [t].[CountryId] AND [t].[CountryId] > 0) > 0"); + } + + public override async Task Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT [k].[Id], [k].[CountryId], [k].[Name], [k].[Species], [k].[EagleId], [k].[IsFlightless], NULL AS [Group], [k].[FoundOn], N'Kiwi' AS [Discriminator] + FROM [Kiwi] AS [k] + ) AS [t] + WHERE [c].[Id] = [t].[CountryId] AND [t].[CountryId] > 0) > 0"); + } + + public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs index a21545fa841..b07f2686746 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs @@ -69,8 +69,73 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM [Animals] AS [a] + LEFT JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] + LEFT JOIN [Eagle] AS [e] ON [a].[Id] = [e].[Id] + LEFT JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] + WHERE [a].[CountryId] = 1 AND [c].[Id] = [a].[CountryId] AND [a].[CountryId] > 0) > 0"); + } + + public override async Task Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM [Animals] AS [a] + LEFT JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] + LEFT JOIN [Eagle] AS [e] ON [a].[Id] = [e].[Id] + LEFT JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] + WHERE [a].[CountryId] = 1 AND [c].[Id] = [a].[CountryId] AND [k].[Id] IS NOT NULL AND [a].[CountryId] > 0) > 0"); + } + + public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs index 62c479e7d3e..91ce894d821 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs @@ -57,8 +57,73 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM [Animals] AS [a] + LEFT JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] + LEFT JOIN [Eagle] AS [e] ON [a].[Id] = [e].[Id] + LEFT JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] + WHERE [c].[Id] = [a].[CountryId] AND [a].[CountryId] > 0) > 0"); + } + + public override async Task Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE [c] + SET [c].[Name] = N'Monovia' +FROM [Countries] AS [c] +WHERE ( + SELECT COUNT(*) + FROM [Animals] AS [a] + LEFT JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] + LEFT JOIN [Eagle] AS [e] ON [a].[Id] = [e].[Id] + LEFT JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] + WHERE [c].[Id] = [a].[CountryId] AND [k].[Id] IS NOT NULL AND [a].[CountryId] > 0) > 0"); + } + + public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs index c9720f0e1d1..882dda1cb50 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs @@ -162,6 +162,24 @@ WHEN [t].[Active] IS NOT NULL THEN [t].[Name] ORDER BY [v].[Name]"); } + public override async Task ExecuteUpdate_works_for_table_sharing(bool async) + { + await base.ExecuteUpdate_works_for_table_sharing(async); + + AssertSql( + @"UPDATE [v] + SET [v].[SeatingCapacity] = 1 +FROM [Vehicles] AS [v]", + // + @"SELECT CASE + WHEN NOT EXISTS ( + SELECT 1 + FROM [Vehicles] AS [v] + WHERE [v].[SeatingCapacity] <> 1) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END"); + } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqliteTest.cs index 493d16f27a4..6bc4bf8078d 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqliteTest.cs @@ -71,8 +71,75 @@ public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Animals"" AS ""a"" + SET ""Name"" = 'Animal' + +WHERE ""a"".""CountryId"" = 1 AND ""a"".""Name"" = 'Great spotted kiwi'"); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Animals"" AS ""a"" + SET ""Name"" = 'Kiwi' + +WHERE ""a"".""Discriminator"" = 'Kiwi' AND ""a"".""CountryId"" = 1 AND ""a"".""Name"" = 'Great spotted kiwi'"); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +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 Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + 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 Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqliteTest.cs index 4deb68dfe59..c4dd13c0280 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqliteTest.cs @@ -71,8 +71,75 @@ public override async Task Delete_where_keyless_entity_mapped_to_sql_query(bool AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Animals"" AS ""a"" + SET ""Name"" = 'Animal' + +WHERE ""a"".""Name"" = 'Great spotted kiwi'"); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Animals"" AS ""a"" + SET ""Name"" = 'Kiwi' + +WHERE ""a"".""Discriminator"" = 'Kiwi' AND ""a"".""Name"" = 'Great spotted kiwi'"); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + FROM ""Animals"" AS ""a"" + WHERE ""c"".""Id"" = ""a"".""CountryId"" AND ""a"".""CountryId"" > 0) > 0"); + } + + public override async Task Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + FROM ""Animals"" AS ""a"" + WHERE ""c"".""Id"" = ""a"".""CountryId"" AND ""a"".""Discriminator"" = 'Kiwi' AND ""a"".""CountryId"" > 0) > 0"); + } + + public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs index 7402be76bf0..dfff622f6b8 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs @@ -481,6 +481,139 @@ public override async Task Delete_with_outer_apply(bool async) SqliteStrings.ApplyNotSupported, (await Assert.ThrowsAsync(() => base.Delete_with_outer_apply(async))).Message); + public override async Task Update_where_constant(bool async) + { + await base.Update_where_constant(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Customers"" AS ""c"" + SET ""ContactName"" = 'Updated' + +WHERE ""c"".""CustomerID"" LIKE 'F%'"); + } + + public override async Task Update_where_parameter(bool async) + { + await base.Update_where_parameter(async); + + AssertExecuteUpdateSql( + @"@__value_0='Abc' (Size = 3) + +UPDATE ""Customers"" AS ""c"" + SET ""ContactName"" = @__value_0 + +WHERE ""c"".""CustomerID"" LIKE 'F%'"); + } + + public override async Task Update_where_using_property_plus_constant(bool async) + { + await base.Update_where_using_property_plus_constant(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Customers"" AS ""c"" + SET ""ContactName"" = COALESCE(""c"".""ContactName"", '') || 'Abc' + +WHERE ""c"".""CustomerID"" LIKE 'F%'"); + } + + public override async Task Update_where_using_property_plus_parameter(bool async) + { + await base.Update_where_using_property_plus_parameter(async); + + AssertExecuteUpdateSql( + @"@__value_0='Abc' (Size = 3) + +UPDATE ""Customers"" AS ""c"" + SET ""ContactName"" = COALESCE(""c"".""ContactName"", '') || @__value_0 + +WHERE ""c"".""CustomerID"" LIKE 'F%'"); + } + + public override async Task Update_where_using_property_plus_property(bool async) + { + await base.Update_where_using_property_plus_property(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Customers"" AS ""c"" + SET ""ContactName"" = COALESCE(""c"".""ContactName"", '') || ""c"".""CustomerID"" + +WHERE ""c"".""CustomerID"" LIKE 'F%'"); + } + + public override async Task Update_where_constant_using_ef_property(bool async) + { + await base.Update_where_constant_using_ef_property(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Customers"" AS ""c"" + SET ""ContactName"" = 'Updated' + +WHERE ""c"".""CustomerID"" LIKE 'F%'"); + } + + public override async Task Update_where_null(bool async) + { + await base.Update_where_null(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Customers"" AS ""c"" + SET ""ContactName"" = NULL + +WHERE ""c"".""CustomerID"" LIKE 'F%'"); + } + + public override async Task Update_without_property_to_set_throws(bool async) + { + await base.Update_without_property_to_set_throws(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_with_invalid_lambda_throws(bool async) + { + await base.Update_with_invalid_lambda_throws(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_multi_property_update(bool async) + { + await base.Update_where_multi_property_update(async); + + AssertExecuteUpdateSql( + @"@__value_0='Abc' (Size = 3) + +UPDATE ""Customers"" AS ""c"" + SET ""City"" = 'Seattle', + ""ContactName"" = @__value_0 + +WHERE ""c"".""CustomerID"" LIKE 'F%'"); + } + + public override async Task Update_with_invalid_lambda_in_set_property_throws(bool async) + { + await base.Update_with_invalid_lambda_in_set_property_throws(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_multiple_entity_update(bool async) + { + await base.Update_multiple_entity_update(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_unmapped_property(bool async) + { + await base.Update_unmapped_property(async); + + AssertExecuteUpdateSql(); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqliteTest.cs index c1077bef8c3..9a4a9ba6b20 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqliteTest.cs @@ -78,8 +78,80 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Kiwi"" AS ""k"" + SET ""Name"" = 'Kiwi' + +WHERE ""k"".""CountryId"" = 1 AND ""k"".""Name"" = 'Great spotted kiwi'"); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + 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 Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + 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 Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqliteTest.cs index 8966b586522..0efba9d697c 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqliteTest.cs @@ -78,8 +78,80 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Kiwi"" AS ""k"" + SET ""Name"" = 'Kiwi' + +WHERE ""k"".""Name"" = 'Great spotted kiwi'"); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + 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 Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + 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 Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs index 81107741b64..d3ad342b150 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs @@ -69,8 +69,73 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + FROM ""Animals"" AS ""a"" + LEFT JOIN ""Birds"" AS ""b"" ON ""a"".""Id"" = ""b"".""Id"" + LEFT JOIN ""Eagle"" AS ""e"" ON ""a"".""Id"" = ""e"".""Id"" + LEFT JOIN ""Kiwi"" AS ""k"" ON ""a"".""Id"" = ""k"".""Id"" + WHERE ""a"".""CountryId"" = 1 AND ""c"".""Id"" = ""a"".""CountryId"" AND ""a"".""CountryId"" > 0) > 0"); + } + + public override async Task Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + FROM ""Animals"" AS ""a"" + LEFT JOIN ""Birds"" AS ""b"" ON ""a"".""Id"" = ""b"".""Id"" + LEFT JOIN ""Eagle"" AS ""e"" ON ""a"".""Id"" = ""e"".""Id"" + LEFT JOIN ""Kiwi"" AS ""k"" ON ""a"".""Id"" = ""k"".""Id"" + WHERE ""a"".""CountryId"" = 1 AND ""c"".""Id"" = ""a"".""CountryId"" AND ""k"".""Id"" IS NOT NULL AND ""a"".""CountryId"" > 0) > 0"); + } + + public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqliteTest.cs index 32679b52455..e69820f79e0 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqliteTest.cs @@ -57,8 +57,73 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } + public override async Task Update_where_hierarchy(bool async) + { + await base.Update_where_hierarchy(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_subquery(bool async) + { + await base.Update_where_hierarchy_subquery(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_hierarchy_derived(bool async) + { + await base.Update_where_hierarchy_derived(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_where_using_hierarchy(bool async) + { + await base.Update_where_using_hierarchy(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + FROM ""Animals"" AS ""a"" + LEFT JOIN ""Birds"" AS ""b"" ON ""a"".""Id"" = ""b"".""Id"" + LEFT JOIN ""Eagle"" AS ""e"" ON ""a"".""Id"" = ""e"".""Id"" + LEFT JOIN ""Kiwi"" AS ""k"" ON ""a"".""Id"" = ""k"".""Id"" + WHERE ""c"".""Id"" = ""a"".""CountryId"" AND ""a"".""CountryId"" > 0) > 0"); + } + + public override async Task Update_where_using_hierarchy_derived(bool async) + { + await base.Update_where_using_hierarchy_derived(async); + + AssertExecuteUpdateSql( + @"UPDATE ""Countries"" AS ""c"" + SET ""Name"" = 'Monovia' + +WHERE ( + SELECT COUNT(*) + FROM ""Animals"" AS ""a"" + LEFT JOIN ""Birds"" AS ""b"" ON ""a"".""Id"" = ""b"".""Id"" + LEFT JOIN ""Eagle"" AS ""e"" ON ""a"".""Id"" = ""e"".""Id"" + LEFT JOIN ""Kiwi"" AS ""k"" ON ""a"".""Id"" = ""k"".""Id"" + WHERE ""c"".""Id"" = ""a"".""CountryId"" AND ""k"".""Id"" IS NOT NULL AND ""a"".""CountryId"" > 0) > 0"); + } + + public override async Task Update_where_keyless_entity_mapped_to_sql_query(bool async) + { + await base.Update_where_keyless_entity_mapped_to_sql_query(async); + + AssertExecuteUpdateSql(); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private void AssertExecuteUpdateSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, forUpdate: true); } diff --git a/test/EFCore.Sqlite.FunctionalTests/TableSplittingSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/TableSplittingSqliteTest.cs index 2ad4d82c4d3..8eebeab0e48 100644 --- a/test/EFCore.Sqlite.FunctionalTests/TableSplittingSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/TableSplittingSqliteTest.cs @@ -12,6 +12,20 @@ public TableSplittingSqliteTest(ITestOutputHelper testOutputHelper) { } + public override async Task ExecuteUpdate_works_for_table_sharing(bool async) + { + await base.ExecuteUpdate_works_for_table_sharing(async); + + AssertSql( + @"UPDATE ""Vehicles"" AS ""v"" + SET ""SeatingCapacity"" = 1", + // + @"SELECT NOT EXISTS ( + SELECT 1 + FROM ""Vehicles"" AS ""v"" + WHERE ""v"".""SeatingCapacity"" <> 1)"); + } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder);