diff --git a/All.sln.DotSettings b/All.sln.DotSettings
index 8ed3bce7378..5d6edfea13f 100644
--- a/All.sln.DotSettings
+++ b/All.sln.DotSettings
@@ -299,6 +299,7 @@ The .NET Foundation licenses this file to you under the MIT license.
True
True
True
+ True
True
True
True
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index 65b4b33eb2f..6a16c3e39a0 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -1155,6 +1155,12 @@ public static string JsonReaderInvalidTokenType(object? tokenType)
GetString("JsonReaderInvalidTokenType", nameof(tokenType)),
tokenType);
+ ///
+ /// Composing LINQ operators over collections inside JSON documents isn't supported or hasn't been implemented by your EF provider.
+ ///
+ public static string JsonQueryLinqOperatorsNotSupported
+ => GetString("JsonQueryLinqOperatorsNotSupported");
+
///
/// Entity {entity} is required but the JSON element containing it is null.
///
@@ -1503,6 +1509,12 @@ public static string ReadonlyEntitySaved(object? entityType)
public static string RelationalNotInUse
=> GetString("RelationalNotInUse");
+ ///
+ /// SelectExpression can only be built over a JsonQueryExpression that represents a collection within the JSON document.
+ ///
+ public static string SelectCanOnlyBeBuiltOnCollectionJsonQuery
+ => GetString("SelectCanOnlyBeBuiltOnCollectionJsonQuery");
+
///
/// Cannot create a 'SelectExpression' with a custom 'TableExpressionBase' since the result type '{entityType}' is part of a hierarchy and does not contain a discriminator property.
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 014bea98e58..d680d55eb6b 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -553,6 +553,9 @@
Invalid token type: '{tokenType}'.
+
+ Composing LINQ operators over collections inside JSON documents isn't supported or hasn't been implemented by your EF provider.
+
Entity {entity} is required but the JSON element containing it is null.
@@ -989,6 +992,9 @@
Relational-specific methods can only be used when the context is using a relational database provider.
+
+ SelectExpression can only be built over a JsonQueryExpression that represents a collection within the JSON document.
+
Cannot create a 'SelectExpression' with a custom 'TableExpressionBase' since the result type '{entityType}' is part of a hierarchy and does not contain a discriminator property.
diff --git a/src/EFCore.Relational/Query/JsonQueryExpression.cs b/src/EFCore.Relational/Query/JsonQueryExpression.cs
index 3d35d28eb7e..1db6b039d1a 100644
--- a/src/EFCore.Relational/Query/JsonQueryExpression.cs
+++ b/src/EFCore.Relational/Query/JsonQueryExpression.cs
@@ -16,8 +16,6 @@ namespace Microsoft.EntityFrameworkCore.Query;
///
public class JsonQueryExpression : Expression, IPrintableExpression
{
- private readonly IReadOnlyDictionary _keyPropertyMap;
-
///
/// Creates a new instance of the class.
///
@@ -57,7 +55,7 @@ private JsonQueryExpression(
EntityType = entityType;
JsonColumn = jsonColumn;
IsCollection = collection;
- _keyPropertyMap = keyPropertyMap;
+ KeyPropertyMap = keyPropertyMap;
Type = type;
Path = path;
IsNullable = nullable;
@@ -88,6 +86,15 @@ private JsonQueryExpression(
///
public virtual bool IsNullable { get; }
+ ///
+ /// 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.
+ ///
+ [EntityFrameworkInternal]
+ public virtual IReadOnlyDictionary KeyPropertyMap { get; }
+
///
public override ExpressionType NodeType
=> ExpressionType.Extension;
@@ -107,7 +114,7 @@ public virtual SqlExpression BindProperty(IProperty property)
RelationalStrings.UnableToBindMemberToEntityProjection("property", property.Name, EntityType.DisplayName()));
}
- if (_keyPropertyMap.TryGetValue(property, out var match))
+ if (KeyPropertyMap.TryGetValue(property, out var match))
{
return match;
}
@@ -145,11 +152,11 @@ public virtual JsonQueryExpression BindNavigation(INavigation navigation)
newPath.Add(new PathSegment(targetEntityType.GetJsonPropertyName()!));
var newKeyPropertyMap = new Dictionary();
- var targetPrimaryKeyProperties = targetEntityType.FindPrimaryKey()!.Properties.Take(_keyPropertyMap.Count);
- var sourcePrimaryKeyProperties = EntityType.FindPrimaryKey()!.Properties.Take(_keyPropertyMap.Count);
+ var targetPrimaryKeyProperties = targetEntityType.FindPrimaryKey()!.Properties.Take(KeyPropertyMap.Count);
+ var sourcePrimaryKeyProperties = EntityType.FindPrimaryKey()!.Properties.Take(KeyPropertyMap.Count);
foreach (var (target, source) in targetPrimaryKeyProperties.Zip(sourcePrimaryKeyProperties, (t, s) => (t, s)))
{
- newKeyPropertyMap[target] = _keyPropertyMap[source];
+ newKeyPropertyMap[target] = KeyPropertyMap[source];
}
return new JsonQueryExpression(
@@ -178,7 +185,7 @@ public virtual JsonQueryExpression BindCollectionElement(SqlExpression collectio
return new JsonQueryExpression(
EntityType,
JsonColumn,
- _keyPropertyMap,
+ KeyPropertyMap,
newPath,
EntityType.ClrType,
collection: false,
@@ -194,7 +201,7 @@ public virtual JsonQueryExpression BindCollectionElement(SqlExpression collectio
public virtual JsonQueryExpression MakeNullable()
{
var keyPropertyMap = new Dictionary();
- foreach (var (property, columnExpression) in _keyPropertyMap)
+ foreach (var (property, columnExpression) in KeyPropertyMap)
{
keyPropertyMap[property] = columnExpression.MakeNullable();
}
@@ -223,7 +230,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var jsonColumn = (ColumnExpression)visitor.Visit(JsonColumn);
var newKeyPropertyMap = new Dictionary();
- foreach (var (property, column) in _keyPropertyMap)
+ foreach (var (property, column) in KeyPropertyMap)
{
newKeyPropertyMap[property] = (ColumnExpression)visitor.Visit(column);
}
@@ -242,8 +249,8 @@ public virtual JsonQueryExpression Update(
ColumnExpression jsonColumn,
IReadOnlyDictionary keyPropertyMap)
=> jsonColumn != JsonColumn
- || keyPropertyMap.Count != _keyPropertyMap.Count
- || keyPropertyMap.Zip(_keyPropertyMap, (n, o) => n.Value != o.Value).Any(x => x)
+ || keyPropertyMap.Count != KeyPropertyMap.Count
+ || keyPropertyMap.Zip(KeyPropertyMap, (n, o) => n.Value != o.Value).Any(x => x)
? new JsonQueryExpression(EntityType, jsonColumn, keyPropertyMap, Path, Type, IsCollection, IsNullable)
: this;
@@ -260,16 +267,16 @@ private bool Equals(JsonQueryExpression jsonQueryExpression)
&& IsCollection.Equals(jsonQueryExpression.IsCollection)
&& IsNullable == jsonQueryExpression.IsNullable
&& Path.SequenceEqual(jsonQueryExpression.Path)
- && KeyPropertyMapEquals(jsonQueryExpression._keyPropertyMap);
+ && KeyPropertyMapEquals(jsonQueryExpression.KeyPropertyMap);
private bool KeyPropertyMapEquals(IReadOnlyDictionary other)
{
- if (_keyPropertyMap.Count != other.Count)
+ if (KeyPropertyMap.Count != other.Count)
{
return false;
}
- foreach (var (key, value) in _keyPropertyMap)
+ foreach (var (key, value) in KeyPropertyMap)
{
if (!other.TryGetValue(key, out var column) || !value.Equals(column))
{
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index 7ae4d0e782a..1ae159c5fe1 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -223,6 +223,9 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
char.ToLowerInvariant(sqlParameterExpression.Name.First(c => c != '_')).ToString())
?? base.VisitExtension(extensionExpression);
+ case JsonQueryExpression jsonQueryExpression:
+ return TransformJsonQueryToTable(jsonQueryExpression) ?? base.VisitExtension(extensionExpression);
+
default:
return base.VisitExtension(extensionExpression);
}
@@ -326,6 +329,19 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
string tableAlias)
=> null;
+ ///
+ /// Invoked when LINQ operators are composed over a collection within a JSON document.
+ /// Transforms the provided - representing access to the collection - into a provider-specific
+ /// means to expand the JSON array into a relational table/rowset (e.g. SQL Server OPENJSON).
+ ///
+ /// The referencing the JSON array.
+ /// A if the translation was successful, otherwise .
+ protected virtual ShapedQueryExpression? TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression)
+ {
+ AddTranslationErrorDetails(RelationalStrings.JsonQueryLinqOperatorsNotSupported);
+ return null;
+ }
+
///
/// Translates an inline collection into a queryable SQL VALUES expression.
///
@@ -606,9 +622,9 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s
protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression source)
{
var selectExpression = (SelectExpression)source.QueryExpression;
- if (selectExpression.Orderings.Count > 0
- && selectExpression.Limit == null
- && selectExpression.Offset == null)
+
+ if (selectExpression is { Orderings.Count: > 0, Limit: null, Offset: null }
+ && !IsNaturallyOrdered(selectExpression))
{
_queryCompilationContext.Logger.DistinctAfterOrderByWithoutRowLimitingOperatorWarning();
}
@@ -1862,6 +1878,16 @@ protected virtual Expression ApplyInferredTypeMappings(
protected virtual bool IsOrdered(SelectExpression selectExpression)
=> selectExpression.Orderings.Count > 0;
+ ///
+ /// Determines whether the given is naturally ordered, meaning that any ordering has been added
+ /// automatically by EF to preserve e.g. the natural ordering of a JSON array, and not because the original LINQ query contained
+ /// an explicit ordering.
+ ///
+ /// The to check for ordering.
+ /// Whether is ordered.
+ protected virtual bool IsNaturallyOrdered(SelectExpression selectExpression)
+ => false;
+
private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression)
{
var lambdaBody = ReplacingExpressionVisitor.Replace(
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index f746e53b096..19fcc3d1661 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -494,13 +494,13 @@ internal SelectExpression(IEntityType entityType, TableExpressionBase tableExpre
throw new InvalidOperationException(RelationalStrings.SelectExpressionNonTphWithCustomTable(entityType.DisplayName()));
}
- var table = (tableExpressionBase as ITableBasedExpression)?.Table;
- Check.DebugAssert(table is not null, "SelectExpression with unexpected missing table");
-
var tableReferenceExpression = new TableReferenceExpression(this, tableExpressionBase.Alias!);
AddTable(tableExpressionBase, tableReferenceExpression);
var propertyExpressions = new Dictionary();
+ var table = (tableExpressionBase as ITableBasedExpression)?.Table;
+ Check.DebugAssert(table is not null, "SelectExpression with unexpected missing table");
+
foreach (var property in GetAllPropertiesInHierarchy(entityType))
{
propertyExpressions[property] = CreateColumnExpression(property, table, tableReferenceExpression, nullable: false);
@@ -520,6 +520,95 @@ internal SelectExpression(IEntityType entityType, TableExpressionBase tableExpre
}
}
+ ///
+ /// Constructs a over a collection within a JSON document.
+ ///
+ ///
+ /// The collection within a JSON document which the will represent.
+ ///
+ ///
+ /// The table for the ; typically a provider-specific table-valued function that converts a JSON
+ /// array to a relational table/rowset (e.g. SQL Server OPENJSON)
+ ///
+ public SelectExpression(JsonQueryExpression jsonQueryExpression, TableExpressionBase tableExpressionBase)
+ : base(null)
+ {
+ if (!jsonQueryExpression.IsCollection)
+ {
+ throw new ArgumentException(RelationalStrings.SelectCanOnlyBeBuiltOnCollectionJsonQuery, nameof(jsonQueryExpression));
+ }
+
+ var entityType = jsonQueryExpression.EntityType;
+
+ Check.DebugAssert(
+ entityType.BaseType is null && !entityType.GetDirectlyDerivedTypes().Any(),
+ "Inheritance encountered inside a JSON document");
+
+ var tableReferenceExpression = new TableReferenceExpression(this, tableExpressionBase.Alias!);
+ AddTable(tableExpressionBase, tableReferenceExpression);
+
+ // Create a dictionary mapping all properties to their ColumnExpressions, for the SelectExpression's projection.
+ var propertyExpressions = new Dictionary();
+
+ foreach (var property in GetAllPropertiesInHierarchy(entityType))
+ {
+ // Skip also properties with no JSON name (i.e. shadow keys containing the index in the collection, which don't actually exist
+ // in the JSON document and can't be bound to)
+ if (property.GetJsonPropertyName() is string jsonPropertyName)
+ {
+ propertyExpressions[property] = CreateColumnExpression(
+ tableExpressionBase, jsonPropertyName, property.ClrType, property.GetRelationalTypeMapping(), property.IsNullable);
+ }
+ }
+
+ var entityProjection = new EntityProjectionExpression(entityType, propertyExpressions);
+
+ var containerColumnName = jsonQueryExpression.EntityType.GetContainerColumnName()!;
+ var jsonColumnTypeMapping = (entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
+ ?? entityType.GetDefaultMappings().Single().Table)
+ .FindColumn(containerColumnName)!.StoreTypeMapping;
+
+ foreach (var navigation in GetAllNavigationsInHierarchy(entityType)
+ .Where(
+ n => n.ForeignKey.IsOwnership
+ && n.TargetEntityType.IsMappedToJson()
+ && n.ForeignKey.PrincipalToDependent == n))
+ {
+ var targetEntityType = navigation.TargetEntityType;
+
+ var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName();
+ Check.DebugAssert(jsonNavigationName is not null, "Invalid navigation found on JSON-mapped entity");
+
+ // The TableExpressionBase represents a relational expansion of the JSON collection. We now need a ColumnExpression to represent
+ // the specific JSON property (projected as a relational column) which holds the JSON subtree for the target entity.
+ var jsonColumn = new ConcreteColumnExpression(
+ jsonNavigationName,
+ tableReferenceExpression,
+ jsonColumnTypeMapping.ClrType,
+ jsonColumnTypeMapping,
+ nullable: !navigation.ForeignKey.IsRequiredDependent || navigation.IsCollection);
+
+ var entityShaperExpression = new RelationalEntityShaperExpression(
+ targetEntityType,
+ new JsonQueryExpression(
+ targetEntityType,
+ jsonColumn,
+ jsonQueryExpression.KeyPropertyMap,
+ navigation.ClrType,
+ navigation.IsCollection),
+ !navigation.ForeignKey.IsRequiredDependent);
+
+ entityProjection.AddNavigationBinding(navigation, entityShaperExpression);
+ }
+
+ _projectionMapping[new ProjectionMember()] = entityProjection;
+
+ foreach (var (property, column) in jsonQueryExpression.KeyPropertyMap)
+ {
+ _identifier.Add((column, property.GetKeyValueComparer()));
+ }
+ }
+
private void AddJsonNavigationBindings(
IEntityType entityType,
EntityProjectionExpression entityProjection,
@@ -534,16 +623,20 @@ private void AddJsonNavigationBindings(
{
var targetEntityType = ownedJsonNavigation.TargetEntityType;
var jsonColumnName = targetEntityType.GetContainerColumnName()!;
- var jsonColumnTypeMapping = (entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
+ var jsonColumn = (entityType.GetViewOrTableMappings().SingleOrDefault()?.Table
?? entityType.GetDefaultMappings().Single().Table)
- .FindColumn(jsonColumnName)!.StoreTypeMapping;
+ .FindColumn(jsonColumnName)!;
+ var jsonColumnTypeMapping = jsonColumn.StoreTypeMapping;
+ var isNullable = jsonColumn.IsNullable
+ || !ownedJsonNavigation.ForeignKey.IsRequiredDependent
+ || ownedJsonNavigation.IsCollection;
- var jsonColumn = new ConcreteColumnExpression(
+ var jsonColumnExpression = new ConcreteColumnExpression(
jsonColumnName,
tableReferenceExpression,
jsonColumnTypeMapping.ClrType,
jsonColumnTypeMapping,
- nullable: !ownedJsonNavigation.ForeignKey.IsRequiredDependent || ownedJsonNavigation.IsCollection);
+ isNullable);
// for json collections we need to skip ordinal key (which is always the last one)
// simple copy from parent is safe here, because we only do it at top level
@@ -564,11 +657,11 @@ private void AddJsonNavigationBindings(
targetEntityType,
new JsonQueryExpression(
targetEntityType,
- jsonColumn,
+ jsonColumnExpression,
keyPropertiesMap,
ownedJsonNavigation.ClrType,
ownedJsonNavigation.IsCollection),
- !ownedJsonNavigation.ForeignKey.IsRequiredDependent);
+ isNullable);
entityProjection.AddNavigationBinding(ownedJsonNavigation, entityShaperExpression);
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
index b4869e373dc..85e38459a57 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
@@ -128,19 +128,9 @@ public override string? Alias
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
- {
- var changed = false;
- var arguments = new SqlExpression[Arguments.Count];
- for (var i = 0; i < arguments.Length; i++)
- {
- arguments[i] = (SqlExpression)visitor.Visit(Arguments[i]);
- changed |= arguments[i] != Arguments[i];
- }
-
- return changed
- ? new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, arguments, GetAnnotations())
- : this;
- }
+ => visitor.VisitAndConvert(Arguments) is var visitedArguments && visitedArguments == Arguments
+ ? this
+ : new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, visitedArguments, GetAnnotations());
///
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
index 2a12317536f..7fed63fd339 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
@@ -37,8 +37,7 @@ public virtual SqlExpression JsonExpression
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public virtual SqlExpression? Path
- => Arguments.Count == 1 ? null : Arguments[1];
+ public virtual IReadOnlyList? Path { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -54,16 +53,74 @@ public virtual SqlExpression? Path
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
+
public SqlServerOpenJsonExpression(
string alias,
SqlExpression jsonExpression,
- SqlExpression? path = null,
+ IReadOnlyList? path = null,
IReadOnlyList? columnInfos = null)
- : base(alias, "OPENJSON", schema: null, builtIn: true, path is null ? new[] { jsonExpression } : new[] { jsonExpression, path })
+ : base(alias, "OPENJSON", schema: null, builtIn: true, new[] { jsonExpression })
{
+ Path = path;
ColumnInfos = columnInfos;
}
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ var visitedJsonExpression = (SqlExpression)visitor.Visit(JsonExpression);
+
+ PathSegment[]? visitedPath = null;
+
+ if (Path is not null)
+ {
+ for (var i = 0; i < Path.Count; i++)
+ {
+ var segment = Path[i];
+ PathSegment newSegment;
+
+ if (segment.PropertyName is not null)
+ {
+ // PropertyName segments are (currently) constants, nothing to visit.
+ newSegment = segment;
+ }
+ else
+ {
+ var newArrayIndex = (SqlExpression)visitor.Visit(segment.ArrayIndex)!;
+ if (newArrayIndex == segment.ArrayIndex)
+ {
+ newSegment = segment;
+ }
+ else
+ {
+ newSegment = new PathSegment(newArrayIndex);
+
+ if (visitedPath is null)
+ {
+ visitedPath = new PathSegment[Path.Count];
+ for (var j = 0; j < i; i++)
+ {
+ visitedPath[j] = Path[j];
+ }
+ }
+ }
+ }
+
+ if (visitedPath is not null)
+ {
+ visitedPath[i] = newSegment;
+ }
+ }
+ }
+
+ return Update(visitedJsonExpression, visitedPath ?? Path, ColumnInfos);
+ }
+
///
/// 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
@@ -72,11 +129,11 @@ public SqlServerOpenJsonExpression(
///
public virtual SqlServerOpenJsonExpression Update(
SqlExpression jsonExpression,
- SqlExpression? path,
+ IReadOnlyList? path,
IReadOnlyList? columnInfos = null)
=> jsonExpression == JsonExpression
- && path == Path
- && (columnInfos is null ? ColumnInfos is null : ColumnInfos is not null && columnInfos.SequenceEqual(ColumnInfos))
+ && (ReferenceEquals(path, Path) || path is not null && Path is not null && path.SequenceEqual(Path))
+ && (ReferenceEquals(columnInfos, ColumnInfos) || columnInfos is not null && ColumnInfos is not null && columnInfos.SequenceEqual(ColumnInfos))
? this
: new SqlServerOpenJsonExpression(Alias, jsonExpression, path, columnInfos);
@@ -100,12 +157,26 @@ public virtual TableExpressionBase Clone()
return clone;
}
- ///
+ ///
+ /// 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.
+ ///
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append(Name);
expressionPrinter.Append("(");
- expressionPrinter.VisitCollection(Arguments);
+ expressionPrinter.Visit(JsonExpression);
+
+ if (Path is not null)
+ {
+ expressionPrinter
+ .Append(", '")
+ .Append(string.Join(".", Path.Select(e => e.ToString())))
+ .Append("'");
+ }
+
expressionPrinter.Append(")");
if (ColumnInfos is not null)
@@ -124,11 +195,14 @@ protected override void Print(ExpressionPrinter expressionPrinter)
expressionPrinter
.Append(columnInfo.Name)
.Append(" ")
- .Append(columnInfo.StoreType ?? "");
+ .Append(columnInfo.TypeMapping.StoreType);
if (columnInfo.Path is not null)
{
- expressionPrinter.Append(" ").Append("'" + columnInfo.Path + "'");
+ expressionPrinter
+ .Append(" '")
+ .Append(string.Join(".", columnInfo.Path.Select(e => e.ToString())))
+ .Append("'");
}
if (columnInfo.AsJson)
@@ -141,6 +215,7 @@ protected override void Print(ExpressionPrinter expressionPrinter)
}
PrintAnnotations(expressionPrinter);
+
expressionPrinter.Append(" AS ");
expressionPrinter.Append(Alias);
}
@@ -149,11 +224,35 @@ protected override void Print(ExpressionPrinter expressionPrinter)
public override bool Equals(object? obj)
=> ReferenceEquals(this, obj) || (obj is SqlServerOpenJsonExpression openJsonExpression && Equals(openJsonExpression));
- private bool Equals(SqlServerOpenJsonExpression openJsonExpression)
- => base.Equals(openJsonExpression)
- && (ColumnInfos is null
- ? openJsonExpression.ColumnInfos is null
- : openJsonExpression.ColumnInfos is not null && ColumnInfos.SequenceEqual(openJsonExpression.ColumnInfos));
+ private bool Equals(SqlServerOpenJsonExpression other)
+ {
+ if (!base.Equals(other) || ColumnInfos?.Count != other.ColumnInfos?.Count)
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(ColumnInfos, other.ColumnInfos))
+ {
+ return true;
+ }
+
+ for (var i = 0; i < ColumnInfos!.Count; i++)
+ {
+ var (columnInfo, otherColumnInfo) = (ColumnInfos[i], other.ColumnInfos![i]);
+
+ if (columnInfo.Name != otherColumnInfo.Name
+ || !columnInfo.TypeMapping.Equals(otherColumnInfo.TypeMapping)
+ || (columnInfo.Path is null != otherColumnInfo.Path is null
+ || (columnInfo.Path is not null
+ && otherColumnInfo.Path is not null
+ && columnInfo.Path.SequenceEqual(otherColumnInfo.Path))))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
///
public override int GetHashCode()
@@ -165,5 +264,9 @@ public override int GetHashCode()
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public readonly record struct ColumnInfo(string Name, string StoreType, string? Path = null, bool AsJson = false);
+ public readonly record struct ColumnInfo(
+ string Name,
+ RelationalTypeMapping TypeMapping,
+ IReadOnlyList? Path = null,
+ bool AsJson = false);
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
index 22bf1c34c2f..730c2e8cc5b 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
@@ -418,8 +418,25 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
Visit(jsonScalarExpression.Json);
- Sql.Append(", '$");
- foreach (var pathSegment in jsonScalarExpression.Path)
+ Sql.Append(", ");
+ GenerateJsonPath(jsonScalarExpression.Path);
+ Sql.Append(")");
+
+ if (jsonScalarExpression.TypeMapping is not SqlServerJsonTypeMapping and not StringTypeMapping)
+ {
+ Sql.Append(" AS ");
+ Sql.Append(jsonScalarExpression.TypeMapping!.StoreType);
+ Sql.Append(")");
+ }
+
+ return jsonScalarExpression;
+ }
+
+ private void GenerateJsonPath(IReadOnlyList path)
+ {
+ Sql.Append("'$");
+
+ foreach (var pathSegment in path)
{
switch (pathSegment)
{
@@ -434,7 +451,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
// above; before that, arguments must be constant strings.
if (arrayIndex is SqlConstantExpression)
{
- Visit(pathSegment.ArrayIndex);
+ Visit(arrayIndex);
}
else if (_sqlServerCompatibilityLevel >= 140)
{
@@ -458,16 +475,7 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
}
}
- Sql.Append("')");
-
- if (jsonScalarExpression.TypeMapping is not SqlServerJsonTypeMapping and not StringTypeMapping)
- {
- Sql.Append(" AS ");
- Sql.Append(jsonScalarExpression.TypeMapping!.StoreType);
- Sql.Append(")");
- }
-
- return jsonScalarExpression;
+ Sql.Append("'");
}
///
@@ -480,11 +488,18 @@ protected virtual Expression VisitOpenJsonExpression(SqlServerOpenJsonExpression
{
// OPENJSON docs: https://learn.microsoft.com/sql/t-sql/functions/openjson-transact-sql
- // OPENJSON is a regular table-valued function with a special WITH clause at the end
- // Copy-paste from VisitTableValuedFunction, because that appends the 'AS ' but we need to insert WITH before that
+ // OPENJSON is a regular table-valued function with an optional special WITH clause at the end.
+ // The second argument is the JSON path, which can either be a regular SqlExpression, or a list of PathSegments, from which we
+ // generate a constant JSONPATH from.
Sql.Append("OPENJSON(");
- GenerateList(openJsonExpression.Arguments, e => Visit(e));
+ Visit(openJsonExpression.JsonExpression);
+
+ if (openJsonExpression.Path is not null)
+ {
+ Sql.Append(", ");
+ GenerateJsonPath(openJsonExpression.Path);
+ }
Sql.Append(")");
@@ -492,27 +507,43 @@ protected virtual Expression VisitOpenJsonExpression(SqlServerOpenJsonExpression
{
Sql.Append(" WITH (");
- for (var i = 0; i < openJsonExpression.ColumnInfos.Count; i++)
+ if (openJsonExpression.ColumnInfos is [var singleColumnInfo])
{
- var columnInfo = openJsonExpression.ColumnInfos[i];
+ GenerateColumnInfo(singleColumnInfo);
+ }
+ else
+ {
+ Sql.AppendLine();
+ using var _ = Sql.Indent();
- if (i > 0)
+ for (var i = 0; i < openJsonExpression.ColumnInfos.Count; i++)
{
- Sql.Append(", ");
+ var columnInfo = openJsonExpression.ColumnInfos[i];
+
+ if (i > 0)
+ {
+ Sql.AppendLine(",");
+ }
+
+ GenerateColumnInfo(columnInfo);
}
- Check.DebugAssert(columnInfo.StoreType is not null, "Unset OPENJSON column store type");
+ Sql.AppendLine();
+ }
+ Sql.Append(")");
+
+ void GenerateColumnInfo(SqlServerOpenJsonExpression.ColumnInfo columnInfo)
+ {
Sql
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(columnInfo.Name))
.Append(" ")
- .Append(columnInfo.StoreType);
+ .Append(columnInfo.TypeMapping.StoreType);
if (columnInfo.Path is not null)
{
- Sql
- .Append(" ")
- .Append(_typeMappingSource.GetMapping("varchar(max)").GenerateSqlLiteral(columnInfo.Path));
+ Sql.Append(" ");
+ GenerateJsonPath(columnInfo.Path);
}
if (columnInfo.AsJson)
@@ -520,8 +551,6 @@ protected virtual Expression VisitOpenJsonExpression(SqlServerOpenJsonExpression
Sql.Append(" AS JSON");
}
}
-
- Sql.Append(")");
}
Sql.Append(AliasSeparator).Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(openJsonExpression.Alias));
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs
index 5d86c0a7704..345646f7228 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs
@@ -2,9 +2,9 @@
// 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 System.Text;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
@@ -18,7 +18,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
///
public class SqlServerQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor
{
- private readonly OpenJsonPostprocessor _openJsonPostprocessor;
+ private readonly JsonPostprocessor _openJsonPostprocessor;
private readonly SkipWithoutOrderByInSplitQueryVerifier _skipWithoutOrderByInSplitQueryVerifier = new();
///
@@ -91,18 +91,22 @@ private sealed class SkipWithoutOrderByInSplitQueryVerifier : ExpressionVisitor
/// ordering still exists on the [key] column, i.e. when the ordering of the original JSON array needs to be preserved
/// (e.g. limit/offset).
///
- private sealed class OpenJsonPostprocessor : ExpressionVisitor
+ private sealed class JsonPostprocessor : ExpressionVisitor
{
private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
- private readonly Dictionary<(SqlServerOpenJsonExpression, string), RelationalTypeMapping> _castsToApply = new();
+ private readonly Dictionary<(SqlServerOpenJsonExpression, string), (SelectExpression, SqlServerOpenJsonExpression.ColumnInfo)> _columnsToRewrite = new();
- public OpenJsonPostprocessor(IRelationalTypeMappingSource typeMappingSource, ISqlExpressionFactory sqlExpressionFactory)
+ private RelationalTypeMapping? _nvarcharMaxTypeMapping, _nvarchar4000TypeMapping;
+
+ public JsonPostprocessor(
+ IRelationalTypeMappingSource typeMappingSource,
+ ISqlExpressionFactory sqlExpressionFactory)
=> (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, sqlExpressionFactory);
public Expression Process(Expression expression)
{
- _castsToApply.Clear();
+ _columnsToRewrite.Clear();
return Visit(expression);
}
@@ -176,6 +180,11 @@ public Expression Process(Expression expression)
}
_castsToApply.Add((newOpenJsonExpression, column.Name), typeMapping);
+
+jhjkshjkshfkjshkj
+ _columnsToRewrite.Add((newOpenJsonExpression, columnInfo.Name), new(newSelectExpression, columnInfo));
+fshjhsjkhfkjshjkfshkj
+
appliedCasts.Add((newOpenJsonExpression, column.Name));
}
@@ -203,18 +212,19 @@ public Expression Process(Expression expression)
: 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.
+ // clause. Then visit the select expression, replacing all matching ColumnExpressions - see below for the details.
var result = base.Visit(newSelectExpression);
foreach (var appliedCast in appliedCasts)
{
- _castsToApply.Remove(appliedCast);
+ _columnsToRewrite.Remove((newOpenJsonExpression, column.Name));
}
return result;
}
- case ColumnExpression columnExpression:
+ case ColumnExpression { Table: SqlServerOpenJsonExpression openJsonTable, Name: var name } columnExpression
+ when _columnsToRewrite.TryGetValue((openJsonTable, name), out var columnRewriteInfo):
{
return columnExpression.Table switch
{
@@ -226,6 +236,76 @@ when _castsToApply.TryGetValue((innerOpenJsonTable, columnExpression.Name), out
=> _sqlExpressionFactory.Convert(columnExpression, columnExpression.Type, innerTypeMapping),
_ => base.Visit(expression)
};
+ // We found a ColumnExpression that refers to the OPENJSON table, we need to rewrite it.
+
+ var (selectExpression, columnInfo) = columnRewriteInfo;
+
+ // The new OPENJSON (without WITH) always projects a `value` column, instead of a properly named column for individual
+ // values inside; create a new ColumnExpression with that name.
+ SqlExpression rewrittenColumn = selectExpression.CreateColumnExpression(
+ columnExpression.Table, "value", columnExpression.Type, _nvarcharMaxTypeMapping, columnExpression.IsNullable);
+
+ // If the WITH column info contained a path, we need to wrap the new column expression with a JSON_VALUE for that path.
+ if (columnInfo.Path is not (null or []))
+ {
+ if (columnInfo.AsJson)
+ {
+ throw new InvalidOperationException(
+ "IMPOSSIBLE. AS JSON signifies an owned sub-entity being projected out of OPENJSON/WITH. "
+ + "Columns referring to that must be wrapped be Json{Scalar,Query}Expression and will have been already " +
+ "dealt with below");
+ }
+
+ _nvarchar4000TypeMapping ??= _typeMappingSource.FindMapping("nvarchar(4000)");
+
+ rewrittenColumn = new JsonScalarExpression(
+ rewrittenColumn, columnInfo.Path, rewrittenColumn.Type, _nvarchar4000TypeMapping, columnExpression.IsNullable);
+ }
+
+ // OPENJSON with WITH specified the store type in the WITH, but the version without just always projects
+ // nvarchar(max); add a CAST to convert. Note that for AS JSON the type mapping is always nvarchar(max), and we don't
+ // need to add a CAST over the JSON_QUERY returned above.
+ if (columnInfo.TypeMapping.StoreType != "nvarchar(max)")
+ {
+ _nvarcharMaxTypeMapping ??= _typeMappingSource.FindMapping("nvarchar(max)");
+
+ rewrittenColumn = _sqlExpressionFactory.Convert(
+ rewrittenColumn,
+ columnExpression.Type,
+ columnInfo.TypeMapping);
+ }
+
+ return rewrittenColumn;
+ }
+
+ // JsonScalarExpression over a column coming out of OPENJSON/WITH; this means that the column represents an owned sub-
+ // entity, and therefore must have AS JSON. Rewrite the column and simply collapse the paths together.
+ case JsonScalarExpression
+ {
+ Json: ColumnExpression { Table: SqlServerOpenJsonExpression openJsonTable } columnExpression
+ } jsonScalarExpression
+ when _columnsToRewrite.TryGetValue((openJsonTable, columnExpression.Name), out var columnRewriteInfo):
+ {
+ var (selectExpression, columnInfo) = columnRewriteInfo;
+
+ Check.DebugAssert(
+ columnInfo.AsJson,
+ "JsonScalarExpression over a column coming out of OPENJSON is only valid when that column represents an owned "
+ + "sub-entity, which means it must have AS JSON");
+
+ // The new OPENJSON (without WITH) always projects a `value` column, instead of a properly named column for individual
+ // values inside; create a new ColumnExpression with that name.
+ SqlExpression rewrittenColumn = selectExpression.CreateColumnExpression(
+ columnExpression.Table, "value", columnExpression.Type, _nvarcharMaxTypeMapping, columnExpression.IsNullable);
+
+ // Prepend the path from the OPENJSON/WITH to the path in the JsonScalarExpression
+ var path = columnInfo.Path is null
+ ? jsonScalarExpression.Path
+ : columnInfo.Path.Concat(jsonScalarExpression.Path).ToList();
+
+ return new JsonScalarExpression(
+ rewrittenColumn, path, jsonScalarExpression.Type, jsonScalarExpression.TypeMapping,
+ jsonScalarExpression.IsNullable);
}
default:
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
index 8223af7a1a2..2f3ca276932 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
@@ -23,6 +23,8 @@ public class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQu
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly int _sqlServerCompatibilityLevel;
+ private RelationalTypeMapping? _nvarcharMaxTypeMapping;
+
///
/// 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
@@ -152,8 +154,8 @@ protected override Expression VisitExtension(Expression extensionExpression)
new SqlServerOpenJsonExpression.ColumnInfo
{
Name = "value",
- StoreType = elementTypeMapping.StoreType,
- Path = "$"
+ TypeMapping = elementTypeMapping,
+ Path = Array.Empty()
}
});
@@ -208,6 +210,94 @@ protected override Expression VisitExtension(Expression extensionExpression)
return new ShapedQueryExpression(selectExpression, shaperExpression);
}
+ ///
+ /// 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.
+ ///
+ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression)
+ {
+ // Calculate the table alias for the OPENJSON expression based on the last named path segment
+ // (or the JSON column name if there are none)
+ var lastNamedPathSegment = jsonQueryExpression.Path.LastOrDefault(ps => ps.PropertyName is not null);
+ var tableAlias = char.ToLowerInvariant((lastNamedPathSegment.PropertyName ?? jsonQueryExpression.JsonColumn.Name)[0]).ToString();
+
+ // We now add all of projected entity's the properties and navigations into the OPENJSON's WITH clause. Note that navigations
+ // get AS JSON, which projects out the JSON sub-document for them as text, which can be further navigated into.
+ var columnInfos = new List();
+
+ // We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys
+ foreach (var property in GetAllPropertiesInHierarchy(jsonQueryExpression.EntityType))
+ {
+ if (property.GetJsonPropertyName() is string jsonPropertyName)
+ {
+ columnInfos.Add(new()
+ {
+ Name = jsonPropertyName,
+ TypeMapping = property.GetRelationalTypeMapping(),
+ Path = new PathSegment[] { new(jsonPropertyName) }
+ });
+ }
+ }
+
+ foreach (var navigation in GetAllNavigationsInHierarchy(jsonQueryExpression.EntityType)
+ .Where(
+ n => n.ForeignKey.IsOwnership
+ && n.TargetEntityType.IsMappedToJson()
+ && n.ForeignKey.PrincipalToDependent == n))
+ {
+ var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName();
+ Check.DebugAssert(jsonNavigationName is not null, $"No JSON property name for navigation {navigation.Name}");
+
+ columnInfos.Add(new()
+ {
+ Name = jsonNavigationName,
+ TypeMapping = _nvarcharMaxTypeMapping ??= _typeMappingSource.FindMapping("nvarchar(max)")!,
+ Path = new PathSegment[] { new(jsonNavigationName) },
+ AsJson = true
+ });
+ }
+
+ var openJsonExpression = new SqlServerOpenJsonExpression(
+ tableAlias, jsonQueryExpression.JsonColumn, jsonQueryExpression.Path, columnInfos);
+
+ var selectExpression = new SelectExpression(jsonQueryExpression, openJsonExpression);
+
+ // See note on OPENJSON and ordering in TranslateCollection
+ selectExpression.AppendOrdering(
+ new OrderingExpression(
+ _sqlExpressionFactory.Convert(
+ selectExpression.CreateColumnExpression(
+ openJsonExpression,
+ "key",
+ typeof(string),
+ typeMapping: _typeMappingSource.FindMapping("nvarchar(4000)"),
+ columnNullable: false),
+ typeof(int),
+ _typeMappingSource.FindMapping(typeof(int))),
+ ascending: true));
+
+ return new ShapedQueryExpression(
+ selectExpression,
+ new RelationalEntityShaperExpression(
+ jsonQueryExpression.EntityType,
+ new ProjectionBindingExpression(
+ selectExpression,
+ new ProjectionMember(),
+ typeof(ValueBuffer)),
+ false));
+
+ // TODO: Move these to IEntityType?
+ static IEnumerable GetAllPropertiesInHierarchy(IEntityType entityType)
+ => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())
+ .SelectMany(t => t.GetDeclaredProperties());
+
+ static IEnumerable GetAllNavigationsInHierarchy(IEntityType entityType)
+ => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())
+ .SelectMany(t => t.GetDeclaredNavigations());
+ }
+
///
/// 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
@@ -293,6 +383,30 @@ protected override Expression VisitExtension(Expression extensionExpression)
return base.TranslateElementAtOrDefault(source, index, returnDefault);
}
+ ///
+ /// 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.
+ ///
+ protected override bool IsNaturallyOrdered(SelectExpression selectExpression)
+ => selectExpression is
+ {
+ Tables: [SqlServerOpenJsonExpression openJsonExpression, ..],
+ Orderings:
+ [
+ {
+ Expression: SqlUnaryExpression
+ {
+ OperatorType: ExpressionType.Convert,
+ Operand: ColumnExpression { Name: "key", Table: var orderingTable }
+ },
+ IsAscending: true
+ }
+ ]
+ }
+ && orderingTable == openJsonExpression;
+
///
/// 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
@@ -477,8 +591,10 @@ protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpress
return openJsonExpression;
}
- Check.DebugAssert(openJsonExpression.Path is null, "openJsonExpression.Path is null");
- Check.DebugAssert(openJsonExpression.ColumnInfos is null, "Invalid SqlServerOpenJsonExpression");
+ Check.DebugAssert(
+ openJsonExpression.Path is null, "OpenJsonExpression path is non-null when applying an inferred type mapping");
+ Check.DebugAssert(
+ openJsonExpression.ColumnInfos is null, "OpenJsonExpression has no ColumnInfos when applying an inferred type mapping");
// We need to apply the inferred type mapping in two places: the collection type mapping on the parameter expanded by OPENJSON,
// and on the WITH clause determining the conversion out on the SQL Server side
@@ -496,7 +612,7 @@ protected virtual SqlServerOpenJsonExpression ApplyTypeMappingsOnOpenJsonExpress
return openJsonExpression.Update(
parameterExpression.ApplyTypeMapping(parameterTypeMapping),
path: null,
- new[] { new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping.StoreType, "$") });
+ new[] { new SqlServerOpenJsonExpression.ColumnInfo("value", elementTypeMapping, Array.Empty()) });
}
}
}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
index 8e0622a64ae..656d144b856 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
@@ -43,6 +43,10 @@ protected override Expression VisitExtension(Expression extensionExpression)
GenerateRegexp(regexpExpression);
return extensionExpression;
+ case JsonEachExpression jsonEachExpression:
+ GenerateJsonEach(jsonEachExpression);
+ return extensionExpression;
+
default:
return base.VisitExtension(extensionExpression);
}
@@ -123,6 +127,76 @@ private void GenerateRegexp(RegexpExpression regexpExpression, bool negated = fa
Visit(regexpExpression.Pattern);
}
+ ///
+ /// 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.
+ ///
+ protected virtual void GenerateJsonEach(JsonEachExpression jsonEachExpression)
+ {
+ // json_each docs: https://www.sqlite.org/json1.html#jeach
+
+ // json_each is a regular table-valued function; however, since it accepts an (optional) JSONPATH argument - which we represent
+ // as IReadOnlyList, and that can only be rendered as a string here in the QuerySqlGenerator, we have a special
+ // expression type for it.
+ Sql.Append("json_each(");
+
+ Visit(jsonEachExpression.JsonExpression);
+
+ var path = jsonEachExpression.Path;
+
+ if (path is not null)
+ {
+ Sql.Append(", ");
+
+ // Note the difference with the JSONPATH rendering in VisitJsonScalar below, where we take advantage of SQLite's ->> operator
+ // (we can't do that here).
+ Sql.Append("'$");
+
+ var inJsonpathString = true;
+
+ for (var i = 0; i < path.Count; i++)
+ {
+ switch (path[i])
+ {
+ case { PropertyName: string propertyName }:
+ Sql.Append(".").Append(propertyName);
+ break;
+
+ case { ArrayIndex: SqlExpression arrayIndex }:
+ Sql.Append("[");
+
+ if (arrayIndex is SqlConstantExpression)
+ {
+ Visit(arrayIndex);
+ }
+ else
+ {
+ Sql.Append("' || ");
+ Visit(arrayIndex);
+ Sql.Append(" || '");
+ }
+
+ Sql.Append("]");
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ if (inJsonpathString)
+ {
+ Sql.Append("'");
+ }
+ }
+
+ Sql.Append(")");
+
+ Sql.Append(AliasSeparator).Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(jsonEachExpression.Alias));
+ }
+
///
/// 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
@@ -131,16 +205,15 @@ private void GenerateRegexp(RegexpExpression regexpExpression, bool negated = fa
///
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
{
+ Visit(jsonScalarExpression.Json);
+
// TODO: Stop producing empty JsonScalarExpressions, #30768
var path = jsonScalarExpression.Path;
if (path.Count == 0)
{
- Visit(jsonScalarExpression.Json);
return jsonScalarExpression;
}
- Visit(jsonScalarExpression.Json);
-
var inJsonpathString = false;
for (var i = 0; i < path.Count; i++)
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
index e13d8407632..929fcd9c39e 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
@@ -4,6 +4,7 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Sqlite.Internal;
+using Microsoft.EntityFrameworkCore.Sqlite.Query.SqlExpressions.Internal;
using Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;
namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
@@ -54,6 +55,15 @@ protected SqliteQueryableMethodTranslatingExpressionVisitor(
_areJsonFunctionsSupported = parentVisitor._areJsonFunctionsSupported;
}
+ ///
+ /// 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.
+ ///
+ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor()
+ => new SqliteQueryableMethodTranslatingExpressionVisitor(this);
+
///
/// 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
@@ -90,15 +100,6 @@ protected SqliteQueryableMethodTranslatingExpressionVisitor(
return base.TranslateAny(source, predicate);
}
- ///
- /// 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.
- ///
- protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor()
- => new SqliteQueryableMethodTranslatingExpressionVisitor(this);
-
///
/// 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
@@ -215,7 +216,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
}
var elementClrType = sqlExpression.Type.GetSequenceType();
- var jsonEachExpression = new TableValuedFunctionExpression(tableAlias, "json_each", new[] { sqlExpression });
+ var jsonEachExpression = new JsonEachExpression(tableAlias, sqlExpression);
// 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();
@@ -250,7 +251,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
"key",
typeof(int),
typeMapping: _typeMappingSource.FindMapping(typeof(int)),
- isColumnNullable),
+ columnNullable: false),
ascending: true));
Expression shaperExpression = new ProjectionBindingExpression(
@@ -268,6 +269,146 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
return new ShapedQueryExpression(selectExpression, shaperExpression);
}
+ ///
+ /// 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.
+ ///
+ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpression jsonQueryExpression)
+ {
+ var entityType = jsonQueryExpression.EntityType;
+ var textTypeMapping = _typeMappingSource.FindMapping(typeof(string));
+
+ // TODO: Refactor this out
+ // Calculate the table alias for the json_each expression based on the last named path segment
+ // (or the JSON column name if there are none)
+ var lastNamedPathSegment = jsonQueryExpression.Path.LastOrDefault(ps => ps.PropertyName is not null);
+ var tableAlias = char.ToLowerInvariant((lastNamedPathSegment.PropertyName ?? jsonQueryExpression.JsonColumn.Name)[0]).ToString();
+
+ // Handling a non-primitive JSON array is complicated on SQLite; unlike SQL Server OPENJSON and PostgreSQL jsonb_to_recordset,
+ // SQLite's json_each can only project elements of the array, and not properties within those elements. For example:
+ // SELECT value FROM json_each('[{"a":1,"b":"foo"}, {"a":2,"b":"bar"}]')
+ // This will return two rows, each with a string column representing an array element (i.e. {"a":1,"b":"foo"}). To decompose that
+ // into a and b columns, a further extraction is needed:
+ // SELECT value ->> 'a' AS a, value ->> 'b' AS b FROM json_each('[{"a":1,"b":"foo"}, {"a":2,"b":"bar"}]')
+
+ // We therefore generate a minimal subquery projecting out all the properties and navigations, wrapped by a SelectExpression
+ // containing that:
+ // SELECT ...
+ // FROM (SELECT value ->> 'a' AS a, value ->> 'b' AS b FROM json_each(, )) AS j
+ // WHERE j.a = 8;
+
+ // Unfortunately, while the subquery projects the entity, our EntityProjectionExpression currently supports only bare
+ // ColumnExpression (the above requires JsonScalarExpression). So we hack as if the subquery projects an anonymous type instead,
+ // with a member for each JSON property that needs to be projected. We then wrap it with a SelectExpression the projects a proper
+ // EntityProjectionExpression.
+
+ var jsonEachExpression = new JsonEachExpression(tableAlias, jsonQueryExpression.JsonColumn, jsonQueryExpression.Path);
+
+ var selectExpression = new SelectExpression(jsonQueryExpression, jsonEachExpression);
+
+ selectExpression.AppendOrdering(
+ new OrderingExpression(
+ selectExpression.CreateColumnExpression(
+ jsonEachExpression,
+ "key",
+ typeof(int),
+ typeMapping: _typeMappingSource.FindMapping(typeof(int)),
+ columnNullable: false),
+ ascending: true));
+
+ var propertyJsonScalarExpression = new Dictionary();
+
+ var jsonColumn = selectExpression.CreateColumnExpression(
+ jsonEachExpression, "value", typeof(string), _typeMappingSource.FindMapping(typeof(string))); // TODO: nullable?
+
+ var containerColumnName = entityType.GetContainerColumnName();
+ Check.DebugAssert(containerColumnName is not null, "JsonQueryExpression to entity type without a container column name");
+
+ // First step: build a SelectExpression that will execute json_each and project all properties and navigations out, e.g.
+ // (SELECT value ->> 'a' AS a, value ->> 'b' AS b FROM json_each(c."JsonColumn", '$.Something.SomeCollection')
+
+ // We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys
+ foreach (var property in GetAllPropertiesInHierarchy(entityType))
+ {
+ if (property.GetJsonPropertyName() is string jsonPropertyName)
+ {
+ // HACK: currently the only way to project multiple values from a SelectExpression is to simulate a Select out to an anonymous
+ // type; this requires the MethodInfos of the anonymous type properties, from which the projection alias gets taken.
+ // So we create fake members to hold the JSON property name for the alias.
+ var projectionMember = new ProjectionMember().Append(new FakeMemberInfo(jsonPropertyName));
+
+ propertyJsonScalarExpression[projectionMember] = new JsonScalarExpression(
+ jsonColumn,
+ new[] { new PathSegment(property.GetJsonPropertyName()!) },
+ property.ClrType.UnwrapNullableType(),
+ property.GetRelationalTypeMapping(),
+ property.IsNullable);
+ }
+ }
+
+ foreach (var navigation in GetAllNavigationsInHierarchy(jsonQueryExpression.EntityType)
+ .Where(
+ n => n.ForeignKey.IsOwnership
+ && n.TargetEntityType.IsMappedToJson()
+ && n.ForeignKey.PrincipalToDependent == n))
+ {
+ var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName();
+ Check.DebugAssert(jsonNavigationName is not null, "Invalid navigation found on JSON-mapped entity");
+
+ var projectionMember = new ProjectionMember().Append(new FakeMemberInfo(jsonNavigationName));
+
+ propertyJsonScalarExpression[projectionMember] = new JsonScalarExpression(
+ jsonColumn,
+ new[] { new PathSegment(jsonNavigationName) },
+ typeof(string),
+ textTypeMapping,
+ !navigation.ForeignKey.IsRequiredDependent);
+ }
+
+ selectExpression.ReplaceProjection(propertyJsonScalarExpression);
+
+ // Second step: push the above SelectExpression down to a subquery, and project an entity projection from the outer
+ // SelectExpression, i.e.
+ // SELECT "t"."a", "t"."b"
+ // FROM (SELECT value ->> 'a' ... FROM json_each(...))
+
+ selectExpression.PushdownIntoSubquery();
+ var subquery = selectExpression.Tables[0];
+
+ var newOuterSelectExpression = new SelectExpression(jsonQueryExpression, subquery);
+
+ newOuterSelectExpression.AppendOrdering(
+ new OrderingExpression(
+ selectExpression.CreateColumnExpression(
+ subquery,
+ "key",
+ typeof(int),
+ typeMapping: _typeMappingSource.FindMapping(typeof(int)),
+ columnNullable: false),
+ ascending: true));
+
+ return new ShapedQueryExpression(
+ newOuterSelectExpression,
+ new RelationalEntityShaperExpression(
+ jsonQueryExpression.EntityType,
+ new ProjectionBindingExpression(
+ newOuterSelectExpression,
+ new ProjectionMember(),
+ typeof(ValueBuffer)),
+ false));
+
+ // TODO: Move these to IEntityType?
+ static IEnumerable GetAllPropertiesInHierarchy(IEntityType entityType)
+ => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())
+ .SelectMany(t => t.GetDeclaredProperties());
+
+ static IEnumerable GetAllNavigationsInHierarchy(IEntityType entityType)
+ => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())
+ .SelectMany(t => t.GetDeclaredNavigations());
+ }
+
///
/// 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
@@ -335,6 +476,35 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
return base.TranslateElementAtOrDefault(source, index, returnDefault);
}
+ ///
+ /// 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.
+ ///
+ protected override bool IsNaturallyOrdered(SelectExpression selectExpression)
+ {
+ return selectExpression is
+ {
+ Tables: [var mainTable, ..],
+ Orderings:
+ [
+ {
+ Expression: ColumnExpression { Name: "key", Table: var orderingTable } orderingColumn,
+ IsAscending: true
+ }
+ ]
+ }
+ && orderingTable == mainTable
+ && IsJsonEachKeyColumn(orderingColumn);
+
+ bool IsJsonEachKeyColumn(ColumnExpression orderingColumn)
+ => orderingColumn.Table is JsonEachExpression
+ || (orderingColumn.Table is SelectExpression subquery
+ && subquery.Projection.FirstOrDefault(p => p.Alias == "key")?.Expression is ColumnExpression projectedColumn
+ && IsJsonEachKeyColumn(projectedColumn));
+ }
+
private static Type GetProviderType(SqlExpression expression)
=> expression.TypeMapping?.Converter?.ProviderClrType
?? expression.TypeMapping?.ClrType
@@ -531,4 +701,22 @@ private static SqlExpression ApplyJsonSqlConversion(
_ => expression
};
+
+ private class FakeMemberInfo : MemberInfo
+ {
+ public FakeMemberInfo(string name)
+ => Name = name;
+
+ public override string Name { get; }
+
+ public override object[] GetCustomAttributes(bool inherit)
+ => throw new NotSupportedException();
+ public override object[] GetCustomAttributes(Type attributeType, bool inherit)
+ => throw new NotSupportedException();
+ public override bool IsDefined(Type attributeType, bool inherit)
+ => throw new NotSupportedException();
+ public override Type? DeclaringType => throw new NotSupportedException();
+ public override MemberTypes MemberType => throw new NotSupportedException();
+ public override Type? ReflectedType => throw new NotSupportedException();
+ }
}
diff --git a/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/JsonEachExpression.cs b/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/JsonEachExpression.cs
new file mode 100644
index 00000000000..0a45c8c784d
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Query/SqlExpressions/Internal/JsonEachExpression.cs
@@ -0,0 +1,185 @@
+// 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.Query.SqlExpressions;
+
+namespace Microsoft.EntityFrameworkCore.Sqlite.Query.SqlExpressions.Internal;
+
+///
+/// An expression that represents a SQLite json_each function call in a SQL tree.
+///
+///
+///
+/// See json_each for more information and examples.
+///
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+///
+public class JsonEachExpression : TableValuedFunctionExpression, IClonableTableExpressionBase
+{
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual SqlExpression JsonExpression
+ => Arguments[0];
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IReadOnlyList? Path { get; }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public JsonEachExpression(
+ string alias,
+ SqlExpression jsonExpression,
+ IReadOnlyList? path = null)
+ : base(alias, "json_each", schema: null, builtIn: true, new[] { jsonExpression })
+ {
+ Path = path;
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ var visitedJsonExpression = (SqlExpression)visitor.Visit(JsonExpression);
+
+ PathSegment[]? visitedPath = null;
+
+ if (Path is not null)
+ {
+ for (var i = 0; i < Path.Count; i++)
+ {
+ var segment = Path[i];
+ PathSegment newSegment;
+
+ if (segment.PropertyName is not null)
+ {
+ // PropertyName segments are (currently) constants, nothing to visit.
+ newSegment = segment;
+ }
+ else
+ {
+ var newArrayIndex = (SqlExpression)visitor.Visit(segment.ArrayIndex)!;
+ if (newArrayIndex == segment.ArrayIndex)
+ {
+ newSegment = segment;
+ }
+ else
+ {
+ newSegment = new PathSegment(newArrayIndex);
+
+ if (visitedPath is null)
+ {
+ visitedPath = new PathSegment[Path.Count];
+ for (var j = 0; j < i; i++)
+ {
+ visitedPath[j] = Path[j];
+ }
+ }
+ }
+ }
+
+ if (visitedPath is not null)
+ {
+ visitedPath[i] = newSegment;
+ }
+ }
+ }
+
+ return Update(visitedJsonExpression, visitedPath ?? Path);
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual JsonEachExpression Update(
+ SqlExpression jsonExpression,
+ IReadOnlyList? path)
+ => jsonExpression == JsonExpression
+ && (ReferenceEquals(path, Path) || path is not null && Path is not null && path.SequenceEqual(Path))
+ ? this
+ : new JsonEachExpression(Alias, jsonExpression, path);
+
+ ///
+ /// 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.
+ ///
+ // TODO: Deep clone, see #30982
+ public virtual TableExpressionBase Clone()
+ {
+ var clone = new JsonEachExpression(Alias, JsonExpression, Path);
+
+ foreach (var annotation in GetAnnotations())
+ {
+ clone.AddAnnotation(annotation.Name, annotation.Value);
+ }
+
+ return clone;
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append(Name);
+ expressionPrinter.Append("(");
+ expressionPrinter.Visit(JsonExpression);
+
+ if (Path is not null)
+ {
+ expressionPrinter
+ .Append(", '")
+ .Append(string.Join(".", Path.Select(e => e.ToString())))
+ .Append("'");
+ }
+
+ expressionPrinter.Append(")");
+
+ PrintAnnotations(expressionPrinter);
+
+ expressionPrinter.Append(" AS ");
+ expressionPrinter.Append(Alias);
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => ReferenceEquals(this, obj) || (obj is JsonEachExpression jsonEachExpression && Equals(jsonEachExpression));
+
+ private bool Equals(JsonEachExpression other)
+ => base.Equals(other)
+ && (ReferenceEquals(Path, other.Path)
+ || (Path is not null && other.Path is not null && Path.SequenceEqual(other.Path)));
+
+ ///
+ public override int GetHashCode()
+ => base.GetHashCode();
+}
diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs
index a7a9047061f..7fa889511ef 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryFixtureBase.cs
@@ -63,12 +63,15 @@ public virtual ISetSource GetExpectedData()
Assert.Equal(ee.Id, aa.Id);
Assert.Equal(ee.Name, aa.Name);
- AssertOwnedRoot(ee.OwnedReferenceRoot, aa.OwnedReferenceRoot);
-
- Assert.Equal(ee.OwnedCollectionRoot.Count, aa.OwnedCollectionRoot.Count);
- for (var i = 0; i < ee.OwnedCollectionRoot.Count; i++)
+ if (ee.OwnedReferenceRoot is not null || aa.OwnedReferenceRoot is not null)
{
- AssertOwnedRoot(ee.OwnedCollectionRoot[i], aa.OwnedCollectionRoot[i]);
+ AssertOwnedRoot(ee.OwnedReferenceRoot, aa.OwnedReferenceRoot);
+
+ Assert.Equal(ee.OwnedCollectionRoot.Count, aa.OwnedCollectionRoot.Count);
+ for (var i = 0; i < ee.OwnedCollectionRoot.Count; i++)
+ {
+ AssertOwnedRoot(ee.OwnedCollectionRoot[i], aa.OwnedCollectionRoot[i]);
+ }
}
}
}
@@ -320,7 +323,7 @@ public virtual ISetSource GetExpectedData()
},
}.ToDictionary(e => e.Key, e => (object)e.Value);
- private static void AssertOwnedRoot(JsonOwnedRoot expected, JsonOwnedRoot actual)
+ public static void AssertOwnedRoot(JsonOwnedRoot expected, JsonOwnedRoot actual)
{
Assert.Equal(expected.Name, actual.Name);
Assert.Equal(expected.Number, actual.Number);
@@ -335,7 +338,7 @@ private static void AssertOwnedRoot(JsonOwnedRoot expected, JsonOwnedRoot actual
}
}
- private static void AssertOwnedBranch(JsonOwnedBranch expected, JsonOwnedBranch actual)
+ public static void AssertOwnedBranch(JsonOwnedBranch expected, JsonOwnedBranch actual)
{
Assert.Equal(expected.Date, actual.Date);
Assert.Equal(expected.Fraction, actual.Fraction);
@@ -352,7 +355,7 @@ private static void AssertOwnedBranch(JsonOwnedBranch expected, JsonOwnedBranch
}
}
- private static void AssertOwnedLeaf(JsonOwnedLeaf expected, JsonOwnedLeaf actual)
+ public static void AssertOwnedLeaf(JsonOwnedLeaf expected, JsonOwnedLeaf actual)
=> Assert.Equal(expected.SomethingSomething, actual.SomethingSomething);
public static void AssertCustomNameRoot(JsonOwnedCustomNameRoot expected, JsonOwnedCustomNameRoot actual)
diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs
index e727769044f..16cee0af707 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs
@@ -795,28 +795,28 @@ public virtual Task Json_entity_backtracking(bool async)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_basic(bool async)
+ public virtual Task Json_collection_index_in_projection_basic(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot[1]).AsNoTracking());
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_ElementAt(bool async)
+ public virtual Task Json_collection_ElementAt_in_projection(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAt(1)).AsNoTracking());
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_ElementAtOrDefault(bool async)
+ public virtual Task Json_collection_ElementAtOrDefault_in_projection(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot.AsQueryable().ElementAtOrDefault(1)).AsNoTracking());
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_project_collection(bool async)
+ public virtual Task Json_collection_index_in_projection_project_collection(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot[1].OwnedCollectionBranch).AsNoTracking(),
@@ -824,7 +824,7 @@ public virtual Task Json_collection_element_access_in_projection_project_collect
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_ElementAt_project_collection(bool async)
+ public virtual Task Json_collection_ElementAt_project_collection(bool async)
=> AssertQuery(
async,
ss => ss.Set()
@@ -834,7 +834,7 @@ public virtual Task Json_collection_element_access_in_projection_using_ElementAt
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(bool async)
+ public virtual Task Json_collection_ElementAtOrDefault_project_collection(bool async)
=> AssertQuery(
async,
ss => ss.Set()
@@ -844,7 +844,7 @@ public virtual Task Json_collection_element_access_in_projection_using_ElementAt
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_parameter(bool async)
+ public virtual Task Json_collection_index_in_projection_using_parameter(bool async)
{
var prm = 0;
@@ -855,7 +855,7 @@ public virtual Task Json_collection_element_access_in_projection_using_parameter
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_column(bool async)
+ public virtual Task Json_collection_index_in_projection_using_column(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot[x.Id]).AsNoTracking());
@@ -865,7 +865,7 @@ private static int MyMethod(int value)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_in_projection_using_untranslatable_client_method(bool async)
+ public virtual async Task Json_collection_index_in_projection_using_untranslatable_client_method(bool async)
{
var message = (await Assert.ThrowsAsync(
() => AssertQuery(
@@ -879,7 +879,7 @@ public virtual async Task Json_collection_element_access_in_projection_using_unt
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_in_projection_using_untranslatable_client_method2(bool async)
+ public virtual async Task Json_collection_index_in_projection_using_untranslatable_client_method2(bool async)
{
var message = (await Assert.ThrowsAsync(
() => AssertQuery(
@@ -893,7 +893,7 @@ public virtual async Task Json_collection_element_access_in_projection_using_unt
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_outside_bounds(bool async)
+ public virtual Task Json_collection_index_outside_bounds(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => x.OwnedCollectionRoot[25]).AsNoTracking(),
@@ -909,7 +909,7 @@ public virtual Task Json_collection_element_access_outside_bounds2(bool async)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_outside_bounds_with_property_access(bool async)
+ public virtual Task Json_collection_index_outside_bounds_with_property_access(bool async)
=> AssertQueryScalar(
async,
ss => ss.Set().OrderBy(x => x.Id).Select(x => (int?)x.OwnedCollectionRoot[25].Number),
@@ -917,7 +917,7 @@ public virtual Task Json_collection_element_access_outside_bounds_with_property_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_nested(bool async)
+ public virtual Task Json_collection_index_in_projection_nested(bool async)
{
var prm = 1;
@@ -928,7 +928,7 @@ public virtual Task Json_collection_element_access_in_projection_nested(bool asy
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_nested_project_scalar(bool async)
+ public virtual Task Json_collection_index_in_projection_nested_project_scalar(bool async)
{
var prm = 1;
@@ -939,7 +939,7 @@ public virtual Task Json_collection_element_access_in_projection_nested_project_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_nested_project_reference(bool async)
+ public virtual Task Json_collection_index_in_projection_nested_project_reference(bool async)
{
var prm = 1;
@@ -950,7 +950,7 @@ public virtual Task Json_collection_element_access_in_projection_nested_project_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_nested_project_collection(bool async)
+ public virtual Task Json_collection_index_in_projection_nested_project_collection(bool async)
{
var prm = 1;
@@ -966,7 +966,7 @@ public virtual Task Json_collection_element_access_in_projection_nested_project_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(bool async)
+ public virtual Task Json_collection_index_in_projection_nested_project_collection_anonymous_projection(bool async)
{
var prm = 1;
@@ -985,14 +985,14 @@ public virtual Task Json_collection_element_access_in_projection_nested_project_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_constant(bool async)
+ public virtual Task Json_collection_index_in_predicate_using_constant(bool async)
=> AssertQueryScalar(
async,
ss => ss.Set().Where(x => x.OwnedCollectionRoot[0].Name != "Foo").Select(x => x.Id));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_variable(bool async)
+ public virtual Task Json_collection_index_in_predicate_using_variable(bool async)
{
var prm = 1;
@@ -1003,7 +1003,7 @@ public virtual Task Json_collection_element_access_in_predicate_using_variable(b
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_column(bool async)
+ public virtual Task Json_collection_index_in_predicate_using_column(bool async)
=> AssertQuery(
async,
ss => ss.Set().Where(x => x.OwnedCollectionRoot[x.Id].Name == "e1_c2").Select(x => new { x.Id, x }),
@@ -1017,7 +1017,7 @@ public virtual Task Json_collection_element_access_in_predicate_using_column(boo
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_complex_expression1(bool async)
+ public virtual Task Json_collection_index_in_predicate_using_complex_expression1(bool async)
=> AssertQuery(
async,
ss => ss.Set().Where(x => x.OwnedCollectionRoot[x.Id == 1 ? 0 : 1].Name == "e1_c1").Select(x => new { x.Id, x }),
@@ -1031,7 +1031,7 @@ public virtual Task Json_collection_element_access_in_predicate_using_complex_ex
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_complex_expression2(bool async)
+ public virtual Task Json_collection_index_in_predicate_using_complex_expression2(bool async)
=> AssertQuery(
async,
ss => ss.Set().Where(x => x.OwnedCollectionRoot[ss.Set().Max(x => x.Id)].Name == "e1_c2").Select(x => new { x.Id, x }),
@@ -1045,14 +1045,14 @@ public virtual Task Json_collection_element_access_in_predicate_using_complex_ex
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_using_ElementAt(bool async)
+ public virtual Task Json_collection_ElementAt_in_predicate(bool async)
=> AssertQueryScalar(
async,
ss => ss.Set().Where(x => x.OwnedCollectionRoot.AsQueryable().ElementAt(1).Name != "Foo").Select(x => x.Id));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_predicate_nested_mix(bool async)
+ public virtual Task Json_collection_index_in_predicate_nested_mix(bool async)
{
var prm = 0;
@@ -1064,7 +1064,7 @@ public virtual Task Json_collection_element_access_in_predicate_nested_mix(bool
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_manual_Element_at_and_pushdown(bool async)
+ public virtual Task Json_collection_ElementAt_and_pushdown(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => new
@@ -1075,101 +1075,143 @@ public virtual Task Json_collection_element_access_manual_Element_at_and_pushdow
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative(bool async)
- {
- var prm = 0;
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
- {
- x.Id,
- CollectionElement = x.OwnedCollectionRoot[prm].OwnedCollectionBranch.Select(xx => "Foo").ElementAt(0)
- })))).Message;
+ public virtual Task Json_collection_Any_with_predicate(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(j => j.OwnedReferenceRoot.OwnedCollectionBranch.Any(b => b.OwnedReferenceLeaf.SomethingSomething == "e1_c2_c1_c1")));
+ // TODO: Need entries
- Assert.Equal(CoreStrings.TranslationFailed("j.OwnedCollectionRoot Q-> [__prm_0].OwnedCollectionBranch"), message);
- }
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_Where_ElementAt(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(j =>
+ j.OwnedReferenceRoot.OwnedCollectionBranch
+ .Where(o => o.Enum == JsonEnum.Three)
+ .ElementAt(0).OwnedReferenceLeaf.SomethingSomething == "e1_r_c2_r"),
+ entryCount: 40);
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative2(bool async)
- {
- var prm = 0;
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
- {
- x.Id,
- CollectionElement = x.OwnedCollectionRoot[prm + x.Id].OwnedCollectionBranch.Select(xx => x.Id).ElementAt(0)
- })))).Message;
+ public virtual Task Json_collection_Skip(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Where(j => j.OwnedReferenceRoot.OwnedCollectionBranch
+ .Skip(1)
+ .ElementAt(0).OwnedReferenceLeaf.SomethingSomething == "e1_r_c2_r"),
+ entryCount: 40);
- Assert.Equal(CoreStrings.TranslationFailed("j.OwnedCollectionRoot Q-> [(...)].OwnedCollectionBranch"), message);
- }
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_OrderByDescending_Skip_ElementAt(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Where(j => j.OwnedReferenceRoot.OwnedCollectionBranch
+ .OrderByDescending(b => b.Date)
+ .Skip(1)
+ .ElementAt(0).OwnedReferenceLeaf.SomethingSomething == "e1_r_c1_r"),
+ entryCount: 40);
+ // If this test is failing because of DistinctAfterOrderByWithoutRowLimitingOperatorWarning, this is because EF warns/errors by
+ // default for Distinct after OrderBy (without Skip/Take); but you likely have a naturally-ordered JSON collection, where the
+ // ordering has been added by the provider as part of the collection translation.
+ // Consider overriding RelationalQueryableMethodTranslatingExpressionVisitor.IsNaturallyOrdered() to identify such naturally-ordered
+ // collections, exempting them from the warning.
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative3(bool async)
- {
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
- {
- x.Id,
- CollectionElement = x.OwnedCollectionRoot.Select(xx => x.OwnedReferenceRoot).ElementAt(0)
- })))).Message;
+ public virtual Task Json_collection_Distinct_Count_with_predicate(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Where(j => j.OwnedReferenceRoot.OwnedCollectionBranch
+ .Distinct()
+ .Count(b => b.OwnedReferenceLeaf.SomethingSomething == "e1_r_c2_r") == 1),
+ entryCount: 40);
- Assert.Equal(CoreStrings.TranslationFailed("j.OwnedCollectionRoot Q-> "), message);
- }
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Json_collection_within_collection_Count(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set()
+ .Where(j => j.OwnedCollectionRoot.Any(c => c.OwnedCollectionBranch.Count == 2)),
+ entryCount: 40);
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative4(bool async)
+ public virtual async Task Json_collection_index_with_parameter_Select_ElementAt(bool async)
{
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
- {
- x.Id,
- CollectionElement = x.OwnedCollectionRoot.Select(xx => x.OwnedCollectionRoot).ElementAt(0)
- })))).Message;
+ var prm = 0;
- Assert.Equal(CoreStrings.TranslationFailed("j.OwnedCollectionRoot Q-> "), message);
+ await AssertQuery(
+ async,
+ ss => ss.Set().Select(
+ x => new { x.Id, CollectionElement = x.OwnedCollectionRoot[prm].OwnedCollectionBranch.Select(xx => "Foo").ElementAt(0) }));
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative5(bool async)
+ public virtual async Task Json_collection_index_with_expression_Select_ElementAt(bool async)
{
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
- {
- x.Id,
- CollectionElement = x.OwnedCollectionRoot.Select(xx => new { xx.OwnedReferenceBranch }).ElementAt(0)
- })))).Message;
+ var prm = 0;
- Assert.Equal(CoreStrings.TranslationFailed("j.OwnedCollectionRoot Q-> "), message);
+ await AssertQuery(
+ async,
+ ss => ss.Set().Select(
+ j => j.OwnedCollectionRoot[prm + j.Id].OwnedCollectionBranch
+ .Select(b => b.OwnedReferenceLeaf.SomethingSomething)
+ .ElementAt(0)),
+ ss => ss.Set().Select(
+ j => j.OwnedCollectionRoot.Count > prm + j.Id
+ ? j.OwnedCollectionRoot[prm + j.Id].OwnedCollectionBranch
+ .Select(b => b.OwnedReferenceLeaf.SomethingSomething)
+ .ElementAt(0)
+ : null));
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative6(bool async)
- {
- var message = (await Assert.ThrowsAsync(
- () => AssertQuery(
- async,
- ss => ss.Set().Select(x => new
+ public virtual async Task Json_collection_Select_entity_collection_ElementAt(bool async)
+ => await AssertQuery(
+ async,
+ ss => ss.Set()
+ .AsNoTracking()
+ .Select(x => x.OwnedCollectionRoot.Select(xx => xx.OwnedCollectionBranch).ElementAt(0)),
+ elementAsserter: (e, a) =>
+ {
+ Assert.Equal(e.Count, a.Count);
+ for (var i = 0; i < e.Count; i++)
{
- x.Id,
- CollectionElement = x.OwnedCollectionRoot.Select(xx => new JsonEntityBasic { Id = x.Id }).ElementAt(0)
- })))).Message;
+ JsonQueryFixtureBase.AssertOwnedBranch(e[i], a[i]);
+ }
+ });
- Assert.Equal(CoreStrings.TranslationFailed("j.OwnedCollectionRoot Q-> "), message);
- }
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Json_collection_Select_entity_ElementAt(bool async)
+ => await AssertQuery(
+ async,
+ ss => ss.Set().AsNoTracking().Select(x =>
+ x.OwnedCollectionRoot.Select(xx => xx.OwnedReferenceBranch).ElementAt(0)));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Json_collection_Select_entity_in_anonymous_object_ElementAt(bool async)
+ => await AssertQuery(
+ async,
+ ss => ss.Set().AsNoTracking().Select(x =>
+ x.OwnedCollectionRoot.Select(xx => new { xx.OwnedReferenceBranch }).ElementAt(0)));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Json_collection_Select_entity_with_initializer_ElementAt(bool async)
+ => await AssertQuery(
+ async,
+ ss => ss.Set().Select(
+ x => x.OwnedCollectionRoot.Select(xx => new JsonEntityBasic { Id = x.Id }).ElementAt(0)));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
@@ -1246,7 +1288,7 @@ public virtual Task Json_projection_deduplication_with_collection_in_original_an
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_constant_when_owner_is_present(bool async)
+ public virtual Task Json_collection_index_in_projection_using_constant_when_owner_is_present(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => new
@@ -1280,7 +1322,7 @@ public virtual Task Json_collection_element_access_in_projection_using_constant_
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_using_parameter_when_owner_is_present(bool async)
+ public virtual Task Json_collection_index_in_projection_using_parameter_when_owner_is_present(bool async)
{
var prm = 1;
@@ -1322,7 +1364,7 @@ public virtual Task Json_collection_element_access_in_projection_using_parameter
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_after_collection_element_access_in_projection_using_constant_when_owner_is_present(bool async)
+ public virtual Task Json_collection_after_collection_index_in_projection_using_constant_when_owner_is_present(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => new
@@ -1356,7 +1398,7 @@ public virtual Task Json_collection_after_collection_element_access_in_projectio
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_after_collection_element_access_in_projection_using_parameter_when_owner_is_present(bool async)
+ public virtual Task Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_present(bool async)
{
var prm = 1;
@@ -1398,7 +1440,7 @@ public virtual Task Json_collection_after_collection_element_access_in_projectio
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_when_owner_is_present_misc1(bool async)
+ public virtual Task Json_collection_index_in_projection_when_owner_is_present_misc1(bool async)
{
var prm = 1;
@@ -1440,7 +1482,7 @@ public virtual Task Json_collection_element_access_in_projection_when_owner_is_n
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_when_owner_is_present_misc2(bool async)
+ public virtual Task Json_collection_index_in_projection_when_owner_is_present_misc2(bool async)
=> AssertQuery(
async,
ss => ss.Set().Select(x => new
@@ -1474,7 +1516,7 @@ public virtual Task Json_collection_element_access_in_projection_when_owner_is_n
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
- public virtual Task Json_collection_element_access_in_projection_when_owner_is_present_multiple(bool async)
+ public virtual Task Json_collection_index_in_projection_when_owner_is_present_multiple(bool async)
{
var prm = 1;
diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
index f4bdaa5e302..d9576f758e4 100644
--- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
@@ -455,6 +455,19 @@ public virtual Task Column_collection_Any(bool async)
ss => ss.Set().Where(c => c.Ints.Any()),
entryCount: 4);
+ // If this test is failing because of DistinctAfterOrderByWithoutRowLimitingOperatorWarning, this is because EF warns/errors by
+ // default for Distinct after OrderBy (without Skip/Take); but you likely have a naturally-ordered JSON collection, where the
+ // ordering has been added by the provider as part of the collection translation.
+ // Consider overriding RelationalQueryableMethodTranslatingExpressionVisitor.IsNaturallyOrdered() to identify such naturally-ordered
+ // collections, exempting them from the warning.
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Distinct(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Distinct().Count() == 3),
+ entryCount: 1);
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Column_collection_projection_from_top_level(bool async)
@@ -810,9 +823,6 @@ protected override string StoreName
public Func GetContextCreator()
=> () => CreateContext();
- public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
- => base.AddOptions(builder).ConfigureWarnings(c => c.Log(CoreEventId.DistinctAfterOrderByWithoutRowLimitingOperatorWarning));
-
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
=> modelBuilder.Entity().Property(p => p.Id).ValueGeneratedNever();
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs
index 30a49a728c0..519ea1a0e56 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs
@@ -671,9 +671,9 @@ public override async Task Json_entity_backtracking(bool async)
@"");
}
- public override async Task Json_collection_element_access_in_projection_basic(bool async)
+ public override async Task Json_collection_index_in_projection_basic(bool async)
{
- await base.Json_collection_element_access_in_projection_basic(async);
+ await base.Json_collection_index_in_projection_basic(async);
AssertSql(
"""
@@ -682,9 +682,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_ElementAt(bool async)
+ public override async Task Json_collection_ElementAt_in_projection(bool async)
{
- await base.Json_collection_element_access_in_projection_using_ElementAt(async);
+ await base.Json_collection_ElementAt_in_projection(async);
AssertSql(
"""
@@ -693,9 +693,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_ElementAtOrDefault(bool async)
+ public override async Task Json_collection_ElementAtOrDefault_in_projection(bool async)
{
- await base.Json_collection_element_access_in_projection_using_ElementAtOrDefault(async);
+ await base.Json_collection_ElementAtOrDefault_in_projection(async);
AssertSql(
"""
@@ -704,9 +704,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_project_collection(bool async)
+ public override async Task Json_collection_index_in_projection_project_collection(bool async)
{
- await base.Json_collection_element_access_in_projection_project_collection(async);
+ await base.Json_collection_index_in_projection_project_collection(async);
AssertSql(
"""
@@ -715,9 +715,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_ElementAt_project_collection(bool async)
+ public override async Task Json_collection_ElementAt_project_collection(bool async)
{
- await base.Json_collection_element_access_in_projection_using_ElementAt_project_collection(async);
+ await base.Json_collection_ElementAt_project_collection(async);
AssertSql(
"""
@@ -726,9 +726,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(bool async)
+ public override async Task Json_collection_ElementAtOrDefault_project_collection(bool async)
{
- await base.Json_collection_element_access_in_projection_using_ElementAtOrDefault_project_collection(async);
+ await base.Json_collection_ElementAtOrDefault_project_collection(async);
AssertSql(
"""
@@ -738,9 +738,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_using_parameter(bool async)
+ public override async Task Json_collection_index_in_projection_using_parameter(bool async)
{
- await base.Json_collection_element_access_in_projection_using_parameter(async);
+ await base.Json_collection_index_in_projection_using_parameter(async);
AssertSql(
"""
@@ -752,9 +752,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_using_column(bool async)
+ public override async Task Json_collection_index_in_projection_using_column(bool async)
{
- await base.Json_collection_element_access_in_projection_using_column(async);
+ await base.Json_collection_index_in_projection_using_column(async);
AssertSql(
"""
@@ -763,23 +763,23 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_untranslatable_client_method(bool async)
+ public override async Task Json_collection_index_in_projection_using_untranslatable_client_method(bool async)
{
- await base.Json_collection_element_access_in_projection_using_untranslatable_client_method(async);
+ await base.Json_collection_index_in_projection_using_untranslatable_client_method(async);
AssertSql();
}
- public override async Task Json_collection_element_access_in_projection_using_untranslatable_client_method2(bool async)
+ public override async Task Json_collection_index_in_projection_using_untranslatable_client_method2(bool async)
{
- await base.Json_collection_element_access_in_projection_using_untranslatable_client_method2(async);
+ await base.Json_collection_index_in_projection_using_untranslatable_client_method2(async);
AssertSql();
}
- public override async Task Json_collection_element_access_outside_bounds(bool async)
+ public override async Task Json_collection_index_outside_bounds(bool async)
{
- await base.Json_collection_element_access_outside_bounds(async);
+ await base.Json_collection_index_outside_bounds(async);
AssertSql(
"""
@@ -799,9 +799,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_outside_bounds_with_property_access(bool async)
+ public override async Task Json_collection_index_outside_bounds_with_property_access(bool async)
{
- await base.Json_collection_element_access_outside_bounds_with_property_access(async);
+ await base.Json_collection_index_outside_bounds_with_property_access(async);
AssertSql(
"""
@@ -812,9 +812,9 @@ ORDER BY [j].[Id]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_nested(bool async)
+ public override async Task Json_collection_index_in_projection_nested(bool async)
{
- await base.Json_collection_element_access_in_projection_nested(async);
+ await base.Json_collection_index_in_projection_nested(async);
AssertSql(
"""
@@ -826,9 +826,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_nested_project_scalar(bool async)
+ public override async Task Json_collection_index_in_projection_nested_project_scalar(bool async)
{
- await base.Json_collection_element_access_in_projection_nested_project_scalar(async);
+ await base.Json_collection_index_in_projection_nested_project_scalar(async);
AssertSql(
"""
@@ -840,9 +840,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_nested_project_reference(bool async)
+ public override async Task Json_collection_index_in_projection_nested_project_reference(bool async)
{
- await base.Json_collection_element_access_in_projection_nested_project_reference(async);
+ await base.Json_collection_index_in_projection_nested_project_reference(async);
AssertSql(
"""
@@ -854,9 +854,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_nested_project_collection(bool async)
+ public override async Task Json_collection_index_in_projection_nested_project_collection(bool async)
{
- await base.Json_collection_element_access_in_projection_nested_project_collection(async);
+ await base.Json_collection_index_in_projection_nested_project_collection(async);
AssertSql(
"""
@@ -869,9 +869,9 @@ ORDER BY [j].[Id]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(bool async)
+ public override async Task Json_collection_index_in_projection_nested_project_collection_anonymous_projection(bool async)
{
- await base.Json_collection_element_access_in_projection_nested_project_collection_anonymous_projection(async);
+ await base.Json_collection_index_in_projection_nested_project_collection_anonymous_projection(async);
AssertSql(
"""
@@ -882,9 +882,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_predicate_using_constant(bool async)
+ public override async Task Json_collection_index_in_predicate_using_constant(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_constant(async);
+ await base.Json_collection_index_in_predicate_using_constant(async);
AssertSql(
"""
@@ -895,9 +895,9 @@ WHERE JSON_VALUE([j].[OwnedCollectionRoot], '$[0].Name') <> N'Foo' OR JSON_VALUE
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_predicate_using_variable(bool async)
+ public override async Task Json_collection_index_in_predicate_using_variable(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_variable(async);
+ await base.Json_collection_index_in_predicate_using_variable(async);
AssertSql(
"""
@@ -910,9 +910,9 @@ WHERE JSON_VALUE([j].[OwnedCollectionRoot], '$[' + CAST(@__prm_0 AS nvarchar(max
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_predicate_using_column(bool async)
+ public override async Task Json_collection_index_in_predicate_using_column(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_column(async);
+ await base.Json_collection_index_in_predicate_using_column(async);
AssertSql(
"""
@@ -923,9 +923,9 @@ WHERE JSON_VALUE([j].[OwnedCollectionRoot], '$[' + CAST([j].[Id] AS nvarchar(max
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_predicate_using_complex_expression1(bool async)
+ public override async Task Json_collection_index_in_predicate_using_complex_expression1(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_complex_expression1(async);
+ await base.Json_collection_index_in_predicate_using_complex_expression1(async);
AssertSql(
"""
@@ -939,9 +939,9 @@ END AS nvarchar(max)) + '].Name') = N'e1_c1'
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_predicate_using_complex_expression2(bool async)
+ public override async Task Json_collection_index_in_predicate_using_complex_expression2(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_complex_expression2(async);
+ await base.Json_collection_index_in_predicate_using_complex_expression2(async);
AssertSql(
"""
@@ -953,9 +953,9 @@ SELECT MAX([j0].[Id])
""");
}
- public override async Task Json_collection_element_access_in_predicate_using_ElementAt(bool async)
+ public override async Task Json_collection_ElementAt_in_predicate(bool async)
{
- await base.Json_collection_element_access_in_predicate_using_ElementAt(async);
+ await base.Json_collection_ElementAt_in_predicate(async);
AssertSql(
"""
@@ -966,9 +966,9 @@ WHERE JSON_VALUE([j].[OwnedCollectionRoot], '$[1].Name') <> N'Foo' OR JSON_VALUE
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_predicate_nested_mix(bool async)
+ public override async Task Json_collection_index_in_predicate_nested_mix(bool async)
{
- await base.Json_collection_element_access_in_predicate_nested_mix(async);
+ await base.Json_collection_index_in_predicate_nested_mix(async);
AssertSql(
"""
@@ -980,9 +980,9 @@ WHERE JSON_VALUE([j].[OwnedCollectionRoot], '$[1].OwnedCollectionBranch[' + CAST
""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown(bool async)
+ public override async Task Json_collection_ElementAt_and_pushdown(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown(async);
+ await base.Json_collection_ElementAt_and_pushdown(async);
AssertSql(
"""
@@ -991,46 +991,208 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative(bool async)
+ public override async Task Json_collection_Any_with_predicate(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative(async);
+ await base.Json_collection_Any_with_predicate(async);
- AssertSql();
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE EXISTS (
+ SELECT 1
+ FROM OPENJSON([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch') WITH (
+ [Date] datetime2 '$.Date',
+ [Enum] nvarchar(max) '$.Enum',
+ [Fraction] decimal(18,2) '$.Fraction',
+ [NullableEnum] nvarchar(max) '$.NullableEnum',
+ [OwnedCollectionLeaf] nvarchar(max) '$.OwnedCollectionLeaf' AS JSON,
+ [OwnedReferenceLeaf] nvarchar(max) '$.OwnedReferenceLeaf' AS JSON
+ ) AS [o]
+ WHERE JSON_VALUE([o].[OwnedReferenceLeaf], '$.SomethingSomething') = N'e1_c2_c1_c1')
+""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative2(bool async)
+ public override async Task Json_collection_Where_ElementAt(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative2(async);
+ await base.Json_collection_Where_ElementAt(async);
- AssertSql();
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE (
+ SELECT JSON_VALUE([o].[value], '$.OwnedReferenceLeaf.SomethingSomething')
+ FROM OPENJSON([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch') AS [o]
+ WHERE JSON_VALUE([o].[value], '$.Enum') = N'Three'
+ ORDER BY CAST([o].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) = N'e1_r_c2_r'
+""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative3(bool async)
+ public override async Task Json_collection_Skip(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative3(async);
+ await base.Json_collection_Skip(async);
- AssertSql();
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE (
+ SELECT [t].[c]
+ FROM (
+ SELECT JSON_VALUE([o].[value], '$.OwnedReferenceLeaf.SomethingSomething') AS [c], [j].[Id], CAST([o].[key] AS int) AS [c0]
+ FROM OPENJSON([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch') AS [o]
+ ORDER BY CAST([o].[key] AS int)
+ OFFSET 1 ROWS
+ ) AS [t]
+ ORDER BY [t].[c0]
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) = N'e1_r_c2_r'
+""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative4(bool async)
+ public override async Task Json_collection_OrderByDescending_Skip_ElementAt(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative4(async);
+ await base.Json_collection_OrderByDescending_Skip_ElementAt(async);
- AssertSql();
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE (
+ SELECT [t].[c]
+ FROM (
+ SELECT JSON_VALUE([o].[OwnedReferenceLeaf], '$.SomethingSomething') AS [c], [j].[Id], [o].[Date] AS [c0]
+ FROM OPENJSON([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch') WITH (
+ [Date] datetime2 '$.Date',
+ [Enum] nvarchar(max) '$.Enum',
+ [Fraction] decimal(18,2) '$.Fraction',
+ [NullableEnum] nvarchar(max) '$.NullableEnum',
+ [OwnedCollectionLeaf] nvarchar(max) '$.OwnedCollectionLeaf' AS JSON,
+ [OwnedReferenceLeaf] nvarchar(max) '$.OwnedReferenceLeaf' AS JSON
+ ) AS [o]
+ ORDER BY [o].[Date] DESC
+ OFFSET 1 ROWS
+ ) AS [t]
+ ORDER BY [t].[c0] DESC
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) = N'e1_r_c1_r'
+""");
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative5(bool async)
+ public override async Task Json_collection_Distinct_Count_with_predicate(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative5(async);
+ await base.Json_collection_Distinct_Count_with_predicate(async);
AssertSql();
}
- public override async Task Json_collection_element_access_manual_Element_at_and_pushdown_negative6(bool async)
+ public override async Task Json_collection_within_collection_Count(bool async)
{
- await base.Json_collection_element_access_manual_Element_at_and_pushdown_negative6(async);
+ await base.Json_collection_within_collection_Count(async);
- AssertSql();
+ AssertSql(
+"""
+SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot]
+FROM [JsonEntitiesBasic] AS [j]
+WHERE EXISTS (
+ SELECT 1
+ FROM OPENJSON([j].[OwnedCollectionRoot], '$') WITH (
+ [Name] nvarchar(max) '$.Name',
+ [Number] int '$.Number',
+ [OwnedCollectionBranch] nvarchar(max) '$.OwnedCollectionBranch' AS JSON,
+ [OwnedReferenceBranch] nvarchar(max) '$.OwnedReferenceBranch' AS JSON
+ ) AS [o]
+ WHERE (
+ SELECT COUNT(*)
+ FROM OPENJSON([o].[OwnedCollectionBranch], '$') WITH (
+ [Date] datetime2 '$.Date',
+ [Enum] nvarchar(max) '$.Enum',
+ [Fraction] decimal(18,2) '$.Fraction',
+ [NullableEnum] nvarchar(max) '$.NullableEnum',
+ [OwnedCollectionLeaf] nvarchar(max) '$.OwnedCollectionLeaf' AS JSON,
+ [OwnedReferenceLeaf] nvarchar(max) '$.OwnedReferenceLeaf' AS JSON
+ ) AS [o0]) = 2)
+""");
+ }
+
+ public override async Task Json_collection_index_with_parameter_Select_ElementAt(bool async)
+ {
+ await base.Json_collection_index_with_parameter_Select_ElementAt(async);
+
+ AssertSql(
+"""
+@__prm_0='0'
+
+SELECT [j].[Id], (
+ SELECT N'Foo'
+ FROM OPENJSON([j].[OwnedCollectionRoot], '$[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedCollectionBranch') AS [o]
+ ORDER BY CAST([o].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS [CollectionElement]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_index_with_expression_Select_ElementAt(bool async)
+ {
+ await base.Json_collection_index_with_expression_Select_ElementAt(async);
+
+ AssertSql(
+"""
+@__prm_0='0'
+
+SELECT JSON_VALUE([j].[OwnedCollectionRoot], '$[' + CAST(@__prm_0 + [j].[Id] AS nvarchar(max)) + '].OwnedCollectionBranch[0].OwnedReferenceLeaf.SomethingSomething')
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_Select_entity_collection_ElementAt(bool async)
+ {
+ await base.Json_collection_Select_entity_collection_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot], '$[0].OwnedCollectionBranch'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_Select_entity_ElementAt(bool async)
+ {
+ await base.Json_collection_Select_entity_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot], '$[0].OwnedReferenceBranch'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_Select_entity_in_anonymous_object_ElementAt(bool async)
+ {
+ await base.Json_collection_Select_entity_in_anonymous_object_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT JSON_QUERY([j].[OwnedCollectionRoot], '$[0].OwnedReferenceBranch'), [j].[Id]
+FROM [JsonEntitiesBasic] AS [j]
+""");
+ }
+
+ public override async Task Json_collection_Select_entity_with_initializer_ElementAt(bool async)
+ {
+ await base.Json_collection_Select_entity_with_initializer_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT [t].[Id], [t].[c]
+FROM [JsonEntitiesBasic] AS [j]
+OUTER APPLY (
+ SELECT [j].[Id], 1 AS [c]
+ FROM OPENJSON([j].[OwnedCollectionRoot], '$') AS [o]
+ ORDER BY CAST([o].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
+) AS [t]
+""");
}
public override async Task Json_projection_deduplication_with_collection_indexer_in_original(bool async)
@@ -1072,9 +1234,9 @@ FROM [JsonEntitiesBasic] AS [j]
""");
}
- public override async Task Json_collection_element_access_in_projection_using_constant_when_owner_is_present(bool async)
+ public override async Task Json_collection_index_in_projection_using_constant_when_owner_is_present(bool async)
{
- await base.Json_collection_element_access_in_projection_using_constant_when_owner_is_present(async);
+ await base.Json_collection_index_in_projection_using_constant_when_owner_is_present(async);
AssertSql(
"""
@@ -1095,9 +1257,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_using_parameter_when_owner_is_present(bool async)
+ public override async Task Json_collection_index_in_projection_using_parameter_when_owner_is_present(bool async)
{
- await base.Json_collection_element_access_in_projection_using_parameter_when_owner_is_present(async);
+ await base.Json_collection_index_in_projection_using_parameter_when_owner_is_present(async);
AssertSql(
"""
@@ -1147,7 +1309,7 @@ FROM [JsonEntitiesBasic] AS [j]
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
public override async Task Json_collection_after_collection_element_access_in_projection_using_parameter_when_owner_is_present(bool async)
{
- await base.Json_collection_after_collection_element_access_in_projection_using_parameter_when_owner_is_present(async);
+ await base.Json_collection_after_collection_index_in_projection_using_parameter_when_owner_is_present(async);
AssertSql(
"""
@@ -1175,7 +1337,7 @@ FROM [JsonEntitiesBasic] AS [j]
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
public override async Task Json_collection_element_access_in_projection_when_owner_is_present_misc1(bool async)
{
- await base.Json_collection_element_access_in_projection_when_owner_is_present_misc1(async);
+ await base.Json_collection_index_in_projection_when_owner_is_present_misc1(async);
AssertSql(
"""
@@ -1202,7 +1364,7 @@ FROM [JsonEntitiesBasic] AS [j]
public override async Task Json_collection_element_access_in_projection_when_owner_is_present_misc2(bool async)
{
- await base.Json_collection_element_access_in_projection_when_owner_is_present_misc2(async);
+ await base.Json_collection_index_in_projection_when_owner_is_present_misc2(async);
AssertSql(
"""
@@ -1223,9 +1385,9 @@ FROM [JsonEntitiesBasic] AS [j]
}
[SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)]
- public override async Task Json_collection_element_access_in_projection_when_owner_is_present_multiple(bool async)
+ public override async Task Json_collection_index_in_projection_when_owner_is_present_multiple(bool async)
{
- await base.Json_collection_element_access_in_projection_when_owner_is_present_multiple(async);
+ await base.Json_collection_index_in_projection_when_owner_is_present_multiple(async);
AssertSql(
"""
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
index ba83bc7c47f..ba8054e16d2 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
@@ -396,6 +396,9 @@ public override Task Column_collection_OrderByDescending_ElementAt(bool async)
public override Task Column_collection_Any(bool async)
=> AssertCompatibilityLevelTooLow(() => base.Column_collection_Any(async));
+ public override Task Column_collection_Distinct(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Distinct(async));
+
public override async Task Column_collection_projection_from_top_level(bool async)
{
await base.Column_collection_projection_from_top_level(async);
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
index 00e227b6e8b..46d6b8029d0 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
@@ -660,6 +660,23 @@ FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i])
""");
}
+ public override async Task Column_collection_Distinct(bool async)
+ {
+ await base.Column_collection_Distinct(async);
+
+ AssertSql(
+"""
+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 DISTINCT [i].[value]
+ FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i]
+ ) AS [t]) = 3
+""");
+ }
+
public override async Task Column_collection_projection_from_top_level(bool async)
{
await base.Column_collection_projection_from_top_level(async);
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs
index 7d73ffcec7a..07777fcc77f 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs
@@ -67,6 +67,122 @@ public override async Task Project_json_entity_FirstOrDefault_subquery_deduplica
() => base.Project_json_entity_FirstOrDefault_subquery_deduplication_outer_reference_and_pruning(async)))
.Message);
+ public override async Task Json_collection_Any_with_predicate(bool async)
+ {
+ await base.Json_collection_Any_with_predicate(async);
+
+ AssertSql(
+"""
+SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot"
+FROM "JsonEntitiesBasic" AS "j"
+WHERE EXISTS (
+ SELECT 1
+ FROM (
+ SELECT "o"."value" ->> 'Date' AS "Date", "o"."value" ->> 'Enum' AS "Enum", "o"."value" ->> 'Fraction' AS "Fraction", "o"."value" ->> 'NullableEnum' AS "NullableEnum", "o"."value" ->> 'OwnedCollectionLeaf' AS "OwnedCollectionLeaf", "o"."value" ->> 'OwnedReferenceLeaf' AS "OwnedReferenceLeaf", "j"."Id", "o"."key"
+ FROM json_each("j"."OwnedReferenceRoot", '$.OwnedCollectionBranch') AS "o"
+ ) AS "t"
+ WHERE "t"."OwnedReferenceLeaf" ->> 'SomethingSomething' = 'e1_c2_c1_c1')
+""");
+ }
+
+ public override async Task Json_collection_Where_ElementAt(bool async)
+ {
+ await base.Json_collection_Where_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot"
+FROM "JsonEntitiesBasic" AS "j"
+WHERE (
+ SELECT "t"."OwnedReferenceLeaf" ->> 'SomethingSomething'
+ FROM (
+ SELECT "o"."value" ->> 'Date' AS "Date", "o"."value" ->> 'Enum' AS "Enum", "o"."value" ->> 'Fraction' AS "Fraction", "o"."value" ->> 'NullableEnum' AS "NullableEnum", "o"."value" ->> 'OwnedCollectionLeaf' AS "OwnedCollectionLeaf", "o"."value" ->> 'OwnedReferenceLeaf' AS "OwnedReferenceLeaf", "j"."Id", "o"."key"
+ FROM json_each("j"."OwnedReferenceRoot", '$.OwnedCollectionBranch') AS "o"
+ ) AS "t"
+ WHERE "t"."Enum" = 'Three'
+ ORDER BY "t"."key"
+ LIMIT 1 OFFSET 0) = 'e1_r_c2_r'
+""");
+ }
+
+ public override async Task Json_collection_Skip(bool async)
+ {
+ await base.Json_collection_Skip(async);
+
+ AssertSql(
+"""
+SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot"
+FROM "JsonEntitiesBasic" AS "j"
+WHERE (
+ SELECT "t0"."c"
+ FROM (
+ SELECT "t"."OwnedReferenceLeaf" ->> 'SomethingSomething' AS "c", "j"."Id", "t"."key"
+ FROM (
+ SELECT "o"."value" ->> 'Date' AS "Date", "o"."value" ->> 'Enum' AS "Enum", "o"."value" ->> 'Fraction' AS "Fraction", "o"."value" ->> 'NullableEnum' AS "NullableEnum", "o"."value" ->> 'OwnedCollectionLeaf' AS "OwnedCollectionLeaf", "o"."value" ->> 'OwnedReferenceLeaf' AS "OwnedReferenceLeaf", "j"."Id", "o"."key"
+ FROM json_each("j"."OwnedReferenceRoot", '$.OwnedCollectionBranch') AS "o"
+ ) AS "t"
+ ORDER BY "t"."key"
+ LIMIT -1 OFFSET 1
+ ) AS "t0"
+ ORDER BY "t0"."key"
+ LIMIT 1 OFFSET 0) = 'e1_r_c2_r'
+""");
+ }
+
+ public override async Task Json_collection_OrderByDescending_Skip_ElementAt(bool async)
+ {
+ await base.Json_collection_OrderByDescending_Skip_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot"
+FROM "JsonEntitiesBasic" AS "j"
+WHERE (
+ SELECT "t0"."c"
+ FROM (
+ SELECT "t"."OwnedReferenceLeaf" ->> 'SomethingSomething' AS "c", "j"."Id", "t"."Date" AS "c0"
+ FROM (
+ SELECT "o"."value" ->> 'Date' AS "Date", "o"."value" ->> 'Enum' AS "Enum", "o"."value" ->> 'Fraction' AS "Fraction", "o"."value" ->> 'NullableEnum' AS "NullableEnum", "o"."value" ->> 'OwnedCollectionLeaf' AS "OwnedCollectionLeaf", "o"."value" ->> 'OwnedReferenceLeaf' AS "OwnedReferenceLeaf", "j"."Id", "o"."key"
+ FROM json_each("j"."OwnedReferenceRoot", '$.OwnedCollectionBranch') AS "o"
+ ) AS "t"
+ ORDER BY "t"."Date" DESC
+ LIMIT -1 OFFSET 1
+ ) AS "t0"
+ ORDER BY "t0"."c0" DESC
+ LIMIT 1 OFFSET 0) = 'e1_r_c1_r'
+""");
+ }
+
+ public override async Task Json_collection_within_collection_Count(bool async)
+ {
+ await base.Json_collection_within_collection_Count(async);
+
+ AssertSql(
+"""
+SELECT "j"."Id", "j"."EntityBasicId", "j"."Name", "j"."OwnedCollectionRoot", "j"."OwnedReferenceRoot"
+FROM "JsonEntitiesBasic" AS "j"
+WHERE EXISTS (
+ SELECT 1
+ FROM (
+ SELECT "o"."value" ->> 'Name' AS "Name", "o"."value" ->> 'Number' AS "Number", "o"."value" ->> 'OwnedCollectionBranch' AS "OwnedCollectionBranch", "o"."value" ->> 'OwnedReferenceBranch' AS "OwnedReferenceBranch", "j"."Id", "o"."key"
+ FROM json_each("j"."OwnedCollectionRoot", '$') AS "o"
+ ) AS "t"
+ WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT "o0"."value" ->> 'Date' AS "Date", "o0"."value" ->> 'Enum' AS "Enum", "o0"."value" ->> 'Fraction' AS "Fraction", "o0"."value" ->> 'NullableEnum' AS "NullableEnum", "o0"."value" ->> 'OwnedCollectionLeaf' AS "OwnedCollectionLeaf", "o0"."value" ->> 'OwnedReferenceLeaf' AS "OwnedReferenceLeaf", "j"."Id", "o0"."key"
+ FROM json_each("t"."OwnedCollectionBranch", '$') AS "o0"
+ ) AS "t0") = 2)
+""");
+ }
+
+ public override async Task Json_collection_Select_entity_with_initializer_ElementAt(bool async)
+ => Assert.Equal(
+ SqliteStrings.ApplyNotSupported,
+ (await Assert.ThrowsAsync(
+ () => base.Project_json_entity_FirstOrDefault_subquery_deduplication_outer_reference_and_pruning(async)))
+ .Message);
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSqlInterpolated_on_entity_with_json_with_predicate(bool async)
@@ -90,9 +206,9 @@ await AssertQuery(
""");
}
- public override async Task Json_collection_element_access_in_predicate_nested_mix(bool async)
+ public override async Task Json_collection_index_in_predicate_nested_mix(bool async)
{
- await base.Json_collection_element_access_in_predicate_nested_mix(async);
+ await base.Json_collection_index_in_predicate_nested_mix(async);
AssertSql(
"""
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
index d3ef5ed33c9..f7d783a5a87 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
@@ -646,6 +646,23 @@ WHERE json_array_length("p"."Ints") > 0
""");
}
+ public override async Task Column_collection_Distinct(bool async)
+ {
+ await base.Column_collection_Distinct(async);
+
+ AssertSql(
+"""
+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 DISTINCT "i"."value"
+ FROM json_each("p"."Ints") AS "i"
+ ) AS "t") = 3
+""");
+ }
+
public override async Task Column_collection_projection_from_top_level(bool async)
{
await base.Column_collection_projection_from_top_level(async);