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..d3ef5ed33c9 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( + () => base.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());