diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index a78aed237c8..4fffe2709cc 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1017,6 +1017,14 @@ public static string InvalidPropertyInSetProperty(object? propertyExpression) GetString("InvalidPropertyInSetProperty", nameof(propertyExpression)), propertyExpression); + /// + /// The following lambda argument to 'SetProperty' does not represent a valid value: '{valueExpression}'. + /// + public static string InvalidValueInSetProperty(object? valueExpression) + => string.Format( + GetString("InvalidValueInSetProperty", nameof(valueExpression)), + valueExpression); + /// /// Can't navigate from JSON-mapped entity '{jsonEntity}' to its parent entity '{parentEntity}' using navigation '{navigation}'. Entities mapped to JSON can only navigate to their children. /// @@ -1300,12 +1308,12 @@ public static string ModificationCommandInvalidEntityStateSensitive(object? enti 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. + /// Multiple 'SetProperty' invocations refer to different tables ('{propertySelector1}' and '{propertySelector2}'). A single 'ExecuteUpdate' call can only update the columns of a single table. /// - public static string MultipleEntityPropertiesInSetProperty(object? entityType1, object? entityType2) + public static string MultipleTablesInExecuteUpdate(object? propertySelector1, object? propertySelector2) => string.Format( - GetString("MultipleEntityPropertiesInSetProperty", nameof(entityType1), nameof(entityType2)), - entityType1, entityType2); + GetString("MultipleTablesInExecuteUpdate", nameof(propertySelector1), nameof(propertySelector2)), + propertySelector1, propertySelector2); /// /// Multiple relational database provider configurations found. A context can only be configured to use a single database provider. @@ -1989,14 +1997,6 @@ 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}'. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 6ca3e9b0d28..2834edcc92e 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -499,6 +499,9 @@ The following lambda argument to 'SetProperty' does not represent a valid property to be set: '{propertyExpression}'. + + The following lambda argument to 'SetProperty' does not represent a valid value: '{valueExpression}'. + Can't navigate from JSON-mapped entity '{jsonEntity}' to its parent entity '{parentEntity}' using navigation '{navigation}'. Entities mapped to JSON can only navigate to their children. @@ -905,8 +908,8 @@ 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 'SetProperty' invocations refer to different tables ('{propertySelector1}' and '{propertySelector2}'). A single 'ExecuteUpdate' call can only update the columns of a single table. Multiple relational database provider configurations found. A context can only be configured to use a single database provider. @@ -1178,9 +1181,6 @@ 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}'. diff --git a/src/EFCore.Relational/Query/Internal/TpcTablesExpression.cs b/src/EFCore.Relational/Query/Internal/TpcTablesExpression.cs new file mode 100644 index 00000000000..06f161edce7 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/TpcTablesExpression.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Query.Internal; + +/// +/// 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. +/// +public sealed class TpcTablesExpression : TableExpressionBase +{ + /// + /// 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. + /// + public TpcTablesExpression( + string? alias, + IEntityType entityType, + IReadOnlyList subSelectExpressions) + : base(alias) + { + EntityType = entityType; + SelectExpressions = subSelectExpressions; + } + + private TpcTablesExpression( + string? alias, + IEntityType entityType, + IReadOnlyList subSelectExpressions, + IEnumerable? annotations) + : base(alias, annotations) + { + EntityType = entityType; + SelectExpressions = subSelectExpressions; + } + + /// + /// 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. + /// + [NotNull] + public override string? Alias + { + get => base.Alias!; + internal set => base.Alias = value; + } + + /// + /// 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. + /// + public IEntityType EntityType { get; } + + /// + /// 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. + /// + public IReadOnlyList SelectExpressions { get; } + + /// + /// 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. + /// + public TpcTablesExpression Prune(IReadOnlyList discriminatorValues) + { + var subSelectExpressions = discriminatorValues.Count == 0 + ? new List { SelectExpressions[0] } + : SelectExpressions.Where( + se => + discriminatorValues.Contains((string)((SqlConstantExpression)se.Projection[^1].Expression).Value!)).ToList(); + + Check.DebugAssert(subSelectExpressions.Count > 0, "TPC must have at least 1 table selected."); + + return new TpcTablesExpression(Alias, EntityType, subSelectExpressions, GetAnnotations()); + } + + /// + /// 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. + /// + // This is implementation detail hence visitors are not supposed to see inside unless they really need to. + protected override Expression VisitChildren(ExpressionVisitor visitor) + => this; + + /// + /// 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 TableExpressionBase CreateWithAnnotations(IEnumerable annotations) + => new TpcTablesExpression(Alias, EntityType, SelectExpressions, annotations); + + /// + /// 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 void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.AppendLine("("); + using (expressionPrinter.Indent()) + { + expressionPrinter.VisitCollection(SelectExpressions, e => e.AppendLine().AppendLine("UNION ALL")); + } + + expressionPrinter.AppendLine() + .AppendLine(") AS " + Alias); + PrintAnnotations(expressionPrinter); + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is TpcTablesExpression tpcTablesExpression + && Equals(tpcTablesExpression)); + + private bool Equals(TpcTablesExpression tpcTablesExpression) + { + if (!base.Equals(tpcTablesExpression) + || EntityType != tpcTablesExpression.EntityType) + { + return false; + } + + return SelectExpressions.SequenceEqual(tpcTablesExpression.SelectExpressions); + } + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), EntityType); +} diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 01db9271b87..a78e1ffe97a 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; @@ -1416,9 +1415,7 @@ static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBa /// statements. /// /// The non query after translation. - protected virtual NonQueryExpression? TranslateExecuteUpdate( - ShapedQueryExpression source, - LambdaExpression setPropertyCalls) + protected virtual NonQueryExpression? TranslateExecuteUpdate(ShapedQueryExpression source, LambdaExpression setPropertyCalls) { // Our source may have IncludeExpressions because of owned entities or auto-include; unwrap these, as they're meaningless for // ExecuteUpdate's lambdas. Note that we don't currently support updates across tables. @@ -1427,7 +1424,7 @@ static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBa source = source.UpdateShaperExpression(PruneIncludes(includeExpression)); } - var propertyValueLambdaExpressions = new List<(LambdaExpression, Expression)>(); + var propertyValueLambdaExpressions = new List<(LambdaExpression PropertySelector, Expression ValueExpression)>(); PopulateSetPropertyCalls(setPropertyCalls.Body, propertyValueLambdaExpressions, setPropertyCalls.Parameters[0]); if (TranslationErrorDetails != null) { @@ -1440,194 +1437,57 @@ static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBa return null; } - EntityShaperExpression? entityShaperExpression = null; - var remappedUnwrappedLeftExpressions = new List(); - foreach (var (propertyExpression, _) in propertyValueLambdaExpressions) + // Go over the SetProperty calls, and translate the property selectors (left lambda). + // The property selectors should get translated to ColumnExpressions (otherwise they' invalid - columns are what we need to update). + // All columns must also refer to the same table (since that's how SQL UPDATE works), extract that target table from the translated + // columns and validate that only one table is being referenced. + // Note that we don't translate the value expressions in this pass, since if the query is complicated, we may need to do a pushdown + // (see PushdownWithPkInnerJoinPredicate below); so we defer translation until we have the final source/select. For the property + // selectors we need to translate now since we need the table. + TableExpressionBase? targetTable = null; + Expression? targetTablePropertySelector = null; + var columns = new ColumnExpression[propertyValueLambdaExpressions.Count]; + for (var i = 0; i < propertyValueLambdaExpressions.Count; i++) { - var left = RemapLambdaBody(source, propertyExpression); - - if (!TryProcessPropertyAccess(RelationalDependencies.Model, ref left, out var ese)) + var (propertySelector, _) = propertyValueLambdaExpressions[i]; + var propertySelectorBody = RemapLambdaBody(source, propertySelector).UnwrapTypeConversion(out _); + if (_sqlTranslator.Translate(propertySelectorBody) is not ColumnExpression column) { - AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertyExpression.Print())); + AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertySelector.Print())); return null; } - if (entityShaperExpression is null) + if (targetTable is null) { - entityShaperExpression = ese; + (targetTable, targetTablePropertySelector) = (column.Table, propertySelector); } - else if (!ReferenceEquals(ese, entityShaperExpression)) + else if (!ReferenceEquals(column.Table, targetTable)) { AddTranslationErrorDetails( - RelationalStrings.MultipleEntityPropertiesInSetProperty( - entityShaperExpression.EntityType.DisplayName(), ese.EntityType.DisplayName())); + RelationalStrings.MultipleTablesInExecuteUpdate(propertySelector.Print(), targetTablePropertySelector!.Print())); return null; } - remappedUnwrappedLeftExpressions.Add(left); - } - - 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; + columns[i] = column; } - 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; - } + Check.DebugAssert(targetTable is not null, "Target table should have a value"); - if (entityType.GetViewOrTableMappings().Count() != 1) + if (targetTable is TpcTablesExpression tpcTablesExpression) { AddTranslationErrorDetails( - RelationalStrings.ExecuteOperationOnEntitySplitting( - nameof(RelationalQueryableExtensions.ExecuteUpdate), entityType.DisplayName())); + RelationalStrings.ExecuteOperationOnTPC( + nameof(RelationalQueryableExtensions.ExecuteUpdate), tpcTablesExpression.EntityType.DisplayName())); return null; } // First, check if the provider has a native translation for the update represented by the select expression. // The default relational implementation handles simple, universally-supported cases (i.e. no operators except for predicate). - // Providers may override IsValidSelectExpressionForExecuteDelete to add support for more cases via provider-specific UPDATE syntax. + // Providers may override IsValidSelectExpressionForExecuteUpdate to add support for more cases via provider-specific UPDATE syntax. var selectExpression = (SelectExpression)source.QueryExpression; - if (IsValidSelectExpressionForExecuteUpdate(selectExpression, entityShaperExpression, out var tableExpression)) - { - return TranslateSetPropertyExpressions( - this, source, selectExpression, tableExpression, - propertyValueLambdaExpressions, remappedUnwrappedLeftExpressions); - } - - // The provider doesn't natively support the update. - // As a fallback, we place the original query in a subquery and user an INNER JOIN on the primary key columns. - // Unlike with ExecuteDelete, we cannot use a Contains subquery (which would produce the simpler WHERE Id IN (SELECT ...) syntax), - // since we allow projecting out to arbitrary shapes (e.g. anonymous types) before the ExecuteUpdate. - var pk = entityType.FindPrimaryKey(); - if (pk == null) - { - AddTranslationErrorDetails( - RelationalStrings.ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator( - nameof(RelationalQueryableExtensions.ExecuteUpdate), - entityType.DisplayName())); - return null; - } - - var outer = (ShapedQueryExpression)Visit(new EntityQueryRootExpression(entityType)); - var inner = source; - var outerParameter = Expression.Parameter(entityType.ClrType); - var outerKeySelector = Expression.Lambda(outerParameter.CreateKeyValuesExpression(pk.Properties), outerParameter); - var firstPropertyLambdaExpression = propertyValueLambdaExpressions[0].Item1; - var entitySource = GetEntitySource(RelationalDependencies.Model, firstPropertyLambdaExpression.Body); - var innerKeySelector = Expression.Lambda( - entitySource.CreateKeyValuesExpression(pk.Properties), firstPropertyLambdaExpression.Parameters); - - var joinPredicate = CreateJoinPredicate(outer, outerKeySelector, inner, innerKeySelector); - - Check.DebugAssert(joinPredicate != null, "Join predicate shouldn't be null"); - - var outerSelectExpression = (SelectExpression)outer.QueryExpression; - var outerShaperExpression = outerSelectExpression.AddInnerJoin(inner, joinPredicate, outer.ShaperExpression); - outer = outer.UpdateShaperExpression(outerShaperExpression); - var transparentIdentifierType = outer.ShaperExpression.Type; - var transparentIdentifierParameter = Expression.Parameter(transparentIdentifierType); - - var propertyReplacement = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Outer"); - var valueReplacement = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Inner"); - for (var i = 0; i < propertyValueLambdaExpressions.Count; i++) - { - var (propertyExpression, valueExpression) = propertyValueLambdaExpressions[i]; - propertyExpression = Expression.Lambda( - ReplacingExpressionVisitor.Replace( - ReplacingExpressionVisitor.Replace( - firstPropertyLambdaExpression.Parameters[0], - propertyExpression.Parameters[0], - entitySource), - propertyReplacement, propertyExpression.Body), - transparentIdentifierParameter); - - valueExpression = valueExpression is LambdaExpression lambdaExpression - ? Expression.Lambda( - ReplacingExpressionVisitor.Replace(lambdaExpression.Parameters[0], valueReplacement, lambdaExpression.Body), - transparentIdentifierParameter) - : valueExpression; - - propertyValueLambdaExpressions[i] = (propertyExpression, valueExpression); - } - - tableExpression = (TableExpression)outerSelectExpression.Tables[0]; - - return TranslateSetPropertyExpressions(this, outer, outerSelectExpression, tableExpression, propertyValueLambdaExpressions, null); - - static NonQueryExpression? TranslateSetPropertyExpressions( - RelationalQueryableMethodTranslatingExpressionVisitor visitor, - ShapedQueryExpression source, - SelectExpression selectExpression, - TableExpression tableExpression, - List<(LambdaExpression, Expression)> propertyValueLambdaExpressions, - List? leftExpressions) - { - var columnValueSetters = new List(); - for (var i = 0; i < propertyValueLambdaExpressions.Count; i++) - { - var (propertyExpression, valueExpression) = propertyValueLambdaExpressions[i]; - Expression left; - if (leftExpressions != null) - { - left = leftExpressions[i]; - } - else - { - left = visitor.RemapLambdaBody(source, propertyExpression); - left = left.UnwrapTypeConversion(out _); - } - - var right = valueExpression is LambdaExpression lambdaExpression - ? visitor.RemapLambdaBody(source, lambdaExpression) - : valueExpression; - - if (right.Type != left.Type) - { - right = Expression.Convert(right, left.Type); - } - - // We generate equality between property = value while translating so that we infer the 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.CreateEqualsExpression(left, right); - var translation = visitor._sqlTranslator.Translate(setter); - if (translation is SqlBinaryExpression - { - OperatorType: ExpressionType.Equal, Left: ColumnExpression column - } sqlBinaryExpression) - { - columnValueSetters.Add( - new ColumnValueSetter( - column, - selectExpression.AssignUniqueAliases(sqlBinaryExpression.Right))); - } - else - { - // We would reach here only if the property is unmapped or value fails to translate. - visitor.AddTranslationErrorDetails( - RelationalStrings.UnableToTranslateSetProperty( - propertyExpression.Print(), valueExpression.Print(), visitor._sqlTranslator.TranslationErrorDetails)); - return null; - } - } - - selectExpression.ReplaceProjection(new List()); - selectExpression.ApplyProjection(); - - return new NonQueryExpression(new UpdateExpression(tableExpression, selectExpression, columnValueSetters)); - } + return IsValidSelectExpressionForExecuteUpdate(selectExpression, targetTable, out var tableExpression) + ? TranslateValueExpressions(this, source, selectExpression, tableExpression, propertyValueLambdaExpressions, columns) + : PushdownWithPkInnerJoinPredicate(); void PopulateSetPropertyCalls( Expression expression, @@ -1662,70 +1522,195 @@ when methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typ } } - // For property setter selectors in ExecuteUpdate, we support only simple member access, EF.Function, etc. - // We also unwrap casts to interface/base class (#29618). Note that owned IncludeExpressions have already been pruned from the - // source before remapping the lambda (#28727). - static bool TryProcessPropertyAccess( - IModel model, - ref Expression expression, - [NotNullWhen(true)] out EntityShaperExpression? entityShaperExpression) + static NonQueryExpression? TranslateValueExpressions( + RelationalQueryableMethodTranslatingExpressionVisitor visitor, + ShapedQueryExpression source, + SelectExpression selectExpression, + TableExpression tableExpression, + List<(LambdaExpression PropertySelector, Expression ValueExpression)> propertyValueLambdaExpression, + ColumnExpression[] columns) { - expression = expression.UnwrapTypeConversion(out _); + var setters = new ColumnValueSetter[columns.Length]; - if (expression is MemberExpression { Expression : not null } memberExpression - && memberExpression.Expression.UnwrapTypeConversion(out _) is EntityShaperExpression ese) + for (var i = 0; i < propertyValueLambdaExpression.Count; i++) { - expression = memberExpression.Update(ese); - entityShaperExpression = ese; - return true; - } + var column = columns[i]; + var (_, valueSelector) = propertyValueLambdaExpression[i]; - if (expression is MethodCallExpression mce) - { - if (mce.TryGetEFPropertyArguments(out var source, out _) - && source.UnwrapTypeConversion(out _) is EntityShaperExpression ese1) - { - if (source != ese1) - { - var rewrittenArguments = mce.Arguments.ToArray(); - rewrittenArguments[0] = ese1; - expression = mce.Update(mce.Object, rewrittenArguments); - } + var remappedValueSelector = valueSelector is LambdaExpression lambdaExpression + ? visitor.RemapLambdaBody(source, lambdaExpression) + : valueSelector; - entityShaperExpression = ese1; - return true; + if (remappedValueSelector.Type != column.Type) + { + remappedValueSelector = Expression.Convert(remappedValueSelector, column.Type); } - if (mce.TryGetIndexerArguments(model, out var source2, out _) - && source2.UnwrapTypeConversion(out _) is EntityShaperExpression ese2) + if (visitor.TranslateExpression(remappedValueSelector, applyDefaultTypeMapping: false) + is not SqlExpression translatedValueSelector) { - expression = mce.Update(ese2, mce.Arguments); - entityShaperExpression = ese2; - return true; + visitor.AddTranslationErrorDetails(RelationalStrings.InvalidValueInSetProperty(valueSelector.Print())); + return null; } + + // Apply the type mapping of the column (translated from the property selector above) to the value, + // and apply alias uniquification to it. + translatedValueSelector = visitor._sqlExpressionFactory.ApplyTypeMapping(translatedValueSelector, column.TypeMapping); + translatedValueSelector = selectExpression.AssignUniqueAliases(translatedValueSelector); + + setters[i] = new ColumnValueSetter(column, translatedValueSelector); } - entityShaperExpression = null; - return false; + selectExpression.ReplaceProjection(new List()); + selectExpression.ApplyProjection(); + + return new NonQueryExpression(new UpdateExpression(tableExpression, selectExpression, setters)); } - static Expression GetEntitySource(IModel model, Expression propertyAccessExpression) + NonQueryExpression? PushdownWithPkInnerJoinPredicate() { - propertyAccessExpression = propertyAccessExpression.UnwrapTypeConversion(out _); - if (propertyAccessExpression is MethodCallExpression mce) + // The provider doesn't natively support the update. + // As a fallback, we place the original query in a subquery and user an INNER JOIN on the primary key columns. + + // Note that unlike with ExecuteDelete, we cannot use a Contains subquery (which would produce the simpler + // WHERE Id IN (SELECT ...) syntax), since we allow projecting out to arbitrary shapes (e.g. anonymous types) before the + // ExecuteUpdate. + + // To rewrite the query, we need to know the primary key properties, which requires getting the entity type. + // Although there may be several entity types involved, we've already verified that they all map to the same table. + // Since we don't support table sharing of multiple entity types with different keys, simply get the entity type and key from the + // first property selector. + var firstPropertySelector = propertyValueLambdaExpressions[0].PropertySelector; + var left = RemapLambdaBody(source, firstPropertySelector); + if (!TryExtractEntityType(left, RelationalDependencies.Model, out var entityType)) { - if (mce.TryGetEFPropertyArguments(out var source, out _)) + AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(firstPropertySelector)); + return null; + } + + var pk = entityType.FindPrimaryKey(); + if (pk == null) + { + AddTranslationErrorDetails( + RelationalStrings.ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator( + nameof(RelationalQueryableExtensions.ExecuteUpdate), + entityType.DisplayName())); + return null; + } + + // Generate the INNER JOIN around the original query, on the PK properties. + var outer = (ShapedQueryExpression)Visit(new EntityQueryRootExpression(entityType)); + var inner = source; + var outerParameter = Expression.Parameter(entityType.ClrType); + var outerKeySelector = Expression.Lambda(outerParameter.CreateKeyValuesExpression(pk.Properties), outerParameter); + var firstPropertyLambdaExpression = propertyValueLambdaExpressions[0].Item1; + var entitySource = GetEntitySource(RelationalDependencies.Model, firstPropertyLambdaExpression.Body); + var innerKeySelector = Expression.Lambda( + entitySource.CreateKeyValuesExpression(pk.Properties), firstPropertyLambdaExpression.Parameters); + + var joinPredicate = CreateJoinPredicate(outer, outerKeySelector, inner, innerKeySelector); + + Check.DebugAssert(joinPredicate != null, "Join predicate shouldn't be null"); + + var outerSelectExpression = (SelectExpression)outer.QueryExpression; + var outerShaperExpression = outerSelectExpression.AddInnerJoin(inner, joinPredicate, outer.ShaperExpression); + outer = outer.UpdateShaperExpression(outerShaperExpression); + var transparentIdentifierType = outer.ShaperExpression.Type; + var transparentIdentifierParameter = Expression.Parameter(transparentIdentifierType); + + var propertyReplacement = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Outer"); + var valueReplacement = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Inner"); + for (var i = 0; i < propertyValueLambdaExpressions.Count; i++) + { + var (propertyExpression, valueExpression) = propertyValueLambdaExpressions[i]; + propertyExpression = Expression.Lambda( + ReplacingExpressionVisitor.Replace( + ReplacingExpressionVisitor.Replace( + firstPropertyLambdaExpression.Parameters[0], + propertyExpression.Parameters[0], + entitySource), + propertyReplacement, propertyExpression.Body), + transparentIdentifierParameter); + + valueExpression = valueExpression is LambdaExpression lambdaExpression + ? Expression.Lambda( + ReplacingExpressionVisitor.Replace(lambdaExpression.Parameters[0], valueReplacement, lambdaExpression.Body), + transparentIdentifierParameter) + : valueExpression; + + propertyValueLambdaExpressions[i] = (propertyExpression, valueExpression); + } + + tableExpression = (TableExpression)outerSelectExpression.Tables[0]; + + // Re-translate the property selectors to get column expressions pointing to the new outer select expression (the original one + // has been pushed down into a subquery). + for (var i = 0; i < propertyValueLambdaExpressions.Count; i++) + { + var (propertySelector, _) = propertyValueLambdaExpressions[i]; + var propertySelectorBody = RemapLambdaBody(outer, propertySelector).UnwrapTypeConversion(out _); + + if (TranslateExpression(propertySelectorBody) is not ColumnExpression column) { - return source; + AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertySelector.Print())); + return null; } - if (mce.TryGetIndexerArguments(model, out var source2, out _)) + columns[i] = column; + } + + return TranslateValueExpressions(this, outer, outerSelectExpression, tableExpression, propertyValueLambdaExpressions, columns); + + // For property setter selectors in ExecuteUpdate, we support only simple member access, EF.Function, etc. + // We also unwrap casts to interface/base class (#29618). Note that owned IncludeExpressions have already been pruned from the + // source before remapping the lambda (#28727). + bool TryExtractEntityType(Expression expression, IModel model, [NotNullWhen(true)] out IEntityType? entityType) + { + expression = expression.UnwrapTypeConversion(out _); + + switch (expression) { - return source2; + case MemberExpression { Expression : not null } memberExpression + when memberExpression.Expression.UnwrapTypeConversion(out _) is EntityShaperExpression ese: + entityType = ese.EntityType; + return true; + + case MethodCallExpression mce when mce.TryGetEFPropertyArguments(out var source, out _) + && source.UnwrapTypeConversion(out _) is EntityShaperExpression ese: + { + entityType = ese.EntityType; + return true; + } + + case MethodCallExpression mce when mce.TryGetIndexerArguments(model, out var source2, out _) + && source2.UnwrapTypeConversion(out _) is EntityShaperExpression ese: + entityType = ese.EntityType; + return true; + + default: + entityType = null; + return false; } } - return ((MemberExpression)propertyAccessExpression).Expression!; + static Expression GetEntitySource(IModel model, Expression propertyAccessExpression) + { + propertyAccessExpression = propertyAccessExpression.UnwrapTypeConversion(out _); + if (propertyAccessExpression is MethodCallExpression mce) + { + if (mce.TryGetEFPropertyArguments(out var source, out _)) + { + return source; + } + + if (mce.TryGetIndexerArguments(model, out var source2, out _)) + { + return source2; + } + } + + return ((MemberExpression)propertyAccessExpression).Expression!; + } } } @@ -1787,43 +1772,33 @@ protected virtual bool IsValidSelectExpressionForExecuteDelete( /// /// /// The select expression to validate. - /// The entity shaper expression on which the update operation is being applied. + /// TODO /// The table expression from which rows are being deleted. /// Returns if the current select expression can be used for update as-is, otherwise. protected virtual bool IsValidSelectExpressionForExecuteUpdate( SelectExpression selectExpression, - EntityShaperExpression entityShaperExpression, + TableExpressionBase table, [NotNullWhen(true)] out TableExpression? tableExpression) { tableExpression = null; - 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 > 0) - { - TableExpressionBase table; - if (selectExpression.Tables.Count == 1) + if (selectExpression is { - table = selectExpression.Tables[0]; - } - else + Offset: null, + Limit: null, + IsDistinct: false, + GroupBy: [], + Having: null, + Orderings: [], + Tables.Count: > 0 + }) + { + if (selectExpression.Tables.Count > 1) { - var projectionBindingExpression = (ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression; - var entityProjectionExpression = (EntityProjectionExpression)selectExpression.GetProjection(projectionBindingExpression); - var column = entityProjectionExpression.BindProperty(entityShaperExpression.EntityType.GetProperties().First()); - table = column.Table; - if (ReferenceEquals(selectExpression.Tables[0], table)) + // If the table we are looking for it first table, then we need to verify if we can lift the next table in FROM clause + if (ReferenceEquals(selectExpression.Tables[0], table) + && selectExpression.Tables[1] is not InnerJoinExpression and not CrossJoinExpression) { - // If the table we are looking for it first table, then we need to verify if we can lift the next table in FROM clause - var secondTable = selectExpression.Tables[1]; - if (secondTable is not InnerJoinExpression and not CrossJoinExpression) - { - return false; - } + return false; } if (table is JoinExpressionBase joinExpressionBase) diff --git a/src/EFCore.Relational/Query/SqlExpressions/ColumnValueSetter.cs b/src/EFCore.Relational/Query/SqlExpressions/ColumnValueSetter.cs index cd5bdb838eb..fb0ec5cce09 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/ColumnValueSetter.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/ColumnValueSetter.cs @@ -12,41 +12,11 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; /// not used in application code. /// /// -public class ColumnValueSetter +/// A column to be updated. +/// A value to be assigned to the column. +[DebuggerDisplay("{DebuggerDisplay(),nq}")] +public readonly record struct ColumnValueSetter(ColumnExpression Column, SqlExpression Value) { - /// - /// Creates a new instance of the class. - /// - /// A column to be updated. - /// A value to be assigned to the column. - public ColumnValueSetter(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 ColumnValueSetter columnValueSetter - && Equals(columnValueSetter)); - - private bool Equals(ColumnValueSetter columnValueSetter) - => Column == columnValueSetter.Column - && Value == columnValueSetter.Value; - - /// - public override int GetHashCode() - => HashCode.Combine(Column, Value); + private string DebuggerDisplay() + => $"{Column.Print()} = {Value.Print()}"; } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs index 74870389a66..16c5b9fb6e5 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Query.Internal; namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; @@ -480,96 +481,6 @@ public override int GetHashCode() => 0; } - private sealed class TpcTablesExpression : TableExpressionBase - { - public TpcTablesExpression( - string? alias, - IEntityType entityType, - IReadOnlyList subSelectExpressions) - : base(alias) - { - EntityType = entityType; - SelectExpressions = subSelectExpressions; - } - - private TpcTablesExpression( - string? alias, - IEntityType entityType, - IReadOnlyList subSelectExpressions, - IEnumerable? annotations) - : base(alias, annotations) - { - EntityType = entityType; - SelectExpressions = subSelectExpressions; - } - - [NotNull] - public override string? Alias - { - get => base.Alias!; - internal set => base.Alias = value; - } - - public IEntityType EntityType { get; } - - public IReadOnlyList SelectExpressions { get; } - - public TpcTablesExpression Prune(IReadOnlyList discriminatorValues) - { - var subSelectExpressions = discriminatorValues.Count == 0 - ? new List { SelectExpressions[0] } - : SelectExpressions.Where( - se => - discriminatorValues.Contains((string)((SqlConstantExpression)se.Projection[^1].Expression).Value!)).ToList(); - - Check.DebugAssert(subSelectExpressions.Count > 0, "TPC must have at least 1 table selected."); - - return new TpcTablesExpression(Alias, EntityType, subSelectExpressions, GetAnnotations()); - } - - // This is implementation detail hence visitors are not supposed to see inside unless they really need to. - protected override Expression VisitChildren(ExpressionVisitor visitor) - => this; - - protected override TableExpressionBase CreateWithAnnotations(IEnumerable annotations) - => new TpcTablesExpression(Alias, EntityType, SelectExpressions, annotations); - - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.AppendLine("("); - using (expressionPrinter.Indent()) - { - expressionPrinter.VisitCollection(SelectExpressions, e => e.AppendLine().AppendLine("UNION ALL")); - } - - expressionPrinter.AppendLine() - .AppendLine(") AS " + Alias); - PrintAnnotations(expressionPrinter); - } - - /// - public override bool Equals(object? obj) - => obj != null - && (ReferenceEquals(this, obj) - || obj is TpcTablesExpression tpcTablesExpression - && Equals(tpcTablesExpression)); - - private bool Equals(TpcTablesExpression tpcTablesExpression) - { - if (!base.Equals(tpcTablesExpression) - || EntityType != tpcTablesExpression.EntityType) - { - return false; - } - - return SelectExpressions.SequenceEqual(tpcTablesExpression.SelectExpressions); - } - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), EntityType); - } - private sealed class ConcreteColumnExpression : ColumnExpression { private readonly TableReferenceExpression _table; diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs index 36101a611e9..b09202bddc4 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs @@ -467,31 +467,21 @@ protected override bool IsValidSelectExpressionForExecuteDelete( /// protected override bool IsValidSelectExpressionForExecuteUpdate( SelectExpression selectExpression, - EntityShaperExpression entityShaperExpression, + TableExpressionBase table, [NotNullWhen(true)] out TableExpression? tableExpression) { - if (selectExpression.Offset == null - // If entity type has primary key then Distinct is no-op - && (!selectExpression.IsDistinct || entityShaperExpression.EntityType.FindPrimaryKey() != null) - && selectExpression.GroupBy.Count == 0 - && selectExpression.Having == null - && selectExpression.Orderings.Count == 0) - { - TableExpressionBase table; - if (selectExpression.Tables.Count == 1) + if (selectExpression is { - table = selectExpression.Tables[0]; - } - else + Offset: null, + IsDistinct: false, + GroupBy: [], + Having: null, + Orderings: [] + }) + { + if (selectExpression.Tables.Count > 1 && table is JoinExpressionBase joinExpressionBase) { - var projectionBindingExpression = (ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression; - var entityProjectionExpression = (EntityProjectionExpression)selectExpression.GetProjection(projectionBindingExpression); - var column = entityProjectionExpression.BindProperty(entityShaperExpression.EntityType.GetProperties().First()); - table = column.Table; - if (table is JoinExpressionBase joinExpressionBase) - { - table = joinExpressionBase.Table; - } + table = joinExpressionBase.Table; } if (table is TableExpression te) diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/FiltersInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/FiltersInheritanceBulkUpdatesTestBase.cs index 686fb24805d..50ae5b0d70d 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/FiltersInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/FiltersInheritanceBulkUpdatesTestBase.cs @@ -97,7 +97,7 @@ public virtual Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Update_where_hierarchy(bool async) + public virtual Task Update_base_type(bool async) => AssertUpdate( async, ss => ss.Set().Where(e => e.Name == "Great spotted kiwi"), @@ -106,6 +106,17 @@ public virtual Task Update_where_hierarchy(bool async) rowsAffectedCount: 1, (b, a) => a.ForEach(e => Assert.Equal("Animal", e.Name))); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_base_type_with_OfType(bool async) + => AssertUpdate( + async, + ss => ss.Set().OfType(), + e => e, + s => s.SetProperty(e => e.Name, "NewBird"), + rowsAffectedCount: 1, + (b, a) => a.ForEach(e => Assert.Equal("NewBird", e.Name))); + [ConditionalTheory(Skip = "InnerJoin")] [MemberData(nameof(IsAsyncData))] public virtual Task Update_where_hierarchy_subquery(bool async) @@ -118,12 +129,34 @@ public virtual Task Update_where_hierarchy_subquery(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Update_where_hierarchy_derived(bool async) + public virtual Task Update_base_property_on_derived_type(bool async) => AssertUpdate( async, - ss => ss.Set().Where(e => e.Name == "Great spotted kiwi"), + ss => ss.Set(), + e => e, + s => s.SetProperty(e => e.Name, "SomeOtherKiwi"), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_derived_property_on_derived_type(bool async) + => AssertUpdate( + async, + ss => ss.Set(), + e => e, + s => s.SetProperty(e => e.FoundOn, Island.North), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_base_and_derived_types(bool async) + => AssertUpdate( + async, + ss => ss.Set(), e => e, - s => s.SetProperty(e => e.Name, "Kiwi"), + s => s + .SetProperty(e => e.Name, "Kiwi") + .SetProperty(e => e.FoundOn, Island.North), rowsAffectedCount: 1); [ConditionalTheory] diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/InheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/InheritanceBulkUpdatesTestBase.cs index 684ef3b26cb..37829115a79 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/InheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/InheritanceBulkUpdatesTestBase.cs @@ -97,7 +97,7 @@ public virtual Task Delete_where_keyless_entity_mapped_to_sql_query(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Update_where_hierarchy(bool async) + public virtual Task Update_base_type(bool async) => AssertUpdate( async, ss => ss.Set().Where(e => e.Name == "Great spotted kiwi"), @@ -106,6 +106,17 @@ public virtual Task Update_where_hierarchy(bool async) rowsAffectedCount: 1, (b, a) => a.ForEach(e => Assert.Equal("Animal", e.Name))); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_base_type_with_OfType(bool async) + => AssertUpdate( + async, + ss => ss.Set().OfType(), + e => e, + s => s.SetProperty(e => e.Name, "NewBird"), + rowsAffectedCount: 1, + (b, a) => a.ForEach(e => Assert.Equal("NewBird", e.Name))); + [ConditionalTheory(Skip = "InnerJoin")] [MemberData(nameof(IsAsyncData))] public virtual Task Update_where_hierarchy_subquery(bool async) @@ -118,12 +129,34 @@ public virtual Task Update_where_hierarchy_subquery(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Update_where_hierarchy_derived(bool async) + public virtual Task Update_base_property_on_derived_type(bool async) => AssertUpdate( async, - ss => ss.Set().Where(e => e.Name == "Great spotted kiwi"), + ss => ss.Set(), + e => e, + s => s.SetProperty(e => e.Name, "SomeOtherKiwi"), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_derived_property_on_derived_type(bool async) + => AssertUpdate( + async, + ss => ss.Set(), + e => e, + s => s.SetProperty(e => e.FoundOn, Island.North), + rowsAffectedCount: 1); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_base_and_derived_types(bool async) + => AssertUpdate( + async, + ss => ss.Set(), e => e, - s => s.SetProperty(e => e.Name, "Kiwi"), + s => s + .SetProperty(e => e.Name, "Kiwi") + .SetProperty(e => e.FoundOn, Island.North), rowsAffectedCount: 1); [ConditionalTheory] diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs index fa128a8c0d0..ec8258347e9 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs @@ -127,6 +127,82 @@ await AssertUpdate( rowsAffectedCount: 0); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Update_owned_and_non_owned_properties_with_table_sharing(bool async) + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => + { + mb.Entity().OwnsOne(o => o.OwnedReference); + }); + + await AssertUpdate( + async, + contextFactory.CreateContext, + ss => ss.Set(), + s => s + .SetProperty(o => o.Title, o => o.OwnedReference.Number.ToString()) + .SetProperty(o => o.OwnedReference.Number, o => o.Title.Length), + rowsAffectedCount: 0); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Update_main_table_in_entity_with_entity_splitting(bool async) + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => mb.Entity() + .ToTable("Blogs") + .SplitToTable( + "BlogsPart1", tb => + { + tb.Property(b => b.Title); + tb.Property(b => b.Rating); + }), + seed: context => + { + context.Set().Add(new() { Title = "SomeBlog" }); + context.SaveChanges(); + }); + + await AssertUpdate( + async, + contextFactory.CreateContext, + ss => ss.Set(), + s => s.SetProperty(b => b.CreationTimestamp, b => new DateTime(2020, 1, 1)), + rowsAffectedCount: 1); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Update_non_main_table_in_entity_with_entity_splitting(bool async) + { + var contextFactory = await InitializeAsync( + onModelCreating: mb => mb.Entity() + .ToTable("Blogs") + .SplitToTable( + "BlogsPart1", tb => + { + tb.Property(b => b.Title); + tb.Property(b => b.Rating); + }), + seed: context => + { + context.Set().Add(new() { Title = "SomeBlog" }); + context.SaveChanges(); + }); + + await AssertUpdate( + async, + contextFactory.CreateContext, + ss => ss.Set(), + s => s + .SetProperty(b => b.Title, b => b.Rating.ToString()) + .SetProperty(b => b.Rating, b => b.Title!.Length), + rowsAffectedCount: 1); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Delete_entity_with_auto_include(bool async) @@ -261,6 +337,8 @@ public class Blog { public int Id { get; set; } public string? Title { get; set; } + public int Rating { get; set; } + public DateTime CreationTimestamp { get; set; } public virtual ICollection Posts { get; } = new List(); } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs index d98df4b96e4..260692ee99f 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NorthwindBulkUpdatesTestBase.cs @@ -8,10 +8,11 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public abstract class NorthwindBulkUpdatesTestBase : BulkUpdatesTestBase where TFixture : NorthwindBulkUpdatesFixture, new() { - protected NorthwindBulkUpdatesTestBase(TFixture fixture) + protected NorthwindBulkUpdatesTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalTheory] @@ -779,24 +780,25 @@ public virtual Task Update_with_invalid_lambda_in_set_property_throws(bool async [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Update_multiple_entity_throws(bool async) + public virtual Task Update_multiple_tables_throws(bool async) => AssertTranslationFailed( - RelationalStrings.MultipleEntityPropertiesInSetProperty("Order", "Customer"), + RelationalStrings.MultipleTablesInExecuteUpdate("c => c.Customer.ContactName", "c => c.e.OrderDate"), () => AssertUpdate( async, - ss => ss.Set().Where(o => o.CustomerID.StartsWith("F")) + ss => ss.Set() + .Where(o => o.CustomerID.StartsWith("F")) .Select(e => new { e, e.Customer }), e => e.Customer, - s => s.SetProperty(c => c.Customer.ContactName, "Name").SetProperty(c => c.e.OrderDate, new DateTime(2020, 1, 1)), + s => s + .SetProperty(c => c.Customer.ContactName, "Name") + .SetProperty(c => c.e.OrderDate, new DateTime(2020, 1, 1)), rowsAffectedCount: 0)); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Update_unmapped_property_throws(bool async) => AssertTranslationFailed( - RelationalStrings.UnableToTranslateSetProperty( - "c => c.IsLondon", "True", - CoreStrings.QueryUnableToTranslateMember("IsLondon", "Customer")), + RelationalStrings.InvalidPropertyInSetProperty("c => c.IsLondon"), () => AssertUpdate( async, ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")), diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesTestBase.cs index e585a687079..f8a51fe0940 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesTestBase.cs @@ -6,9 +6,11 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public abstract class TPCFiltersInheritanceBulkUpdatesTestBase : FiltersInheritanceBulkUpdatesTestBase where TFixture : TPCInheritanceBulkUpdatesFixture, new() { - protected TPCFiltersInheritanceBulkUpdatesTestBase(TFixture fixture) + protected TPCFiltersInheritanceBulkUpdatesTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { + ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } // Keyless entities are mapped as TPH only @@ -34,8 +36,13 @@ public override Task Delete_GroupBy_Where_Select_First_3(bool async) public override Task Update_where_keyless_entity_mapped_to_sql_query(bool async) => Task.CompletedTask; - public override Task Update_where_hierarchy(bool async) + public override Task Update_base_type(bool async) => AssertTranslationFailed( RelationalStrings.ExecuteOperationOnTPC("ExecuteUpdate", "Animal"), - () => base.Update_where_hierarchy(async)); + () => base.Update_base_type(async)); + + public override Task Update_base_type_with_OfType(bool async) + => AssertTranslationFailed( + RelationalStrings.ExecuteOperationOnTPC("ExecuteUpdate", "Animal"), + () => base.Update_base_type_with_OfType(async)); } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCInheritanceBulkUpdatesTestBase.cs index cfa978ebd86..9f8b5ff75d9 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPCInheritanceBulkUpdatesTestBase.cs @@ -6,9 +6,11 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public abstract class TPCInheritanceBulkUpdatesTestBase : InheritanceBulkUpdatesTestBase where TFixture : TPCInheritanceBulkUpdatesFixture, new() { - protected TPCInheritanceBulkUpdatesTestBase(TFixture fixture) + protected TPCInheritanceBulkUpdatesTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { + ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } // Keyless entities are mapped as TPH only @@ -34,8 +36,13 @@ public override Task Delete_GroupBy_Where_Select_First_3(bool async) public override Task Update_where_keyless_entity_mapped_to_sql_query(bool async) => Task.CompletedTask; - public override Task Update_where_hierarchy(bool async) + public override Task Update_base_type(bool async) => AssertTranslationFailed( RelationalStrings.ExecuteOperationOnTPC("ExecuteUpdate", "Animal"), - () => base.Update_where_hierarchy(async)); + () => base.Update_base_type(async)); + + public override Task Update_base_type_with_OfType(bool async) + => AssertTranslationFailed( + RelationalStrings.ExecuteOperationOnTPC("ExecuteUpdate", "Animal"), + () => base.Update_base_type_with_OfType(async)); } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs index c930587aadb..d6ee12069a1 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesTestBase.cs @@ -6,9 +6,11 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public abstract class TPTFiltersInheritanceBulkUpdatesTestBase : FiltersInheritanceBulkUpdatesTestBase where TFixture : TPTInheritanceBulkUpdatesFixture, new() { - protected TPTFiltersInheritanceBulkUpdatesTestBase(TFixture fixture) + protected TPTFiltersInheritanceBulkUpdatesTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { + ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } // Keyless entities are mapped as TPH only @@ -35,17 +37,12 @@ public override Task Delete_GroupBy_Where_Select_First_3(bool async) RelationalStrings.ExecuteOperationOnTPT("ExecuteDelete", "Animal"), () => base.Delete_GroupBy_Where_Select_First_3(async)); + public override Task Update_base_and_derived_types(bool async) + => AssertTranslationFailed( + RelationalStrings.MultipleTablesInExecuteUpdate("e => e.Name", "e => e.FoundOn"), + () => base.Update_base_and_derived_types(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 1a4fe584e5e..b77f8214791 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/TPTInheritanceBulkUpdatesTestBase.cs @@ -6,9 +6,11 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public abstract class TPTInheritanceBulkUpdatesTestBase : InheritanceBulkUpdatesTestBase where TFixture : TPTInheritanceBulkUpdatesFixture, new() { - protected TPTInheritanceBulkUpdatesTestBase(TFixture fixture) + protected TPTInheritanceBulkUpdatesTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + ClearLog(); } // Keyless entities are mapped as TPH only @@ -43,27 +45,12 @@ public override Task Delete_where_using_hierarchy(bool async) public override Task Delete_where_using_hierarchy_derived(bool async) => base.Delete_where_using_hierarchy_derived(async); + public override Task Update_base_and_derived_types(bool async) + => AssertTranslationFailed( + RelationalStrings.MultipleTablesInExecuteUpdate("e => e.Name", "e => e.FoundOn"), + () => base.Update_base_and_derived_types(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)); - - public override Task Update_with_interface_in_property_expression(bool async) - => AssertTranslationFailed( - RelationalStrings.ExecuteOperationOnTPT("ExecuteUpdate", "Coke"), - () => base.Update_with_interface_in_property_expression(async)); - - public override Task Update_with_interface_in_EF_Property_in_property_expression(bool async) - => AssertTranslationFailed( - RelationalStrings.ExecuteOperationOnTPT("ExecuteUpdate", "Coke"), - () => base.Update_with_interface_in_EF_Property_in_property_expression(async)); } diff --git a/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs b/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs index 40c3b4ee043..bf220d55065 100644 --- a/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs @@ -67,35 +67,7 @@ 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, "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, "Value"))).Message)); - } - } + // See additional tests bulk update tests in NonSharedModelBulkUpdatesTestBase 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 d2288b03edf..ab35341fddf 100644 --- a/test/EFCore.Relational.Specification.Tests/TPTTableSplittingTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/TPTTableSplittingTestBase.cs @@ -21,12 +21,6 @@ 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.Specification.Tests/Query/OwnedQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs index 04967e9564f..8274b492d5d 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs @@ -1876,27 +1876,13 @@ protected class OwnedPerson public object this[string name] { - get - { - if (string.Equals(name, "Name", StringComparison.Ordinal)) - { - return _name; - } + get => string.Equals(name, "Name", StringComparison.Ordinal) + ? _name + : throw new InvalidOperationException($"Indexer property with key {name} is not defined on {nameof(OwnedPerson)}."); - throw new InvalidOperationException($"Indexer property with key {name} is not defined on {nameof(OwnedPerson)}."); - } - - set - { - if (string.Equals(name, "Name", StringComparison.Ordinal)) - { - _name = (string)value; - } - else - { - throw new InvalidOperationException($"Indexer property with key {name} is not defined on {nameof(OwnedPerson)}."); - } - } + set => _name = string.Equals(name, "Name", StringComparison.Ordinal) + ? (string)value + : throw new InvalidOperationException($"Indexer property with key {name} is not defined on {nameof(OwnedPerson)}."); } public OwnedAddress PersonAddress { get; set; } @@ -1914,27 +1900,13 @@ protected class Order public object this[string name] { - get - { - if (string.Equals(name, "OrderDate", StringComparison.Ordinal)) - { - return _orderDate; - } + get => string.Equals(name, "OrderDate", StringComparison.Ordinal) + ? _orderDate + : throw new InvalidOperationException($"Indexer property with key {name} is not defined on {nameof(OwnedPerson)}."); - throw new InvalidOperationException($"Indexer property with key {name} is not defined on {nameof(OwnedPerson)}."); - } - - set - { - if (string.Equals(name, "OrderDate", StringComparison.Ordinal)) - { - _orderDate = (DateTime)value; - } - else - { - throw new InvalidOperationException($"Indexer property with key {name} is not defined on {nameof(OwnedPerson)}."); - } - } + set => _orderDate = string.Equals(name, "OrderDate", StringComparison.Ordinal) + ? (DateTime)value + : throw new InvalidOperationException($"Indexer property with key {name} is not defined on {nameof(OwnedPerson)}."); } public OwnedPerson Client { get; set; } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqlServerTest.cs index ea96534a9ed..6adf756c725 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqlServerTest.cs @@ -6,10 +6,13 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public class FiltersInheritanceBulkUpdatesSqlServerTest : FiltersInheritanceBulkUpdatesTestBase< FiltersInheritanceBulkUpdatesSqlServerFixture> { - public FiltersInheritanceBulkUpdatesSqlServerTest(FiltersInheritanceBulkUpdatesSqlServerFixture fixture) + public FiltersInheritanceBulkUpdatesSqlServerTest( + FiltersInheritanceBulkUpdatesSqlServerFixture fixture, + ITestOutputHelper testOutputHelper) : base(fixture) { ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -133,9 +136,9 @@ OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY """); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); AssertExecuteUpdateSql( """ @@ -146,6 +149,19 @@ FROM [Animals] AS [a] """); } + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); + + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[Name] = N'NewBird' +FROM [Animals] AS [a] +WHERE [a].[CountryId] = 1 AND [a].[Discriminator] = N'Kiwi' +"""); + } + public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -153,16 +169,43 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_base_property_on_derived_type(async); AssertExecuteUpdateSql( """ UPDATE [a] -SET [a].[Name] = N'Kiwi' +SET [a].[Name] = N'SomeOtherKiwi' FROM [Animals] AS [a] -WHERE [a].[Discriminator] = N'Kiwi' AND [a].[CountryId] = 1 AND [a].[Name] = N'Great spotted kiwi' +WHERE [a].[Discriminator] = N'Kiwi' AND [a].[CountryId] = 1 +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[FoundOn] = CAST(0 AS tinyint) +FROM [Animals] AS [a] +WHERE [a].[Discriminator] = N'Kiwi' AND [a].[CountryId] = 1 +"""); + } + + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[FoundOn] = CAST(0 AS tinyint), + [a].[Name] = N'Kiwi' +FROM [Animals] AS [a] +WHERE [a].[Discriminator] = N'Kiwi' AND [a].[CountryId] = 1 """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqlServerTest.cs index 042ba6095d5..df6b054cc47 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqlServerTest.cs @@ -135,9 +135,9 @@ OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY """); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); AssertExecuteUpdateSql( """ @@ -148,6 +148,19 @@ FROM [Animals] AS [a] """); } + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); + + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[Name] = N'NewBird' +FROM [Animals] AS [a] +WHERE [a].[Discriminator] = N'Kiwi' +"""); + } + public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -155,16 +168,29 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_base_property_on_derived_type(async); AssertExecuteUpdateSql( """ UPDATE [a] -SET [a].[Name] = N'Kiwi' +SET [a].[Name] = N'SomeOtherKiwi' FROM [Animals] AS [a] -WHERE [a].[Discriminator] = N'Kiwi' AND [a].[Name] = N'Great spotted kiwi' +WHERE [a].[Discriminator] = N'Kiwi' +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[FoundOn] = CAST(0 AS tinyint) +FROM [Animals] AS [a] +WHERE [a].[Discriminator] = N'Kiwi' """); } @@ -184,6 +210,20 @@ FROM [Animals] AS [a] """); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[FoundOn] = CAST(0 AS tinyint), + [a].[Name] = N'Kiwi' +FROM [Animals] AS [a] +WHERE [a].[Discriminator] = N'Kiwi' +"""); + } + public override async Task Update_where_using_hierarchy_derived(bool async) { await base.Update_where_using_hierarchy_derived(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs index 21f550176c6..5bd99552d72 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs @@ -65,6 +65,40 @@ FROM [Owner] AS [o] """); } + public override async Task Update_owned_and_non_owned_properties_with_table_sharing(bool async) + { + await base.Update_owned_and_non_owned_properties_with_table_sharing(async); + + AssertSql( +""" +UPDATE [o] +SET [o].[OwnedReference_Number] = CAST(LEN([o].[Title]) AS int), + [o].[Title] = CONVERT(varchar(11), [o].[OwnedReference_Number]) +FROM [Owner] AS [o] +"""); + } + + public override async Task Update_main_table_in_entity_with_entity_splitting(bool async) + { + await base.Update_main_table_in_entity_with_entity_splitting(async); + + AssertSql( +""" +UPDATE [b] +SET [b].[CreationTimestamp] = '2020-01-01T00:00:00.0000000' +FROM [Blogs] AS [b] +"""); + } + + public override async Task Update_non_main_table_in_entity_with_entity_splitting(bool async) + { + // #28643 + await Assert.ThrowsAsync( + () => base.Update_non_main_table_in_entity_with_entity_splitting(async)); + + AssertSql(); + } + public override async Task Delete_entity_with_auto_include(bool async) { await base.Delete_entity_with_auto_include(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs index 72467e0a3bd..49430899ea3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs @@ -8,10 +8,8 @@ public class NorthwindBulkUpdatesSqlServerTest : NorthwindBulkUpdatesTestBase fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -971,7 +969,11 @@ public override async Task Update_Where_Distinct_set_constant(bool async) UPDATE [c] SET [c].[ContactName] = N'Updated' FROM [Customers] AS [c] -WHERE [c].[CustomerID] LIKE N'F%' +INNER JOIN ( + SELECT DISTINCT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] + FROM [Customers] AS [c0] + WHERE [c0].[CustomerID] LIKE N'F%' +) AS [t] ON [c].[CustomerID] = [t].[CustomerID] """); } @@ -1122,9 +1124,9 @@ public override async Task Update_with_invalid_lambda_in_set_property_throws(boo AssertExecuteUpdateSql(); } - public override async Task Update_multiple_entity_throws(bool async) + public override async Task Update_multiple_tables_throws(bool async) { - await base.Update_multiple_entity_throws(async); + await base.Update_multiple_tables_throws(async); AssertExecuteUpdateSql(); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs index 61342d0e34c..6aa33917b35 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqlServerTest.cs @@ -6,10 +6,11 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public class TPCFiltersInheritanceBulkUpdatesSqlServerTest : TPCFiltersInheritanceBulkUpdatesTestBase< TPCFiltersInheritanceBulkUpdatesSqlServerFixture> { - public TPCFiltersInheritanceBulkUpdatesSqlServerTest(TPCFiltersInheritanceBulkUpdatesSqlServerFixture fixture) - : base(fixture) + public TPCFiltersInheritanceBulkUpdatesSqlServerTest( + TPCFiltersInheritanceBulkUpdatesSqlServerFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { - ClearLog(); } [ConditionalFact] @@ -109,9 +110,16 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); AssertExecuteUpdateSql(); } @@ -123,16 +131,29 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_base_property_on_derived_type(async); AssertExecuteUpdateSql( """ UPDATE [k] -SET [k].[Name] = N'Kiwi' +SET [k].[Name] = N'SomeOtherKiwi' FROM [Kiwi] AS [k] -WHERE [k].[CountryId] = 1 AND [k].[Name] = N'Great spotted kiwi' +WHERE [k].[CountryId] = 1 +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE [k] +SET [k].[FoundOn] = CAST(0 AS tinyint) +FROM [Kiwi] AS [k] +WHERE [k].[CountryId] = 1 """); } @@ -158,6 +179,20 @@ FROM [Kiwi] AS [k] """); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql( +""" +UPDATE [k] +SET [k].[FoundOn] = CAST(0 AS tinyint), + [k].[Name] = N'Kiwi' +FROM [Kiwi] AS [k] +WHERE [k].[CountryId] = 1 +"""); + } + public override async Task Update_where_using_hierarchy_derived(bool async) { await base.Update_where_using_hierarchy_derived(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs index 8912d92fafb..9d8853ec1f8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqlServerTest.cs @@ -8,10 +8,8 @@ public class TPCInheritanceBulkUpdatesSqlServerTest : TPCInheritanceBulkUpdatesT public TPCInheritanceBulkUpdatesSqlServerTest( TPCInheritanceBulkUpdatesSqlServerFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -111,9 +109,16 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); AssertExecuteUpdateSql(); } @@ -125,16 +130,27 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_base_property_on_derived_type(async); AssertExecuteUpdateSql( """ UPDATE [k] -SET [k].[Name] = N'Kiwi' +SET [k].[Name] = N'SomeOtherKiwi' +FROM [Kiwi] AS [k] +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE [k] +SET [k].[FoundOn] = CAST(0 AS tinyint) FROM [Kiwi] AS [k] -WHERE [k].[Name] = N'Great spotted kiwi' """); } @@ -160,6 +176,19 @@ FROM [Kiwi] AS [k] """); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql( +""" +UPDATE [k] +SET [k].[FoundOn] = CAST(0 AS tinyint), + [k].[Name] = N'Kiwi' +FROM [Kiwi] AS [k] +"""); + } + public override async Task Update_where_using_hierarchy_derived(bool async) { await base.Update_where_using_hierarchy_derived(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs index 37f7432301a..0d36ccb22eb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqlServerTest.cs @@ -6,10 +6,11 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public class TPTFiltersInheritanceBulkUpdatesSqlServerTest : TPTFiltersInheritanceBulkUpdatesTestBase< TPTFiltersInheritanceBulkUpdatesSqlServerFixture> { - public TPTFiltersInheritanceBulkUpdatesSqlServerTest(TPTFiltersInheritanceBulkUpdatesSqlServerFixture fixture) - : base(fixture) + public TPTFiltersInheritanceBulkUpdatesSqlServerTest( + TPTFiltersInheritanceBulkUpdatesSqlServerFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { - ClearLog(); } [ConditionalFact] @@ -101,11 +102,31 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); - AssertExecuteUpdateSql(); + 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_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); + + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[Name] = N'NewBird' +FROM [Animals] AS [a] +LEFT JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] +WHERE [a].[CountryId] = 1 AND [k].[Id] IS NOT NULL +"""); } public override async Task Update_where_hierarchy_subquery(bool async) @@ -115,9 +136,39 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) + { + await base.Update_base_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[Name] = N'SomeOtherKiwi' +FROM [Animals] AS [a] +INNER JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] +INNER JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] +WHERE [a].[CountryId] = 1 +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE [k] +SET [k].[FoundOn] = CAST(0 AS tinyint) +FROM [Animals] AS [a] +INNER JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] +INNER JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] +WHERE [a].[CountryId] = 1 +"""); + } + + public override async Task Update_base_and_derived_types(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_base_and_derived_types(async); AssertExecuteUpdateSql(); } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs index 8cc494db1a1..c3275ee1610 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqlServerTest.cs @@ -5,8 +5,8 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public class TPTInheritanceBulkUpdatesSqlServerTest : TPTInheritanceBulkUpdatesTestBase { - public TPTInheritanceBulkUpdatesSqlServerTest(TPTInheritanceBulkUpdatesSqlServerFixture fixture) - : base(fixture) + public TPTInheritanceBulkUpdatesSqlServerTest(TPTInheritanceBulkUpdatesSqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { ClearLog(); } @@ -78,11 +78,31 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); - AssertExecuteUpdateSql(); + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[Name] = N'Animal' +FROM [Animals] AS [a] +WHERE [a].[Name] = N'Great spotted kiwi' +"""); + } + + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); + + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[Name] = N'NewBird' +FROM [Animals] AS [a] +LEFT JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] +WHERE [k].[Id] IS NOT NULL +"""); } public override async Task Update_where_hierarchy_subquery(bool async) @@ -92,9 +112,37 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) + { + await base.Update_base_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE [a] +SET [a].[Name] = N'SomeOtherKiwi' +FROM [Animals] AS [a] +INNER JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] +INNER JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE [k] +SET [k].[FoundOn] = CAST(0 AS tinyint) +FROM [Animals] AS [a] +INNER JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id] +INNER JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id] +"""); + } + + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); AssertExecuteUpdateSql(); } @@ -148,14 +196,26 @@ public override async Task Update_with_interface_in_property_expression(bool asy { await base.Update_with_interface_in_property_expression(async); - AssertExecuteUpdateSql(); + AssertExecuteUpdateSql( +""" +UPDATE [c] +SET [c].[SugarGrams] = 0 +FROM [Drinks] AS [d] +INNER JOIN [Coke] AS [c] ON [d].[Id] = [c].[Id] +"""); } public override async Task Update_with_interface_in_EF_Property_in_property_expression(bool async) { await base.Update_with_interface_in_EF_Property_in_property_expression(async); - AssertExecuteUpdateSql(); + AssertExecuteUpdateSql( +""" +UPDATE [c] +SET [c].[SugarGrams] = 0 +FROM [Drinks] AS [d] +INNER JOIN [Coke] AS [c] ON [d].[Id] = [c].[Id] +"""); } protected override void ClearLog() diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqliteTest.cs index cb70ca68fd4..db45171e8a6 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/FiltersInheritanceBulkUpdatesSqliteTest.cs @@ -5,10 +5,13 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public class FiltersInheritanceBulkUpdatesSqliteTest : FiltersInheritanceBulkUpdatesTestBase { - public FiltersInheritanceBulkUpdatesSqliteTest(FiltersInheritanceBulkUpdatesSqliteFixture fixture) + public FiltersInheritanceBulkUpdatesSqliteTest( + FiltersInheritanceBulkUpdatesSqliteFixture fixture, + ITestOutputHelper testOutputHelper) : base(fixture) { ClearLog(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -127,9 +130,9 @@ HAVING COUNT(*) < 3 """); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); AssertExecuteUpdateSql( """ @@ -139,6 +142,18 @@ public override async Task Update_where_hierarchy(bool async) """); } + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); + + AssertExecuteUpdateSql( +""" +UPDATE "Animals" AS "a" +SET "Name" = 'NewBird' +WHERE "a"."CountryId" = 1 AND "a"."Discriminator" = 'Kiwi' +"""); + } + public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -146,15 +161,27 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_base_property_on_derived_type(async); AssertExecuteUpdateSql( """ UPDATE "Animals" AS "a" -SET "Name" = 'Kiwi' -WHERE "a"."Discriminator" = 'Kiwi' AND "a"."CountryId" = 1 AND "a"."Name" = 'Great spotted kiwi' +SET "Name" = 'SomeOtherKiwi' +WHERE "a"."Discriminator" = 'Kiwi' AND "a"."CountryId" = 1 +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE "Animals" AS "a" +SET "FoundOn" = 0 +WHERE "a"."Discriminator" = 'Kiwi' AND "a"."CountryId" = 1 """); } @@ -173,6 +200,19 @@ SELECT COUNT(*) """); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql( +""" +UPDATE "Animals" AS "a" +SET "FoundOn" = 0, + "Name" = 'Kiwi' +WHERE "a"."Discriminator" = 'Kiwi' AND "a"."CountryId" = 1 +"""); + } + public override async Task Update_where_using_hierarchy_derived(bool async) { await base.Update_where_using_hierarchy_derived(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqliteTest.cs index 775e7fd914e..e82624853d9 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/InheritanceBulkUpdatesSqliteTest.cs @@ -129,9 +129,9 @@ HAVING COUNT(*) < 3 """); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); AssertExecuteUpdateSql( """ @@ -141,6 +141,18 @@ public override async Task Update_where_hierarchy(bool async) """); } + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); + + AssertExecuteUpdateSql( +""" +UPDATE "Animals" AS "a" +SET "Name" = 'NewBird' +WHERE "a"."Discriminator" = 'Kiwi' +"""); + } + public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -148,15 +160,27 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_base_property_on_derived_type(async); AssertExecuteUpdateSql( """ UPDATE "Animals" AS "a" -SET "Name" = 'Kiwi' -WHERE "a"."Discriminator" = 'Kiwi' AND "a"."Name" = 'Great spotted kiwi' +SET "Name" = 'SomeOtherKiwi' +WHERE "a"."Discriminator" = 'Kiwi' +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE "Animals" AS "a" +SET "FoundOn" = 0 +WHERE "a"."Discriminator" = 'Kiwi' """); } @@ -175,6 +199,19 @@ SELECT COUNT(*) """); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql( +""" +UPDATE "Animals" AS "a" +SET "FoundOn" = 0, + "Name" = 'Kiwi' +WHERE "a"."Discriminator" = 'Kiwi' +"""); + } + public override async Task Update_where_using_hierarchy_derived(bool async) { await base.Update_where_using_hierarchy_derived(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs index 92f1d4ef8b9..86a4199b3dc 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs @@ -61,6 +61,41 @@ public override async Task Update_non_owned_property_on_entity_with_owned2(bool """); } + public override async Task Update_owned_and_non_owned_properties_with_table_sharing(bool async) + { + await base.Update_owned_and_non_owned_properties_with_table_sharing(async); + + AssertSql( +""" +UPDATE "Owner" AS "o" +SET "OwnedReference_Number" = length("o"."Title"), + "Title" = CAST("o"."OwnedReference_Number" AS TEXT) +"""); + } + + public override async Task Update_main_table_in_entity_with_entity_splitting(bool async) + { + await base.Update_main_table_in_entity_with_entity_splitting(async); + + AssertSql( +""" +UPDATE "Blogs" AS "b" +SET "CreationTimestamp" = '2020-01-01 00:00:00' +"""); + } + + public override async Task Update_non_main_table_in_entity_with_entity_splitting(bool async) + { + await base.Update_non_main_table_in_entity_with_entity_splitting(async); + + AssertSql( +""" +UPDATE "BlogsPart1" AS "b0" +SET "Rating" = length("b0"."Title"), + "Title" = CAST("b0"."Rating" AS TEXT) +"""); + } + public override Task Delete_entity_with_auto_include(bool async) => Assert.ThrowsAsync(() => base.Delete_entity_with_auto_include(async)); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs index 8b11f496cc1..73e572a8f1c 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs @@ -10,10 +10,8 @@ public class NorthwindBulkUpdatesSqliteTest : NorthwindBulkUpdatesTestBase fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -946,7 +944,12 @@ public override async Task Update_Where_Distinct_set_constant(bool async) """ UPDATE "Customers" AS "c" SET "ContactName" = 'Updated' -WHERE "c"."CustomerID" LIKE 'F%' +FROM ( + SELECT DISTINCT "c0"."CustomerID", "c0"."Address", "c0"."City", "c0"."CompanyName", "c0"."ContactName", "c0"."ContactTitle", "c0"."Country", "c0"."Fax", "c0"."Phone", "c0"."PostalCode", "c0"."Region" + FROM "Customers" AS "c0" + WHERE "c0"."CustomerID" LIKE 'F%' +) AS "t" +WHERE "c"."CustomerID" = "t"."CustomerID" """); } @@ -1093,9 +1096,9 @@ public override async Task Update_with_invalid_lambda_in_set_property_throws(boo AssertExecuteUpdateSql(); } - public override async Task Update_multiple_entity_throws(bool async) + public override async Task Update_multiple_tables_throws(bool async) { - await base.Update_multiple_entity_throws(async); + await base.Update_multiple_tables_throws(async); AssertExecuteUpdateSql(); } diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqliteTest.cs index 31a8814172b..bf6bac6dd4d 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCFiltersInheritanceBulkUpdatesSqliteTest.cs @@ -6,10 +6,11 @@ namespace Microsoft.EntityFrameworkCore.BulkUpdates; public class TPCFiltersInheritanceBulkUpdatesSqliteTest : TPCFiltersInheritanceBulkUpdatesTestBase< TPCFiltersInheritanceBulkUpdatesSqliteFixture> { - public TPCFiltersInheritanceBulkUpdatesSqliteTest(TPCFiltersInheritanceBulkUpdatesSqliteFixture fixture) - : base(fixture) + public TPCFiltersInheritanceBulkUpdatesSqliteTest( + TPCFiltersInheritanceBulkUpdatesSqliteFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { - ClearLog(); } [ConditionalFact] @@ -106,9 +107,16 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); AssertExecuteUpdateSql(); } @@ -120,15 +128,27 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_base_property_on_derived_type(async); AssertExecuteUpdateSql( """ UPDATE "Kiwi" AS "k" -SET "Name" = 'Kiwi' -WHERE "k"."CountryId" = 1 AND "k"."Name" = 'Great spotted kiwi' +SET "Name" = 'SomeOtherKiwi' +WHERE "k"."CountryId" = 1 +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE "Kiwi" AS "k" +SET "FoundOn" = 0 +WHERE "k"."CountryId" = 1 """); } @@ -153,6 +173,19 @@ UNION ALL """); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql( + """ +UPDATE "Kiwi" AS "k" +SET "FoundOn" = 0, + "Name" = 'Kiwi' +WHERE "k"."CountryId" = 1 +"""); + } + public override async Task Update_where_using_hierarchy_derived(bool async) { await base.Update_where_using_hierarchy_derived(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqliteTest.cs index 4a52aad1c83..14f0c7e7641 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPCInheritanceBulkUpdatesSqliteTest.cs @@ -8,10 +8,8 @@ public class TPCInheritanceBulkUpdatesSqliteTest : TPCInheritanceBulkUpdatesTest public TPCInheritanceBulkUpdatesSqliteTest( TPCInheritanceBulkUpdatesSqliteFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -108,9 +106,16 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); + + AssertExecuteUpdateSql(); + } + + public override async Task Update_base_type_with_OfType(bool async) + { + await base.Update_base_type_with_OfType(async); AssertExecuteUpdateSql(); } @@ -122,15 +127,25 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + public override async Task Update_base_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_base_property_on_derived_type(async); AssertExecuteUpdateSql( """ UPDATE "Kiwi" AS "k" -SET "Name" = 'Kiwi' -WHERE "k"."Name" = 'Great spotted kiwi' +SET "Name" = 'SomeOtherKiwi' +"""); + } + + public override async Task Update_derived_property_on_derived_type(bool async) + { + await base.Update_derived_property_on_derived_type(async); + + AssertExecuteUpdateSql( +""" +UPDATE "Kiwi" AS "k" +SET "FoundOn" = 0 """); } @@ -155,6 +170,18 @@ UNION ALL """); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql( +""" +UPDATE "Kiwi" AS "k" +SET "FoundOn" = 0, + "Name" = 'Kiwi' +"""); + } + public override async Task Update_where_using_hierarchy_derived(bool async) { await base.Update_where_using_hierarchy_derived(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs index 396a55f3bd5..c42ad2bdb65 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs @@ -1,15 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Data.Sqlite; + namespace Microsoft.EntityFrameworkCore.BulkUpdates; public class TPTFiltersInheritanceBulkUpdatesSqliteTest : TPTFiltersInheritanceBulkUpdatesTestBase< TPTFiltersInheritanceBulkUpdatesSqliteFixture> { - public TPTFiltersInheritanceBulkUpdatesSqliteTest(TPTFiltersInheritanceBulkUpdatesSqliteFixture fixture) - : base(fixture) + public TPTFiltersInheritanceBulkUpdatesSqliteTest( + TPTFiltersInheritanceBulkUpdatesSqliteFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { - ClearLog(); } [ConditionalFact] @@ -99,13 +102,33 @@ public override async Task Delete_GroupBy_Where_Select_First_3(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); - AssertExecuteUpdateSql(); + AssertExecuteUpdateSql( +""" +UPDATE "Animals" AS "a" +SET "Name" = 'Animal' +FROM ( + SELECT "a0"."Id", "a0"."CountryId", "a0"."Name", "a0"."Species", "b0"."EagleId", "b0"."IsFlightless", "e0"."Group", "k0"."FoundOn", CASE + WHEN "k0"."Id" IS NOT NULL THEN 'Kiwi' + WHEN "e0"."Id" IS NOT NULL THEN 'Eagle' + END AS "Discriminator" + FROM "Animals" AS "a0" + LEFT JOIN "Birds" AS "b0" ON "a0"."Id" = "b0"."Id" + LEFT JOIN "Eagle" AS "e0" ON "a0"."Id" = "e0"."Id" + LEFT JOIN "Kiwi" AS "k0" ON "a0"."Id" = "k0"."Id" + WHERE "a0"."CountryId" = 1 AND "a0"."Name" = 'Great spotted kiwi' +) AS "t" +WHERE "a"."Id" = "t"."Id" +"""); } + // #31402 + public override Task Update_base_type_with_OfType(bool async) + => Assert.ThrowsAsync(() => base.Update_base_property_on_derived_type(async)); + public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -113,11 +136,22 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + // #31402 + public override Task Update_base_property_on_derived_type(bool async) + => Assert.ThrowsAsync(() => base.Update_base_property_on_derived_type(async)); + + public override async Task Update_derived_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_derived_property_on_derived_type(async); - AssertExecuteUpdateSql(); + AssertExecuteUpdateSql( +""" +UPDATE "Kiwi" AS "k" +SET "FoundOn" = 0 +FROM "Animals" AS "a" +INNER JOIN "Birds" AS "b" ON "a"."Id" = "b"."Id" +WHERE "a"."Id" = "k"."Id" AND "a"."CountryId" = 1 +"""); } public override async Task Update_where_using_hierarchy(bool async) @@ -138,6 +172,13 @@ SELECT COUNT(*) """); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql(); + } + public override async Task Update_where_using_hierarchy_derived(bool async) { await base.Update_where_using_hierarchy_derived(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqliteTest.cs index 04dddbe476a..e111a974deb 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/TPTInheritanceBulkUpdatesSqliteTest.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Data.Sqlite; + namespace Microsoft.EntityFrameworkCore.BulkUpdates; public class TPTInheritanceBulkUpdatesSqliteTest : TPTInheritanceBulkUpdatesTestBase @@ -8,10 +10,8 @@ public class TPTInheritanceBulkUpdatesSqliteTest : TPTInheritanceBulkUpdatesTest public TPTInheritanceBulkUpdatesSqliteTest( TPTInheritanceBulkUpdatesSqliteFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { - ClearLog(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } [ConditionalFact] @@ -81,13 +81,33 @@ public override async Task Delete_where_hierarchy_subquery(bool async) AssertSql(); } - public override async Task Update_where_hierarchy(bool async) + public override async Task Update_base_type(bool async) { - await base.Update_where_hierarchy(async); + await base.Update_base_type(async); - AssertExecuteUpdateSql(); + AssertExecuteUpdateSql( +""" +UPDATE "Animals" AS "a" +SET "Name" = 'Animal' +FROM ( + SELECT "a0"."Id", "a0"."CountryId", "a0"."Name", "a0"."Species", "b0"."EagleId", "b0"."IsFlightless", "e0"."Group", "k0"."FoundOn", CASE + WHEN "k0"."Id" IS NOT NULL THEN 'Kiwi' + WHEN "e0"."Id" IS NOT NULL THEN 'Eagle' + END AS "Discriminator" + FROM "Animals" AS "a0" + LEFT JOIN "Birds" AS "b0" ON "a0"."Id" = "b0"."Id" + LEFT JOIN "Eagle" AS "e0" ON "a0"."Id" = "e0"."Id" + LEFT JOIN "Kiwi" AS "k0" ON "a0"."Id" = "k0"."Id" + WHERE "a0"."Name" = 'Great spotted kiwi' +) AS "t" +WHERE "a"."Id" = "t"."Id" +"""); } + // #31402 + public override Task Update_base_type_with_OfType(bool async) + => Assert.ThrowsAsync(() => base.Update_base_property_on_derived_type(async)); + public override async Task Update_where_hierarchy_subquery(bool async) { await base.Update_where_hierarchy_subquery(async); @@ -95,11 +115,22 @@ public override async Task Update_where_hierarchy_subquery(bool async) AssertExecuteUpdateSql(); } - public override async Task Update_where_hierarchy_derived(bool async) + // #31402 + public override Task Update_base_property_on_derived_type(bool async) + => Assert.ThrowsAsync(() => base.Update_base_property_on_derived_type(async)); + + public override async Task Update_derived_property_on_derived_type(bool async) { - await base.Update_where_hierarchy_derived(async); + await base.Update_derived_property_on_derived_type(async); - AssertExecuteUpdateSql(); + AssertExecuteUpdateSql( +""" +UPDATE "Kiwi" AS "k" +SET "FoundOn" = 0 +FROM "Animals" AS "a" +INNER JOIN "Birds" AS "b" ON "a"."Id" = "b"."Id" +WHERE "a"."Id" = "k"."Id" +"""); } public override async Task Update_where_using_hierarchy(bool async) @@ -120,6 +151,13 @@ SELECT COUNT(*) """); } + public override async Task Update_base_and_derived_types(bool async) + { + await base.Update_base_and_derived_types(async); + + AssertExecuteUpdateSql(); + } + public override async Task Update_where_using_hierarchy_derived(bool async) { await base.Update_where_using_hierarchy_derived(async); @@ -149,14 +187,26 @@ public override async Task Update_with_interface_in_property_expression(bool asy { await base.Update_with_interface_in_property_expression(async); - AssertExecuteUpdateSql(); + AssertExecuteUpdateSql( +""" +UPDATE "Coke" AS "c" +SET "SugarGrams" = 0 +FROM "Drinks" AS "d" +WHERE "d"."Id" = "c"."Id" +"""); } public override async Task Update_with_interface_in_EF_Property_in_property_expression(bool async) { await base.Update_with_interface_in_EF_Property_in_property_expression(async); - AssertExecuteUpdateSql(); + AssertExecuteUpdateSql( +""" +UPDATE "Coke" AS "c" +SET "SugarGrams" = 0 +FROM "Drinks" AS "d" +WHERE "d"."Id" = "c"."Id" +"""); } protected override void ClearLog()