Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s
}

// Otherwise, attempt to translate as Any since that passes through Where predicate translation. This will e.g. take care of
// entity , which e.g. does entity equality/containment for entities with composite keys.
// entity, which e.g. does entity equality/containment for entities with composite keys.
var anyLambdaParameter = Expression.Parameter(item.Type, "p");
var anyLambda = Expression.Lambda(
Infrastructure.ExpressionExtensions.CreateEqualsExpression(anyLambdaParameter, item),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,66 +333,77 @@ bool TryRewriteComplexTypeEquality(bool collection, [NotNullWhen(true)] out SqlE
// into complex properties to generate a flattened list of comparisons.
// The moment we reach a a complex property that's mapped to JSON, we stop and generate a single comparison
// for the whole complex type.
bool TryGenerateComparisons(IComplexType type, Expression left, Expression right, [NotNullWhen(true)] ref SqlExpression? comparisons, out bool exitImmediately)
bool TryGenerateComparisons(
IComplexType type,
Expression left,
Expression right,
[NotNullWhen(true)] ref SqlExpression? comparisons,
out bool exitImmediately)
{
exitImmediately = false;

if (type.IsMappedToJson())
{
var leftScalar = Process(left);
var rightScalar = Process(right);

var comparison = _sqlExpressionFactory.MakeBinary(nodeType, leftScalar, rightScalar, boolTypeMapping)!;
// The fact that a type is mapped to JSON doesn't necessary mean that we simply compare its JSON column/value:
// a JSON *collection* may have been converted to a relational representation via e.g. OPENJSON, at which point
// it behaves just like a table-splitting complex type, where we have to compare column-by-column.
// TryProcessJson() attempts to extract a single JSON column/value from both sides: if we succeed, we just compare
// both sides. Otherwise, we flow down to the column-by-column flow.
if (TryProcessJson(left, out var leftScalar) && TryProcessJson(right, out var rightScalar))
{
var comparison = _sqlExpressionFactory.MakeBinary(nodeType, leftScalar, rightScalar, boolTypeMapping)!;

// A single JSON-mapped complex type requires only a single comparison for the JSON value/column on each side;
// but the JSON-mapped complex type may be nested inside a non-JSON (table-split) complex type, in which
// case this is just one comparison in several.
comparisons = comparisons is null
? comparison
: nodeType == ExpressionType.Equal
? _sqlExpressionFactory.AndAlso(comparisons, comparison)
: _sqlExpressionFactory.OrElse(comparisons, comparison);
// A single JSON-mapped complex type requires only a single comparison for the JSON value/column on each side;
// but the JSON-mapped complex type may be nested inside a non-JSON (table-split) complex type, in which
// case this is just one comparison in several.
comparisons = comparisons is null
? comparison
: nodeType == ExpressionType.Equal
? _sqlExpressionFactory.AndAlso(comparisons, comparison)
: _sqlExpressionFactory.OrElse(comparisons, comparison);

return true;
return true;
}

SqlExpression Process(Expression expression)
=> expression switch
bool TryProcessJson(Expression expression, [NotNullWhen(true)] out SqlExpression? result)
{
switch (expression)
{
// When a non-collection JSON column - or a nested complex property within a JSON column - is compared,
// we get a StructuralTypeReferenceExpression over a JsonQueryExpression. Convert this to a
// JsonScalarExpression, which is our current representation for a complex JSON in the SQL tree
// (as opposed to in the shaper) - see #36392.
StructuralTypeReferenceExpression
{
Parameter: { ValueBufferExpression: JsonQueryExpression jsonQuery }
}
=> new JsonScalarExpression(
case StructuralTypeReferenceExpression { Parameter.ValueBufferExpression: JsonQueryExpression jsonQuery }:
result = new JsonScalarExpression(
jsonQuery.JsonColumn,
jsonQuery.Path,
jsonQuery.Type.UnwrapNullableType(),
jsonQuery.JsonColumn.TypeMapping,
jsonQuery.IsNullable),
jsonQuery.IsNullable);
return true;

// As above, but for a complex JSON collection
CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery }
=> new JsonScalarExpression(
case CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery }:
result = new JsonScalarExpression(
jsonQuery.JsonColumn,
jsonQuery.Path,
jsonQuery.Type.UnwrapNullableType(),
jsonQuery.JsonColumn.TypeMapping,
jsonQuery.IsNullable),
jsonQuery.IsNullable);
return true;

// When an object is instantiated inline (e.g. Where(c => c.ShippingAddress == new Address { ... })), we get a SqlConstantExpression
// with the .NET instance. Serialize it to JSON and replace the constant (note that the type mapping will be inferred from the
// JSON column on other side above - important for e.g. nvarchar vs. json columns)
SqlConstantExpression constant
=> new SqlConstantExpression(
case SqlConstantExpression constant:
result = new SqlConstantExpression(
SerializeComplexTypeToJson(complexType, constant.Value, collection),
typeof(string),
typeMapping: null),
typeMapping: null);
return true;

SqlParameterExpression parameter
=> (SqlParameterExpression)Visit(
case SqlParameterExpression parameter:
result = (SqlParameterExpression)Visit(
_queryCompilationContext.RegisterRuntimeParameter(
$"{RuntimeParameterPrefix}{parameter.Name}",
Expression.Lambda(
Expand All @@ -405,13 +416,52 @@ SqlParameterExpression parameter
indexer: typeof(Dictionary<string, object>).GetProperty("Item", [typeof(string)]),
[Expression.Constant(parameter.Name, typeof(string))]),
Expression.Constant(collection)),
QueryCompilationContext.QueryContextParameter))),
QueryCompilationContext.QueryContextParameter)));
return true;

case ParameterBasedComplexPropertyChainExpression chainExpression:
{
var lastComplexProperty = chainExpression.ComplexPropertyChain.Last();
var extractComplexPropertyClrType = Expression.Call(
ParameterValueExtractorMethod.MakeGenericMethod(lastComplexProperty.ClrType.MakeNullable()),
QueryCompilationContext.QueryContextParameter,
Expression.Constant(chainExpression.ParameterExpression.Name, typeof(string)),
Expression.Constant(chainExpression.ComplexPropertyChain, typeof(List<IComplexProperty>)),
Expression.Constant(null, typeof(IProperty)));

var lambda =
Expression.Lambda(
Expression.Call(
SerializeComplexTypeToJsonMethod,
Expression.Constant(lastComplexProperty.ComplexType),
extractComplexPropertyClrType,
Expression.Constant(lastComplexProperty.IsCollection)),
QueryCompilationContext.QueryContextParameter);

var parameterNameBuilder = new StringBuilder(RuntimeParameterPrefix)
.Append(chainExpression.ParameterExpression.Name);

foreach (var complexProperty in chainExpression.ComplexPropertyChain)
{
parameterNameBuilder.Append('_').Append(complexProperty.Name);
}

_ => throw new UnreachableException()
result = (SqlParameterExpression)Visit(
_queryCompilationContext.RegisterRuntimeParameter(parameterNameBuilder.ToString(), lambda));

return true;
}

default:
result = null;
return false;
};
}
}

// We handled complex JSON above, from here we handle table splitting
// We handled JSON column comparison above.
// From here we handle table splitting and JSON types that have been converted to a relational table representation via
// e.g. OPENJSON.
foreach (var property in type.GetProperties())
{
if (TryTranslatePropertyAccess(left, property, out var leftTranslation)
Expand Down Expand Up @@ -586,7 +636,7 @@ when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == compl
QueryContext context,
string baseParameterName,
List<IComplexProperty>? complexPropertyChain,
IProperty property)
IProperty? property)
{
var baseValue = context.Parameters[baseParameterName];

Expand All @@ -603,7 +653,11 @@ when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == compl
}
}

return baseValue == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseValue);
return baseValue == null
? (T?)(object?)null
: property is null
? (T?)baseValue
: (T?)property.GetGetter().GetClrValue(baseValue);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,39 @@ public override async Task Nested_collection_with_parameter()
AssertSql();
}

#region Contains

public override async Task Contains_with_inline()
{
// No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter.
await Assert.ThrowsAsync<InvalidOperationException>(() => base.Contains_with_inline());

AssertSql();
}

public override async Task Contains_with_parameter()
{
await AssertTranslationFailed(base.Contains_with_parameter);

AssertSql();
}

public override async Task Contains_with_operators_composed_on_the_collection()
{
await AssertTranslationFailed(base.Contains_with_operators_composed_on_the_collection);

AssertSql();
}

public override async Task Contains_with_nested_and_composed_operators()
{
await AssertTranslationFailed(base.Contains_with_nested_and_composed_operators);

AssertSql();
}

#endregion Contains

[ConditionalFact]
public virtual void Check_all_tests_overridden()
=> TestHelpers.AssertAllMethodsOverridden(GetType());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,44 @@ public override Task Nested_collection_with_inline()
public override Task Two_nested_collections()
=> AssertTranslationFailed(() => base.Two_nested_collections());

#region Contains

public override async Task Contains_with_inline()
{
// Collections are not supported with table splitting, only JSON
await AssertTranslationFailed(base.Contains_with_inline);

AssertSql();
}

public override async Task Contains_with_parameter()
{
// Collections are not supported with table splitting, only JSON
await AssertTranslationFailed(base.Contains_with_parameter);

AssertSql();
}

public override async Task Contains_with_operators_composed_on_the_collection()
{
// Collections are not supported with table splitting, only JSON
// Note that the exception is correct, since the collections in the test data are null for table splitting
await Assert.ThrowsAsync<InvalidOperationException>(base.Contains_with_operators_composed_on_the_collection);

AssertSql();
}

public override async Task Contains_with_nested_and_composed_operators()
{
// Collections are not supported with table splitting, only JSON
// Note that the exception is correct, since the collections in the test data are null for table splitting
await Assert.ThrowsAsync<InvalidOperationException>(base.Contains_with_nested_and_composed_operators);

AssertSql();
}

#endregion Contains

protected void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,42 @@ public OwnedJsonStructuralEqualityRelationalTestBase(TFixture fixture, ITestOutp
public override Task Related_with_parameter_null()
=> Assert.ThrowsAsync<EqualException>(() => base.Related_with_parameter_null());

#region Contains

public override async Task Contains_with_inline()
{
// The given key 'Property: RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.__synthesizedOrdinal (no field, int) Shadow Required PK AfterSave:Throw ValueGenerated.OnAdd' was not present in the dictionary.
await Assert.ThrowsAsync<InvalidOperationException>(base.Contains_with_inline);

AssertSql();
}

public override async Task Contains_with_parameter()
{
// No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter.
await Assert.ThrowsAsync<KeyNotFoundException>(base.Contains_with_parameter);

AssertSql();
}

public override async Task Contains_with_operators_composed_on_the_collection()
{
// No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter.
await Assert.ThrowsAsync<KeyNotFoundException>(base.Contains_with_operators_composed_on_the_collection);

AssertSql();
}

public override async Task Contains_with_nested_and_composed_operators()
{
// No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter.
await Assert.ThrowsAsync<KeyNotFoundException>(base.Contains_with_nested_and_composed_operators);

AssertSql();
}

#endregion Contains

protected void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,42 @@ public OwnedNavigationsStructuralEqualityRelationalTestBase(TFixture fixture, IT
Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}

#region Contains

public override async Task Contains_with_inline()
{
// The given key 'Property: RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.__synthesizedOrdinal (no field, int) Shadow Required PK AfterSave:Throw ValueGenerated.OnAdd' was not present in the dictionary.
await Assert.ThrowsAsync<InvalidOperationException>(base.Contains_with_inline);

AssertSql();
}

public override async Task Contains_with_parameter()
{
// No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter.
await Assert.ThrowsAsync<InvalidOperationException>(base.Contains_with_parameter);

AssertSql();
}

public override async Task Contains_with_operators_composed_on_the_collection()
{
// No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter.
await Assert.ThrowsAsync<InvalidOperationException>(base.Contains_with_operators_composed_on_the_collection);

AssertSql();
}

public override async Task Contains_with_nested_and_composed_operators()
{
// No backing field could be found for property 'RootEntity.RelatedCollection#RelatedType.RootEntityId' and the property does not have a getter.
await Assert.ThrowsAsync<InvalidOperationException>(base.Contains_with_nested_and_composed_operators);

AssertSql();
}

#endregion Contains

protected void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
Loading
Loading