diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index 092e1b225da..b9141e3d631 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -130,6 +130,51 @@ public SelectExpression(
_projectionMapping[new ProjectionMember()] = columnExpression;
}
+ ///
+ /// Creates a new instance of the class given a , with a single
+ /// column projection.
+ ///
+ /// The name of the column to use as identifier.
+ /// The type of the column to use as identifier.
+ /// The type mapping of the column to use as identifier.
+ /// 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.
+ public SelectExpression(
+ TableExpressionBase tableExpression,
+ string identidierColumnName,
+ Type identifierColumnType,
+ RelationalTypeMapping? identifierColumnTypeMapping,
+ string columnName,
+ Type columnType,
+ RelationalTypeMapping? columnTypeMapping,
+ bool? isColumnNullable = null)
+ : base(null)
+ {
+ var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!);
+ AddTable(tableExpression, tableReferenceExpression);
+
+ var identifierColumn = new ConcreteColumnExpression(
+ identidierColumnName,
+ tableReferenceExpression,
+ identifierColumnType.UnwrapNullableType(),
+ identifierColumnTypeMapping,
+ identifierColumnType.IsNullableType());
+
+ var columnExpression = new ConcreteColumnExpression(
+ columnName,
+ tableReferenceExpression,
+ columnType.UnwrapNullableType(),
+ columnTypeMapping,
+ isColumnNullable ?? columnType.IsNullableType());
+
+ _projectionMapping[new ProjectionMember()] = columnExpression;
+
+ _identifier.Add((identifierColumn, identifierColumnTypeMapping!.Comparer));
+ }
+
internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpressionFactory)
: base(null)
{
@@ -2156,7 +2201,10 @@ public void ApplyIntersect(SelectExpression source2, bool distinct)
public void ApplyUnion(SelectExpression source2, bool distinct)
=> ApplySetOperation(SetOperationType.Union, source2, distinct);
- private void ApplySetOperation(SetOperationType setOperationType, SelectExpression select2, bool distinct)
+ private void ApplySetOperation(
+ SetOperationType setOperationType,
+ SelectExpression select2,
+ bool distinct)
{
// TODO: Introduce clone method? See issue#24460
var select1 = new SelectExpression(
@@ -2205,7 +2253,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, RelationalTypeMapping)>();
// 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 +2356,11 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi
}
}
- otherExpressions.Add(outerProjection);
+ // we need 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
+ var outerTypeMapping = innerProjection1.Expression.TypeMapping ?? innerProjection2.Expression.TypeMapping!;
+ otherExpressions.Add((outerProjection, outerTypeMapping));
}
}
@@ -2349,10 +2401,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.Item1 is ColumnExpression))
{
_identifier.AddRange(entityProjectionIdentifiers.Zip(entityProjectionValueComparers));
- _identifier.AddRange(otherExpressions.Select(e => ((ColumnExpression)e, e.TypeMapping!.KeyComparer)));
+ _identifier.AddRange(otherExpressions.Select(e => ((ColumnExpression)e.Item1, e.Item2.KeyComparer)));
}
}
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs
index a9a249cb8b1..867f93005ba 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
@@ -111,30 +112,73 @@ 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 = new List();
+ var appliedCasts = new List<(SqlServerOpenJsonExpression, string)>();
+
+ foreach (var table in selectExpression.Tables)
+ {
+ if (table is SqlServerOpenJsonExpression { ColumnInfos: not null }
+ or JoinExpressionBase { Table: SqlServerOpenJsonExpression { ColumnInfos: not null } })
{
- Expression: SqlUnaryExpression
+ var keyReferencedInOrderings = selectExpression.Orderings.Any(o => o.Expression is SqlUnaryExpression
+ {
+ OperatorType: ExpressionType.Convert,
+ Operand: ColumnExpression { Name: "key", Table: var keyColumnTable }
+ } && keyColumnTable == table);
+
+ var keyReferencedInProjection = selectExpression.Projection.Any(p => p.Expression is ColumnExpression
+ { Name: "key", Table: var keyColumnTable } && keyColumnTable == table);
+
+ var keyWithConvertReferencedInProjection = selectExpression.Projection.Any(p => p.Expression is SqlUnaryExpression
{
OperatorType: ExpressionType.Convert,
Operand: ColumnExpression { Name: "key", Table: var keyColumnTable }
+ } && keyColumnTable == table);
+
+ if (keyReferencedInOrderings || keyReferencedInProjection || keyWithConvertReferencedInProjection)
+ {
+ // 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,
+ };
+
+ newTables.Add(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");
+
+ _castsToApply.Add((newOpenJsonExpression, column.Name), typeMapping);
+ appliedCasts.Add((newOpenJsonExpression, column.Name));
+ }
+ }
+ else
+ {
+ newTables.Add(table);
}
}
- ]
- } 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;
+ else
+ {
+ newTables.Add(table);
+ }
+ }
var newSelectExpression = selectExpression.Update(
selectExpression.Projection,
@@ -144,36 +188,37 @@ public Expression Process(Expression expression)
selectExpression.Having,
selectExpression.Orderings,
selectExpression.Limit,
- selectExpression.Offset);
+ 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.
// TODO: Need to pass through the type mapping API for converting the JSON value (nvarchar) to the relational store type
// (e.g. datetime2), see #30677
- 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");
-
- _castsToApply.Add((newOpenJsonExpression, column.Name), typeMapping);
- }
-
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);
+ if (columnExpression.Table is SqlServerOpenJsonExpression openJsonTable
+ && _castsToApply.TryGetValue((openJsonTable, columnExpression.Name), out var typeMapping))
+ {
+ return _sqlExpressionFactory.Convert(columnExpression, columnExpression.Type, typeMapping);
+ }
+
+ if (columnExpression.Table is JoinExpressionBase { Table: SqlServerOpenJsonExpression innerOpenJsonTable }
+ && _castsToApply.TryGetValue((innerOpenJsonTable, columnExpression.Name), out var innerTypeMapping))
+ {
+ return _sqlExpressionFactory.Convert(columnExpression, columnExpression.Type, innerTypeMapping);
+ }
+
+ return base.Visit(expression);
}
default:
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
index 63ea9ea02a1..02385c071ea 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
@@ -156,7 +156,14 @@ protected override Expression VisitExtension(Expression extensionExpression)
var isColumnNullable = elementClrType.IsNullableType();
var selectExpression = new SelectExpression(
- openJsonExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping, isColumnNullable);
+ openJsonExpression,
+ identidierColumnName: "key",
+ identifierColumnType: typeof(string),
+ identifierColumnTypeMapping: _typeMappingSource.FindMapping("nvarchar(4000)"),
+ columnName: "value",
+ columnType: elementClrType,
+ columnTypeMapping: elementTypeMapping,
+ isColumnNullable);
// 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.
@@ -180,7 +187,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 8b1268f397a..18df3cd69e2 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
@@ -220,7 +220,14 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
var isColumnNullable = elementClrType.IsNullableType();
var selectExpression = new SelectExpression(
- jsonEachExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping, isColumnNullable);
+ jsonEachExpression,
+ identidierColumnName: "key",
+ identifierColumnType: typeof(int),
+ identifierColumnTypeMapping: _typeMappingSource.FindMapping(typeof(int)),
+ columnName: "value",
+ columnType: elementClrType,
+ columnTypeMapping: elementTypeMapping,
+ isColumnNullable);
// TODO: SQLite does have REAL and BLOB types, which JSON does not. Need to possibly cast to that.
if (elementTypeMapping is not null)
diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
index b1d6d437487..f79f61183ba 100644
--- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
@@ -33,7 +33,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 +58,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 +66,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 +153,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 +164,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 +200,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 +240,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 +252,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 +273,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 +281,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 +289,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 +305,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 +340,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 +351,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 +443,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 +451,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 +514,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 +525,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 +612,18 @@ 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);
+ }
+
[ConditionalFact]
public virtual void Parameter_collection_in_subquery_and_Convert_as_compiled_query()
{
@@ -641,9 +653,65 @@ 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_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 +870,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/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
index 4c335a6e0b0..65695e2fdde 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
@@ -469,6 +469,9 @@ 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 void Parameter_collection_in_subquery_and_Convert_as_compiled_query()
{
// Base implementation asserts that a different exception is thrown
@@ -480,6 +483,30 @@ 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 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_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 b3993a6484d..933f99f5583 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
@@ -588,7 +588,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
@@ -843,7 +843,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
@@ -855,6 +855,28 @@ 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 void Parameter_collection_in_subquery_and_Convert_as_compiled_query()
{
base.Parameter_collection_in_subquery_and_Convert_as_compiled_query();
@@ -876,7 +898,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
@@ -900,7 +922,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
@@ -912,6 +934,77 @@ 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_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 5cdad15f18e..7341487c537 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
@@ -849,7 +850,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 +862,28 @@ 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 void Parameter_collection_in_subquery_and_Convert_as_compiled_query()
{
base.Parameter_collection_in_subquery_and_Convert_as_compiled_query();
@@ -883,7 +906,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 +918,36 @@ 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_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());