From 44fbabf3d6ff1331c34d3694399012ca45ffccf4 Mon Sep 17 00:00:00 2001 From: maumar Date: Wed, 12 Jul 2023 16:00:13 -0700 Subject: [PATCH] Fix to #30922 - Most cases of projecting a primitive collection don't work Problem was that for cases that compose on the collection of primitives and then project it, we need identifier to properly bucket the results. On SqlServer we can use hidden key column that is a result of OPENJSON - we need something that is unique so we can't use the value itself. Fix is to add identifier to the SelectExpression based on OPENJSON and also modify the logic that figures out if we need key (and thus need to revert to OPENJSON without WITH clause). Also some minor fixes around nullability - when projecting element of a collection of primitives using OUTER APPLY/JOIN we must set the projection binding to nullable, as the value can always be null, in case of empty collection. Fixes #30922 --- .../Properties/RelationalStrings.Designer.cs | 8 + .../Properties/RelationalStrings.resx | 3 + .../Query/SqlExpressions/SelectExpression.cs | 55 +++- .../SqlServerQueryTranslationPostprocessor.cs | 151 +++++++---- ...yableMethodTranslatingExpressionVisitor.cs | 21 +- ...yableMethodTranslatingExpressionVisitor.cs | 11 +- .../PrimitiveCollectionsQueryTestBase.cs | 247 ++++++++++++++++-- ...dPrimitiveCollectionsQuerySqlServerTest.cs | 28 +- ...imitiveCollectionsQueryOldSqlServerTest.cs | 87 ++++++ .../PrimitiveCollectionsQuerySqlServerTest.cs | 236 ++++++++++++++++- .../PrimitiveCollectionsQuerySqliteTest.cs | 142 +++++++++- 11 files changed, 882 insertions(+), 107 deletions(-) diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 9b91b2a47f5..3ccac52ced7 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1515,6 +1515,14 @@ public static string SetOperationsNotAllowedAfterClientEvaluation public static string SetOperationsOnDifferentStoreTypes => GetString("SetOperationsOnDifferentStoreTypes"); + /// + /// A set operation 'setOperationType' requires valid type mapping for at least one of its sides. + /// + public static string SetOperationsRequireAtLeastOneSideWithValidTypeMapping(object? setOperationType) + => string.Format( + GetString("SetOperationsRequireAtLeastOneSideWithValidTypeMapping", nameof(setOperationType)), + setOperationType); + /// /// The SetProperty<TProperty> method can only be used within 'ExecuteUpdate' method. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index c3569a0a2d1..3084affafe9 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -995,6 +995,9 @@ Unable to translate set operation when matching columns on both sides have different store types. + + A set operation 'setOperationType' requires valid type mapping for at least one of its sides. + The SetProperty<TProperty> method can only be used within 'ExecuteUpdate' method. diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 82bea2dd9c7..f746e53b096 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -101,20 +101,21 @@ internal SelectExpression(SqlExpression? projection) } /// - /// Creates a new instance of the class given a , with a single - /// column projection. + /// 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. /// - /// The table expression. - /// The name of the column to add as the projection. - /// The type of the column to add as the projection. - /// The type mapping of the column to add as the projection. - /// Whether the column projected out is nullable. + [EntityFrameworkInternal] public SelectExpression( TableExpressionBase tableExpression, string columnName, Type columnType, RelationalTypeMapping? columnTypeMapping, - bool? isColumnNullable = null) + bool? isColumnNullable = null, + string? identifierColumnName = null, + Type? identifierColumnType = null, + RelationalTypeMapping? identifierColumnTypeMapping = null) : base(null) { var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!); @@ -128,6 +129,24 @@ public SelectExpression( isColumnNullable ?? columnType.IsNullableType()); _projectionMapping[new ProjectionMember()] = columnExpression; + + if (identifierColumnName != null && identifierColumnType != null && identifierColumnTypeMapping != null) + { + var identifierColumn = new ConcreteColumnExpression( + identifierColumnName, + tableReferenceExpression, + identifierColumnType.UnwrapNullableType(), + identifierColumnTypeMapping, + identifierColumnType.IsNullableType()); + + _identifier.Add((identifierColumn, identifierColumnTypeMapping!.Comparer)); + } + else + { + Debug.Assert( + identifierColumnName == null && identifierColumnType == null && identifierColumnTypeMapping == null, + "Either provide all identity information (column name, type and type mapping), or don't provide any."); + } } internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpressionFactory) @@ -2205,7 +2224,7 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi : Array.Empty(); var entityProjectionIdentifiers = new List(); var entityProjectionValueComparers = new List(); - var otherExpressions = new List(); + var otherExpressions = new List<(SqlExpression Expression, ValueComparer Comparer)>(); // Push down into a subquery if limit/offset are defined. If not, any orderings can be discarded as set operations don't preserve // them. @@ -2308,7 +2327,19 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi } } - otherExpressions.Add(outerProjection); + // we need comparer (that we get from type mapping) for identifiers + // it may happen that one side of the set operation comes from collection parameter + // and therefore doesn't have type mapping (yet - we infer those after the translation is complete) + // but for set operation at least one side should have type mapping, otherwise whole thing would have been parameterized out + // this can only happen in compiled query, since we always parameterize parameters there - if this happens we throw + var outerTypeMapping = innerProjection1.Expression.TypeMapping ?? innerProjection2.Expression.TypeMapping; + if (outerTypeMapping == null) + { + throw new InvalidOperationException( + RelationalStrings.SetOperationsRequireAtLeastOneSideWithValidTypeMapping(setOperationType)); + } + + otherExpressions.Add((outerProjection, outerTypeMapping.KeyComparer)); } } @@ -2349,10 +2380,10 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi // If there are no other expressions then we can use all entityProjectionIdentifiers _identifier.AddRange(entityProjectionIdentifiers.Zip(entityProjectionValueComparers)); } - else if (otherExpressions.All(e => e is ColumnExpression)) + else if (otherExpressions.All(e => e.Expression is ColumnExpression)) { _identifier.AddRange(entityProjectionIdentifiers.Zip(entityProjectionValueComparers)); - _identifier.AddRange(otherExpressions.Select(e => ((ColumnExpression)e, e.TypeMapping!.KeyComparer))); + _identifier.AddRange(otherExpressions.Select(e => ((ColumnExpression)e.Expression, e.Comparer))); } } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs index 315b9679c68..5d86c0a7704 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.SqlServer.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -112,80 +114,129 @@ public Expression Process(Expression expression) case ShapedQueryExpression shapedQueryExpression: return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)); - case SelectExpression + case SelectExpression selectExpression: { - Tables: [SqlServerOpenJsonExpression { ColumnInfos: not null } openJsonExpression, ..], - Orderings: - [ + var newTables = default(TableExpressionBase[]); + var appliedCasts = new List<(SqlServerOpenJsonExpression, string)>(); + + for (var i = 0; i < selectExpression.Tables.Count; i++) + { + var table = selectExpression.Tables[i]; + if ((table is SqlServerOpenJsonExpression { ColumnInfos: not null } + or JoinExpressionBase { Table: SqlServerOpenJsonExpression { ColumnInfos: not null } }) + && selectExpression.Orderings.Select(o => o.Expression) + .Concat(selectExpression.Projection.Select(p => p.Expression)) + .Any(x => IsKeyColumn(x, table))) { - Expression: SqlUnaryExpression + // Remove the WITH clause from the OPENJSON expression + var openJsonExpression = (SqlServerOpenJsonExpression)((table as JoinExpressionBase)?.Table ?? table); + var newOpenJsonExpression = openJsonExpression.Update( + openJsonExpression.JsonExpression, + openJsonExpression.Path, + columnInfos: null); + + TableExpressionBase newTable = table switch + { + InnerJoinExpression ij => ij.Update(newOpenJsonExpression, ij.JoinPredicate), + LeftJoinExpression lj => lj.Update(newOpenJsonExpression, lj.JoinPredicate), + CrossJoinExpression cj => cj.Update(newOpenJsonExpression), + CrossApplyExpression ca => ca.Update(newOpenJsonExpression), + OuterApplyExpression oa => oa.Update(newOpenJsonExpression), + _ => newOpenJsonExpression, + }; + + if (newTables is not null) + { + newTables[i] = newTable; + } + else if (!table.Equals(newTable)) { - OperatorType: ExpressionType.Convert, - Operand: ColumnExpression { Name: "key", Table: var keyColumnTable } + newTables = new TableExpressionBase[selectExpression.Tables.Count]; + for (var j = 0; j < i; j++) + { + newTables[j] = selectExpression.Tables[j]; + } + + newTables[i] = newTable; } + + foreach (var column in openJsonExpression.ColumnInfos!) + { + var typeMapping = _typeMappingSource.FindMapping(column.StoreType); + Check.DebugAssert( + typeMapping is not null, + $"Could not find mapping for store type {column.StoreType} when converting OPENJSON/WITH"); + + // Binary data (varbinary) is stored in JSON as base64, which OPENJSON knows how to decode as long the type is + // specified in the WITH clause. We're now removing the WITH and applying a relational CAST, but that doesn't work + // for base64 data. + if (typeMapping is SqlServerByteArrayTypeMapping) + { + throw new InvalidOperationException(SqlServerStrings.QueryingOrderedBinaryJsonCollectionsNotSupported); + } + + _castsToApply.Add((newOpenJsonExpression, column.Name), typeMapping); + appliedCasts.Add((newOpenJsonExpression, column.Name)); + } + + continue; } - ] - } selectExpression - when keyColumnTable == openJsonExpression: - { - // Remove the WITH clause from the OPENJSON expression - var newOpenJsonExpression = openJsonExpression.Update( - openJsonExpression.JsonExpression, - openJsonExpression.Path, - columnInfos: null); - - var newTables = selectExpression.Tables.ToArray(); - newTables[0] = newOpenJsonExpression; - - var newSelectExpression = selectExpression.Update( - selectExpression.Projection, - newTables, - selectExpression.Predicate, - selectExpression.GroupBy, - selectExpression.Having, - selectExpression.Orderings, - selectExpression.Limit, - selectExpression.Offset); - // Record the OPENJSON expression and its projected column(s), along with the store type we just removed from the WITH - // clause. Then visit the select expression, adding a cast around the matching ColumnExpressions. - foreach (var column in openJsonExpression.ColumnInfos) - { - var typeMapping = _typeMappingSource.FindMapping(column.StoreType); - Check.DebugAssert( - typeMapping is not null, - $"Could not find mapping for store type {column.StoreType} when converting OPENJSON/WITH"); - - // Binary data (varbinary) is stored in JSON as base64, which OPENJSON knows how to decode as long the type is - // specified in the WITH clause. We're now removing the WITH and applying a relational CAST, but that doesn't work - // for base64 data. - if (typeMapping is SqlServerByteArrayTypeMapping) + if (newTables is not null) { - throw new InvalidOperationException(SqlServerStrings.QueryingOrderedBinaryJsonCollectionsNotSupported); + newTables[i] = table; } - - _castsToApply.Add((newOpenJsonExpression, column.Name), typeMapping); } + // SelectExpression.Update always creates a new instance - we should avoid it when tables haven't changed + // see #31276 + var newSelectExpression = newTables is not null + ? selectExpression.Update( + selectExpression.Projection, + newTables, + selectExpression.Predicate, + selectExpression.GroupBy, + selectExpression.Having, + selectExpression.Orderings, + selectExpression.Limit, + selectExpression.Offset) + : selectExpression; + + // Record the OPENJSON expression and its projected column(s), along with the store type we just removed from the WITH + // clause. Then visit the select expression, adding a cast around the matching ColumnExpressions. var result = base.Visit(newSelectExpression); - foreach (var column in openJsonExpression.ColumnInfos) + foreach (var appliedCast in appliedCasts) { - _castsToApply.Remove((newOpenJsonExpression, column.Name)); + _castsToApply.Remove(appliedCast); } return result; } - case ColumnExpression { Table: SqlServerOpenJsonExpression openJsonTable, Name: var name } columnExpression - when _castsToApply.TryGetValue((openJsonTable, name), out var typeMapping): + case ColumnExpression columnExpression: { - return _sqlExpressionFactory.Convert(columnExpression, columnExpression.Type, typeMapping); + return columnExpression.Table switch + { + SqlServerOpenJsonExpression openJsonTable + when _castsToApply.TryGetValue((openJsonTable, columnExpression.Name), out var typeMapping) + => _sqlExpressionFactory.Convert(columnExpression, columnExpression.Type, typeMapping), + JoinExpressionBase { Table: SqlServerOpenJsonExpression innerOpenJsonTable } + when _castsToApply.TryGetValue((innerOpenJsonTable, columnExpression.Name), out var innerTypeMapping) + => _sqlExpressionFactory.Convert(columnExpression, columnExpression.Type, innerTypeMapping), + _ => base.Visit(expression) + }; } default: return base.Visit(expression); } + + static bool IsKeyColumn(SqlExpression sqlExpression, TableExpressionBase table) + => (sqlExpression is ColumnExpression { Name: "key", Table: var keyColumnTable } + && keyColumnTable == table) + || (sqlExpression is SqlUnaryExpression { OperatorType: ExpressionType.Convert, + Operand: SqlExpression operand } && IsKeyColumn(operand, table)); } } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs index e74f28ec782..8223af7a1a2 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs @@ -161,8 +161,17 @@ protected override Expression VisitExtension(Expression extensionExpression) var elementClrType = sqlExpression.Type.GetSequenceType(); var isColumnNullable = elementClrType.IsNullableType(); +#pragma warning disable EF1001 // Internal EF Core API usage. var selectExpression = new SelectExpression( - openJsonExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping, isColumnNullable); + openJsonExpression, + columnName: "value", + columnType: elementClrType, + columnTypeMapping: elementTypeMapping, + isColumnNullable, + identifierColumnName: "key", + identifierColumnType: typeof(string), + identifierColumnTypeMapping: _typeMappingSource.FindMapping("nvarchar(4000)")); +#pragma warning restore EF1001 // Internal EF Core API usage. // OPENJSON doesn't guarantee the ordering of the elements coming out; when using OPENJSON without WITH, a [key] column is returned // with the JSON array's ordering, which we can ORDER BY; this option doesn't exist with OPENJSON with WITH, unfortunately. @@ -186,7 +195,15 @@ protected override Expression VisitExtension(Expression extensionExpression) _typeMappingSource.FindMapping(typeof(int))), ascending: true)); - var shaperExpression = new ProjectionBindingExpression(selectExpression, new ProjectionMember(), elementClrType); + var shaperExpression = (Expression)new ProjectionBindingExpression(selectExpression, new ProjectionMember(), elementClrType.MakeNullable()); + if (shaperExpression.Type != elementClrType) + { + Check.DebugAssert( + elementClrType.MakeNullable() == shaperExpression.Type, + "expression.Type must be nullable of targetType"); + + shaperExpression = Expression.Convert(shaperExpression, elementClrType); + } return new ShapedQueryExpression(selectExpression, shaperExpression); } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs index 8b1dc22ae41..e13d8407632 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs @@ -220,8 +220,17 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis // TODO: This is a temporary CLR type-based check; when we have proper metadata to determine if the element is nullable, use it here var isColumnNullable = elementClrType.IsNullableType(); +#pragma warning disable EF1001 // Internal EF Core API usage. var selectExpression = new SelectExpression( - jsonEachExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping, isColumnNullable); + jsonEachExpression, + columnName: "value", + columnType: elementClrType, + columnTypeMapping: elementTypeMapping, + isColumnNullable, + identifierColumnName: "key", + identifierColumnType: typeof(int), + identifierColumnTypeMapping: _typeMappingSource.FindMapping(typeof(int))); +#pragma warning restore EF1001 // Internal EF Core API usage. // If we have a collection column, we know the type mapping at this point (as opposed to parameters, whose type mapping will get // inferred later based on usage in SqliteInferredTypeMappingApplier); we should be able to apply any SQL logic needed to convert diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index b1d6d437487..f4bdaa5e302 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Diagnostics; + namespace Microsoft.EntityFrameworkCore.Query; public abstract class PrimitiveCollectionsQueryTestBase : QueryTestBase @@ -33,7 +35,7 @@ public virtual Task Inline_collection_of_nullable_ints_Contains_null(bool async) => AssertQuery( async, ss => ss.Set().Where(c => new int?[] { null, 999 }.Contains(c.NullableInt)), - entryCount: 2); + entryCount: 3); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -58,7 +60,7 @@ public virtual Task Inline_collection_Count_with_two_values(bool async) => AssertQuery( async, ss => ss.Set().Where(c => new[] { 2, 999 }.Count(i => i > c.Id) == 1), - entryCount: 2); + entryCount: 4); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -66,7 +68,7 @@ public virtual Task Inline_collection_Count_with_three_values(bool async) => AssertQuery( async, ss => ss.Set().Where(c => new[] { 2, 999, 1000 }.Count(i => i > c.Id) == 2), - entryCount: 2); + entryCount: 4); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -153,7 +155,7 @@ public virtual Task Inline_collection_negated_Contains_as_All(bool async) => AssertQuery( async, ss => ss.Set().Where(c => new[] { 2, 999 }.All(i => i != c.Id)), - entryCount: 2); + entryCount: 4); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -164,7 +166,7 @@ public virtual Task Parameter_collection_Count(bool async) return AssertQuery( async, ss => ss.Set().Where(c => ids.Count(i => i > c.Id) == 1), - entryCount: 2); + entryCount: 4); } [ConditionalTheory] @@ -200,7 +202,7 @@ public virtual Task Parameter_collection_of_nullable_ints_Contains_nullable_int( return AssertQuery( async, ss => ss.Set().Where(c => nullableInts.Contains(c.NullableInt)), - entryCount: 2); + entryCount: 3); } [ConditionalTheory] @@ -240,7 +242,7 @@ public virtual Task Parameter_collection_of_bools_Contains(bool async) return AssertQuery( async, ss => ss.Set().Where(c => bools.Contains(c.Bool)), - entryCount: 1); + entryCount: 2); } [ConditionalTheory] @@ -252,7 +254,7 @@ public virtual Task Parameter_collection_of_enums_Contains(bool async) return AssertQuery( async, ss => ss.Set().Where(c => enums.Contains(c.Enum)), - entryCount: 2); + entryCount: 3); } [ConditionalTheory] @@ -273,7 +275,7 @@ public virtual Task Column_collection_of_ints_Contains(bool async) => AssertQuery( async, ss => ss.Set().Where(c => c.Ints.Contains(10)), - entryCount: 1); + entryCount: 2); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -281,7 +283,7 @@ public virtual Task Column_collection_of_nullable_ints_Contains(bool async) => AssertQuery( async, ss => ss.Set().Where(c => c.NullableInts.Contains(10)), - entryCount: 1); + entryCount: 2); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -289,7 +291,7 @@ public virtual Task Column_collection_of_nullable_ints_Contains_null(bool async) => AssertQuery( async, ss => ss.Set().Where(c => c.NullableInts.Contains(null)), - entryCount: 1); + entryCount: 3); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -305,7 +307,7 @@ public virtual Task Column_collection_of_bools_Contains(bool async) => AssertQuery( async, ss => ss.Set().Where(c => c.Bools.Contains(true)), - entryCount: 1); + entryCount: 2); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -340,7 +342,7 @@ public virtual Task Column_collection_index_string(bool async) async, ss => ss.Set().Where(c => c.Strings[1] == "10"), ss => ss.Set().Where(c => (c.Strings.Length >= 2 ? c.Strings[1] : "-1") == "10"), - entryCount: 1); + entryCount: 2); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -351,7 +353,7 @@ public virtual Task Column_collection_index_datetime(bool async) c => c.DateTimes[1] == new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc)), ss => ss.Set().Where( c => (c.DateTimes.Length >= 2 ? c.DateTimes[1] : default) == new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc)), - entryCount: 1); + entryCount: 2); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -443,7 +445,7 @@ public virtual Task Column_collection_OrderByDescending_ElementAt(bool async) .Where(c => c.Ints.OrderByDescending(i => i).ElementAt(0) == 111), ss => ss.Set() .Where(c => c.Ints.Length > 0 && c.Ints.OrderByDescending(i => i).ElementAt(0) == 111), - entryCount: 1); + entryCount: 2); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -451,7 +453,7 @@ public virtual Task Column_collection_Any(bool async) => AssertQuery( async, ss => ss.Set().Where(c => c.Ints.Any()), - entryCount: 2); + entryCount: 4); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -514,7 +516,7 @@ public virtual Task Column_collection_Intersect_inline_collection(bool async) => AssertQuery( async, ss => ss.Set().Where(c => c.Ints.Intersect(new[] { 11, 111 }).Count() == 2), - entryCount: 1); + entryCount: 2); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -525,7 +527,7 @@ public virtual Task Inline_collection_Except_column_collection(bool async) async, ss => ss.Set().Where( c => new[] { 11, 111 }.Except(c.Ints).Count(i => i % 2 == 1) == 2), - entryCount: 2); + entryCount: 3); [ConditionalTheory] [MemberData(nameof(IsAsyncData))] @@ -612,6 +614,30 @@ public virtual async Task Parameter_collection_in_subquery_Union_column_collecti var results = compiledQuery(context, ints).ToList(); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_collection_in_subquery_Union_column_collection(bool async) + { + var ints = new[] { 10, 111 }; + + return AssertQuery( + async, + ss => ss.Set().Where(p => ints.Skip(1).Union(p.Ints).Count() == 3), + entryCount: 4); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_collection_in_subquery_Union_column_collection_nested(bool async) + { + var ints = new[] { 10, 111 }; + + return AssertQuery( + async, + ss => ss.Set().Where(p => ints.Skip(1).Union(p.Ints.OrderBy(x => x).Skip(1).Distinct().OrderByDescending(x => x).Take(20)).Count() == 3), + entryCount: 2); + } + [ConditionalFact] public virtual void Parameter_collection_in_subquery_and_Convert_as_compiled_query() { @@ -630,6 +656,26 @@ public virtual void Parameter_collection_in_subquery_and_Convert_as_compiled_que Assert.Contains("in the SQL tree does not have a type mapping assigned", exception.Message); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(bool async) + { + var compiledQuery = EF.CompileQuery( + (PrimitiveCollectionsContext context, int[] ints1, int[] ints2) + => context.Set().Where(p => ints1.Skip(1).Union(ints2).Count() == 3)); + + await using var context = Fixture.CreateContext(); + var ints1 = new[] { 10, 111 }; + var ints2 = new[] { 7, 42 }; + + compiledQuery(context, ints1, ints2).ToList(); + + //var message = Assert.Throws( + // () => compiledQuery(context, ints1, ints2).ToList()).Message; + + //Assert.Equal(RelationalStrings.SetOperationsRequireAtLeastOneSideWithValidTypeMapping("Union"), message); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Column_collection_in_subquery_Union_parameter_collection(bool async) @@ -641,9 +687,119 @@ public virtual Task Column_collection_in_subquery_Union_parameter_collection(boo return AssertQuery( async, ss => ss.Set().Where(c => c.Ints.Skip(1).Union(ints).Count() == 3), - entryCount: 1); + entryCount: 2); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_collection_of_ints_simple(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => x.Ints), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_collection_of_ints_ordered(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => x.Ints.OrderByDescending(xx => xx).ToList()), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_collection_of_datetimes_filtered(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => x.DateTimes.Where(xx => xx.Day != 1).ToList()), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, elementSorter: ee => ee)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_collection_of_ints_with_paging(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => x.NullableInts.Take(20).ToList()), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, elementSorter: ee => ee)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_collection_of_ints_with_paging2(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => x.NullableInts.OrderBy(x => x).Skip(1).ToList()), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, elementSorter: ee => ee)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_collection_of_ints_with_paging3(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => x.NullableInts.Skip(2).ToList()), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, elementSorter: ee => ee)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_collection_of_ints_with_distinct(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => x.Ints.Distinct().ToList()), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, elementSorter: ee => ee)); + + [ConditionalTheory(Skip = "issue #31277")] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_collection_of_nullable_ints_with_distinct(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => x.NullableInts.Distinct().ToList()), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a, elementSorter: ee => ee)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_empty_collection_of_nullables_and_collection_only_containing_nulls(bool async) + => AssertQuery( + async, + 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(), + }), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertCollection(e.Empty, a.Empty, ordered: true); + AssertCollection(e.OnlyNull, a.OnlyNull, ordered: true); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Project_multiple_collections(bool async) + => AssertQuery( + async, + ss => ss.Set().OrderBy(x => x.Id).Select(x => new + { + Ints = x.Ints.ToList(), + OrderedInts = x.Ints.OrderByDescending(xx => xx).ToList(), + FilteredDateTimes = x.DateTimes.Where(xx => xx.Day != 1).ToList(), + FilteredDateTimes2 = x.DateTimes.Where(xx => xx > new DateTime(2000, 1, 1)).ToList() + }), + elementAsserter: (e, a) => + { + AssertCollection(e.Ints, a.Ints, ordered: true); + AssertCollection(e.OrderedInts, a.OrderedInts, ordered: true); + AssertCollection(e.FilteredDateTimes, a.FilteredDateTimes, elementSorter: ee => ee); + AssertCollection(e.FilteredDateTimes2, a.FilteredDateTimes2, elementSorter: ee => ee); + }, + assertOrder: true); + public abstract class PrimitiveCollectionsQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase { private PrimitiveArrayData _expectedData; @@ -802,6 +958,59 @@ private static IReadOnlyList CreatePrimitiveArrayEnt { Id = 3, + Int = 20, + String = "20", + DateTime = new DateTime(2022, 1, 10, 12, 30, 0, DateTimeKind.Utc), + Bool = true, + Enum = MyEnum.Value1, + NullableInt = 20, + + Ints = new[] { 1, 1, 10, 10, 10, 1, 10 }, + Strings = new[] { "1", "10", "10", "1", "1" }, + DateTimes = new DateTime[] + { + new(2020, 1, 1, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 1, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 1, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc), + }, + Bools = new[] { true, false }, + Enums = new[] { MyEnum.Value1, MyEnum.Value2 }, + NullableInts = new int?[] { 1, 1, 10, 10, null, 1 }, + }, + new() + { + Id = 4, + + Int = 41, + String = "41", + DateTime = new DateTime(2024, 1, 11, 12, 30, 0, DateTimeKind.Utc), + Bool = false, + Enum = MyEnum.Value2, + NullableInt = null, + + Ints = new[] { 1, 1, 111, 11, 1, 111 }, + Strings = new[] { "1", "11", "111", "11" }, + DateTimes = new DateTime[] + { + new(2020, 1, 1, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 11, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 1, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 11, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 31, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 1, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 31, 12, 30, 0, DateTimeKind.Utc), + new(2020, 1, 31, 12, 30, 0, DateTimeKind.Utc), + }, + Bools = new[] { false }, + Enums = new[] { MyEnum.Value2, MyEnum.Value3 }, + NullableInts = new int?[] { null, null }, + }, + new() + { + Id = 5, + Int = 0, String = "", DateTime = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc), diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs index 3723d50d3cd..fd2ec6fc1f2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs @@ -332,7 +332,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS nvarchar(max)) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS nvarchar(max)) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -353,7 +353,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS int) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS int) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -374,7 +374,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS bigint) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS bigint) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -395,7 +395,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS smallint) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS smallint) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -421,7 +421,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS float) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS float) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -442,7 +442,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS real) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS real) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -463,7 +463,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS decimal(18,2)) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS decimal(18,2)) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -484,7 +484,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS datetime2) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS datetime2) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -505,7 +505,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS date) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS date) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -526,7 +526,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS time) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS time) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -549,7 +549,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS datetimeoffset) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS datetimeoffset) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -570,7 +570,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS bit) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS bit) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -593,7 +593,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS uniqueidentifier) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS uniqueidentifier) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS @@ -623,7 +623,7 @@ FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([s].[value] AS int) AS [value], CAST([s].[key] AS int) AS [c] + SELECT CAST([s].[value] AS int) AS [value], [s].[key], CAST([s].[key] AS int) AS [c] FROM OPENJSON([t].[SomeArray]) AS [s] ORDER BY CAST([s].[key] AS int) OFFSET 1 ROWS diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index 4c335a6e0b0..ba83bc7c47f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -469,6 +469,12 @@ public override async Task Column_collection_equality_inline_collection_with_par public override Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async) => AssertCompatibilityLevelTooLow(() => base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async)); + public override Task Parameter_collection_in_subquery_Union_column_collection(bool async) + => AssertCompatibilityLevelTooLow(() => base.Parameter_collection_in_subquery_Union_column_collection(async)); + + public override Task Parameter_collection_in_subquery_Union_column_collection_nested(bool async) + => AssertCompatibilityLevelTooLow(() => base.Parameter_collection_in_subquery_Union_column_collection_nested(async)); + public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query() { // Base implementation asserts that a different exception is thrown @@ -480,6 +486,87 @@ public override Task Parameter_collection_in_subquery_Count_as_compiled_query(bo public override Task Column_collection_in_subquery_Union_parameter_collection(bool async) => AssertCompatibilityLevelTooLow(() => base.Column_collection_in_subquery_Union_parameter_collection(async)); + public override Task Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(bool async) + => AssertCompatibilityLevelTooLow(() => base.Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(async)); + + public override async Task Project_collection_of_ints_simple(bool async) + { + await base.Project_collection_of_ints_simple(async); + + AssertSql( +""" +SELECT [p].[Ints] +FROM [PrimitiveCollectionsEntity] AS [p] +ORDER BY [p].[Id] +"""); + } + + public override Task Project_collection_of_ints_ordered(bool async) + // we don't propagate error details from projection + => AssertTranslationFailed(() => base.Project_collection_of_ints_ordered(async)); + + public override Task Project_collection_of_datetimes_filtered(bool async) + // we don't propagate error details from projection + => AssertTranslationFailed(() => base.Project_collection_of_datetimes_filtered(async)); + + public override async Task Project_collection_of_ints_with_paging(bool async) + { + await base.Project_collection_of_ints_with_paging(async); + + // client eval + AssertSql( +""" +SELECT [p].[NullableInts] +FROM [PrimitiveCollectionsEntity] AS [p] +ORDER BY [p].[Id] +"""); + } + + public override Task Project_collection_of_ints_with_paging2(bool async) + // we don't propagate error details from projection + => AssertTranslationFailed(() => base.Project_collection_of_ints_with_paging2(async)); + + public override async Task Project_collection_of_ints_with_paging3(bool async) + { + await base.Project_collection_of_ints_with_paging3(async); + + // client eval + AssertSql( +""" +SELECT [p].[NullableInts] +FROM [PrimitiveCollectionsEntity] AS [p] +ORDER BY [p].[Id] +"""); + } + + public override async Task Project_collection_of_ints_with_distinct(bool async) + { + await base.Project_collection_of_ints_with_distinct(async); + + // client eval + AssertSql( +""" +SELECT [p].[Ints] +FROM [PrimitiveCollectionsEntity] AS [p] +ORDER BY [p].[Id] +"""); + } + + public override async Task Project_collection_of_nullable_ints_with_distinct(bool async) + { + await base.Project_collection_of_nullable_ints_with_distinct(async); + + AssertSql(""); + } + + public override Task Project_multiple_collections(bool async) + // we don't propagate error details from projection + => AssertTranslationFailed(() => base.Project_multiple_collections(async)); + + public override Task Project_empty_collection_of_nullables_and_collection_only_containing_nulls(bool async) + // we don't propagate error details from projection + => AssertTranslationFailed(() => base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(async)); + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 4df3661d951..00e227b6e8b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -589,7 +589,7 @@ FROM [PrimitiveCollectionsEntity] AS [p] WHERE ( SELECT COUNT(*) FROM ( - SELECT 1 AS empty + SELECT [i].[key] FROM OPENJSON([p].[Ints]) AS [i] ORDER BY CAST([i].[key] AS int) OFFSET 1 ROWS @@ -844,7 +844,7 @@ SELECT COUNT(*) FROM ( SELECT [t].[value] FROM ( - SELECT CAST([i].[value] AS int) AS [value] + SELECT CAST([i].[value] AS int) AS [value], [i].[key] FROM OPENJSON(@__ints) AS [i] ORDER BY CAST([i].[key] AS int) OFFSET 1 ROWS @@ -856,6 +856,62 @@ FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i0] """); } + public override async Task Parameter_collection_in_subquery_Union_column_collection(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection(async); + + AssertSql( +""" +@__Skip_0='[111]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT [s].[value] + FROM OPENJSON(@__Skip_0) WITH ([value] int '$') AS [s] + UNION + SELECT [i].[value] + FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i] + ) AS [t]) = 3 +"""); + } + + public override async Task Parameter_collection_in_subquery_Union_column_collection_nested(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection_nested(async); + + AssertSql( +""" +@__Skip_0='[111]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT [s].[value] + FROM OPENJSON(@__Skip_0) WITH ([value] int '$') AS [s] + UNION + SELECT [t1].[value] + FROM ( + SELECT TOP(20) [t0].[value] + FROM ( + SELECT DISTINCT [t2].[value] + FROM ( + SELECT CAST([i].[value] AS int) AS [value], [i].[key] + FROM OPENJSON([p].[Ints]) AS [i] + ORDER BY CAST([i].[value] AS int) + OFFSET 1 ROWS + ) AS [t2] + ) AS [t0] + ORDER BY [t0].[value] DESC + ) AS [t1] + ) AS [t]) = 3 +"""); + } + public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query() { base.Parameter_collection_in_subquery_and_Convert_as_compiled_query(); @@ -877,7 +933,7 @@ FROM [PrimitiveCollectionsEntity] AS [p] WHERE ( SELECT COUNT(*) FROM ( - SELECT CAST([i].[value] AS int) AS [value], CAST([i].[key] AS int) AS [c], CAST([i].[value] AS int) AS [value0] + SELECT CAST([i].[value] AS int) AS [value], [i].[key], CAST([i].[key] AS int) AS [c], CAST([i].[value] AS int) AS [value0] FROM OPENJSON(@__ints) AS [i] ORDER BY CAST([i].[key] AS int) OFFSET 1 ROWS @@ -886,6 +942,14 @@ OFFSET 1 ROWS """); } + public override async Task Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(async))).Message; + + Assert.Equal(RelationalStrings.SetOperationsRequireAtLeastOneSideWithValidTypeMapping("Union"), message); + } + public override async Task Column_collection_in_subquery_Union_parameter_collection(bool async) { await base.Column_collection_in_subquery_Union_parameter_collection(async); @@ -901,7 +965,7 @@ SELECT COUNT(*) FROM ( SELECT [t].[value] FROM ( - SELECT CAST([i].[value] AS int) AS [value] + SELECT CAST([i].[value] AS int) AS [value], [i].[key] FROM OPENJSON([p].[Ints]) AS [i] ORDER BY CAST([i].[key] AS int) OFFSET 1 ROWS @@ -913,6 +977,170 @@ FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i0] """); } + public override async Task Project_collection_of_ints_simple(bool async) + { + await base.Project_collection_of_ints_simple(async); + + AssertSql( +""" +SELECT [p].[Ints] +FROM [PrimitiveCollectionsEntity] AS [p] +ORDER BY [p].[Id] +"""); + } + + public override async Task Project_collection_of_ints_ordered(bool async) + { + await base.Project_collection_of_ints_ordered(async); + + AssertSql( +""" +SELECT [p].[Id], CAST([i].[value] AS int) AS [value], [i].[key] +FROM [PrimitiveCollectionsEntity] AS [p] +OUTER APPLY OPENJSON([p].[Ints]) AS [i] +ORDER BY [p].[Id], CAST([i].[value] AS int) DESC +"""); + } + + public override async Task Project_collection_of_datetimes_filtered(bool async) + { + await base.Project_collection_of_datetimes_filtered(async); + + AssertSql( +""" +SELECT [p].[Id], [t].[value], [t].[key] +FROM [PrimitiveCollectionsEntity] AS [p] +OUTER APPLY ( + SELECT CAST([d].[value] AS datetime2) AS [value], [d].[key], CAST([d].[key] AS int) AS [c] + FROM OPENJSON([p].[DateTimes]) AS [d] + WHERE DATEPART(day, CAST([d].[value] AS datetime2)) <> 1 +) AS [t] +ORDER BY [p].[Id], [t].[c] +"""); + } + + public override async Task Project_collection_of_ints_with_paging(bool async) + { + await base.Project_collection_of_ints_with_paging(async); + + AssertSql( +""" +SELECT [p].[Id], [t].[value], [t].[key] +FROM [PrimitiveCollectionsEntity] AS [p] +OUTER APPLY ( + SELECT TOP(20) CAST([n].[value] AS int) AS [value], [n].[key], CAST([n].[key] AS int) AS [c] + FROM OPENJSON([p].[NullableInts]) AS [n] + ORDER BY CAST([n].[key] AS int) +) AS [t] +ORDER BY [p].[Id], [t].[c] +"""); + } + + public override async Task Project_collection_of_ints_with_paging2(bool async) + { + await base.Project_collection_of_ints_with_paging2(async); + + AssertSql( +""" +SELECT [p].[Id], [t].[value], [t].[key] +FROM [PrimitiveCollectionsEntity] AS [p] +OUTER APPLY ( + SELECT CAST([n].[value] AS int) AS [value], [n].[key] + FROM OPENJSON([p].[NullableInts]) AS [n] + ORDER BY CAST([n].[value] AS int) + OFFSET 1 ROWS +) AS [t] +ORDER BY [p].[Id], [t].[value] +"""); + } + + public override async Task Project_collection_of_ints_with_paging3(bool async) + { + await base.Project_collection_of_ints_with_paging3(async); + + AssertSql( +""" +SELECT [p].[Id], [t].[value], [t].[key] +FROM [PrimitiveCollectionsEntity] AS [p] +OUTER APPLY ( + SELECT CAST([n].[value] AS int) AS [value], [n].[key], CAST([n].[key] AS int) AS [c] + FROM OPENJSON([p].[NullableInts]) AS [n] + ORDER BY CAST([n].[key] AS int) + OFFSET 2 ROWS +) AS [t] +ORDER BY [p].[Id], [t].[c] +"""); + } + + public override async Task Project_collection_of_ints_with_distinct(bool async) + { + await base.Project_collection_of_ints_with_distinct(async); + + AssertSql( +""" +SELECT [p].[Id], [t].[value] +FROM [PrimitiveCollectionsEntity] AS [p] +OUTER APPLY ( + SELECT DISTINCT [i].[value] + FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i] +) AS [t] +ORDER BY [p].[Id] +"""); + } + + public override async Task Project_collection_of_nullable_ints_with_distinct(bool async) + { + await base.Project_collection_of_nullable_ints_with_distinct(async); + + AssertSql(""); + } + + public override async Task Project_empty_collection_of_nullables_and_collection_only_containing_nulls(bool async) + { + await base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(async); + + AssertSql( +""" +SELECT [p].[Id], [t].[value], [t].[key], [t0].[value], [t0].[key] +FROM [PrimitiveCollectionsEntity] AS [p] +OUTER APPLY ( + SELECT CAST([n].[value] AS int) AS [value], [n].[key], CAST([n].[key] AS int) AS [c] + FROM OPENJSON([p].[NullableInts]) AS [n] + WHERE 0 = 1 +) AS [t] +OUTER APPLY ( + SELECT CAST([n0].[value] AS int) AS [value], [n0].[key], CAST([n0].[key] AS int) AS [c] + FROM OPENJSON([p].[NullableInts]) AS [n0] + WHERE [n0].[value] IS NULL +) AS [t0] +ORDER BY [p].[Id], [t].[c], [t].[key], [t0].[c] +"""); + } + + public override async Task Project_multiple_collections(bool async) + { + await base.Project_multiple_collections(async); + + AssertSql( +""" +SELECT [p].[Id], CAST([i].[value] AS int) AS [value], [i].[key], CAST([i0].[value] AS int) AS [value], [i0].[key], [t].[value], [t].[key], [t0].[value], [t0].[key] +FROM [PrimitiveCollectionsEntity] AS [p] +OUTER APPLY OPENJSON([p].[Ints]) AS [i] +OUTER APPLY OPENJSON([p].[Ints]) AS [i0] +OUTER APPLY ( + SELECT CAST([d].[value] AS datetime2) AS [value], [d].[key], CAST([d].[key] AS int) AS [c] + FROM OPENJSON([p].[DateTimes]) AS [d] + WHERE DATEPART(day, CAST([d].[value] AS datetime2)) <> 1 +) AS [t] +OUTER APPLY ( + SELECT CAST([d0].[value] AS datetime2) AS [value], [d0].[key], CAST([d0].[key] AS int) AS [c] + FROM OPENJSON([p].[DateTimes]) AS [d0] + WHERE CAST([d0].[value] AS datetime2) > '2000-01-01T00:00:00.0000000' +) AS [t0] +ORDER BY [p].[Id], CAST([i].[key] AS int), [i].[key], CAST([i0].[value] AS int) DESC, [i0].[key], [t].[c], [t].[key], [t0].[c] +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index f5a54b51f18..b7261146410 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore.Sqlite.Internal; namespace Microsoft.EntityFrameworkCore.Query; @@ -575,7 +576,7 @@ public override async Task Column_collection_Skip(bool async) WHERE ( SELECT COUNT(*) FROM ( - SELECT 1 + SELECT "i"."key" FROM json_each("p"."Ints") AS "i" ORDER BY "i"."key" LIMIT -1 OFFSET 1 @@ -817,7 +818,7 @@ public override async Task Parameter_collection_in_subquery_Count_as_compiled_qu await base.Parameter_collection_in_subquery_Count_as_compiled_query(async); AssertSql( - """ +""" @__ints='[10,111]' (Size = 8) SELECT COUNT(*) @@ -834,12 +835,20 @@ ORDER BY "i"."key" """); } + public override async Task Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(bool async) + { + var message = (await Assert.ThrowsAsync( + () => Parameter_collection_in_subquery_Union_another_parameter_collection_as_compiled_query(async))).Message; + + Assert.Equal(RelationalStrings.SetOperationsRequireAtLeastOneSideWithValidTypeMapping("Union"), message); + } + public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async) { await base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async); AssertSql( - """ +""" @__ints='[10,111]' (Size = 8) SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" @@ -849,7 +858,7 @@ SELECT COUNT(*) FROM ( SELECT "t"."value" FROM ( - SELECT "i"."value" + SELECT "i"."value", "i"."key" FROM json_each(@__ints) AS "i" ORDER BY "i"."key" LIMIT -1 OFFSET 1 @@ -861,6 +870,63 @@ FROM json_each("p"."Ints") AS "i0" """); } + public override async Task Parameter_collection_in_subquery_Union_column_collection(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection(async); + + AssertSql( +""" +@__Skip_0='[111]' (Size = 5) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT "s"."value" + FROM json_each(@__Skip_0) AS "s" + UNION + SELECT "i"."value" + FROM json_each("p"."Ints") AS "i" + ) AS "t") = 3 +"""); + } + + public override async Task Parameter_collection_in_subquery_Union_column_collection_nested(bool async) + { + await base.Parameter_collection_in_subquery_Union_column_collection_nested(async); + + AssertSql( +""" +@__Skip_0='[111]' (Size = 5) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT "s"."value" + FROM json_each(@__Skip_0) AS "s" + UNION + SELECT "t1"."value" + FROM ( + SELECT "t0"."value" + FROM ( + SELECT DISTINCT "t2"."value" + FROM ( + SELECT "i"."value", "i"."key" + FROM json_each("p"."Ints") AS "i" + ORDER BY "i"."value" + LIMIT -1 OFFSET 1 + ) AS "t2" + ) AS "t0" + ORDER BY "t0"."value" DESC + LIMIT 20 + ) AS "t1" + ) AS "t") = 3 +"""); + } + public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query() { base.Parameter_collection_in_subquery_and_Convert_as_compiled_query(); @@ -883,7 +949,7 @@ SELECT COUNT(*) FROM ( SELECT "t"."value" FROM ( - SELECT "i"."value" + SELECT "i"."value", "i"."key" FROM json_each("p"."Ints") AS "i" ORDER BY "i"."key" LIMIT -1 OFFSET 1 @@ -895,6 +961,72 @@ FROM json_each(@__ints_0) AS "i0" """); } + public override async Task Project_collection_of_ints_simple(bool async) + { + await base.Project_collection_of_ints_simple(async); + + AssertSql( +""" +SELECT "p"."Ints" +FROM "PrimitiveCollectionsEntity" AS "p" +ORDER BY "p"."Id" +"""); + } + + public override async Task Project_collection_of_ints_ordered(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Project_collection_of_ints_ordered(async))).Message); + + public override async Task Project_collection_of_datetimes_filtered(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Project_collection_of_datetimes_filtered(async))).Message); + + public override async Task Project_collection_of_ints_with_paging(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Project_collection_of_ints_with_paging(async))).Message); + + public override async Task Project_collection_of_ints_with_paging2(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Project_collection_of_ints_with_paging2(async))).Message); + + public override async Task Project_collection_of_ints_with_paging3(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Project_collection_of_ints_with_paging3(async))).Message); + + public override async Task Project_collection_of_ints_with_distinct(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Project_collection_of_ints_with_distinct(async))).Message); + + public override async Task Project_collection_of_nullable_ints_with_distinct(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Project_collection_of_nullable_ints_with_distinct(async))).Message); + + public override async Task Project_multiple_collections(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Project_multiple_collections(async))).Message); + + public override async Task Project_empty_collection_of_nullables_and_collection_only_containing_nulls(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(async))).Message); + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType());