From 5ce2e9959db7e2c233f5e97ee46bfd7e09ae3b33 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 3 Dec 2023 13:21:15 +0100 Subject: [PATCH 1/2] Split ExecuteUpdate/Delete to their own partial files No actual code change --- ...nslatingExpressionVisitor.ExecuteDelete.cs | 163 ++++++ ...nslatingExpressionVisitor.ExecuteUpdate.cs | 385 +++++++++++++ ...yableMethodTranslatingExpressionVisitor.cs | 528 +----------------- 3 files changed, 549 insertions(+), 527 deletions(-) create mode 100644 src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteDelete.cs create mode 100644 src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteDelete.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteDelete.cs new file mode 100644 index 00000000000..bae6307727a --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteDelete.cs @@ -0,0 +1,163 @@ +// 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.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Query; + +public partial class RelationalQueryableMethodTranslatingExpressionVisitor +{ + /// + /// Translates method + /// over the given source. + /// + /// The shaped query on which the operator is applied. + /// The non query after translation. + protected virtual NonQueryExpression? TranslateExecuteDelete(ShapedQueryExpression source) + { + if (source.ShaperExpression is IncludeExpression includeExpression) + { + source = source.UpdateShaperExpression(PruneIncludes(includeExpression)); + } + + if (source.ShaperExpression is not StructuralTypeShaperExpression { StructuralType: IEntityType entityType } shaper) + { + AddTranslationErrorDetails(RelationalStrings.ExecuteDeleteOnNonEntityType); + return null; + } + + var mappingStrategy = entityType.GetMappingStrategy(); + if (mappingStrategy == RelationalAnnotationNames.TptMappingStrategy) + { + AddTranslationErrorDetails( + RelationalStrings.ExecuteOperationOnTPT(nameof(RelationalQueryableExtensions.ExecuteDelete), entityType.DisplayName())); + return null; + } + + if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy + && entityType.GetDirectlyDerivedTypes().Any()) + { + // We allow TPC is it is leaf type + AddTranslationErrorDetails( + RelationalStrings.ExecuteOperationOnTPC(nameof(RelationalQueryableExtensions.ExecuteDelete), entityType.DisplayName())); + return null; + } + + if (entityType.GetViewOrTableMappings().Count() != 1) + { + AddTranslationErrorDetails( + RelationalStrings.ExecuteOperationOnEntitySplitting( + nameof(RelationalQueryableExtensions.ExecuteDelete), entityType.DisplayName())); + return null; + } + + // First, check if the provider has a native translation for the delete 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 DELETE syntax. + var selectExpression = (SelectExpression)source.QueryExpression; + if (IsValidSelectExpressionForExecuteDelete(selectExpression, shaper, out var tableExpression)) + { + if (AreOtherNonOwnedEntityTypesInTheTable(entityType.GetRootType(), tableExpression.Table)) + { + AddTranslationErrorDetails( + RelationalStrings.ExecuteDeleteOnTableSplitting(tableExpression.Table.SchemaQualifiedName)); + + return null; + } + + selectExpression.ReplaceProjection(new List()); + selectExpression.ApplyProjection(); + + return new NonQueryExpression(new DeleteExpression(tableExpression, selectExpression)); + + static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBase table) + { + foreach (var entityTypeMapping in table.EntityTypeMappings) + { + var typeBase = entityTypeMapping.TypeBase; + if ((entityTypeMapping.IsSharedTablePrincipal == true + && typeBase != rootType) + || (entityTypeMapping.IsSharedTablePrincipal == false + && typeBase is IEntityType entityType + && entityType.GetRootType() != rootType + && !entityType.IsOwned())) + { + return true; + } + } + + return false; + } + } + + // The provider doesn't natively support the delete. + // As a fallback, we place the original query in a Contains subquery, which will get translated via the regular entity equality/ + // containment mechanism (InExpression for non-composite keys, Any for composite keys) + var pk = entityType.FindPrimaryKey(); + if (pk == null) + { + AddTranslationErrorDetails( + RelationalStrings.ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator( + nameof(RelationalQueryableExtensions.ExecuteDelete), + entityType.DisplayName())); + return null; + } + + var clrType = entityType.ClrType; + var entityParameter = Expression.Parameter(clrType); + var predicateBody = Expression.Call(QueryableMethods.Contains.MakeGenericMethod(clrType), source, entityParameter); + + var newSource = Expression.Call( + QueryableMethods.Where.MakeGenericMethod(clrType), + new EntityQueryRootExpression(entityType), + Expression.Quote(Expression.Lambda(predicateBody, entityParameter))); + + return TranslateExecuteDelete((ShapedQueryExpression)Visit(newSource)); + } + + /// + /// Checks weather the current select expression can be used as-is for executing a delete operation, or whether it must be pushed + /// down into a subquery. + /// + /// + /// + /// By default, only single-table select expressions are supported, and optionally with a predicate. + /// + /// + /// Providers can override this to allow more select expression features to be supported without pushing down into a subquery. + /// When doing this, VisitDelete must also be overridden in the provider's QuerySqlGenerator to add SQL generation support for + /// the feature. + /// + /// + /// The select expression to validate. + /// The structural type shaper expression on which the delete operation is being applied. + /// The table expression from which rows are being deleted. + /// + /// Returns if the current select expression can be used for delete as-is, otherwise. + /// + protected virtual bool IsValidSelectExpressionForExecuteDelete( + SelectExpression selectExpression, + StructuralTypeShaperExpression shaper, + [NotNullWhen(true)] out TableExpression? tableExpression) + { + if (selectExpression is + { + Tables: [TableExpression expression], + Orderings: [], + Offset: null, + Limit: null, + GroupBy: [], + Having: null + }) + { + tableExpression = expression; + + return true; + } + + tableExpression = null; + return false; + } +} diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs new file mode 100644 index 00000000000..29caad46004 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs @@ -0,0 +1,385 @@ +// 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.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Query; + +public partial class RelationalQueryableMethodTranslatingExpressionVisitor +{ + /// + /// Translates + /// + /// method + /// over the given source. + /// + /// The shaped query on which the operator is applied. + /// + /// The lambda expression containing + /// + /// statements. + /// + /// The non query after translation. + protected virtual NonQueryExpression? TranslateExecuteUpdate(ShapedQueryExpression source, LambdaExpression 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. + if (source.ShaperExpression is IncludeExpression includeExpression) + { + source = source.UpdateShaperExpression(PruneIncludes(includeExpression)); + } + + var propertyValueLambdaExpressions = new List<(LambdaExpression PropertySelector, Expression ValueExpression)>(); + PopulateSetPropertyCalls(setPropertyCalls.Body, propertyValueLambdaExpressions, setPropertyCalls.Parameters[0]); + if (TranslationErrorDetails != null) + { + return null; + } + + if (propertyValueLambdaExpressions.Count == 0) + { + AddTranslationErrorDetails(RelationalStrings.NoSetPropertyInvocation); + return null; + } + + // 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 (propertySelector, _) = propertyValueLambdaExpressions[i]; + var propertySelectorBody = RemapLambdaBody(source, propertySelector).UnwrapTypeConversion(out _); + if (_sqlTranslator.Translate(propertySelectorBody) is not ColumnExpression column) + { + AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertySelector.Print())); + return null; + } + + if (targetTable is null) + { + targetTable = column.Table; + targetTablePropertySelector = propertySelector; + } + else if (!ReferenceEquals(column.Table, targetTable)) + { + AddTranslationErrorDetails( + RelationalStrings.MultipleTablesInExecuteUpdate(propertySelector.Print(), targetTablePropertySelector!.Print())); + return null; + } + + columns[i] = column; + } + + Check.DebugAssert(targetTable is not null, "Target table should have a value"); + + if (targetTable is TpcTablesExpression tpcTablesExpression) + { + AddTranslationErrorDetails( + 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 IsValidSelectExpressionForExecuteUpdate to add support for more cases via provider-specific UPDATE syntax. + var selectExpression = (SelectExpression)source.QueryExpression; + return IsValidSelectExpressionForExecuteUpdate(selectExpression, targetTable, out var tableExpression) + ? TranslateValueExpressions(this, source, selectExpression, tableExpression, propertyValueLambdaExpressions, columns) + : PushdownWithPkInnerJoinPredicate(); + + void PopulateSetPropertyCalls( + Expression expression, + List<(LambdaExpression, Expression)> list, + ParameterExpression parameter) + { + switch (expression) + { + case ParameterExpression p + when parameter == p: + break; + + case MethodCallExpression + { + Method: + { + IsGenericMethod: true, + Name: nameof(SetPropertyCalls.SetProperty), + DeclaringType.IsGenericType: true + } + } methodCallExpression + when methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(SetPropertyCalls<>): + list.Add(((LambdaExpression)methodCallExpression.Arguments[0], methodCallExpression.Arguments[1])); + + PopulateSetPropertyCalls(methodCallExpression.Object!, list, parameter); + + break; + + default: + AddTranslationErrorDetails(RelationalStrings.InvalidArgumentToExecuteUpdate); + break; + } + } + + static NonQueryExpression? TranslateValueExpressions( + RelationalQueryableMethodTranslatingExpressionVisitor visitor, + ShapedQueryExpression source, + SelectExpression selectExpression, + TableExpression tableExpression, + List<(LambdaExpression PropertySelector, Expression ValueExpression)> propertyValueLambdaExpression, + ColumnExpression[] columns) + { + var setters = new ColumnValueSetter[columns.Length]; + + for (var i = 0; i < propertyValueLambdaExpression.Count; i++) + { + var column = columns[i]; + var (_, valueSelector) = propertyValueLambdaExpression[i]; + + var remappedValueSelector = valueSelector is LambdaExpression lambdaExpression + ? visitor.RemapLambdaBody(source, lambdaExpression) + : valueSelector; + + if (remappedValueSelector.Type != column.Type) + { + remappedValueSelector = Expression.Convert(remappedValueSelector, column.Type); + } + + if (visitor.TranslateExpression(remappedValueSelector, applyDefaultTypeMapping: false) + is not SqlExpression translatedValueSelector) + { + 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); + } + + selectExpression.ReplaceProjection(new List()); + selectExpression.ApplyProjection(); + + return new NonQueryExpression(new UpdateExpression(tableExpression, selectExpression, setters)); + } + + NonQueryExpression? PushdownWithPkInnerJoinPredicate() + { + // 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. + + // The following mechanism for extracting the entity type from property selectors only supports 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). + + var firstPropertySelector = propertyValueLambdaExpressions[0].PropertySelector; + var shaper = RemapLambdaBody(source, firstPropertySelector).UnwrapTypeConversion(out _) switch + { + MemberExpression { Expression : not null } memberExpression + when memberExpression.Expression.UnwrapTypeConversion(out _) is StructuralTypeShaperExpression s + => s, + + MethodCallExpression mce when mce.TryGetEFPropertyArguments(out var source, out _) + && source.UnwrapTypeConversion(out _) is StructuralTypeShaperExpression s + => s, + + MethodCallExpression mce when mce.TryGetIndexerArguments(RelationalDependencies.Model, out var source2, out _) + && source2.UnwrapTypeConversion(out _) is StructuralTypeShaperExpression s + => s, + + _ => null + }; + + if (shaper is null) + { + AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(firstPropertySelector)); + return null; + } + + if (shaper.StructuralType is not IEntityType entityType) + { + AddTranslationErrorDetails( + RelationalStrings.ExecuteUpdateSubqueryNotSupportedOverComplexTypes(shaper.StructuralType.DisplayName())); + return null; + } + + if (entityType.FindPrimaryKey() is not IKey pk) + { + 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) + { + AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertySelector.Print())); + return null; + } + + columns[i] = column; + } + + return TranslateValueExpressions(this, outer, outerSelectExpression, tableExpression, propertyValueLambdaExpressions, columns); + } + + 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!; + } + } + + /// + /// Validates if the current select expression can be used for execute update operation or it requires to be joined as a subquery. + /// + /// + /// + /// By default, only multi-table select expressions are supported, and optionally with a predicate. + /// + /// + /// Providers can override this to allow more select expression features to be supported without pushing down into a subquery. + /// When doing this, VisitUpdate must also be overridden in the provider's QuerySqlGenerator to add SQL generation support for + /// the feature. + /// + /// + /// The select expression to validate. + /// The target table containing the rows to be updated. + /// + /// The table expression corresponding to the provided , containing the rows to be updated. + /// + /// + /// Returns if the current select expression can be used for update as-is, otherwise. + /// + protected virtual bool IsValidSelectExpressionForExecuteUpdate( + SelectExpression selectExpression, + TableExpressionBase targetTable, + [NotNullWhen(true)] out TableExpression? tableExpression) + { + tableExpression = null; + if (selectExpression is + { + Offset: null, + Limit: null, + IsDistinct: false, + GroupBy: [], + Having: null, + Orderings: [], + Tables.Count: > 0 + }) + { + if (selectExpression.Tables.Count > 1) + { + // If the table we are looking for is the first table, then we need to verify whether we can lift the next table in FROM + // clause + if (ReferenceEquals(selectExpression.Tables[0], targetTable) + && selectExpression.Tables[1] is not InnerJoinExpression and not CrossJoinExpression) + { + return false; + } + + if (targetTable is JoinExpressionBase joinExpressionBase) + { + targetTable = joinExpressionBase.Table; + } + } + + if (targetTable is TableExpression te) + { + tableExpression = te; + return true; + } + } + + return false; + } +} diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 62d9e026b0c..c13870e590e 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Query; /// -public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMethodTranslatingExpressionVisitor +public partial class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMethodTranslatingExpressionVisitor { private const string SqlQuerySingleColumnAlias = "Value"; private const string ValuesOrderingColumnName = "_ord", ValuesValueColumnName = "Value"; @@ -1259,532 +1259,6 @@ protected override ShapedQueryExpression TranslateUnion(ShapedQueryExpression so return source; } - /// - /// Translates method - /// over the given source. - /// - /// The shaped query on which the operator is applied. - /// The non query after translation. - protected virtual NonQueryExpression? TranslateExecuteDelete(ShapedQueryExpression source) - { - if (source.ShaperExpression is IncludeExpression includeExpression) - { - source = source.UpdateShaperExpression(PruneIncludes(includeExpression)); - } - - if (source.ShaperExpression is not StructuralTypeShaperExpression { StructuralType: IEntityType entityType } shaper) - { - AddTranslationErrorDetails(RelationalStrings.ExecuteDeleteOnNonEntityType); - return null; - } - - var mappingStrategy = entityType.GetMappingStrategy(); - if (mappingStrategy == RelationalAnnotationNames.TptMappingStrategy) - { - AddTranslationErrorDetails( - RelationalStrings.ExecuteOperationOnTPT(nameof(RelationalQueryableExtensions.ExecuteDelete), entityType.DisplayName())); - return null; - } - - if (mappingStrategy == RelationalAnnotationNames.TpcMappingStrategy - && entityType.GetDirectlyDerivedTypes().Any()) - { - // We allow TPC is it is leaf type - AddTranslationErrorDetails( - RelationalStrings.ExecuteOperationOnTPC(nameof(RelationalQueryableExtensions.ExecuteDelete), entityType.DisplayName())); - return null; - } - - if (entityType.GetViewOrTableMappings().Count() != 1) - { - AddTranslationErrorDetails( - RelationalStrings.ExecuteOperationOnEntitySplitting( - nameof(RelationalQueryableExtensions.ExecuteDelete), entityType.DisplayName())); - return null; - } - - // First, check if the provider has a native translation for the delete 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 DELETE syntax. - var selectExpression = (SelectExpression)source.QueryExpression; - if (IsValidSelectExpressionForExecuteDelete(selectExpression, shaper, out var tableExpression)) - { - if (AreOtherNonOwnedEntityTypesInTheTable(entityType.GetRootType(), tableExpression.Table)) - { - AddTranslationErrorDetails( - RelationalStrings.ExecuteDeleteOnTableSplitting(tableExpression.Table.SchemaQualifiedName)); - - return null; - } - - selectExpression.ReplaceProjection(new List()); - selectExpression.ApplyProjection(); - - return new NonQueryExpression(new DeleteExpression(tableExpression, selectExpression)); - - static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBase table) - { - foreach (var entityTypeMapping in table.EntityTypeMappings) - { - var typeBase = entityTypeMapping.TypeBase; - if ((entityTypeMapping.IsSharedTablePrincipal == true - && typeBase != rootType) - || (entityTypeMapping.IsSharedTablePrincipal == false - && typeBase is IEntityType entityType - && entityType.GetRootType() != rootType - && !entityType.IsOwned())) - { - return true; - } - } - - return false; - } - } - - // The provider doesn't natively support the delete. - // As a fallback, we place the original query in a Contains subquery, which will get translated via the regular entity equality/ - // containment mechanism (InExpression for non-composite keys, Any for composite keys) - var pk = entityType.FindPrimaryKey(); - if (pk == null) - { - AddTranslationErrorDetails( - RelationalStrings.ExecuteOperationOnKeylessEntityTypeWithUnsupportedOperator( - nameof(RelationalQueryableExtensions.ExecuteDelete), - entityType.DisplayName())); - return null; - } - - var clrType = entityType.ClrType; - var entityParameter = Expression.Parameter(clrType); - var predicateBody = Expression.Call(QueryableMethods.Contains.MakeGenericMethod(clrType), source, entityParameter); - - var newSource = Expression.Call( - QueryableMethods.Where.MakeGenericMethod(clrType), - new EntityQueryRootExpression(entityType), - Expression.Quote(Expression.Lambda(predicateBody, entityParameter))); - - return TranslateExecuteDelete((ShapedQueryExpression)Visit(newSource)); - } - - /// - /// Translates - /// - /// method - /// over the given source. - /// - /// The shaped query on which the operator is applied. - /// - /// The lambda expression containing - /// - /// statements. - /// - /// The non query after translation. - protected virtual NonQueryExpression? TranslateExecuteUpdate(ShapedQueryExpression source, LambdaExpression 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. - if (source.ShaperExpression is IncludeExpression includeExpression) - { - source = source.UpdateShaperExpression(PruneIncludes(includeExpression)); - } - - var propertyValueLambdaExpressions = new List<(LambdaExpression PropertySelector, Expression ValueExpression)>(); - PopulateSetPropertyCalls(setPropertyCalls.Body, propertyValueLambdaExpressions, setPropertyCalls.Parameters[0]); - if (TranslationErrorDetails != null) - { - return null; - } - - if (propertyValueLambdaExpressions.Count == 0) - { - AddTranslationErrorDetails(RelationalStrings.NoSetPropertyInvocation); - return null; - } - - // 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 (propertySelector, _) = propertyValueLambdaExpressions[i]; - var propertySelectorBody = RemapLambdaBody(source, propertySelector).UnwrapTypeConversion(out _); - if (_sqlTranslator.Translate(propertySelectorBody) is not ColumnExpression column) - { - AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertySelector.Print())); - return null; - } - - if (targetTable is null) - { - targetTable = column.Table; - targetTablePropertySelector = propertySelector; - } - else if (!ReferenceEquals(column.Table, targetTable)) - { - AddTranslationErrorDetails( - RelationalStrings.MultipleTablesInExecuteUpdate(propertySelector.Print(), targetTablePropertySelector!.Print())); - return null; - } - - columns[i] = column; - } - - Check.DebugAssert(targetTable is not null, "Target table should have a value"); - - if (targetTable is TpcTablesExpression tpcTablesExpression) - { - AddTranslationErrorDetails( - 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 IsValidSelectExpressionForExecuteUpdate to add support for more cases via provider-specific UPDATE syntax. - var selectExpression = (SelectExpression)source.QueryExpression; - return IsValidSelectExpressionForExecuteUpdate(selectExpression, targetTable, out var tableExpression) - ? TranslateValueExpressions(this, source, selectExpression, tableExpression, propertyValueLambdaExpressions, columns) - : PushdownWithPkInnerJoinPredicate(); - - void PopulateSetPropertyCalls( - Expression expression, - List<(LambdaExpression, Expression)> list, - ParameterExpression parameter) - { - switch (expression) - { - case ParameterExpression p - when parameter == p: - break; - - case MethodCallExpression - { - Method: - { - IsGenericMethod: true, - Name: nameof(SetPropertyCalls.SetProperty), - DeclaringType.IsGenericType: true - } - } methodCallExpression - when methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(SetPropertyCalls<>): - list.Add(((LambdaExpression)methodCallExpression.Arguments[0], methodCallExpression.Arguments[1])); - - PopulateSetPropertyCalls(methodCallExpression.Object!, list, parameter); - - break; - - default: - AddTranslationErrorDetails(RelationalStrings.InvalidArgumentToExecuteUpdate); - break; - } - } - - static NonQueryExpression? TranslateValueExpressions( - RelationalQueryableMethodTranslatingExpressionVisitor visitor, - ShapedQueryExpression source, - SelectExpression selectExpression, - TableExpression tableExpression, - List<(LambdaExpression PropertySelector, Expression ValueExpression)> propertyValueLambdaExpression, - ColumnExpression[] columns) - { - var setters = new ColumnValueSetter[columns.Length]; - - for (var i = 0; i < propertyValueLambdaExpression.Count; i++) - { - var column = columns[i]; - var (_, valueSelector) = propertyValueLambdaExpression[i]; - - var remappedValueSelector = valueSelector is LambdaExpression lambdaExpression - ? visitor.RemapLambdaBody(source, lambdaExpression) - : valueSelector; - - if (remappedValueSelector.Type != column.Type) - { - remappedValueSelector = Expression.Convert(remappedValueSelector, column.Type); - } - - if (visitor.TranslateExpression(remappedValueSelector, applyDefaultTypeMapping: false) - is not SqlExpression translatedValueSelector) - { - 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); - } - - selectExpression.ReplaceProjection(new List()); - selectExpression.ApplyProjection(); - - return new NonQueryExpression(new UpdateExpression(tableExpression, selectExpression, setters)); - } - - NonQueryExpression? PushdownWithPkInnerJoinPredicate() - { - // 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. - - // The following mechanism for extracting the entity type from property selectors only supports 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). - - var firstPropertySelector = propertyValueLambdaExpressions[0].PropertySelector; - var shaper = RemapLambdaBody(source, firstPropertySelector).UnwrapTypeConversion(out _) switch - { - MemberExpression { Expression : not null } memberExpression - when memberExpression.Expression.UnwrapTypeConversion(out _) is StructuralTypeShaperExpression s - => s, - - MethodCallExpression mce when mce.TryGetEFPropertyArguments(out var source, out _) - && source.UnwrapTypeConversion(out _) is StructuralTypeShaperExpression s - => s, - - MethodCallExpression mce when mce.TryGetIndexerArguments(RelationalDependencies.Model, out var source2, out _) - && source2.UnwrapTypeConversion(out _) is StructuralTypeShaperExpression s - => s, - - _ => null - }; - - if (shaper is null) - { - AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(firstPropertySelector)); - return null; - } - - if (shaper.StructuralType is not IEntityType entityType) - { - AddTranslationErrorDetails( - RelationalStrings.ExecuteUpdateSubqueryNotSupportedOverComplexTypes(shaper.StructuralType.DisplayName())); - return null; - } - - if (entityType.FindPrimaryKey() is not IKey pk) - { - 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) - { - AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertySelector.Print())); - return null; - } - - columns[i] = column; - } - - return TranslateValueExpressions(this, outer, outerSelectExpression, tableExpression, propertyValueLambdaExpressions, columns); - } - - 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!; - } - } - - /// - /// Checks weather the current select expression can be used as-is for executing a delete operation, or whether it must be pushed - /// down into a subquery. - /// - /// - /// - /// By default, only single-table select expressions are supported, and optionally with a predicate. - /// - /// - /// Providers can override this to allow more select expression features to be supported without pushing down into a subquery. - /// When doing this, VisitDelete must also be overridden in the provider's QuerySqlGenerator to add SQL generation support for - /// the feature. - /// - /// - /// The select expression to validate. - /// The structural type shaper expression on which the delete operation is being applied. - /// The table expression from which rows are being deleted. - /// - /// Returns if the current select expression can be used for delete as-is, otherwise. - /// - protected virtual bool IsValidSelectExpressionForExecuteDelete( - SelectExpression selectExpression, - StructuralTypeShaperExpression shaper, - [NotNullWhen(true)] out TableExpression? tableExpression) - { - if (selectExpression is - { - Tables: [TableExpression expression], - Orderings: [], - Offset: null, - Limit: null, - GroupBy: [], - Having: null - }) - { - tableExpression = expression; - - return true; - } - - tableExpression = null; - return false; - } - - /// - /// Validates if the current select expression can be used for execute update operation or it requires to be joined as a subquery. - /// - /// - /// - /// By default, only multi-table select expressions are supported, and optionally with a predicate. - /// - /// - /// Providers can override this to allow more select expression features to be supported without pushing down into a subquery. - /// When doing this, VisitUpdate must also be overridden in the provider's QuerySqlGenerator to add SQL generation support for - /// the feature. - /// - /// - /// The select expression to validate. - /// The target table containing the rows to be updated. - /// - /// The table expression corresponding to the provided , containing the rows to be updated. - /// - /// - /// Returns if the current select expression can be used for update as-is, otherwise. - /// - protected virtual bool IsValidSelectExpressionForExecuteUpdate( - SelectExpression selectExpression, - TableExpressionBase targetTable, - [NotNullWhen(true)] out TableExpression? tableExpression) - { - tableExpression = null; - if (selectExpression is - { - Offset: null, - Limit: null, - IsDistinct: false, - GroupBy: [], - Having: null, - Orderings: [], - Tables.Count: > 0 - }) - { - if (selectExpression.Tables.Count > 1) - { - // If the table we are looking for is the first table, then we need to verify whether we can lift the next table in FROM - // clause - if (ReferenceEquals(selectExpression.Tables[0], targetTable) - && selectExpression.Tables[1] is not InnerJoinExpression and not CrossJoinExpression) - { - return false; - } - - if (targetTable is JoinExpressionBase joinExpressionBase) - { - targetTable = joinExpressionBase.Table; - } - } - - if (targetTable is TableExpression te) - { - tableExpression = te; - return true; - } - } - - return false; - } - /// /// Translates the given expression into equivalent SQL representation. /// From 13bb7d28e0022edab5e7a47ca5538bf7aab496f0 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 3 Dec 2023 13:27:11 +0100 Subject: [PATCH 2/2] Handle updating complex type properties in ExecuteUpdate Closes #32058 --- ...nslatingExpressionVisitor.ExecuteUpdate.cs | 416 ++++++++++++++---- .../ComplexTypeBulkUpdatesTestBase.cs | 115 +++++ .../TestUtilities/TestSqlLoggerFactory.cs | 2 +- .../Query/ComplexTypeQueryTestBase.cs | 34 +- .../ComplexTypeModel/ComplexTypeData.cs | 12 +- .../TestModels/ComplexTypeModel/Model.cs | 1 + .../ComplexTypeBulkUpdatesSqlServerTest.cs | 128 ++++++ .../Query/ComplexTypeQuerySqlServerTest.cs | 62 +-- .../Query/ComplexTypeQuerySqliteTest.cs | 62 +-- 9 files changed, 669 insertions(+), 163 deletions(-) diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs index 29caad46004..9473fb1ab9a 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs @@ -9,6 +9,11 @@ namespace Microsoft.EntityFrameworkCore.Query; public partial class RelationalQueryableMethodTranslatingExpressionVisitor { + private const string ExecuteUpdateRuntimeParameterPrefix = QueryCompilationContext.QueryParameterPrefix + "complex_type_"; + + private static readonly MethodInfo ParameterValueExtractorMethod = + typeof(RelationalSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!; + /// /// Translates /// (); - PopulateSetPropertyCalls(setPropertyCalls.Body, propertyValueLambdaExpressions, setPropertyCalls.Parameters[0]); + var setters = new List<(LambdaExpression PropertySelector, Expression ValueExpression)>(); + PopulateSetPropertyCalls(setPropertyCalls.Body, setters, setPropertyCalls.Parameters[0]); if (TranslationErrorDetails != null) { return null; } - if (propertyValueLambdaExpressions.Count == 0) + if (setters.Count == 0) { AddTranslationErrorDetails(RelationalStrings.NoSetPropertyInvocation); return null; } - // 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 (propertySelector, _) = propertyValueLambdaExpressions[i]; - var propertySelectorBody = RemapLambdaBody(source, propertySelector).UnwrapTypeConversion(out _); - if (_sqlTranslator.Translate(propertySelectorBody) is not ColumnExpression column) - { - AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertySelector.Print())); - return null; - } - - if (targetTable is null) - { - targetTable = column.Table; - targetTablePropertySelector = propertySelector; - } - else if (!ReferenceEquals(column.Table, targetTable)) - { - AddTranslationErrorDetails( - RelationalStrings.MultipleTablesInExecuteUpdate(propertySelector.Print(), targetTablePropertySelector!.Print())); - return null; - } - - columns[i] = column; - } - - Check.DebugAssert(targetTable is not null, "Target table should have a value"); - - if (targetTable is TpcTablesExpression tpcTablesExpression) + // Translate the setters: the left (property) selectors get translated to ColumnExpressions, the right (value) selectors to + // arbitrary SqlExpressions. + // Note that if the query isn't natively supported, we'll do a pushdown (see PushdownWithPkInnerJoinPredicate below); if that + // happens, we'll have to re-translate the setters over the new query (which includes a JOIN). However, we still translate here + // since we need the target table in order to perform the check below. + if (!TranslateSetters(source, setters, out var translatedSetters, out var targetTable)) { - AddTranslationErrorDetails( - 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. + // 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 IsValidSelectExpressionForExecuteUpdate to add support for more cases via provider-specific UPDATE syntax. var selectExpression = (SelectExpression)source.QueryExpression; - return IsValidSelectExpressionForExecuteUpdate(selectExpression, targetTable, out var tableExpression) - ? TranslateValueExpressions(this, source, selectExpression, tableExpression, propertyValueLambdaExpressions, columns) - : PushdownWithPkInnerJoinPredicate(); + if (IsValidSelectExpressionForExecuteUpdate(selectExpression, targetTable, out var tableExpression)) + { + selectExpression.ReplaceProjection(new List()); + selectExpression.ApplyProjection(); + + return new NonQueryExpression(new UpdateExpression(tableExpression, selectExpression, translatedSetters)); + } + + return PushdownWithPkInnerJoinPredicate(); void PopulateSetPropertyCalls( Expression expression, @@ -132,49 +108,274 @@ when methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typ } } - static NonQueryExpression? TranslateValueExpressions( - RelationalQueryableMethodTranslatingExpressionVisitor visitor, + bool TranslateSetters( ShapedQueryExpression source, - SelectExpression selectExpression, - TableExpression tableExpression, - List<(LambdaExpression PropertySelector, Expression ValueExpression)> propertyValueLambdaExpression, - ColumnExpression[] columns) + List<(LambdaExpression PropertySelector, Expression ValueExpression)> setters, + [NotNullWhen(true)] out List? translatedSetters, + [NotNullWhen(true)] out TableExpressionBase? targetTable) { - var setters = new ColumnValueSetter[columns.Length]; + var selectExpression = (SelectExpression)source.QueryExpression; + + targetTable = null; + TableExpressionBase? tempTargetTable = null; + var tempTranslatedSetters = new List(); + translatedSetters = null; + + LambdaExpression? propertySelector; + Expression? targetTablePropertySelector = null; + + for (var i = 0; i < setters.Count; i++) + { + (propertySelector, var valueSelector) = setters[i]; + var propertySelectorBody = RemapLambdaBody(source, propertySelector).UnwrapTypeConversion(out _); + + switch (_sqlTranslator.TranslateProjection(propertySelectorBody)) + { + case ColumnExpression column: + { + if (!IsColumnOnSameTable(column, propertySelector) + || TranslateSqlSetterValueSelector(source, valueSelector, column, selectExpression) is not SqlExpression + translatedValueSelector) + { + return false; + } - for (var i = 0; i < propertyValueLambdaExpression.Count; i++) + tempTranslatedSetters.Add(new(column, translatedValueSelector)); + break; + } + + // TODO: This is for column flattening; implement JSON complex type support as well. + case StructuralTypeShaperExpression + { + StructuralType: IComplexType, + ValueBufferExpression: StructuralTypeProjectionExpression + } shaper: + { + if (TranslateSetterValueSelector(source, valueSelector, shaper.Type) is not Expression translatedValueSelector + || !TryProcessComplexType(shaper, translatedValueSelector)) + { + return false; + } + + break; + } + + default: + AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertySelector.Print())); + return false; + } + } + + targetTable = tempTargetTable; + translatedSetters = tempTranslatedSetters; + + Check.DebugAssert(targetTable is not null, "Target table should have a value"); + + if (targetTable is TpcTablesExpression tpcTablesExpression) + { + AddTranslationErrorDetails( + RelationalStrings.ExecuteOperationOnTPC( + nameof(RelationalQueryableExtensions.ExecuteUpdate), tpcTablesExpression.EntityType.DisplayName())); + return false; + } + + return true; + + bool IsColumnOnSameTable(ColumnExpression column, LambdaExpression propertySelector) + { + if (tempTargetTable is null) + { + tempTargetTable = column.Table; + targetTablePropertySelector = propertySelector; + } + else if (!ReferenceEquals(column.Table, tempTargetTable)) + { + AddTranslationErrorDetails( + RelationalStrings.MultipleTablesInExecuteUpdate( + propertySelector.Print(), targetTablePropertySelector!.Print())); + return false; + } + + return true; + } + + bool TryProcessComplexType(StructuralTypeShaperExpression shaperExpression, Expression valueExpression) { - var column = columns[i]; - var (_, valueSelector) = propertyValueLambdaExpression[i]; + if (shaperExpression.StructuralType is not IComplexType complexType + || shaperExpression.ValueBufferExpression is not StructuralTypeProjectionExpression projection) + { + return false; + } + + foreach (var property in complexType.GetProperties()) + { + var column = projection.BindProperty(property); + if (!IsColumnOnSameTable(column, propertySelector)) + { + return false; + } + + var rewrittenValueSelector = CreatePropertyAccessExpression(valueExpression, property); + if (TranslateSqlSetterValueSelector(source, rewrittenValueSelector, column, selectExpression) is not SqlExpression + translatedValueSelector) + { + return false; + } - var remappedValueSelector = valueSelector is LambdaExpression lambdaExpression - ? visitor.RemapLambdaBody(source, lambdaExpression) - : valueSelector; + tempTranslatedSetters.Add(new(column, translatedValueSelector)); + } - if (remappedValueSelector.Type != column.Type) + foreach (var complexProperty in complexType.GetComplexProperties()) { - remappedValueSelector = Expression.Convert(remappedValueSelector, column.Type); + // Note that TranslateProjection currently returns null for StructuralTypeReferenceExpression with a subquery (as + // opposed to a parameter); this ensures that we don't generate an efficient translation where the subquery is + // duplicated for every property on the complex type. + // TODO: Make this work by using a common table expression (CTE) + + var nestedShaperExpression = projection.BindComplexProperty(complexProperty); + var nestedValueExpression = CreateComplexPropertyAccessExpression(valueExpression, complexProperty); + if (!TryProcessComplexType(nestedShaperExpression, nestedValueExpression)) + { + return false; + } } - if (visitor.TranslateExpression(remappedValueSelector, applyDefaultTypeMapping: false) - is not SqlExpression translatedValueSelector) + return true; + } + + Expression CreatePropertyAccessExpression(Expression target, IProperty property) + { + return target is LambdaExpression lambda + ? Expression.Lambda(Core(lambda.Body, property), lambda.Parameters[0]) + : Core(target, property); + + Expression Core(Expression target, IProperty property) { - visitor.AddTranslationErrorDetails(RelationalStrings.InvalidValueInSetProperty(valueSelector.Print())); - return null; + switch (target) + { + case SqlConstantExpression constantExpression: + return Expression.Constant( + constantExpression.Value is null + ? null + : property.GetGetter().GetClrValue(constantExpression.Value), + property.ClrType.MakeNullable()); + + case SqlParameterExpression parameterExpression + when parameterExpression.Name.StartsWith( + QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal): + { + var lambda = Expression.Lambda( + Expression.Call( + ParameterValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), + QueryCompilationContext.QueryContextParameter, + Expression.Constant(parameterExpression.Name, typeof(string)), + Expression.Constant(null, typeof(List)), + Expression.Constant(property, typeof(IProperty))), + QueryCompilationContext.QueryContextParameter); + + var newParameterName = + $"{ExecuteUpdateRuntimeParameterPrefix}" + + $"{parameterExpression.Name[QueryCompilationContext.QueryParameterPrefix.Length..]}_{property.Name}"; + + return _queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); + } + + case ParameterBasedComplexPropertyChainExpression chainExpression: + { + var lambda = Expression.Lambda( + Expression.Call( + ParameterValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), + QueryCompilationContext.QueryContextParameter, + Expression.Constant(chainExpression.ParameterExpression.Name, typeof(string)), + Expression.Constant(chainExpression.ComplexPropertyChain, typeof(List)), + Expression.Constant(property, typeof(IProperty))), + QueryCompilationContext.QueryContextParameter); + + var newParameterName = + $"{ExecuteUpdateRuntimeParameterPrefix}" + + $"{chainExpression.ParameterExpression.Name![QueryCompilationContext.QueryParameterPrefix.Length..]}_{property.Name}"; + + return _queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); + } + + case MemberInitExpression memberInitExpression + when memberInitExpression.Bindings.SingleOrDefault( + mb => mb.Member.Name == property.Name) is MemberAssignment memberAssignment: + return memberAssignment.Expression; + + default: + return target.CreateEFPropertyExpression(property); + } } + } - // 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); + Expression CreateComplexPropertyAccessExpression(Expression target, IComplexProperty complexProperty) + { + return target is LambdaExpression lambda + ? Expression.Lambda(Core(lambda.Body, complexProperty), lambda.Parameters[0]) + : Core(target, complexProperty); - setters[i] = new ColumnValueSetter(column, translatedValueSelector); + Expression Core(Expression target, IComplexProperty complexProperty) + => target switch + { + SqlConstantExpression constant => _sqlExpressionFactory.Constant( + constant.Value is null ? null : complexProperty.GetGetter().GetClrValue(constant.Value), + complexProperty.ClrType.MakeNullable()), + + SqlParameterExpression parameter + when parameter.Name.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) + => new ParameterBasedComplexPropertyChainExpression(parameter, complexProperty), + + StructuralTypeShaperExpression + { + StructuralType: IComplexType, + ValueBufferExpression: StructuralTypeProjectionExpression projection + } + => projection.BindComplexProperty(complexProperty), + + _ => throw new UnreachableException() + }; } + } - selectExpression.ReplaceProjection(new List()); - selectExpression.ApplyProjection(); + SqlExpression? TranslateSqlSetterValueSelector( + ShapedQueryExpression source, + Expression valueSelector, + ColumnExpression column, + SelectExpression selectExpression) + { + if (TranslateSetterValueSelector(source, valueSelector, column.Type) is not SqlExpression translatedSelector) + { + 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. + translatedSelector = _sqlExpressionFactory.ApplyTypeMapping(translatedSelector, column.TypeMapping); + translatedSelector = selectExpression.AssignUniqueAliases(translatedSelector); + return translatedSelector; + } + + Expression? TranslateSetterValueSelector(ShapedQueryExpression source, Expression valueSelector, Type propertyType) + { + var remappedValueSelector = valueSelector is LambdaExpression lambdaExpression + ? RemapLambdaBody(source, lambdaExpression) + : valueSelector; + + if (remappedValueSelector.Type != propertyType) + { + remappedValueSelector = Expression.Convert(remappedValueSelector, propertyType); + } + + if (_sqlTranslator.TranslateProjection(remappedValueSelector, applyDefaultTypeMapping: false) is not Expression + translatedValueSelector) + { + AddTranslationErrorDetails(RelationalStrings.InvalidValueInSetProperty(valueSelector.Print())); + return null; + } - return new NonQueryExpression(new UpdateExpression(tableExpression, selectExpression, setters)); + return translatedValueSelector; } NonQueryExpression? PushdownWithPkInnerJoinPredicate() @@ -195,7 +396,7 @@ when methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typ // 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). - var firstPropertySelector = propertyValueLambdaExpressions[0].PropertySelector; + var firstPropertySelector = setters[0].PropertySelector; var shaper = RemapLambdaBody(source, firstPropertySelector).UnwrapTypeConversion(out _) switch { MemberExpression { Expression : not null } memberExpression @@ -240,7 +441,7 @@ MethodCallExpression mce when mce.TryGetIndexerArguments(RelationalDependencies. var inner = source; var outerParameter = Expression.Parameter(entityType.ClrType); var outerKeySelector = Expression.Lambda(outerParameter.CreateKeyValuesExpression(pk.Properties), outerParameter); - var firstPropertyLambdaExpression = propertyValueLambdaExpressions[0].Item1; + var firstPropertyLambdaExpression = setters[0].Item1; var entitySource = GetEntitySource(RelationalDependencies.Model, firstPropertyLambdaExpression.Body); var innerKeySelector = Expression.Lambda( entitySource.CreateKeyValuesExpression(pk.Properties), firstPropertyLambdaExpression.Parameters); @@ -257,9 +458,9 @@ MethodCallExpression mce when mce.TryGetIndexerArguments(RelationalDependencies. var propertyReplacement = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Outer"); var valueReplacement = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Inner"); - for (var i = 0; i < propertyValueLambdaExpressions.Count; i++) + for (var i = 0; i < setters.Count; i++) { - var (propertyExpression, valueExpression) = propertyValueLambdaExpressions[i]; + var (propertyExpression, valueExpression) = setters[i]; propertyExpression = Expression.Lambda( ReplacingExpressionVisitor.Replace( ReplacingExpressionVisitor.Replace( @@ -275,28 +476,21 @@ MethodCallExpression mce when mce.TryGetIndexerArguments(RelationalDependencies. transparentIdentifierParameter) : valueExpression; - propertyValueLambdaExpressions[i] = (propertyExpression, valueExpression); + setters[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++) + if (!TranslateSetters(outer, setters, out var translatedSetters, out _)) { - var (propertySelector, _) = propertyValueLambdaExpressions[i]; - var propertySelectorBody = RemapLambdaBody(outer, propertySelector).UnwrapTypeConversion(out _); - - if (TranslateExpression(propertySelectorBody) is not ColumnExpression column) - { - AddTranslationErrorDetails(RelationalStrings.InvalidPropertyInSetProperty(propertySelector.Print())); - return null; - } - - columns[i] = column; + return null; } - return TranslateValueExpressions(this, outer, outerSelectExpression, tableExpression, propertyValueLambdaExpressions, columns); + outerSelectExpression.ReplaceProjection(new List()); + outerSelectExpression.ApplyProjection(); + return new NonQueryExpression(new UpdateExpression(tableExpression, outerSelectExpression, translatedSetters)); } static Expression GetEntitySource(IModel model, Expression propertyAccessExpression) @@ -382,4 +576,42 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate( return false; } + + private static T? ParameterValueExtractor( + QueryContext context, + string baseParameterName, + List? complexPropertyChain, + IProperty property) + { + var baseValue = context.ParameterValues[baseParameterName]; + + if (complexPropertyChain is not null) + { + foreach (var complexProperty in complexPropertyChain) + { + if (baseValue is null) + { + break; + } + + baseValue = complexProperty.GetGetter().GetClrValue(baseValue); + } + } + + return baseValue == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseValue); + } + + private sealed class ParameterBasedComplexPropertyChainExpression : Expression + { + public ParameterBasedComplexPropertyChainExpression( + SqlParameterExpression parameterExpression, + IComplexProperty firstComplexProperty) + { + ParameterExpression = parameterExpression; + ComplexPropertyChain = new List { firstComplexProperty }; + } + + public SqlParameterExpression ParameterExpression { get; } + public List ComplexPropertyChain { get; } + } } diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/ComplexTypeBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/ComplexTypeBulkUpdatesTestBase.cs index 45e2a1e1f6e..0296f6c1168 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/ComplexTypeBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/ComplexTypeBulkUpdatesTestBase.cs @@ -106,6 +106,121 @@ public virtual Task Update_projected_complex_type_via_OrderBy_Skip_throws(bool a s => s.SetProperty(c => c.ZipCode, 12345), rowsAffectedCount: 3)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_complex_type_to_parameter(bool async) + { + var newAddress = new Address + { + AddressLine1 = "New AddressLine1", + AddressLine2 = "New AddressLine2", + ZipCode = 99999, + Country = new() + { + Code = "FR", + FullName = "France" + }, + Tags = new List { "new_tag1", "new_tag2" } + }; + + return AssertUpdate( + async, + ss => ss.Set(), + c => c, + s => s.SetProperty(x => x.ShippingAddress, newAddress), + rowsAffectedCount: 3); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_nested_complex_type_to_parameter(bool async) + { + var newCountry = new Country + { + Code = "FR", + FullName = "France" + }; + + return AssertUpdate( + async, + ss => ss.Set(), + c => c, + s => s.SetProperty(x => x.ShippingAddress.Country, newCountry), + rowsAffectedCount: 3); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_complex_type_to_another_database_complex_type(bool async) + => AssertUpdate( + async, + ss => ss.Set(), + c => c, + s => s.SetProperty(x => x.ShippingAddress, x => x.BillingAddress), + rowsAffectedCount: 3); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_complex_type_to_inline_without_lambda(bool async) + => AssertUpdate( + async, + ss => ss.Set(), + c => c, + s => s.SetProperty(x => x.ShippingAddress, new Address + { + AddressLine1 = "New AddressLine1", + AddressLine2 = "New AddressLine2", + ZipCode = 99999, + Country = new() + { + Code = "FR", + FullName = "France" + }, + Tags = new List { "new_tag1", "new_tag2" } + }), + rowsAffectedCount: 3); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_complex_type_to_inline_with_lambda(bool async) + => AssertUpdate( + async, + ss => ss.Set(), + c => c, + s => s.SetProperty(x => x.ShippingAddress, x => new Address + { + AddressLine1 = "New AddressLine1", + AddressLine2 = "New AddressLine2", + ZipCode = 99999, + Country = new() + { + Code = "FR", + FullName = "France" + }, + Tags = new List { "new_tag1", "new_tag2" } + }), + rowsAffectedCount: 3); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_complex_type_to_another_database_complex_type_with_subquery(bool async) + => AssertUpdate( + async, + ss => ss.Set().OrderBy(c => c.Id).Skip(1), + c => c, + s => s.SetProperty(x => x.ShippingAddress, x => x.BillingAddress), + rowsAffectedCount: 2); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Update_collection_inside_complex_type(bool async) + => AssertUpdate( + async, + ss => ss.Set(), + c => c, + s => s.SetProperty(x => x.ShippingAddress.Tags, new List { "new_tag1", "new_tag2" }), + rowsAffectedCount: 3); + private void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); } diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs index bf57b50d51e..12eb14f7557 100644 --- a/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs @@ -254,7 +254,7 @@ void RewriteSourceWithNewBaseline(string fileName, int lineNumber) indentBuilder.Append(" "); var indent = indentBuilder.ToString(); var newBaseLine = $@"Assert{(forUpdate ? "ExecuteUpdate" : "")}Sql( -{string.Join("," + Environment.NewLine + indent + "//" + Environment.NewLine, SqlStatements.Skip(offset).Take(count).Select(sql => "\"\"\"" + Environment.NewLine + sql + Environment.NewLine + "\"\"\""))})"; +{string.Join("," + Environment.NewLine + indent + "//" + Environment.NewLine, SqlStatements.Skip(offset).Take(count).Select(sql => indent + "\"\"\"" + Environment.NewLine + sql + Environment.NewLine + "\"\"\""))})"; var numNewlinesInRewritten = newBaseLine.Count(c => c is '\n' or '\r'); writer.Write(newBaseLine); diff --git a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs index ceb4a5d65af..c91a7ae046b 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs @@ -143,8 +143,15 @@ public virtual Task Complex_type_equals_constant(bool async) { AddressLine1 = "804 S. Lakeshore Road", ZipCode = 38654, - Country = new Country { FullName = "United States", Code = "US" } - })); + Country = new Country { FullName = "United States", Code = "US" }, + Tags = new List { "foo", "bar" } + }), + ss => ss.Set().Where( + c => + c.ShippingAddress.AddressLine1 == "804 S. Lakeshore Road" + && c.ShippingAddress.ZipCode == 38654 + && c.ShippingAddress.Country == new Country { FullName = "United States", Code = "US" } + && c.ShippingAddress.Tags.SequenceEqual(new List { "foo", "bar" }))); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -154,12 +161,19 @@ public virtual Task Complex_type_equals_parameter(bool async) { AddressLine1 = "804 S. Lakeshore Road", ZipCode = 38654, - Country = new Country { FullName = "United States", Code = "US" } + Country = new Country { FullName = "United States", Code = "US" }, + Tags = new List { "foo", "bar" } }; return AssertQuery( async, - ss => ss.Set().Where(c => c.ShippingAddress == address)); + ss => ss.Set().Where(c => c.ShippingAddress == address), + ss => ss.Set().Where( + c => + c.ShippingAddress.AddressLine1 == "804 S. Lakeshore Road" + && c.ShippingAddress.ZipCode == 38654 + && c.ShippingAddress.Country == new Country { FullName = "United States", Code = "US" } + && c.ShippingAddress.Tags.SequenceEqual(new List { "foo", "bar" }))); } [ConditionalTheory] @@ -194,13 +208,21 @@ public virtual Task Contains_over_complex_type(bool async) { AddressLine1 = "804 S. Lakeshore Road", ZipCode = 38654, - Country = new Country { FullName = "United States", Code = "US" } + Country = new Country { FullName = "United States", Code = "US" }, + Tags = new List { "foo", "bar" } }; return AssertQuery( async, ss => ss.Set().Where( - c => ss.Set().Select(c => c.ShippingAddress).Contains(address))); + c => ss.Set().Select(c => c.ShippingAddress).Contains(address)), + ss => ss.Set().Where( + c => ss.Set().Select(c => c.ShippingAddress).Any( + a => + a.AddressLine1 == "804 S. Lakeshore Road" + && a.ZipCode == 38654 + && a.Country == new Country { FullName = "United States", Code = "US" } + && a.Tags.SequenceEqual(new List { "foo", "bar" })))); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/ComplexTypeData.cs b/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/ComplexTypeData.cs index cbba0e79225..e10e3c5a721 100644 --- a/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/ComplexTypeData.cs +++ b/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/ComplexTypeData.cs @@ -52,7 +52,8 @@ private static IReadOnlyList CreateCustomers() { AddressLine1 = "804 S. Lakeshore Road", ZipCode = 38654, - Country = new Country { FullName = "United States", Code = "US" } + Country = new Country { FullName = "United States", Code = "US" }, + Tags = new List { "foo", "bar" } }; var customer1 = new Customer @@ -71,13 +72,15 @@ private static IReadOnlyList CreateCustomers() { AddressLine1 = "72 Hickory Rd.", ZipCode = 07728, - Country = new Country { FullName = "Germany", Code = "DE" } + Country = new Country { FullName = "Germany", Code = "DE" }, + Tags = new List { "baz" } }, BillingAddress = new Address { AddressLine1 = "79 Main St.", ZipCode = 29293, - Country = new Country { FullName = "Germany", Code = "DE" } + Country = new Country { FullName = "Germany", Code = "DE" }, + Tags = new List { "a1", "a2", "a3" } } }; @@ -85,7 +88,8 @@ private static IReadOnlyList CreateCustomers() { AddressLine1 = "79 Main St.", ZipCode = 29293, - Country = new Country { FullName = "Germany", Code = "DE" } + Country = new Country { FullName = "Germany", Code = "DE" }, + Tags = new List { "foo", "moo" } }; var customer3 = new Customer diff --git a/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/Model.cs b/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/Model.cs index 689bc1cdafa..a35c5782bd2 100644 --- a/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/Model.cs +++ b/test/EFCore.Specification.Tests/TestModels/ComplexTypeModel/Model.cs @@ -19,6 +19,7 @@ public record Address public required string AddressLine1 { get; set; } public string? AddressLine2 { get; set; } public int ZipCode { get; set; } + public List Tags { get; set; } = new(); public required Country Country { get; set; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSqlServerTest.cs index 1d6aab91a85..d6d4ffd52da 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/ComplexTypeBulkUpdatesSqlServerTest.cs @@ -103,6 +103,134 @@ public override async Task Update_projected_complex_type_via_OrderBy_Skip_throws AssertExecuteUpdateSql(); } + public override async Task Update_complex_type_to_parameter(bool async) + { + await base.Update_complex_type_to_parameter(async); + + AssertExecuteUpdateSql( + """ +@__complex_type_newAddress_0_AddressLine1='New AddressLine1' (Size = 4000) +@__complex_type_newAddress_0_AddressLine2='New AddressLine2' (Size = 4000) +@__complex_type_newAddress_0_Tags='["new_tag1","new_tag2"]' (Size = 4000) +@__complex_type_newAddress_0_ZipCode='99999' (Nullable = true) +@__complex_type_newAddress_0_Code='FR' (Size = 4000) +@__complex_type_newAddress_0_FullName='France' (Size = 4000) + +UPDATE [c] +SET [c].[ShippingAddress_AddressLine1] = @__complex_type_newAddress_0_AddressLine1, + [c].[ShippingAddress_AddressLine2] = @__complex_type_newAddress_0_AddressLine2, + [c].[ShippingAddress_Tags] = @__complex_type_newAddress_0_Tags, + [c].[ShippingAddress_ZipCode] = @__complex_type_newAddress_0_ZipCode, + [c].[ShippingAddress_Country_Code] = @__complex_type_newAddress_0_Code, + [c].[ShippingAddress_Country_FullName] = @__complex_type_newAddress_0_FullName +FROM [Customer] AS [c] +"""); + } + + public override async Task Update_nested_complex_type_to_parameter(bool async) + { + await base.Update_nested_complex_type_to_parameter(async); + + AssertExecuteUpdateSql( + """ +@__complex_type_newCountry_0_Code='FR' (Size = 4000) +@__complex_type_newCountry_0_FullName='France' (Size = 4000) + +UPDATE [c] +SET [c].[ShippingAddress_Country_Code] = @__complex_type_newCountry_0_Code, + [c].[ShippingAddress_Country_FullName] = @__complex_type_newCountry_0_FullName +FROM [Customer] AS [c] +"""); + } + + public override async Task Update_complex_type_to_another_database_complex_type(bool async) + { + await base.Update_complex_type_to_another_database_complex_type(async); + + AssertExecuteUpdateSql( + """ +UPDATE [c] +SET [c].[ShippingAddress_AddressLine1] = [c].[BillingAddress_AddressLine1], + [c].[ShippingAddress_AddressLine2] = [c].[BillingAddress_AddressLine2], + [c].[ShippingAddress_Tags] = [c].[BillingAddress_Tags], + [c].[ShippingAddress_ZipCode] = [c].[BillingAddress_ZipCode], + [c].[ShippingAddress_Country_Code] = [c].[ShippingAddress_Country_Code], + [c].[ShippingAddress_Country_FullName] = [c].[ShippingAddress_Country_FullName] +FROM [Customer] AS [c] +"""); + } + + public override async Task Update_complex_type_to_inline_without_lambda(bool async) + { + await base.Update_complex_type_to_inline_without_lambda(async); + + AssertExecuteUpdateSql( + """ +UPDATE [c] +SET [c].[ShippingAddress_AddressLine1] = N'New AddressLine1', + [c].[ShippingAddress_AddressLine2] = N'New AddressLine2', + [c].[ShippingAddress_Tags] = N'["new_tag1","new_tag2"]', + [c].[ShippingAddress_ZipCode] = 99999, + [c].[ShippingAddress_Country_Code] = N'FR', + [c].[ShippingAddress_Country_FullName] = N'France' +FROM [Customer] AS [c] +"""); + } + + public override async Task Update_complex_type_to_inline_with_lambda(bool async) + { + await base.Update_complex_type_to_inline_with_lambda(async); + + AssertExecuteUpdateSql( + """ +UPDATE [c] +SET [c].[ShippingAddress_AddressLine1] = N'New AddressLine1', + [c].[ShippingAddress_AddressLine2] = N'New AddressLine2', + [c].[ShippingAddress_Tags] = N'["new_tag1","new_tag2"]', + [c].[ShippingAddress_ZipCode] = 99999, + [c].[ShippingAddress_Country_Code] = N'FR', + [c].[ShippingAddress_Country_FullName] = N'France' +FROM [Customer] AS [c] +"""); + } + + public override async Task Update_complex_type_to_another_database_complex_type_with_subquery(bool async) + { + await base.Update_complex_type_to_another_database_complex_type_with_subquery(async); + + AssertExecuteUpdateSql( + """ +@__p_0='1' + +UPDATE [c] +SET [c].[ShippingAddress_AddressLine1] = [t].[BillingAddress_AddressLine1], + [c].[ShippingAddress_AddressLine2] = [t].[BillingAddress_AddressLine2], + [c].[ShippingAddress_Tags] = [t].[BillingAddress_Tags], + [c].[ShippingAddress_ZipCode] = [t].[BillingAddress_ZipCode], + [c].[ShippingAddress_Country_Code] = [t].[ShippingAddress_Country_Code], + [c].[ShippingAddress_Country_FullName] = [t].[ShippingAddress_Country_FullName] +FROM [Customer] AS [c] +INNER JOIN ( + SELECT [c0].[Id], [c0].[Name], [c0].[BillingAddress_AddressLine1], [c0].[BillingAddress_AddressLine2], [c0].[BillingAddress_Tags], [c0].[BillingAddress_ZipCode], [c0].[BillingAddress_Country_Code], [c0].[BillingAddress_Country_FullName], [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_Tags], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] + FROM [Customer] AS [c0] + ORDER BY [c0].[Id] + OFFSET @__p_0 ROWS +) AS [t] ON [c].[Id] = [t].[Id] +"""); + } + + public override async Task Update_collection_inside_complex_type(bool async) + { + await base.Update_collection_inside_complex_type(async); + + AssertExecuteUpdateSql( + """ +UPDATE [c] +SET [c].[ShippingAddress_Tags] = N'["new_tag1","new_tag2"]' +FROM [Customer] AS [c] +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs index 52ed43e3ea5..c106961995b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs @@ -19,7 +19,7 @@ public override async Task Filter_on_property_inside_complex_type(bool async) AssertSql( """ -SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] WHERE [c].[ShippingAddress_ZipCode] = 7728 """); @@ -31,7 +31,7 @@ public override async Task Filter_on_property_inside_nested_complex_type(bool as AssertSql( """ -SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] WHERE [c].[ShippingAddress_Country_Code] = N'DE' """); @@ -45,9 +45,9 @@ public override async Task Filter_on_property_inside_complex_type_after_subquery """ @__p_0='1' -SELECT DISTINCT [t].[Id], [t].[Name], [t].[BillingAddress_AddressLine1], [t].[BillingAddress_AddressLine2], [t].[BillingAddress_ZipCode], [t].[BillingAddress_Country_Code], [t].[BillingAddress_Country_FullName], [t].[ShippingAddress_AddressLine1], [t].[ShippingAddress_AddressLine2], [t].[ShippingAddress_ZipCode], [t].[ShippingAddress_Country_Code], [t].[ShippingAddress_Country_FullName] +SELECT DISTINCT [t].[Id], [t].[Name], [t].[BillingAddress_AddressLine1], [t].[BillingAddress_AddressLine2], [t].[BillingAddress_Tags], [t].[BillingAddress_ZipCode], [t].[BillingAddress_Country_Code], [t].[BillingAddress_Country_FullName], [t].[ShippingAddress_AddressLine1], [t].[ShippingAddress_AddressLine2], [t].[ShippingAddress_Tags], [t].[ShippingAddress_ZipCode], [t].[ShippingAddress_Country_Code], [t].[ShippingAddress_Country_FullName] FROM ( - SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] + SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] ORDER BY [c].[Id] OFFSET @__p_0 ROWS @@ -64,9 +64,9 @@ public override async Task Filter_on_property_inside_nested_complex_type_after_s """ @__p_0='1' -SELECT DISTINCT [t].[Id], [t].[Name], [t].[BillingAddress_AddressLine1], [t].[BillingAddress_AddressLine2], [t].[BillingAddress_ZipCode], [t].[BillingAddress_Country_Code], [t].[BillingAddress_Country_FullName], [t].[ShippingAddress_AddressLine1], [t].[ShippingAddress_AddressLine2], [t].[ShippingAddress_ZipCode], [t].[ShippingAddress_Country_Code], [t].[ShippingAddress_Country_FullName] +SELECT DISTINCT [t].[Id], [t].[Name], [t].[BillingAddress_AddressLine1], [t].[BillingAddress_AddressLine2], [t].[BillingAddress_Tags], [t].[BillingAddress_ZipCode], [t].[BillingAddress_Country_Code], [t].[BillingAddress_Country_FullName], [t].[ShippingAddress_AddressLine1], [t].[ShippingAddress_AddressLine2], [t].[ShippingAddress_Tags], [t].[ShippingAddress_ZipCode], [t].[ShippingAddress_Country_Code], [t].[ShippingAddress_Country_FullName] FROM ( - SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] + SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] ORDER BY [c].[Id] OFFSET @__p_0 ROWS @@ -81,7 +81,7 @@ public override async Task Filter_on_required_property_inside_required_complex_t AssertSql( """ -SELECT [c].[Id], [c].[OptionalCustomerId], [c].[RequiredCustomerId], [c0].[Id], [c0].[Name], [c0].[BillingAddress_AddressLine1], [c0].[BillingAddress_AddressLine2], [c0].[BillingAddress_ZipCode], [c0].[BillingAddress_Country_Code], [c0].[BillingAddress_Country_FullName], [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName], [c1].[Id], [c1].[Name], [c1].[BillingAddress_AddressLine1], [c1].[BillingAddress_AddressLine2], [c1].[BillingAddress_ZipCode], [c1].[BillingAddress_Country_Code], [c1].[BillingAddress_Country_FullName], [c1].[ShippingAddress_AddressLine1], [c1].[ShippingAddress_AddressLine2], [c1].[ShippingAddress_ZipCode], [c1].[ShippingAddress_Country_Code], [c1].[ShippingAddress_Country_FullName] +SELECT [c].[Id], [c].[OptionalCustomerId], [c].[RequiredCustomerId], [c0].[Id], [c0].[Name], [c0].[BillingAddress_AddressLine1], [c0].[BillingAddress_AddressLine2], [c0].[BillingAddress_Tags], [c0].[BillingAddress_ZipCode], [c0].[BillingAddress_Country_Code], [c0].[BillingAddress_Country_FullName], [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_Tags], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName], [c1].[Id], [c1].[Name], [c1].[BillingAddress_AddressLine1], [c1].[BillingAddress_AddressLine2], [c1].[BillingAddress_Tags], [c1].[BillingAddress_ZipCode], [c1].[BillingAddress_Country_Code], [c1].[BillingAddress_Country_FullName], [c1].[ShippingAddress_AddressLine1], [c1].[ShippingAddress_AddressLine2], [c1].[ShippingAddress_Tags], [c1].[ShippingAddress_ZipCode], [c1].[ShippingAddress_Country_Code], [c1].[ShippingAddress_Country_FullName] FROM [CustomerGroup] AS [c] LEFT JOIN [Customer] AS [c0] ON [c].[OptionalCustomerId] = [c0].[Id] INNER JOIN [Customer] AS [c1] ON [c].[RequiredCustomerId] = [c1].[Id] @@ -95,7 +95,7 @@ public override async Task Filter_on_required_property_inside_required_complex_t AssertSql( """ -SELECT [c].[Id], [c].[OptionalCustomerId], [c].[RequiredCustomerId], [c1].[Id], [c1].[Name], [c1].[BillingAddress_AddressLine1], [c1].[BillingAddress_AddressLine2], [c1].[BillingAddress_ZipCode], [c1].[BillingAddress_Country_Code], [c1].[BillingAddress_Country_FullName], [c1].[ShippingAddress_AddressLine1], [c1].[ShippingAddress_AddressLine2], [c1].[ShippingAddress_ZipCode], [c1].[ShippingAddress_Country_Code], [c1].[ShippingAddress_Country_FullName], [c0].[Id], [c0].[Name], [c0].[BillingAddress_AddressLine1], [c0].[BillingAddress_AddressLine2], [c0].[BillingAddress_ZipCode], [c0].[BillingAddress_Country_Code], [c0].[BillingAddress_Country_FullName], [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] +SELECT [c].[Id], [c].[OptionalCustomerId], [c].[RequiredCustomerId], [c1].[Id], [c1].[Name], [c1].[BillingAddress_AddressLine1], [c1].[BillingAddress_AddressLine2], [c1].[BillingAddress_Tags], [c1].[BillingAddress_ZipCode], [c1].[BillingAddress_Country_Code], [c1].[BillingAddress_Country_FullName], [c1].[ShippingAddress_AddressLine1], [c1].[ShippingAddress_AddressLine2], [c1].[ShippingAddress_Tags], [c1].[ShippingAddress_ZipCode], [c1].[ShippingAddress_Country_Code], [c1].[ShippingAddress_Country_FullName], [c0].[Id], [c0].[Name], [c0].[BillingAddress_AddressLine1], [c0].[BillingAddress_AddressLine2], [c0].[BillingAddress_Tags], [c0].[BillingAddress_ZipCode], [c0].[BillingAddress_Country_Code], [c0].[BillingAddress_Country_FullName], [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_Tags], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] FROM [CustomerGroup] AS [c] INNER JOIN [Customer] AS [c0] ON [c].[RequiredCustomerId] = [c0].[Id] LEFT JOIN [Customer] AS [c1] ON [c].[OptionalCustomerId] = [c1].[Id] @@ -119,7 +119,7 @@ public override async Task Project_complex_type_via_required_navigation(bool asy AssertSql( """ -SELECT [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] +SELECT [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_Tags], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] FROM [CustomerGroup] AS [c] INNER JOIN [Customer] AS [c0] ON [c].[RequiredCustomerId] = [c0].[Id] """); @@ -133,9 +133,9 @@ public override async Task Load_complex_type_after_subquery_on_entity_type(bool """ @__p_0='1' -SELECT DISTINCT [t].[Id], [t].[Name], [t].[BillingAddress_AddressLine1], [t].[BillingAddress_AddressLine2], [t].[BillingAddress_ZipCode], [t].[BillingAddress_Country_Code], [t].[BillingAddress_Country_FullName], [t].[ShippingAddress_AddressLine1], [t].[ShippingAddress_AddressLine2], [t].[ShippingAddress_ZipCode], [t].[ShippingAddress_Country_Code], [t].[ShippingAddress_Country_FullName] +SELECT DISTINCT [t].[Id], [t].[Name], [t].[BillingAddress_AddressLine1], [t].[BillingAddress_AddressLine2], [t].[BillingAddress_Tags], [t].[BillingAddress_ZipCode], [t].[BillingAddress_Country_Code], [t].[BillingAddress_Country_FullName], [t].[ShippingAddress_AddressLine1], [t].[ShippingAddress_AddressLine2], [t].[ShippingAddress_Tags], [t].[ShippingAddress_ZipCode], [t].[ShippingAddress_Country_Code], [t].[ShippingAddress_Country_FullName] FROM ( - SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] + SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] ORDER BY [c].[Id] OFFSET @__p_0 ROWS @@ -149,7 +149,7 @@ public override async Task Select_complex_type(bool async) AssertSql( """ -SELECT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] """); } @@ -182,7 +182,7 @@ public override async Task Select_complex_type_Where(bool async) AssertSql( """ -SELECT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] WHERE [c].[ShippingAddress_ZipCode] = 7728 """); @@ -194,7 +194,7 @@ public override async Task Select_complex_type_Distinct(bool async) AssertSql( """ -SELECT DISTINCT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT DISTINCT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] """); } @@ -205,9 +205,9 @@ public override async Task Complex_type_equals_complex_type(bool async) AssertSql( """ -SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] -WHERE [c].[ShippingAddress_AddressLine1] = [c].[BillingAddress_AddressLine1] AND ([c].[ShippingAddress_AddressLine2] = [c].[BillingAddress_AddressLine2] OR ([c].[ShippingAddress_AddressLine2] IS NULL AND [c].[BillingAddress_AddressLine2] IS NULL)) AND [c].[ShippingAddress_ZipCode] = [c].[BillingAddress_ZipCode] +WHERE [c].[ShippingAddress_AddressLine1] = [c].[BillingAddress_AddressLine1] AND ([c].[ShippingAddress_AddressLine2] = [c].[BillingAddress_AddressLine2] OR ([c].[ShippingAddress_AddressLine2] IS NULL AND [c].[BillingAddress_AddressLine2] IS NULL)) AND [c].[ShippingAddress_Tags] = [c].[BillingAddress_Tags] AND [c].[ShippingAddress_ZipCode] = [c].[BillingAddress_ZipCode] """); } @@ -217,9 +217,9 @@ public override async Task Complex_type_equals_constant(bool async) AssertSql( """ -SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] -WHERE [c].[ShippingAddress_AddressLine1] = N'804 S. Lakeshore Road' AND [c].[ShippingAddress_AddressLine2] IS NULL AND [c].[ShippingAddress_ZipCode] = 38654 AND [c].[ShippingAddress_Country_Code] = N'US' AND [c].[ShippingAddress_Country_FullName] = N'United States' +WHERE [c].[ShippingAddress_AddressLine1] = N'804 S. Lakeshore Road' AND [c].[ShippingAddress_AddressLine2] IS NULL AND [c].[ShippingAddress_Tags] = N'["foo","bar"]' AND [c].[ShippingAddress_ZipCode] = 38654 AND [c].[ShippingAddress_Country_Code] = N'US' AND [c].[ShippingAddress_Country_FullName] = N'United States' """); } @@ -230,13 +230,14 @@ public override async Task Complex_type_equals_parameter(bool async) AssertSql( """ @__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' (Size = 4000) +@__entity_equality_address_0_Tags='["foo","bar"]' (Size = 4000) @__entity_equality_address_0_ZipCode='38654' (Nullable = true) @__entity_equality_address_0_Code='US' (Size = 4000) @__entity_equality_address_0_FullName='United States' (Size = 4000) -SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] -WHERE [c].[ShippingAddress_AddressLine1] = @__entity_equality_address_0_AddressLine1 AND [c].[ShippingAddress_AddressLine2] IS NULL AND [c].[ShippingAddress_ZipCode] = @__entity_equality_address_0_ZipCode AND [c].[ShippingAddress_Country_Code] = @__entity_equality_address_0_Code AND [c].[ShippingAddress_Country_FullName] = @__entity_equality_address_0_FullName +WHERE [c].[ShippingAddress_AddressLine1] = @__entity_equality_address_0_AddressLine1 AND [c].[ShippingAddress_AddressLine2] IS NULL AND [c].[ShippingAddress_Tags] = @__entity_equality_address_0_Tags AND [c].[ShippingAddress_ZipCode] = @__entity_equality_address_0_ZipCode AND [c].[ShippingAddress_Country_Code] = @__entity_equality_address_0_Code AND [c].[ShippingAddress_Country_FullName] = @__entity_equality_address_0_FullName """); } @@ -261,16 +262,17 @@ public override async Task Contains_over_complex_type(bool async) AssertSql( """ @__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' (Size = 4000) +@__entity_equality_address_0_Tags='["foo","bar"]' (Size = 4000) @__entity_equality_address_0_ZipCode='38654' (Nullable = true) @__entity_equality_address_0_Code='US' (Size = 4000) @__entity_equality_address_0_FullName='United States' (Size = 4000) -SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] WHERE EXISTS ( SELECT 1 FROM [Customer] AS [c0] - WHERE [c0].[ShippingAddress_AddressLine1] = @__entity_equality_address_0_AddressLine1 AND [c0].[ShippingAddress_AddressLine2] IS NULL AND [c0].[ShippingAddress_ZipCode] = @__entity_equality_address_0_ZipCode AND [c0].[ShippingAddress_Country_Code] = @__entity_equality_address_0_Code AND [c0].[ShippingAddress_Country_FullName] = @__entity_equality_address_0_FullName) + WHERE [c0].[ShippingAddress_AddressLine1] = @__entity_equality_address_0_AddressLine1 AND [c0].[ShippingAddress_AddressLine2] IS NULL AND [c0].[ShippingAddress_Tags] = @__entity_equality_address_0_Tags AND [c0].[ShippingAddress_ZipCode] = @__entity_equality_address_0_ZipCode AND [c0].[ShippingAddress_Country_Code] = @__entity_equality_address_0_Code AND [c0].[ShippingAddress_Country_FullName] = @__entity_equality_address_0_FullName) """); } @@ -280,11 +282,11 @@ public override async Task Concat_complex_type(bool async) AssertSql( """ -SELECT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] WHERE [c].[Id] = 1 UNION ALL -SELECT [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] +SELECT [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_Tags], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] FROM [Customer] AS [c0] WHERE [c0].[Id] = 2 """); @@ -296,11 +298,11 @@ public override async Task Concat_entity_type_containing_complex_property(bool a AssertSql( """ -SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] WHERE [c].[Id] = 1 UNION ALL -SELECT [c0].[Id], [c0].[Name], [c0].[BillingAddress_AddressLine1], [c0].[BillingAddress_AddressLine2], [c0].[BillingAddress_ZipCode], [c0].[BillingAddress_Country_Code], [c0].[BillingAddress_Country_FullName], [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] +SELECT [c0].[Id], [c0].[Name], [c0].[BillingAddress_AddressLine1], [c0].[BillingAddress_AddressLine2], [c0].[BillingAddress_Tags], [c0].[BillingAddress_ZipCode], [c0].[BillingAddress_Country_Code], [c0].[BillingAddress_Country_FullName], [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_Tags], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] FROM [Customer] AS [c0] WHERE [c0].[Id] = 2 """); @@ -312,11 +314,11 @@ public override async Task Union_entity_type_containing_complex_property(bool as AssertSql( """ -SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[Id], [c].[Name], [c].[BillingAddress_AddressLine1], [c].[BillingAddress_AddressLine2], [c].[BillingAddress_Tags], [c].[BillingAddress_ZipCode], [c].[BillingAddress_Country_Code], [c].[BillingAddress_Country_FullName], [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] WHERE [c].[Id] = 1 UNION -SELECT [c0].[Id], [c0].[Name], [c0].[BillingAddress_AddressLine1], [c0].[BillingAddress_AddressLine2], [c0].[BillingAddress_ZipCode], [c0].[BillingAddress_Country_Code], [c0].[BillingAddress_Country_FullName], [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] +SELECT [c0].[Id], [c0].[Name], [c0].[BillingAddress_AddressLine1], [c0].[BillingAddress_AddressLine2], [c0].[BillingAddress_Tags], [c0].[BillingAddress_ZipCode], [c0].[BillingAddress_Country_Code], [c0].[BillingAddress_Country_FullName], [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_Tags], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] FROM [Customer] AS [c0] WHERE [c0].[Id] = 2 """); @@ -328,11 +330,11 @@ public override async Task Union_complex_type(bool async) AssertSql( """ -SELECT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] +SELECT [c].[ShippingAddress_AddressLine1], [c].[ShippingAddress_AddressLine2], [c].[ShippingAddress_Tags], [c].[ShippingAddress_ZipCode], [c].[ShippingAddress_Country_Code], [c].[ShippingAddress_Country_FullName] FROM [Customer] AS [c] WHERE [c].[Id] = 1 UNION -SELECT [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] +SELECT [c0].[ShippingAddress_AddressLine1], [c0].[ShippingAddress_AddressLine2], [c0].[ShippingAddress_Tags], [c0].[ShippingAddress_ZipCode], [c0].[ShippingAddress_Country_Code], [c0].[ShippingAddress_Country_FullName] FROM [Customer] AS [c0] WHERE [c0].[Id] = 2 """); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs index d3423b9e7d0..3dd4ff5cec3 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs @@ -19,7 +19,7 @@ public override async Task Filter_on_property_inside_complex_type(bool async) AssertSql( """ -SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" WHERE "c"."ShippingAddress_ZipCode" = 7728 """); @@ -31,7 +31,7 @@ public override async Task Filter_on_property_inside_nested_complex_type(bool as AssertSql( """ -SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" WHERE "c"."ShippingAddress_Country_Code" = 'DE' """); @@ -45,9 +45,9 @@ public override async Task Filter_on_property_inside_complex_type_after_subquery """ @__p_0='1' -SELECT DISTINCT "t"."Id", "t"."Name", "t"."BillingAddress_AddressLine1", "t"."BillingAddress_AddressLine2", "t"."BillingAddress_ZipCode", "t"."BillingAddress_Country_Code", "t"."BillingAddress_Country_FullName", "t"."ShippingAddress_AddressLine1", "t"."ShippingAddress_AddressLine2", "t"."ShippingAddress_ZipCode", "t"."ShippingAddress_Country_Code", "t"."ShippingAddress_Country_FullName" +SELECT DISTINCT "t"."Id", "t"."Name", "t"."BillingAddress_AddressLine1", "t"."BillingAddress_AddressLine2", "t"."BillingAddress_Tags", "t"."BillingAddress_ZipCode", "t"."BillingAddress_Country_Code", "t"."BillingAddress_Country_FullName", "t"."ShippingAddress_AddressLine1", "t"."ShippingAddress_AddressLine2", "t"."ShippingAddress_Tags", "t"."ShippingAddress_ZipCode", "t"."ShippingAddress_Country_Code", "t"."ShippingAddress_Country_FullName" FROM ( - SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" + SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" ORDER BY "c"."Id" LIMIT -1 OFFSET @__p_0 @@ -64,9 +64,9 @@ public override async Task Filter_on_property_inside_nested_complex_type_after_s """ @__p_0='1' -SELECT DISTINCT "t"."Id", "t"."Name", "t"."BillingAddress_AddressLine1", "t"."BillingAddress_AddressLine2", "t"."BillingAddress_ZipCode", "t"."BillingAddress_Country_Code", "t"."BillingAddress_Country_FullName", "t"."ShippingAddress_AddressLine1", "t"."ShippingAddress_AddressLine2", "t"."ShippingAddress_ZipCode", "t"."ShippingAddress_Country_Code", "t"."ShippingAddress_Country_FullName" +SELECT DISTINCT "t"."Id", "t"."Name", "t"."BillingAddress_AddressLine1", "t"."BillingAddress_AddressLine2", "t"."BillingAddress_Tags", "t"."BillingAddress_ZipCode", "t"."BillingAddress_Country_Code", "t"."BillingAddress_Country_FullName", "t"."ShippingAddress_AddressLine1", "t"."ShippingAddress_AddressLine2", "t"."ShippingAddress_Tags", "t"."ShippingAddress_ZipCode", "t"."ShippingAddress_Country_Code", "t"."ShippingAddress_Country_FullName" FROM ( - SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" + SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" ORDER BY "c"."Id" LIMIT -1 OFFSET @__p_0 @@ -81,7 +81,7 @@ public override async Task Filter_on_required_property_inside_required_complex_t AssertSql( """ -SELECT "c"."Id", "c"."OptionalCustomerId", "c"."RequiredCustomerId", "c0"."Id", "c0"."Name", "c0"."BillingAddress_AddressLine1", "c0"."BillingAddress_AddressLine2", "c0"."BillingAddress_ZipCode", "c0"."BillingAddress_Country_Code", "c0"."BillingAddress_Country_FullName", "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName", "c1"."Id", "c1"."Name", "c1"."BillingAddress_AddressLine1", "c1"."BillingAddress_AddressLine2", "c1"."BillingAddress_ZipCode", "c1"."BillingAddress_Country_Code", "c1"."BillingAddress_Country_FullName", "c1"."ShippingAddress_AddressLine1", "c1"."ShippingAddress_AddressLine2", "c1"."ShippingAddress_ZipCode", "c1"."ShippingAddress_Country_Code", "c1"."ShippingAddress_Country_FullName" +SELECT "c"."Id", "c"."OptionalCustomerId", "c"."RequiredCustomerId", "c0"."Id", "c0"."Name", "c0"."BillingAddress_AddressLine1", "c0"."BillingAddress_AddressLine2", "c0"."BillingAddress_Tags", "c0"."BillingAddress_ZipCode", "c0"."BillingAddress_Country_Code", "c0"."BillingAddress_Country_FullName", "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_Tags", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName", "c1"."Id", "c1"."Name", "c1"."BillingAddress_AddressLine1", "c1"."BillingAddress_AddressLine2", "c1"."BillingAddress_Tags", "c1"."BillingAddress_ZipCode", "c1"."BillingAddress_Country_Code", "c1"."BillingAddress_Country_FullName", "c1"."ShippingAddress_AddressLine1", "c1"."ShippingAddress_AddressLine2", "c1"."ShippingAddress_Tags", "c1"."ShippingAddress_ZipCode", "c1"."ShippingAddress_Country_Code", "c1"."ShippingAddress_Country_FullName" FROM "CustomerGroup" AS "c" LEFT JOIN "Customer" AS "c0" ON "c"."OptionalCustomerId" = "c0"."Id" INNER JOIN "Customer" AS "c1" ON "c"."RequiredCustomerId" = "c1"."Id" @@ -95,7 +95,7 @@ public override async Task Filter_on_required_property_inside_required_complex_t AssertSql( """ -SELECT "c"."Id", "c"."OptionalCustomerId", "c"."RequiredCustomerId", "c1"."Id", "c1"."Name", "c1"."BillingAddress_AddressLine1", "c1"."BillingAddress_AddressLine2", "c1"."BillingAddress_ZipCode", "c1"."BillingAddress_Country_Code", "c1"."BillingAddress_Country_FullName", "c1"."ShippingAddress_AddressLine1", "c1"."ShippingAddress_AddressLine2", "c1"."ShippingAddress_ZipCode", "c1"."ShippingAddress_Country_Code", "c1"."ShippingAddress_Country_FullName", "c0"."Id", "c0"."Name", "c0"."BillingAddress_AddressLine1", "c0"."BillingAddress_AddressLine2", "c0"."BillingAddress_ZipCode", "c0"."BillingAddress_Country_Code", "c0"."BillingAddress_Country_FullName", "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" +SELECT "c"."Id", "c"."OptionalCustomerId", "c"."RequiredCustomerId", "c1"."Id", "c1"."Name", "c1"."BillingAddress_AddressLine1", "c1"."BillingAddress_AddressLine2", "c1"."BillingAddress_Tags", "c1"."BillingAddress_ZipCode", "c1"."BillingAddress_Country_Code", "c1"."BillingAddress_Country_FullName", "c1"."ShippingAddress_AddressLine1", "c1"."ShippingAddress_AddressLine2", "c1"."ShippingAddress_Tags", "c1"."ShippingAddress_ZipCode", "c1"."ShippingAddress_Country_Code", "c1"."ShippingAddress_Country_FullName", "c0"."Id", "c0"."Name", "c0"."BillingAddress_AddressLine1", "c0"."BillingAddress_AddressLine2", "c0"."BillingAddress_Tags", "c0"."BillingAddress_ZipCode", "c0"."BillingAddress_Country_Code", "c0"."BillingAddress_Country_FullName", "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_Tags", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" FROM "CustomerGroup" AS "c" INNER JOIN "Customer" AS "c0" ON "c"."RequiredCustomerId" = "c0"."Id" LEFT JOIN "Customer" AS "c1" ON "c"."OptionalCustomerId" = "c1"."Id" @@ -119,7 +119,7 @@ public override async Task Project_complex_type_via_required_navigation(bool asy AssertSql( """ -SELECT "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" +SELECT "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_Tags", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" FROM "CustomerGroup" AS "c" INNER JOIN "Customer" AS "c0" ON "c"."RequiredCustomerId" = "c0"."Id" """); @@ -133,9 +133,9 @@ public override async Task Load_complex_type_after_subquery_on_entity_type(bool """ @__p_0='1' -SELECT DISTINCT "t"."Id", "t"."Name", "t"."BillingAddress_AddressLine1", "t"."BillingAddress_AddressLine2", "t"."BillingAddress_ZipCode", "t"."BillingAddress_Country_Code", "t"."BillingAddress_Country_FullName", "t"."ShippingAddress_AddressLine1", "t"."ShippingAddress_AddressLine2", "t"."ShippingAddress_ZipCode", "t"."ShippingAddress_Country_Code", "t"."ShippingAddress_Country_FullName" +SELECT DISTINCT "t"."Id", "t"."Name", "t"."BillingAddress_AddressLine1", "t"."BillingAddress_AddressLine2", "t"."BillingAddress_Tags", "t"."BillingAddress_ZipCode", "t"."BillingAddress_Country_Code", "t"."BillingAddress_Country_FullName", "t"."ShippingAddress_AddressLine1", "t"."ShippingAddress_AddressLine2", "t"."ShippingAddress_Tags", "t"."ShippingAddress_ZipCode", "t"."ShippingAddress_Country_Code", "t"."ShippingAddress_Country_FullName" FROM ( - SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" + SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" ORDER BY "c"."Id" LIMIT -1 OFFSET @__p_0 @@ -149,7 +149,7 @@ public override async Task Select_complex_type(bool async) AssertSql( """ -SELECT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" """); } @@ -182,7 +182,7 @@ public override async Task Select_complex_type_Where(bool async) AssertSql( """ -SELECT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" WHERE "c"."ShippingAddress_ZipCode" = 7728 """); @@ -194,7 +194,7 @@ public override async Task Select_complex_type_Distinct(bool async) AssertSql( """ -SELECT DISTINCT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT DISTINCT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" """); } @@ -205,9 +205,9 @@ public override async Task Complex_type_equals_complex_type(bool async) AssertSql( """ -SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" -WHERE "c"."ShippingAddress_AddressLine1" = "c"."BillingAddress_AddressLine1" AND ("c"."ShippingAddress_AddressLine2" = "c"."BillingAddress_AddressLine2" OR ("c"."ShippingAddress_AddressLine2" IS NULL AND "c"."BillingAddress_AddressLine2" IS NULL)) AND "c"."ShippingAddress_ZipCode" = "c"."BillingAddress_ZipCode" +WHERE "c"."ShippingAddress_AddressLine1" = "c"."BillingAddress_AddressLine1" AND ("c"."ShippingAddress_AddressLine2" = "c"."BillingAddress_AddressLine2" OR ("c"."ShippingAddress_AddressLine2" IS NULL AND "c"."BillingAddress_AddressLine2" IS NULL)) AND "c"."ShippingAddress_Tags" = "c"."BillingAddress_Tags" AND "c"."ShippingAddress_ZipCode" = "c"."BillingAddress_ZipCode" """); } @@ -217,9 +217,9 @@ public override async Task Complex_type_equals_constant(bool async) AssertSql( """ -SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" -WHERE "c"."ShippingAddress_AddressLine1" = '804 S. Lakeshore Road' AND "c"."ShippingAddress_AddressLine2" IS NULL AND "c"."ShippingAddress_ZipCode" = 38654 AND "c"."ShippingAddress_Country_Code" = 'US' AND "c"."ShippingAddress_Country_FullName" = 'United States' +WHERE "c"."ShippingAddress_AddressLine1" = '804 S. Lakeshore Road' AND "c"."ShippingAddress_AddressLine2" IS NULL AND "c"."ShippingAddress_Tags" = '["foo","bar"]' AND "c"."ShippingAddress_ZipCode" = 38654 AND "c"."ShippingAddress_Country_Code" = 'US' AND "c"."ShippingAddress_Country_FullName" = 'United States' """); } @@ -230,13 +230,14 @@ public override async Task Complex_type_equals_parameter(bool async) AssertSql( """ @__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' (Size = 21) +@__entity_equality_address_0_Tags='["foo","bar"]' (Size = 13) @__entity_equality_address_0_ZipCode='38654' (Nullable = true) @__entity_equality_address_0_Code='US' (Size = 2) @__entity_equality_address_0_FullName='United States' (Size = 13) -SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" -WHERE "c"."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND "c"."ShippingAddress_AddressLine2" IS NULL AND "c"."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND "c"."ShippingAddress_Country_Code" = @__entity_equality_address_0_Code AND "c"."ShippingAddress_Country_FullName" = @__entity_equality_address_0_FullName +WHERE "c"."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND "c"."ShippingAddress_AddressLine2" IS NULL AND "c"."ShippingAddress_Tags" = @__entity_equality_address_0_Tags AND "c"."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND "c"."ShippingAddress_Country_Code" = @__entity_equality_address_0_Code AND "c"."ShippingAddress_Country_FullName" = @__entity_equality_address_0_FullName """); } @@ -261,16 +262,17 @@ public override async Task Contains_over_complex_type(bool async) AssertSql( """ @__entity_equality_address_0_AddressLine1='804 S. Lakeshore Road' (Size = 21) +@__entity_equality_address_0_Tags='["foo","bar"]' (Size = 13) @__entity_equality_address_0_ZipCode='38654' (Nullable = true) @__entity_equality_address_0_Code='US' (Size = 2) @__entity_equality_address_0_FullName='United States' (Size = 13) -SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" WHERE EXISTS ( SELECT 1 FROM "Customer" AS "c0" - WHERE "c0"."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND "c0"."ShippingAddress_AddressLine2" IS NULL AND "c0"."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND "c0"."ShippingAddress_Country_Code" = @__entity_equality_address_0_Code AND "c0"."ShippingAddress_Country_FullName" = @__entity_equality_address_0_FullName) + WHERE "c0"."ShippingAddress_AddressLine1" = @__entity_equality_address_0_AddressLine1 AND "c0"."ShippingAddress_AddressLine2" IS NULL AND "c0"."ShippingAddress_Tags" = @__entity_equality_address_0_Tags AND "c0"."ShippingAddress_ZipCode" = @__entity_equality_address_0_ZipCode AND "c0"."ShippingAddress_Country_Code" = @__entity_equality_address_0_Code AND "c0"."ShippingAddress_Country_FullName" = @__entity_equality_address_0_FullName) """); } @@ -280,11 +282,11 @@ public override async Task Concat_complex_type(bool async) AssertSql( """ -SELECT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" WHERE "c"."Id" = 1 UNION ALL -SELECT "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" +SELECT "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_Tags", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" FROM "Customer" AS "c0" WHERE "c0"."Id" = 2 """); @@ -296,11 +298,11 @@ public override async Task Concat_entity_type_containing_complex_property(bool a AssertSql( """ -SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" WHERE "c"."Id" = 1 UNION ALL -SELECT "c0"."Id", "c0"."Name", "c0"."BillingAddress_AddressLine1", "c0"."BillingAddress_AddressLine2", "c0"."BillingAddress_ZipCode", "c0"."BillingAddress_Country_Code", "c0"."BillingAddress_Country_FullName", "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" +SELECT "c0"."Id", "c0"."Name", "c0"."BillingAddress_AddressLine1", "c0"."BillingAddress_AddressLine2", "c0"."BillingAddress_Tags", "c0"."BillingAddress_ZipCode", "c0"."BillingAddress_Country_Code", "c0"."BillingAddress_Country_FullName", "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_Tags", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" FROM "Customer" AS "c0" WHERE "c0"."Id" = 2 """); @@ -312,11 +314,11 @@ public override async Task Union_entity_type_containing_complex_property(bool as AssertSql( """ -SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."Id", "c"."Name", "c"."BillingAddress_AddressLine1", "c"."BillingAddress_AddressLine2", "c"."BillingAddress_Tags", "c"."BillingAddress_ZipCode", "c"."BillingAddress_Country_Code", "c"."BillingAddress_Country_FullName", "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" WHERE "c"."Id" = 1 UNION -SELECT "c0"."Id", "c0"."Name", "c0"."BillingAddress_AddressLine1", "c0"."BillingAddress_AddressLine2", "c0"."BillingAddress_ZipCode", "c0"."BillingAddress_Country_Code", "c0"."BillingAddress_Country_FullName", "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" +SELECT "c0"."Id", "c0"."Name", "c0"."BillingAddress_AddressLine1", "c0"."BillingAddress_AddressLine2", "c0"."BillingAddress_Tags", "c0"."BillingAddress_ZipCode", "c0"."BillingAddress_Country_Code", "c0"."BillingAddress_Country_FullName", "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_Tags", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" FROM "Customer" AS "c0" WHERE "c0"."Id" = 2 """); @@ -328,11 +330,11 @@ public override async Task Union_complex_type(bool async) AssertSql( """ -SELECT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" +SELECT "c"."ShippingAddress_AddressLine1", "c"."ShippingAddress_AddressLine2", "c"."ShippingAddress_Tags", "c"."ShippingAddress_ZipCode", "c"."ShippingAddress_Country_Code", "c"."ShippingAddress_Country_FullName" FROM "Customer" AS "c" WHERE "c"."Id" = 1 UNION -SELECT "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" +SELECT "c0"."ShippingAddress_AddressLine1", "c0"."ShippingAddress_AddressLine2", "c0"."ShippingAddress_Tags", "c0"."ShippingAddress_ZipCode", "c0"."ShippingAddress_Country_Code", "c0"."ShippingAddress_Country_FullName" FROM "Customer" AS "c0" WHERE "c0"."Id" = 2 """);