From 227fa59b14e7f2ae78ec261a2da167a62b680a85 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 22 Jun 2024 11:15:26 +0200 Subject: [PATCH] Cosmos: Fixes around array projection Closes #33797 --- .../CosmosShapedQueryExpressionExtensions.cs | 3 +- ...osmosProjectionBindingExpressionVisitor.cs | 54 +++++++- ...yableMethodTranslatingExpressionVisitor.cs | 29 ++-- .../Expressions/ScalarAccessExpression.cs | 1 + .../Internal/Expressions/SelectExpression.cs | 20 +-- .../Query/Internal/SqlExpressionFactory.cs | 4 +- .../Query/OwnedQueryCosmosTest.cs | 20 +-- .../PrimitiveCollectionsQueryCosmosTest.cs | 129 ++++++++++++------ .../PrimitiveCollectionsQueryTestBase.cs | 3 +- 9 files changed, 174 insertions(+), 89 deletions(-) diff --git a/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs b/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs index ec0f6ea14a8..14f996cb1fe 100644 --- a/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs @@ -63,8 +63,7 @@ public static bool TryConvertToArray( { subquery.ApplyProjection(); - // TODO: Should the type be an array, or enumerable/queryable? - var arrayClrType = projection.Type.MakeArrayType(); + var arrayClrType = typeof(IEnumerable<>).MakeGenericType(projection.Type); switch (projection) { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs index 345e5254253..8013c163892 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs @@ -5,6 +5,8 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -21,7 +23,9 @@ private static readonly MethodInfo GetParameterValueMethodInfo = typeof(CosmosProjectionBindingExpressionVisitor) .GetTypeInfo().GetDeclaredMethod(nameof(GetParameterValue))!; + private readonly CosmosQueryableMethodTranslatingExpressionVisitor _queryableMethodTranslatingExpressionVisitor; private readonly CosmosSqlTranslatingExpressionVisitor _sqlTranslator; + private readonly ITypeMappingSource _typeMappingSource; private readonly IModel _model; private SelectExpression _selectExpression; private bool _clientEval; @@ -39,10 +43,14 @@ private static readonly MethodInfo GetParameterValueMethodInfo /// public CosmosProjectionBindingExpressionVisitor( IModel model, - CosmosSqlTranslatingExpressionVisitor sqlTranslator) + CosmosQueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor, + CosmosSqlTranslatingExpressionVisitor sqlTranslator, + ITypeMappingSource typeMappingSource) { _model = model; + _queryableMethodTranslatingExpressionVisitor = queryableMethodTranslatingExpressionVisitor; _sqlTranslator = sqlTranslator; + _typeMappingSource = typeMappingSource; _selectExpression = null!; } @@ -570,6 +578,50 @@ UnaryExpression unaryExpression lambda); } } + else if (method is { Name: nameof(Enumerable.ToList), IsGenericMethod: true } + && method.DeclaringType == typeof(Enumerable) + && methodCallExpression.Arguments is [var argument] + && argument.Type.TryGetElementType(typeof(IQueryable<>)) != null) + { + if (_queryableMethodTranslatingExpressionVisitor.TranslateSubquery(argument) is not ShapedQueryExpression subquery + || !subquery.TryConvertToArray(_typeMappingSource, out var array)) + { + throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())); + } + + // If ToList() was composed over a subquery with operators, the result here is an ArrayExpression (ARRAY(SELECT ...)), whose + // CLR Type is IEnumerable. This can be directly used in the resulting ProjectingBindingExpression - the shaper will + // simply read the JSON results out successfully. + // But if ToList() is composed directly over an array property, that property could have type e.g. T[], which will be read + // in the shaper, and then the cast from T[] to List will fail. As a result, wrap the array in an additional + // "reprojection" subquery, effectively to change the CLR type. + if (array is SqlExpression scalarArray + && !(array.Type.IsGenericType && array.Type.GetGenericTypeDefinition() == typeof(IEnumerable<>))) + { + Check.DebugAssert( + array is not ScalarArrayExpression and not ObjectArrayExpression, "ArrayExpression should be IEnumerable"); + + if (scalarArray is not { TypeMapping.ElementTypeMapping: CosmosTypeMapping elementTypeMapping }) + { + throw new UnreachableException("Scalar array with no element type mapping"); + } + + // TODO: Proper alias management (#33894). + var arrayReprojectionSubquery = SelectExpression.CreateForCollection( + array, "i", new ScalarReferenceExpression("i", elementTypeMapping.ClrType, elementTypeMapping)); + arrayReprojectionSubquery.ApplyProjection(); + + array = new ScalarArrayExpression( + arrayReprojectionSubquery, + methodCallExpression.Type, // List<> + _typeMappingSource.FindMapping(methodCallExpression.Type, _model, elementTypeMapping)); + } + + return new ProjectionBindingExpression( + _selectExpression, + _selectExpression.AddToProjection(array), + methodCallExpression.Type); + } } var @object = Visit(methodCallExpression.Object); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index e9e17ca9a40..3ba1654c57e 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -54,7 +55,7 @@ public CosmosQueryableMethodTranslatingExpressionVisitor( _methodCallTranslatorProvider, this); _projectionBindingExpressionVisitor = - new CosmosProjectionBindingExpressionVisitor(_queryCompilationContext.Model, _sqlTranslator); + new CosmosProjectionBindingExpressionVisitor(_queryCompilationContext.Model, this, _sqlTranslator, _typeMappingSource); _subquery = false; } @@ -81,7 +82,7 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor( _methodCallTranslatorProvider, parentVisitor); _projectionBindingExpressionVisitor = - new CosmosProjectionBindingExpressionVisitor(_queryCompilationContext.Model, _sqlTranslator); + new CosmosProjectionBindingExpressionVisitor(_queryCompilationContext.Model, this, _sqlTranslator, _typeMappingSource); _subquery = true; } @@ -1131,8 +1132,10 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s // ElementAtOrDefault over an array of scalars case SqlExpression scalarArray when projection is SqlExpression element: { - var slice = _sqlExpressionFactory.Function( - "ARRAY_SLICE", [scalarArray, translatedCount], scalarArray.Type, scalarArray.TypeMapping); + var arrayType = typeof(IEnumerable<>).MakeGenericType(projection.Type); + var arrayTypeMapping = _typeMappingSource.FindMapping(arrayType, _queryCompilationContext.Model, element.TypeMapping); + + var slice = _sqlExpressionFactory.Function("ARRAY_SLICE", [scalarArray, translatedCount], arrayType, arrayTypeMapping); // TODO: Proper alias management (#33894). Ideally reach into the source of the original SelectExpression and use that alias. var translatedSelect = SelectExpression.CreateForCollection( @@ -1145,8 +1148,10 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s // ElementAtOrDefault over an array os structural types case not null when projectedStructuralTypeShaper is not null: { + var arrayType = typeof(IEnumerable<>).MakeGenericType(projectedStructuralTypeShaper.Type); + // TODO: Proper alias management (#33894). - var slice = new ObjectFunctionExpression("ARRAY_SLICE", [array, translatedCount], projectedStructuralTypeShaper.Type); + var slice = new ObjectFunctionExpression("ARRAY_SLICE", [array, translatedCount], arrayType); var translatedSelect = SelectExpression.CreateForCollection( slice, "i", @@ -1591,7 +1596,7 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model, // value conversion). #34026. var elementClrType = inlineQueryRootExpression.ElementType; var elementTypeMapping = _typeMappingSource.FindMapping(elementClrType)!; - var arrayTypeMapping = _typeMappingSource.FindMapping(elementClrType.MakeArrayType()); // TODO: IEnumerable? + var arrayTypeMapping = _typeMappingSource.FindMapping(typeof(IEnumerable<>).MakeGenericType(elementClrType)); var inlineArray = new ArrayConstantExpression(elementClrType, translatedItems, arrayTypeMapping); // TODO: Do proper alias management: #33894 @@ -1620,7 +1625,7 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model, // TODO: Temporary hack - need to perform proper derivation of the array type mapping from the element (e.g. for // value conversion). #34026. var elementClrType = parameterQueryRootExpression.ElementType; - var arrayTypeMapping = _typeMappingSource.FindMapping(elementClrType.MakeArrayType()); // TODO: IEnumerable? + var arrayTypeMapping = _typeMappingSource.FindMapping(typeof(IEnumerable<>).MakeGenericType(elementClrType)); var elementTypeMapping = _typeMappingSource.FindMapping(elementClrType)!; var sqlParameterExpression = new SqlParameterExpression(parameterQueryRootExpression.ParameterExpression, arrayTypeMapping); @@ -1689,13 +1694,17 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model, && source2.TryConvertToArray(_typeMappingSource, out var array2, out var projection2, ignoreOrderings) && projection1.Type == projection2.Type) { + var arrayType = typeof(IEnumerable<>).MakeGenericType(projection1.Type); + // Set operation over arrays of scalars if (projection1 is SqlExpression sqlProjection1 && projection2 is SqlExpression sqlProjection2 - && (sqlProjection1.TypeMapping ?? sqlProjection2.TypeMapping) is CoreTypeMapping typeMapping) + && (sqlProjection1.TypeMapping ?? sqlProjection2.TypeMapping) is CosmosTypeMapping typeMapping) { + var arrayTypeMapping = _typeMappingSource.FindMapping(arrayType, _queryCompilationContext.Model, typeMapping); + // TODO: Proper alias management (#33894). - var translation = _sqlExpressionFactory.Function(functionName, [array1, array2], projection1.Type, typeMapping); + var translation = _sqlExpressionFactory.Function(functionName, [array1, array2], arrayType, arrayTypeMapping); var select = SelectExpression.CreateForCollection( translation, "i", new ScalarReferenceExpression("i", projection1.Type, typeMapping)); return source1.UpdateQueryExpression(select); @@ -1707,7 +1716,7 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model, && structuralType1 == structuralType2) { // TODO: Proper alias management (#33894). - var translation = new ObjectFunctionExpression(functionName, [array1, array2], projection1.Type); + var translation = new ObjectFunctionExpression(functionName, [array1, array2], arrayType); var select = SelectExpression.CreateForCollection( translation, "i", new ObjectReferenceExpression((IEntityType)structuralType1, "i")); return CreateShapedQueryExpression(select, structuralType1.ClrType); diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarAccessExpression.cs index 28fefbce731..23df20780ea 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarAccessExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ScalarAccessExpression.cs @@ -13,6 +13,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] public class ScalarAccessExpression(Expression @object, string propertyName, Type clrType, CoreTypeMapping? typeMapping) : SqlExpression(clrType, typeMapping), IAccessExpression { diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs index ad3505121cf..56a96abf42e 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs @@ -322,27 +322,9 @@ public virtual void ReplaceProjectionMapping(IDictionary - public virtual int AddToProjection(SqlExpression sqlExpression) + public virtual int AddToProjection(Expression sqlExpression) => AddToProjection(sqlExpression, null); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual int AddToProjection(EntityProjectionExpression entityProjection) - => AddToProjection(entityProjection, null); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual int AddToProjection(ObjectArrayAccessExpression objectArrayAccess) - => AddToProjection(objectArrayAccess, null); - private int AddToProjection(Expression expression, string? alias) { var existingIndex = _projection.FindIndex(pe => pe.Expression.Equals(expression)); diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs index d3b6b7035bd..a47640efdd1 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs @@ -166,7 +166,7 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( // TODO: This infers based on the CLR type; need to properly infer based on the element type mapping // TODO: being applied here (e.g. WHERE @p[1] = c.PropertyWithValueConverter). #34026 var arrayTypeMapping = left.TypeMapping - ?? (typeMapping is null ? null : typeMappingSource.FindMapping(typeMapping.ClrType.MakeArrayType())); + ?? (typeMapping is null ? null : typeMappingSource.FindMapping(typeof(IEnumerable<>).MakeGenericType(typeMapping.ClrType))); return new SqlBinaryExpression( ExpressionType.ArrayIndex, ApplyTypeMapping(left, arrayTypeMapping), @@ -291,7 +291,7 @@ private InExpression ApplyTypeMappingOnIn(InExpression inExpression) var arrayClrType = arrayExpression.Type switch { var t when t.TryGetSequenceType() != typeof(object) => t, - { IsArray: true } => itemExpression.Type.MakeArrayType(), + { IsArray: true } => typeof(IEnumerable<>).MakeGenericType(itemExpression.Type), { IsConstructedGenericType: true, GenericTypeArguments.Length: 1 } t => t.GetGenericTypeDefinition().MakeGenericType(itemExpression.Type), _ => throw new InvalidOperationException( diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs index c52a3479aad..befaeddd1ba 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs @@ -373,8 +373,8 @@ public override Task Where_owned_collection_navigation_ToList_Count(bool async) async, async a => { // TODO: #34011 - // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted - // as null instead of []. + // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets + // persisted as null instead of [] when there are no Details. So we change the Count we check to 1. await AssertQuery( a, ss => ss.Set() @@ -388,7 +388,7 @@ await AssertQuery( // TODO: The following should project out a["Details"], not a: #34067 AssertSql( """ -SELECT a +SELECT a["Details"] FROM root c JOIN a IN c["Orders"] WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1)) @@ -401,8 +401,8 @@ public override Task Where_collection_navigation_ToArray_Count(bool async) async, async a => { // TODO: #34011 - // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted - // as null instead of []. + // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets + // persisted as null instead of [] when there are no Details. So we change the Count we check to 1. await AssertQuery( a, ss => ss.Set() @@ -428,8 +428,8 @@ public override Task Where_collection_navigation_AsEnumerable_Count(bool async) async, async a => { // TODO: #34011 - // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted - // as null instead of []. + // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets + // persisted as null instead of [] when there are no Details. So we change the Count we check to 1. await AssertQuery( a, ss => ss.Set() @@ -455,8 +455,8 @@ public override Task Where_collection_navigation_ToList_Count_member(bool async) async, async a => { // TODO: #34011 - // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted - // as null instead of []. + // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets + // persisted as null instead of [] when there are no Details. So we change the Count we check to 1. await AssertQuery( a, ss => ss.Set() @@ -469,7 +469,7 @@ await AssertQuery( AssertSql( """ -SELECT a +SELECT a["Details"] FROM root c JOIN a IN c["Orders"] WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1)) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 9e2864cb8a7..7f65fc88cd7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -1647,15 +1647,23 @@ ORDER BY c["Id"] } } - // TODO: Project out primitive collection subquery: #33797 - public override async Task Project_collection_of_datetimes_filtered(bool async) - { - // Always throws for sync. - if (async) - { - await Assert.ThrowsAsync(() => base.Project_collection_of_datetimes_filtered(async)); - } - } + public override Task Project_collection_of_datetimes_filtered(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Project_collection_of_datetimes_filtered(a); + + AssertSql( + """ +SELECT ARRAY( + SELECT VALUE i + FROM i IN c["DateTimes"] + WHERE (DateTimePart("dd", i) != 1)) AS c +FROM root c +WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity") +ORDER BY c["Id"] +"""); + }); public override async Task Project_collection_of_nullable_ints_with_paging(bool async) { @@ -1693,15 +1701,22 @@ await CosmosTestHelpers.Instance.NoSyncTest( AssertSql(); } - // TODO: Project out primitive collection subquery: #33797 - public override async Task Project_collection_of_ints_with_distinct(bool async) - { - // Always throws for sync. - if (async) - { - await Assert.ThrowsAsync(() => base.Project_collection_of_ints_with_distinct(async)); - } - } + public override Task Project_collection_of_ints_with_distinct(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Project_collection_of_ints_with_distinct(a); + + AssertSql( + """ +SELECT ARRAY( + SELECT DISTINCT VALUE i + FROM i IN c["Ints"]) AS c +FROM root c +WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity") +ORDER BY c["Id"] +"""); + }); public override Task Project_collection_of_nullable_ints_with_distinct(bool async) => CosmosTestHelpers.Instance.NoSyncTest( @@ -1717,26 +1732,49 @@ FROM root c """); }); - // TODO: Project out primitive collection subquery: #33797 - public override async Task Project_collection_of_ints_with_ToList_and_FirstOrDefault(bool async) - { - // Always throws for sync. - if (async) - { - await Assert.ThrowsAsync(() => base.Project_collection_of_ints_with_ToList_and_FirstOrDefault(async)); - } - } + public override Task Project_collection_of_ints_with_ToList_and_FirstOrDefault(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Project_collection_of_ints_with_ToList_and_FirstOrDefault(a); - // TODO: Project out primitive collection subquery: #33797 - public override async Task Project_empty_collection_of_nullables_and_collection_only_containing_nulls(bool async) - { - // Always throws for sync. - if (async) - { - await Assert.ThrowsAsync( - () => base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(async)); - } - } + // TODO: Improve SQL, #34081 + AssertSql( + """ +SELECT ARRAY( + SELECT VALUE i + FROM i IN c["Ints"]) AS c +FROM root c +WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity") +ORDER BY c["Id"] +OFFSET 0 LIMIT 1 +"""); + }); + + public override Task Project_empty_collection_of_nullables_and_collection_only_containing_nulls(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(a); + + AssertSql( + """ +SELECT VALUE +{ + "c" : ARRAY( + SELECT VALUE i + FROM i IN c["NullableInts"] + WHERE false), + "c0" : ARRAY( + SELECT VALUE i + FROM i IN c["NullableInts"] + WHERE (i = null)) +} +FROM root c +WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity") +ORDER BY c["Id"] +"""); + }); public override async Task Project_multiple_collections(bool async) { @@ -1747,20 +1785,23 @@ public override async Task Project_multiple_collections(bool async) Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); + // TODO: Improve SQL, #34081 AssertSql( """ SELECT VALUE { - "Ints" : c["Ints"], "c" : ARRAY( + SELECT VALUE i + FROM i IN c["Ints"]), + "c0" : ARRAY( SELECT VALUE i FROM i IN c["Ints"] ORDER BY i DESC), - "c0" : ARRAY( + "c1" : ARRAY( SELECT VALUE i FROM i IN c["DateTimes"] WHERE (DateTimePart("dd", i) != 1)), - "c1" : ARRAY( + "c2" : ARRAY( SELECT VALUE i FROM i IN c["DateTimes"] WHERE (i > "2000-01-01T00:00:00")) @@ -1807,23 +1848,23 @@ FROM root c """); }); + // Non-correlated queries not supported by Cosmos public override async Task Project_inline_collection_with_Union(bool async) { // Always throws for sync. if (async) { - // TODO: Project out primitive collection subquery: #33797 - await Assert.ThrowsAsync(() => base.Project_inline_collection_with_Union(async)); + await AssertTranslationFailed(() => base.Project_inline_collection_with_Union(async)); } } + // Non-correlated queries not supported by Cosmos public override async Task Project_inline_collection_with_Concat(bool async) { // Always throws for sync. if (async) { - // TODO: Project out primitive collection subquery: #33797 - await Assert.ThrowsAsync(() => base.Project_inline_collection_with_Concat(async)); + await AssertTranslationFailed(() => base.Project_inline_collection_with_Concat(async)); } } diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 45490365abd..d5ca1613162 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -1068,7 +1068,8 @@ public virtual Task Project_empty_collection_of_nullables_and_collection_only_co ss => ss.Set().OrderBy(x => x.Id).Select( x => new { - Empty = x.NullableInts.Where(x => false).ToList(), OnlyNull = x.NullableInts.Where(x => x == null).ToList(), + Empty = x.NullableInts.Where(x => false).ToList(), + OnlyNull = x.NullableInts.Where(x => x == null).ToList(), }), assertOrder: true, elementAsserter: (e, a) =>