diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index 0fe96122904..76cc760510f 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -53,6 +53,14 @@ public static string BadSequenceType
public static string CannotChangeWhenOpen
=> GetString("CannotChangeWhenOpen");
+ ///
+ /// The query contained a new array expression containing non-constant elements, which could not be translated: '{newArrayExpression}'.
+ ///
+ public static string CannotTranslateNonConstantNewArrayExpression(object? newArrayExpression)
+ => string.Format(
+ GetString("CannotTranslateNonConstantNewArrayExpression", nameof(newArrayExpression)),
+ newArrayExpression);
+
///
/// Can't configure a trigger on entity type '{entityType}', which is in a TPH hierarchy and isn't the root. Configure the trigger on the TPH root entity type '{rootEntityType}' instead.
///
@@ -622,12 +630,12 @@ public static string DuplicateSeedDataSensitive(object? entityType, object? keyV
entityType, keyValue, table);
///
- /// Either {param1} or {param2} must be null.
+ /// Exactly one of '{param1}', '{param2}' or '{param3}' must be set.
///
- public static string EitherOfTwoValuesMustBeNull(object? param1, object? param2)
+ public static string OneOfThreeValuesMustBeSet(object? param1, object? param2, object? param3)
=> string.Format(
- GetString("EitherOfTwoValuesMustBeNull", nameof(param1), nameof(param2)),
- param1, param2);
+ GetString("OneOfThreeValuesMustBeSet", nameof(param1), nameof(param2), nameof(param3)),
+ param1, param2, param3);
///
/// Empty collections are not supported as constant query roots.
@@ -1310,11 +1318,11 @@ public static string NoDbCommand
=> GetString("NoDbCommand");
///
- /// Expression of type '{type}' isn't supported as the Values of an InExpression; only constants and parameters are supported.
+ /// Expression of type '{type}' isn't supported in the values of an InExpression; only constants and parameters are supported.
///
- public static string NonConstantOrParameterAsInExpressionValues(object? type)
+ public static string NonConstantOrParameterAsInExpressionValue(object? type)
=> string.Format(
- GetString("NonConstantOrParameterAsInExpressionValues", nameof(type)),
+ GetString("NonConstantOrParameterAsInExpressionValue", nameof(type)),
type);
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 938c9f6d455..c47f644d7ef 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -130,6 +130,9 @@
The instance of DbConnection is currently in use. The connection can only be changed when the existing connection is not being used.
+
+ The query contained a new array expression containing non-constant elements, which could not be translated: '{newArrayExpression}'.
+
Can't configure a trigger on entity type '{entityType}', which is in a TPH hierarchy and isn't the root. Configure the trigger on the TPH root entity type '{rootEntityType}' instead.
@@ -346,8 +349,8 @@
A seed entity for entity type '{entityType}' has the same key value {keyValue} as another seed entity mapped to the same table '{table}'. Key values should be unique across seed entities.
-
- Either {param1} or {param2} must be null.
+
+ Exactly one of '{param1}', '{param2}' or '{param3}' must be set.
Empty collections are not supported as inline query roots.
@@ -911,8 +914,8 @@
Cannot create a DbCommand for a non-relational query.
-
- Expression of type '{type}' isn't supported as the Values of an InExpression; only constants and parameters are supported.
+
+ Expression of type '{type}' isn't supported in the values of an InExpression; only constants and parameters are supported.
'FindMapping' was called on a 'RelationalTypeMappingSource' with a non-relational 'TypeMappingInfo'.
diff --git a/src/EFCore.Relational/Query/ISqlExpressionFactory.cs b/src/EFCore.Relational/Query/ISqlExpressionFactory.cs
index 8780256848d..9f53969ba80 100644
--- a/src/EFCore.Relational/Query/ISqlExpressionFactory.cs
+++ b/src/EFCore.Relational/Query/ISqlExpressionFactory.cs
@@ -401,7 +401,16 @@ SqlFunctionExpression NiladicFunction(
/// A subquery to check existence of.
/// A value indicating if the existence check is negated.
/// An expression representing an EXISTS operation in a SQL tree.
- ExistsExpression Exists(SelectExpression subquery, bool negated);
+ ExistsExpression Exists(SelectExpression subquery, bool negated = false);
+
+ ///
+ /// Creates a new which represents an IN operation in a SQL tree.
+ ///
+ /// An item to look into values.
+ /// A subquery in which item is searched.
+ /// A value indicating if the item should be present in the values or absent.
+ /// An expression representing an IN operation in a SQL tree.
+ InExpression In(SqlExpression item, SelectExpression subquery, bool negated = false);
///
/// Creates a new which represents an IN operation in a SQL tree.
@@ -410,16 +419,16 @@ SqlFunctionExpression NiladicFunction(
/// A list of values in which item is searched.
/// A value indicating if the item should be present in the values or absent.
/// An expression representing an IN operation in a SQL tree.
- InExpression In(SqlExpression item, SqlExpression values, bool negated);
+ InExpression In(SqlExpression item, IReadOnlyList values, bool negated = false);
///
/// Creates a new which represents an IN operation in a SQL tree.
///
/// An item to look into values.
- /// A subquery in which item is searched.
+ /// A parameterized list of values in which the item is searched.
/// A value indicating if the item should be present in the values or absent.
/// An expression representing an IN operation in a SQL tree.
- InExpression In(SqlExpression item, SelectExpression subquery, bool negated);
+ InExpression In(SqlExpression item, SqlParameterExpression valuesParameter, bool negated = false);
///
/// Creates a new which represents a LIKE in a SQL tree.
diff --git a/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs b/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs
index 0e523bf70a9..43d562142cd 100644
--- a/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs
+++ b/src/EFCore.Relational/Query/Internal/ContainsTranslator.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
namespace Microsoft.EntityFrameworkCore.Query.Internal;
@@ -38,11 +39,14 @@ public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
IReadOnlyList arguments,
IDiagnosticsLogger logger)
{
+ SqlExpression? itemExpression = null, valuesExpression = null;
+
+ // Identify static Enumerable.Contains and instance List.Contains
if (method.IsGenericMethod
- && method.GetGenericMethodDefinition().Equals(EnumerableMethods.Contains)
+ && method.GetGenericMethodDefinition() == EnumerableMethods.Contains
&& ValidateValues(arguments[0]))
{
- return _sqlExpressionFactory.In(RemoveObjectConvert(arguments[1]), arguments[0], negated: false);
+ (itemExpression, valuesExpression) = (RemoveObjectConvert(arguments[1]), arguments[0]);
}
if (arguments.Count == 1
@@ -50,14 +54,33 @@ public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
&& instance != null
&& ValidateValues(instance))
{
- return _sqlExpressionFactory.In(RemoveObjectConvert(arguments[0]), instance, negated: false);
+ (itemExpression, valuesExpression) = (RemoveObjectConvert(arguments[0]), instance);
+ }
+
+ if (itemExpression is not null && valuesExpression is not null)
+ {
+ switch (valuesExpression)
+ {
+ case SqlParameterExpression parameter:
+ return _sqlExpressionFactory.In(itemExpression, parameter);
+
+ case SqlConstantExpression { Value: IEnumerable values }:
+ var valuesExpressions = new List();
+
+ foreach (var value in values)
+ {
+ valuesExpressions.Add(_sqlExpressionFactory.Constant(value));
+ }
+
+ return _sqlExpressionFactory.In(itemExpression, valuesExpressions);
+ }
}
return null;
}
private static bool ValidateValues(SqlExpression values)
- => values is SqlConstantExpression || values is SqlParameterExpression;
+ => values is SqlConstantExpression or SqlParameterExpression;
private static SqlExpression RemoveObjectConvert(SqlExpression expression)
=> expression is SqlUnaryExpression sqlUnaryExpression
diff --git a/src/EFCore.Relational/Query/Internal/FromSqlParameterExpandingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/FromSqlParameterExpandingExpressionVisitor.cs
index 6cf339b80a1..f95c11ea530 100644
--- a/src/EFCore.Relational/Query/Internal/FromSqlParameterExpandingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/Internal/FromSqlParameterExpandingExpressionVisitor.cs
@@ -130,38 +130,58 @@ public virtual Expression Expand(
return _visitedFromSqlExpressions[fromSql] = fromSql.Update(
Expression.Constant(new CompositeRelationalParameter(parameterExpression.Name!, subParameters)));
- case ConstantExpression constantExpression:
- var existingValues = constantExpression.GetConstantValue
- protected override bool ShouldConvertToInlineQueryRoot(ConstantExpression constantExpression)
+ protected override bool ShouldConvertToInlineQueryRoot(NewArrayExpression newArrayExpression)
=> true;
///
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index 900f54abbc1..3c3c0331463 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
+using System.Reflection.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
@@ -207,8 +208,8 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
return new ShapedQueryExpression(selectExpression, shaperExpression);
}
- case InlineQueryRootExpression constantQueryRootExpression:
- return VisitInlineQueryRoot(constantQueryRootExpression) ?? base.VisitExtension(extensionExpression);
+ case InlineQueryRootExpression inlineQueryRootExpression:
+ return VisitInlineQueryRoot(inlineQueryRootExpression) ?? base.VisitExtension(extensionExpression);
case ParameterQueryRootExpression parameterQueryRootExpression:
var sqlParameterExpression =
@@ -319,26 +320,34 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
for (var i = 0; i < inlineQueryRootExpression.Values.Count; i++)
{
- var value = inlineQueryRootExpression.Values[i];
-
- // We currently support constants only; supporting non-constant values in VALUES is tracked by #30734.
- if (value is not ConstantExpression constantExpression)
+ // Note that we specifically don't apply the default type mapping to the translation, to allow it to get inferred later based
+ // on usage.
+ if (TranslateExpression(inlineQueryRootExpression.Values[i], applyDefaultTypeMapping: false)
+ is not SqlExpression translatedValue)
{
- AddTranslationErrorDetails(RelationalStrings.OnlyConstantsSupportedInInlineCollectionQueryRoots);
return null;
}
- if (constantExpression.Value is null)
+ // We currently support only constants and parameters in VALUES, see #30734
+ if (translatedValue is not SqlConstantExpression and not SqlParameterExpression)
{
- encounteredNull = true;
+ AddTranslationErrorDetails(RelationalStrings.NonConstantOrParameterAsInExpressionValue(translatedValue.GetType().Name));
+ return null;
}
+ // TODO: Poor man's null semantics - we currently only support constants and parameters in SqlNullabilityProcessor, where we
+ // can see if there's actually a null value or not. When we allow arbitrary expressions (#30734), the ValuesExpression projects
+ // out a non-nullable column if we see only non-null constants or nullable columns.
+ // This should be handled properly, possibly in SqlNullabilityProcessor (e.g. any complex expression is assumed to be nullable).
+ encounteredNull |=
+ translatedValue is not SqlConstantExpression { Value: not null } and not ColumnExpression { IsNullable: false };
+
rowExpressions.Add(new RowValueExpression(new[]
{
// Since VALUES may not guarantee row ordering, we add an _ord value by which we'll order.
_sqlExpressionFactory.Constant(i, intTypeMapping),
// Note that for the actual value, we must leave the type mapping null to allow it to get inferred later based on usage
- _sqlExpressionFactory.Constant(constantExpression.Value, elementType, typeMapping: null)
+ translatedValue
}));
}
@@ -432,7 +441,7 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
selectExpression.ClearOrdering();
}
- translation = _sqlExpressionFactory.Exists(selectExpression, true);
+ translation = _sqlExpressionFactory.Exists(selectExpression, negated: true);
selectExpression = _sqlExpressionFactory.Select(translation);
return source.Update(
@@ -468,7 +477,7 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
selectExpression.ClearOrdering();
}
- var translation = _sqlExpressionFactory.Exists(selectExpression, false);
+ var translation = _sqlExpressionFactory.Exists(selectExpression);
selectExpression = _sqlExpressionFactory.Select(translation);
return source.Update(
@@ -531,7 +540,7 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
selectExpression.ReplaceProjection(new List { sqlExpression });
selectExpression.ApplyProjection();
- translation = _sqlExpressionFactory.In(translation, selectExpression, false);
+ translation = _sqlExpressionFactory.In(translation, selectExpression);
selectExpression = _sqlExpressionFactory.Select(translation);
return source.Update(
@@ -1763,10 +1772,13 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate(
/// Translates the given expression into equivalent SQL representation.
///
/// An expression to translate.
+ ///
+ /// Whether to apply the default type mapping on the top-most element if it has none. Defaults to .
+ ///
/// A which is translation of given expression or .
- protected virtual SqlExpression? TranslateExpression(Expression expression)
+ protected virtual SqlExpression? TranslateExpression(Expression expression, bool applyDefaultTypeMapping = true)
{
- var translation = _sqlTranslator.Translate(expression);
+ var translation = _sqlTranslator.Translate(expression, applyDefaultTypeMapping);
if (translation is null)
{
@@ -1802,7 +1814,7 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate(
protected virtual Expression ApplyInferredTypeMappings(
Expression expression,
IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
- => new RelationalInferredTypeMappingApplier(inferredTypeMappings).Visit(expression);
+ => new RelationalInferredTypeMappingApplier(_sqlExpressionFactory, inferredTypeMappings).Visit(expression);
///
/// Determines whether the given is ordered, typically because orderings have been added to it.
@@ -1870,22 +1882,14 @@ private bool TrySimplifyValuesToInExpression(
return false;
}
- var values = new object?[valuesExpression.RowValues.Count];
+ var values = new SqlExpression[valuesExpression.RowValues.Count];
for (var i = 0; i < values.Length; i++)
{
// Skip the first value (_ord), which is irrelevant for Contains
- if (valuesExpression.RowValues[i].Values[1] is SqlConstantExpression { Value: var constantValue })
- {
- values[i] = constantValue;
- }
- else
- {
- simplifiedQuery = null;
- return false;
- }
+ values[i] = valuesExpression.RowValues[i].Values[1];
}
- var inExpression = _sqlExpressionFactory.In(item, _sqlExpressionFactory.Constant(values), isNegated);
+ var inExpression = _sqlExpressionFactory.In(item, values, isNegated);
simplifiedQuery = source.Update(_sqlExpressionFactory.Select(inExpression), source.ShaperExpression);
return true;
}
@@ -2751,6 +2755,7 @@ private void RegisterInferredTypeMapping(ColumnExpression columnExpression, Rela
///
protected class RelationalInferredTypeMappingApplier : ExpressionVisitor
{
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
private SelectExpression? _currentSelectExpression;
///
@@ -2761,10 +2766,15 @@ protected class RelationalInferredTypeMappingApplier : ExpressionVisitor
///
/// Creates a new instance of the class.
///
+ /// The SQL expression factory.
/// The inferred type mappings to be applied back on their query roots.
public RelationalInferredTypeMappingApplier(
+ ISqlExpressionFactory sqlExpressionFactory,
IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
- => InferredTypeMappings = inferredTypeMappings;
+ {
+ _sqlExpressionFactory = sqlExpressionFactory;
+ InferredTypeMappings = inferredTypeMappings;
+ }
///
protected override Expression VisitExtension(Expression expression)
@@ -2831,30 +2841,27 @@ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExp
var newValues = new SqlExpression[newColumnNames.Count];
for (var j = 0; j < valuesExpression.ColumnNames.Count; j++)
{
- Check.DebugAssert(rowValue.Values[j] is SqlConstantExpression, "Non-constant SqlExpression in ValuesExpression");
-
if (j == 0 && stripOrdering)
{
continue;
}
- var value = (SqlConstantExpression)rowValue.Values[j];
- SqlExpression newValue = value;
+ var value = rowValue.Values[j];
var inferredTypeMapping = inferredTypeMappings[j];
if (inferredTypeMapping is not null && value.TypeMapping is null)
{
- newValue = new SqlConstantExpression(Expression.Constant(value.Value, value.Type), inferredTypeMapping);
+ value = _sqlExpressionFactory.ApplyTypeMapping(value, inferredTypeMapping);
// We currently add explicit conversions on the first row, to ensure that the inferred types are properly typed.
// See #30605 for removing that when not needed.
if (i == 0)
{
- newValue = new SqlUnaryExpression(ExpressionType.Convert, newValue, newValue.Type, newValue.TypeMapping);
+ value = new SqlUnaryExpression(ExpressionType.Convert, value, value.Type, value.TypeMapping);
}
}
- newValues[j - (stripOrdering ? 1 : 0)] = newValue;
+ newValues[j - (stripOrdering ? 1 : 0)] = value;
}
newRowValues[i] = new RowValueExpression(newValues);
diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
index 38569b6d852..38c32d686f0 100644
--- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
@@ -118,33 +118,38 @@ protected virtual void AddTranslationErrorDetails(string details)
/// Translates an expression to an equivalent SQL representation.
///
/// An expression to translate.
+ ///
+ /// Whether to apply the default type mapping on the top-most element if it has none. Defaults to .
+ ///
/// A SQL translation of the given expression.
- public virtual SqlExpression? Translate(Expression expression)
+ public virtual SqlExpression? Translate(Expression expression, bool applyDefaultTypeMapping = true)
{
TranslationErrorDetails = null;
- return TranslateInternal(expression);
+ return TranslateInternal(expression, applyDefaultTypeMapping);
}
- private SqlExpression? TranslateInternal(Expression expression)
+ private SqlExpression? TranslateInternal(Expression expression, bool applyDefaultTypeMapping = true)
{
var result = Visit(expression);
if (result is SqlExpression translation)
{
- if (translation is SqlUnaryExpression sqlUnaryExpression
- && sqlUnaryExpression.OperatorType == ExpressionType.Convert
+ if (translation is SqlUnaryExpression { OperatorType: ExpressionType.Convert } sqlUnaryExpression
&& sqlUnaryExpression.Type == typeof(object))
{
translation = sqlUnaryExpression.Operand;
}
- translation = _sqlExpressionFactory.ApplyDefaultTypeMapping(translation);
-
- if (translation.TypeMapping == null)
+ if (applyDefaultTypeMapping)
{
- // The return type is not-mappable hence return null
- return null;
+ translation = _sqlExpressionFactory.ApplyDefaultTypeMapping(translation);
+
+ if (translation.TypeMapping == null)
+ {
+ // The return type is not-mappable hence return null
+ return null;
+ }
}
return translation;
@@ -480,7 +485,7 @@ Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, T
subSelectExpression.ClearOrdering();
}
- return _sqlExpressionFactory.Exists(subSelectExpression, false);
+ return _sqlExpressionFactory.Exists(subSelectExpression);
}
if (entityReferenceExpression.ParameterEntity != null)
@@ -738,7 +743,9 @@ protected override Expression VisitMember(MemberExpression memberExpression)
///
protected override Expression VisitMemberInit(MemberInitExpression memberInitExpression)
- => GetConstantOrNotTranslated(memberInitExpression);
+ => TryEvaluateToConstant(memberInitExpression, out var sqlConstantExpression)
+ ? sqlConstantExpression
+ : QueryCompilationContext.NotTranslatedExpression;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -997,11 +1004,21 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
///
protected override Expression VisitNew(NewExpression newExpression)
- => GetConstantOrNotTranslated(newExpression);
+ => TryEvaluateToConstant(newExpression, out var sqlConstantExpression)
+ ? sqlConstantExpression
+ : QueryCompilationContext.NotTranslatedExpression;
///
protected override Expression VisitNewArray(NewArrayExpression newArrayExpression)
- => QueryCompilationContext.NotTranslatedExpression;
+ {
+ if (TryEvaluateToConstant(newArrayExpression, out var sqlConstantExpression))
+ {
+ return sqlConstantExpression;
+ }
+
+ AddTranslationErrorDetails(RelationalStrings.CannotTranslateNonConstantNewArrayExpression(newArrayExpression.Print()));
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
///
protected override Expression VisitParameter(ParameterExpression parameterExpression)
@@ -1059,7 +1076,7 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExp
subSelectExpression.ClearOrdering();
}
- return _sqlExpressionFactory.Exists(subSelectExpression, false);
+ return _sqlExpressionFactory.Exists(subSelectExpression);
}
if (entityReferenceExpression.ParameterEntity != null)
@@ -1090,8 +1107,7 @@ SqlExpression GeneratePredicateTpt(EntityProjectionExpression entityProjectionEx
_sqlExpressionFactory.Constant(discriminatorValues[0]))
: _sqlExpressionFactory.In(
entityProjectionExpression.DiscriminatorExpression!,
- _sqlExpressionFactory.Constant(discriminatorValues),
- negated: false);
+ discriminatorValues.Select(d => _sqlExpressionFactory.Constant(d)).ToArray());
}
}
else
@@ -1110,9 +1126,7 @@ SqlExpression GeneratePredicateTpt(EntityProjectionExpression entityProjectionEx
_sqlExpressionFactory.Constant(concreteEntityTypes[0].GetDiscriminatorValue()))
: _sqlExpressionFactory.In(
discriminatorColumn,
- _sqlExpressionFactory.Constant(
- concreteEntityTypes.Select(et => et.GetDiscriminatorValue()).ToList()),
- negated: false);
+ concreteEntityTypes.Select(et => _sqlExpressionFactory.Constant(et.GetDiscriminatorValue())).ToArray());
}
}
else
@@ -1568,16 +1582,23 @@ private static Expression ConvertObjectArrayEqualityComparison(Expression left,
.Aggregate((a, b) => Expression.AndAlso(a, b));
}
- private static Expression GetConstantOrNotTranslated(Expression expression)
- => CanEvaluate(expression)
- ? new SqlConstantExpression(
+ private static bool TryEvaluateToConstant(Expression expression, [NotNullWhen(true)] out SqlConstantExpression? sqlConstantExpression)
+ {
+ if (CanEvaluate(expression))
+ {
+ sqlConstantExpression = new SqlConstantExpression(
Expression.Constant(
Expression.Lambda>(Expression.Convert(expression, typeof(object)))
.Compile(preferInterpretation: true)
.Invoke(),
expression.Type),
- null)
- : QueryCompilationContext.NotTranslatedExpression;
+ null);
+ return true;
+ }
+
+ sqlConstantExpression = null;
+ return false;
+ }
private bool TryRewriteContainsEntity(Expression source, Expression item, [NotNullWhen(true)] out Expression? result)
{
@@ -1861,26 +1882,15 @@ when memberInitExpression.Bindings.SingleOrDefault(
}
private static bool CanEvaluate(Expression expression)
- {
-#pragma warning disable IDE0066 // Convert switch statement to expression
- switch (expression)
-#pragma warning restore IDE0066 // Convert switch statement to expression
- {
- case ConstantExpression:
- return true;
-
- case NewExpression newExpression:
- return newExpression.Arguments.All(e => CanEvaluate(e));
-
- case MemberInitExpression memberInitExpression:
- return CanEvaluate(memberInitExpression.NewExpression)
- && memberInitExpression.Bindings.All(
- mb => mb is MemberAssignment memberAssignment && CanEvaluate(memberAssignment.Expression));
-
- default:
- return false;
- }
- }
+ => expression switch
+ {
+ ConstantExpression => true,
+ NewExpression e => e.Arguments.All(CanEvaluate),
+ NewArrayExpression e => e.Expressions.All(CanEvaluate),
+ MemberInitExpression e => CanEvaluate(e.NewExpression)
+ && e.Bindings.All(mb => mb is MemberAssignment memberAssignment && CanEvaluate(memberAssignment.Expression)),
+ _ => false
+ };
private static bool IsNullSqlConstantExpression(Expression expression)
=> expression is SqlConstantExpression sqlConstant && sqlConstant.Value == null;
diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs
index 53d23cbdfdf..ebd898499d0 100644
--- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs
+++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs
@@ -239,28 +239,80 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(
resultTypeMapping);
}
- private SqlExpression ApplyTypeMappingOnIn(InExpression inExpression)
+ private InExpression ApplyTypeMappingOnIn(InExpression inExpression)
{
- var itemTypeMapping = (inExpression.Values != null
- ? ExpressionExtensions.InferTypeMapping(inExpression.Item, inExpression.Values)
- : inExpression.Subquery != null
- ? ExpressionExtensions.InferTypeMapping(inExpression.Item, inExpression.Subquery.Projection[0].Expression)
- : inExpression.Item.TypeMapping)
- ?? _typeMappingSource.FindMapping(inExpression.Item.Type, Dependencies.Model);
-
- var item = ApplyTypeMapping(inExpression.Item, itemTypeMapping);
- if (inExpression.Values != null)
+ var missingTypeMappingInValues = false;
+
+ RelationalTypeMapping? valuesTypeMapping = null;
+ switch (inExpression)
{
- var values = ApplyTypeMapping(inExpression.Values, itemTypeMapping);
+ case { Subquery: SelectExpression subquery }:
+ valuesTypeMapping = subquery.Projection[0].Expression.TypeMapping;
+ break;
+
+ case { ValuesParameter: SqlParameterExpression parameter }:
+ valuesTypeMapping = parameter.TypeMapping;
+ break;
+
+ case { Values: IReadOnlyList values }:
+ // Note: there could be conflicting type mappings inside the values; we take the first.
+ foreach (var value in values)
+ {
+ if (value.TypeMapping is null)
+ {
+ missingTypeMappingInValues = true;
+ }
+ else
+ {
+ valuesTypeMapping = value.TypeMapping;
+ }
+ }
+
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ var item = ApplyTypeMapping(
+ inExpression.Item,
+ valuesTypeMapping ?? Dependencies.TypeMappingSource.FindMapping(inExpression.Item.Type, Dependencies.Model));
+
+ switch (inExpression)
+ {
+ case { Subquery: SelectExpression subquery }:
+ inExpression = inExpression.Update(item, subquery, values: null, valuesParameter: null);
+ break;
+
+ case { ValuesParameter: SqlParameterExpression parameter }:
+ inExpression = inExpression.Update(
+ item, subquery: null, values: null, (SqlParameterExpression)ApplyTypeMapping(parameter, item.TypeMapping));
+ break;
+
+ case { Values: IReadOnlyList values }:
+ SqlExpression[]? newValues = null;
+
+ if (missingTypeMappingInValues)
+ {
+ newValues = new SqlExpression[values.Count];
+
+ for (var i = 0; i < newValues.Length; i++)
+ {
+ newValues[i] = ApplyTypeMapping(values[i], item.TypeMapping);
+ }
+ }
- return item != inExpression.Item || values != inExpression.Values || inExpression.TypeMapping != _boolTypeMapping
- ? new InExpression(item, values, inExpression.IsNegated, _boolTypeMapping)
- : inExpression;
+ inExpression = inExpression.Update(item, subquery: null, newValues ?? values, valuesParameter: null);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
- return item != inExpression.Item || inExpression.TypeMapping != _boolTypeMapping
- ? new InExpression(item, inExpression.Subquery!, inExpression.IsNegated, _boolTypeMapping)
- : inExpression;
+ return inExpression.TypeMapping == _boolTypeMapping
+ ? inExpression
+ : inExpression.ApplyTypeMapping(_boolTypeMapping);
+
}
private SqlExpression ApplyTypeMappingOnJsonScalar(
@@ -582,29 +634,20 @@ public virtual SqlFunctionExpression NiladicFunction(
ApplyDefaultTypeMapping(instance), name, nullable, instancePropagatesNullability, returnType, typeMapping);
///
- public virtual ExistsExpression Exists(SelectExpression subquery, bool negated)
+ public virtual ExistsExpression Exists(SelectExpression subquery, bool negated = false)
=> new(subquery, negated, _boolTypeMapping);
///
- public virtual InExpression In(SqlExpression item, SqlExpression values, bool negated)
- {
- var typeMapping = item.TypeMapping ?? _typeMappingSource.FindMapping(item.Type, Dependencies.Model);
-
- item = ApplyTypeMapping(item, typeMapping);
- values = ApplyTypeMapping(values, typeMapping);
-
- return new InExpression(item, values, negated, _boolTypeMapping);
- }
+ public virtual InExpression In(SqlExpression item, SelectExpression subquery, bool negated = false)
+ => ApplyTypeMappingOnIn(new InExpression(item, subquery, negated, _boolTypeMapping));
///
- public virtual InExpression In(SqlExpression item, SelectExpression subquery, bool negated)
- {
- var sqlExpression = subquery.Projection.Single().Expression;
- var typeMapping = sqlExpression.TypeMapping;
+ public virtual InExpression In(SqlExpression item, IReadOnlyList values, bool negated = false)
+ => ApplyTypeMappingOnIn(new InExpression(item, values, negated, _boolTypeMapping));
- item = ApplyTypeMapping(item, typeMapping);
- return new InExpression(item, subquery, negated, _boolTypeMapping);
- }
+ ///
+ public virtual InExpression In(SqlExpression item, SqlParameterExpression valuesParameter, bool negated = false)
+ => ApplyTypeMappingOnIn(new InExpression(item, valuesParameter, negated, _boolTypeMapping));
///
public virtual LikeExpression Like(SqlExpression match, SqlExpression pattern, SqlExpression? escapeChar = null)
@@ -667,7 +710,7 @@ private void AddConditions(SelectExpression selectExpression, IEntityType entity
var concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToList();
var predicate = concreteEntityTypes.Count == 1
? (SqlExpression)Equal(discriminatorColumn, Constant(concreteEntityTypes[0].GetDiscriminatorValue()))
- : In(discriminatorColumn, Constant(concreteEntityTypes.Select(et => et.GetDiscriminatorValue()).ToList()), negated: false);
+ : In(discriminatorColumn, concreteEntityTypes.Select(et => Constant(et.GetDiscriminatorValue())).ToArray());
selectExpression.ApplyPredicate(predicate);
diff --git a/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs
index af0777b47bb..45255ba53c4 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs
@@ -17,10 +17,10 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
public class InExpression : SqlExpression
{
///
- /// Creates a new instance of the class which represents a IN subquery expression.
+ /// Creates a new instance of the class, representing a SQL IN expression with a subquery.
///
/// An item to look into values.
- /// A subquery in which item is searched.
+ /// A subquery in which the item is searched.
/// A value indicating if the item should be present in the values or absent.
/// The associated with the expression.
public InExpression(
@@ -28,30 +28,49 @@ public InExpression(
SelectExpression subquery,
bool negated,
RelationalTypeMapping typeMapping)
- : this(item, null, subquery, negated, typeMapping)
+ : this(item, subquery, values: null, valuesParameter: null, negated, typeMapping)
{
}
///
- /// Creates a new instance of the class which represents a IN values expression.
+ /// Creates a new instance of the class, representing a SQL IN expression with a given list
+ /// of values.
///
/// An item to look into values.
- /// A list of values in which item is searched.
+ /// A list of values in which the item is searched.
/// A value indicating if the item should be present in the values or absent.
/// The associated with the expression.
public InExpression(
SqlExpression item,
- SqlExpression values,
+ IReadOnlyList values,
bool negated,
RelationalTypeMapping typeMapping)
- : this(item, values, null, negated, typeMapping)
+ : this(item, subquery: null, values, valuesParameter: null, negated, typeMapping)
+ {
+ }
+
+ ///
+ /// Creates a new instance of the class, representing a SQL IN expression with a given
+ /// parameterized list of values.
+ ///
+ /// An item to look into values.
+ /// A parameterized list of values in which the item is searched.
+ /// A value indicating if the item should be present in the values or absent.
+ /// The associated with the expression.
+ public InExpression(
+ SqlExpression item,
+ SqlParameterExpression valuesParameter,
+ bool negated,
+ RelationalTypeMapping typeMapping)
+ : this(item, subquery: null, values: null, valuesParameter, negated, typeMapping)
{
}
private InExpression(
SqlExpression item,
- SqlExpression? values,
SelectExpression? subquery,
+ IReadOnlyList? values,
+ SqlParameterExpression? valuesParameter,
bool negated,
RelationalTypeMapping? typeMapping)
: base(typeof(bool), typeMapping)
@@ -65,6 +84,7 @@ private InExpression(
Item = item;
Subquery = subquery;
Values = values;
+ ValuesParameter = valuesParameter;
IsNegated = negated;
}
@@ -79,23 +99,54 @@ private InExpression(
public virtual bool IsNegated { get; }
///
- /// The list of values to search item in.
+ /// The subquery to search the item in.
///
- public virtual SqlExpression? Values { get; }
+ public virtual SelectExpression? Subquery { get; }
///
- /// The subquery to search item in.
+ /// The list of values to search the item in.
///
- public virtual SelectExpression? Subquery { get; }
+ public virtual IReadOnlyList? Values { get; }
+
+ ///
+ /// A parameter containing the list of values to search the item in. The parameterized list get expanded to the actual value
+ /// before the query SQL is generated.
+ ///
+ public virtual SqlParameterExpression? ValuesParameter { get; }
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var item = (SqlExpression)visitor.Visit(Item);
var subquery = (SelectExpression?)visitor.Visit(Subquery);
- var values = (SqlExpression?)visitor.Visit(Values);
- return Update(item, values, subquery);
+ SqlExpression[]? values = null;
+ if (Values is not null)
+ {
+ for (var i = 0; i < Values.Count; i++)
+ {
+ var value = Values[i];
+ var newValue = (SqlExpression)visitor.Visit(value);
+
+ if (newValue != value && values is null)
+ {
+ values = new SqlExpression[Values.Count];
+ for (var j = 0; j < i; j++)
+ {
+ values[j] = Values[j];
+ }
+ }
+
+ if (values is not null)
+ {
+ values[i] = newValue;
+ }
+ }
+ }
+
+ var valuesParameter = (SqlParameterExpression?)visitor.Visit(ValuesParameter);
+
+ return Update(item, subquery, values ?? Values, valuesParameter);
}
///
@@ -103,30 +154,40 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
///
/// An expression which is negated form of this expression.
public virtual InExpression Negate()
- => new(Item, Values, Subquery, !IsNegated, TypeMapping);
+ => new(Item, Subquery, Values, ValuesParameter, !IsNegated, TypeMapping);
+
+ ///
+ /// Applies supplied type mapping to this expression.
+ ///
+ /// A relational type mapping to apply.
+ /// A new expression which has supplied type mapping.
+ public virtual InExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
+ => new(Item, Subquery, Values, ValuesParameter, IsNegated, typeMapping);
///
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
/// return this expression.
///
/// The property of the result.
- /// The property of the result.
/// The property of the result.
+ /// The property of the result.
+ /// The property of the result.
/// This expression if no children changed, or an expression with the updated children.
public virtual InExpression Update(
SqlExpression item,
- SqlExpression? values,
- SelectExpression? subquery)
+ SelectExpression? subquery,
+ IReadOnlyList? values,
+ SqlParameterExpression? valuesParameter)
{
- if (values != null
- && subquery != null)
+ if ((subquery is null ? 0 : 1) + (values is null ? 0 : 1) + (valuesParameter is null ? 0 : 1) != 1)
{
- throw new ArgumentException(RelationalStrings.EitherOfTwoValuesMustBeNull(nameof(values), nameof(subquery)));
+ throw new ArgumentException(
+ RelationalStrings.OneOfThreeValuesMustBeSet(nameof(subquery), nameof(values), nameof(valuesParameter)));
}
- return item != Item || subquery != Subquery || values != Values
- ? new InExpression(item, values, subquery, IsNegated, TypeMapping)
- : this;
+ return item == Item && subquery == Subquery && values == Values && valuesParameter == ValuesParameter
+ ? this
+ : new InExpression(item, subquery, values, valuesParameter, IsNegated, TypeMapping);
}
///
@@ -136,31 +197,35 @@ protected override void Print(ExpressionPrinter expressionPrinter)
expressionPrinter.Append(IsNegated ? " NOT IN " : " IN ");
expressionPrinter.Append("(");
- if (Subquery != null)
+ switch (this)
{
- using (expressionPrinter.Indent())
- {
- expressionPrinter.Visit(Subquery);
- }
- }
- else if (Values is SqlConstantExpression constantValuesExpression
- && constantValuesExpression.Value is IEnumerable constantValues)
- {
- var first = true;
- foreach (var item in constantValues)
- {
- if (!first)
+ case { Subquery: not null }:
+ using (expressionPrinter.Indent())
{
- expressionPrinter.Append(", ");
+ expressionPrinter.Visit(Subquery);
}
- first = false;
- expressionPrinter.Append(constantValuesExpression.TypeMapping?.GenerateSqlLiteral(item) ?? item?.ToString() ?? "NULL");
- }
- }
- else
- {
- expressionPrinter.Visit(Values);
+ break;
+
+ case { Values: not null }:
+ for (var i = 0; i < Values.Count; i++)
+ {
+ if (i > 0)
+ {
+ expressionPrinter.Append(", ");
+ }
+
+ expressionPrinter.Visit(Values[i]);
+ }
+
+ break;
+
+ case { ValuesParameter: not null}:
+ expressionPrinter.Visit(ValuesParameter);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
expressionPrinter.Append(")");
@@ -177,10 +242,11 @@ private bool Equals(InExpression inExpression)
=> base.Equals(inExpression)
&& Item.Equals(inExpression.Item)
&& IsNegated.Equals(inExpression.IsNegated)
+ && (Subquery?.Equals(inExpression.Subquery) ?? inExpression.Subquery == null)
&& (Values?.Equals(inExpression.Values) ?? inExpression.Values == null)
- && (Subquery?.Equals(inExpression.Subquery) ?? inExpression.Subquery == null);
+ && (ValuesParameter?.Equals(inExpression.ValuesParameter) ?? inExpression.ValuesParameter == null);
///
public override int GetHashCode()
- => HashCode.Combine(base.GetHashCode(), Item, IsNegated, Values, Subquery);
+ => HashCode.Combine(base.GetHashCode(), Item, IsNegated, Values, Subquery, ValuesParameter);
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index d0815c0bb99..232cea1efa3 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -1904,15 +1904,30 @@ public void ApplyPredicate(SqlExpression sqlExpression)
}
}
}
- else if (sqlExpression is InExpression inExpression
- && inExpression.Item is ColumnExpression itemColumn
- && itemColumn.Table is TpcTablesExpression itemTpc
+ // Identify application of a predicate which narrows the discriminator (e.g. OfType) for TPC, apply it to
+ // _tpcDiscriminatorValues (which will be handled later) instead of as a WHERE predicate.
+ else if (sqlExpression is InExpression
+ {
+ Item: ColumnExpression { Table: TpcTablesExpression itemTpc } itemColumn,
+ Values: IReadOnlyList valueExpressions
+ }
&& _tpcDiscriminatorValues.TryGetValue(itemTpc, out var itemTuple)
- && itemTuple.Item1.Equals(itemColumn)
- && inExpression.Values is SqlConstantExpression itemConstant
- && itemConstant.Value is List values)
+ && itemTuple.Item1.Equals(itemColumn))
{
- var newList = itemTuple.Item2.Intersect(values).ToList();
+ var constantValues = new string[valueExpressions.Count];
+ for (var i = 0; i < constantValues.Length; i++)
+ {
+ if (valueExpressions[i] is SqlConstantExpression { Value: string value })
+ {
+ constantValues[i] = value;
+ }
+ else
+ {
+ goto ApplyPredicate;
+ }
+ }
+
+ var newList = itemTuple.Item2.Intersect(constantValues).ToList();
if (newList.Count > 0)
{
_tpcDiscriminatorValues[itemTpc] = (itemColumn, newList);
@@ -1921,6 +1936,7 @@ public void ApplyPredicate(SqlExpression sqlExpression)
}
}
+ ApplyPredicate:
sqlExpression = AssignUniqueAliases(sqlExpression);
if (_groupBy.Count > 0)
diff --git a/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs
index 56fcde09c15..02de0686118 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs
@@ -42,23 +42,9 @@ public ValuesExpression(
{
Check.NotEmpty(rowValues, nameof(rowValues));
-#if DEBUG
- if (rowValues.Any(rv => rv.Values.Count != columnNames.Count))
- {
- throw new ArgumentException("All number of all row values doesn't match the number of column names");
- }
-
- if (rowValues.SelectMany(rv => rv.Values).Any(
- v => v is not SqlConstantExpression and not SqlUnaryExpression
- {
- Operand: SqlConstantExpression,
- OperatorType: ExpressionType.Convert
- }))
- {
- // See #30734 for non-constants
- throw new ArgumentException("Only constant expressions are supported in ValuesExpression");
- }
-#endif
+ Check.DebugAssert(
+ rowValues.All(rv => rv.Values.Count == columnNames.Count),
+ "All row values must have a value count matching the number of column names");
RowValues = rowValues;
ColumnNames = columnNames;
diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
index bf39f8f5947..1d7ad2c4a86 100644
--- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
+++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
@@ -650,6 +650,7 @@ protected virtual SqlExpression VisitExists(
protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOptimizedExpansion, out bool nullable)
{
var item = Visit(inExpression.Item, out var itemNullable);
+ inExpression = inExpression.Update(item, inExpression.Subquery, inExpression.Values, inExpression.ValuesParameter);
if (inExpression.Subquery != null)
{
@@ -666,37 +667,29 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
// if item is not nullable, and subquery contains a non-nullable column we know the result can never be null
// note: in this case we could broaden the optimization if we knew the nullability of the projection
// but we don't keep that information and we want to avoid double visitation
- nullable = !(!itemNullable
- && subquery.Projection.Count == 1
- && subquery.Projection[0].Expression is ColumnExpression columnProjection
- && !columnProjection.IsNullable);
+ nullable = itemNullable || subquery.Projection is not [{ Expression: ColumnExpression { IsNullable: false } }];
- return inExpression.Update(item, values: null, subquery);
+ return inExpression.Update(item, subquery, values: null, valuesParameter: null);
}
// for relational null semantics we don't need to extract null values from the array
- if (UseRelationalNulls
- || !(inExpression.Values is SqlConstantExpression || inExpression.Values is SqlParameterExpression))
+ if (UseRelationalNulls)
{
- var (valuesExpression, valuesList, _) = ProcessInExpressionValues(inExpression.Values!, extractNullValues: false);
+ var (processedInExpression2, _) = ProcessInExpressionValues(inExpression, extractNullValues: false);
nullable = false;
- return valuesList.Count == 0
+ return processedInExpression2.Values!.Count == 0
? _sqlExpressionFactory.Constant(false, inExpression.TypeMapping)
- : SimplifyInExpression(
- inExpression.Update(item, valuesExpression, subquery: null),
- valuesExpression,
- valuesList);
+ : SimplifyInExpression(processedInExpression2);
}
// for c# null semantics we need to remove nulls from Values and add IsNull/IsNotNull when necessary
- var (inValuesExpression, inValuesList, hasNullValue) = ProcessInExpressionValues(inExpression.Values, extractNullValues: true);
+ var (processedInExpression, hasNullValue) = ProcessInExpressionValues(inExpression, extractNullValues: true);
+ nullable = false;
// either values array is empty or only contains null
- if (inValuesList.Count == 0)
+ if (processedInExpression.Values!.Count == 0)
{
- nullable = false;
-
// a IN () -> false
// non_nullable IN (NULL) -> false
// a NOT IN () -> true
@@ -712,15 +705,11 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
: _sqlExpressionFactory.IsNull(item);
}
- var simplifiedInExpression = SimplifyInExpression(
- inExpression.Update(item, inValuesExpression, subquery: null),
- inValuesExpression,
- inValuesList);
+ var simplifiedInExpression = SimplifyInExpression(processedInExpression);
if (!itemNullable
|| (allowOptimizedExpansion && !inExpression.IsNegated && !hasNullValue))
{
- nullable = false;
// non_nullable IN (1, 2) -> non_nullable IN (1, 2)
// non_nullable IN (1, 2, NULL) -> non_nullable IN (1, 2)
@@ -730,8 +719,6 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
return simplifiedInExpression;
}
- nullable = false;
-
// nullable IN (1, 2) -> nullable IN (1, 2) AND nullable IS NOT NULL (full)
// nullable IN (1, 2, NULL) -> nullable IN (1, 2) OR nullable IS NULL (full)
// nullable NOT IN (1, 2) -> nullable NOT IN (1, 2) OR nullable IS NULL (full)
@@ -744,61 +731,81 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
simplifiedInExpression,
_sqlExpressionFactory.IsNull(item));
- (SqlConstantExpression ProcessedValuesExpression, List ProcessedValuesList, bool HasNullValue)
- ProcessInExpressionValues(SqlExpression valuesExpression, bool extractNullValues)
+ (InExpression ProcessedInExpression, bool HasNullValue) ProcessInExpressionValues(InExpression inExpression, bool extractNullValues)
{
- var inValues = new List();
+ List? processedValues = null;
var hasNullValue = false;
- RelationalTypeMapping? typeMapping;
- IEnumerable values;
- switch (valuesExpression)
+ if (inExpression.ValuesParameter is SqlParameterExpression valuesParameter)
{
- case SqlConstantExpression sqlConstant:
- typeMapping = sqlConstant.TypeMapping;
- values = (IEnumerable)sqlConstant.Value!;
- break;
-
- case SqlParameterExpression sqlParameter:
- DoNotCache();
- typeMapping = sqlParameter.TypeMapping;
- values = (IEnumerable?)ParameterValues[sqlParameter.Name] ?? Array.Empty();
- break;
-
- default:
- throw new InvalidOperationException(
- RelationalStrings.NonConstantOrParameterAsInExpressionValues(valuesExpression.GetType().Name));
- }
+ // The InExpression has a values parameter. Expand it out, embedding its values as constants into the SQL; disable SQL
+ // caching.
+ DoNotCache();
+ var typeMapping = inExpression.ValuesParameter.TypeMapping;
+ var values = (IEnumerable?)ParameterValues[valuesParameter.Name] ?? Array.Empty();
- foreach (var value in values)
- {
- if (value == null && extractNullValues)
+ processedValues = new List();
+
+ foreach (var value in values)
{
- hasNullValue = true;
- continue;
- }
+ if (value == null && extractNullValues)
+ {
+ hasNullValue = true;
+ continue;
+ }
- inValues.Add(value);
+ processedValues.Add(_sqlExpressionFactory.Constant(value, typeMapping));
+ }
}
+ else
+ {
+ Check.DebugAssert(inExpression.Values is not null, "inExpression.Values is not null");
- var processedValuesExpression = _sqlExpressionFactory.Constant(inValues, typeMapping);
+ for (var i = 0; i < inExpression.Values.Count; i++)
+ {
+ var valueExpression = inExpression.Values[i];
+
+ var value = valueExpression switch
+ {
+ SqlConstantExpression c => c.Value,
+ SqlParameterExpression p => ParameterValues[p.Name],
+
+ _ => throw new InvalidOperationException(
+ RelationalStrings.NonConstantOrParameterAsInExpressionValue(valueExpression.GetType().Name))
+ };
- return (processedValuesExpression, inValues, hasNullValue);
+ if (value is null && extractNullValues)
+ {
+ hasNullValue = true;
+
+ if (processedValues is null)
+ {
+ processedValues = new List(inExpression.Values.Count - 1);
+ for (var j = 0; j < i; j++)
+ {
+ processedValues.Add(inExpression.Values[j]);
+ }
+ }
+
+ // Skip the NULL value
+ continue;
+ }
+
+ processedValues?.Add(valueExpression);
+ }
+ }
+
+ var processedInExpression = inExpression.Update(
+ inExpression.Item, subquery: null, values: processedValues ?? inExpression.Values, valuesParameter: null);
+ return (processedInExpression, hasNullValue);
}
- SqlExpression SimplifyInExpression(
- InExpression inExpression,
- SqlConstantExpression inValuesExpression,
- List inValuesList)
- => inValuesList.Count == 1
- ? inExpression.IsNegated
- ? (SqlExpression)_sqlExpressionFactory.NotEqual(
- inExpression.Item,
- _sqlExpressionFactory.Constant(inValuesList[0], inValuesExpression.TypeMapping))
- : _sqlExpressionFactory.Equal(
- inExpression.Item,
- _sqlExpressionFactory.Constant(inValuesList[0], inExpression.Values!.TypeMapping))
- : inExpression;
+ SqlExpression SimplifyInExpression(InExpression inExpression)
+ => inExpression.Values is not [var valueExpression]
+ ? inExpression
+ : inExpression.IsNegated
+ ? _sqlExpressionFactory.NotEqual(inExpression.Item, valueExpression)
+ : _sqlExpressionFactory.Equal(inExpression.Item, valueExpression);
}
///
diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
index 7ea65b2626a..150640dbe0c 100644
--- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
@@ -223,10 +223,36 @@ protected override Expression VisitIn(InExpression inExpression)
_isSearchCondition = false;
var item = (SqlExpression)Visit(inExpression.Item);
var subquery = (SelectExpression?)Visit(inExpression.Subquery);
- var values = (SqlExpression?)Visit(inExpression.Values);
+
+ var values = inExpression.Values;
+ SqlExpression[]? newValues = null;
+ if (values is not null)
+ {
+ for (var i = 0; i < values.Count; i++)
+ {
+ var value = values[i];
+ var newValue = (SqlExpression)Visit(value);
+
+ if (newValue != value && newValues is null)
+ {
+ newValues = new SqlExpression[values.Count];
+ for (var j = 0; j < i; j++)
+ {
+ newValues[j] = values[j];
+ }
+ }
+
+ if (newValues is not null)
+ {
+ newValues[i] = newValue;
+ }
+ }
+ }
+
+ var valuesParameter = (SqlParameterExpression?)Visit(inExpression.ValuesParameter);
_isSearchCondition = parentSearchCondition;
- return ApplyConversion(inExpression.Update(item, values, subquery), condition: true);
+ return ApplyConversion(inExpression.Update(item, subquery, newValues ?? values, valuesParameter), condition: true);
}
///
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
index 8269ac1500d..30f7a65a09f 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
@@ -427,7 +427,7 @@ public SqlServerInferredTypeMappingApplier(
IRelationalTypeMappingSource typeMappingSource,
ISqlExpressionFactory sqlExpressionFactory,
IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
- : base(inferredTypeMappings)
+ : base(sqlExpressionFactory, inferredTypeMappings)
=> (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, sqlExpressionFactory);
///
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
index ccd80eb9f4a..0d67d29347d 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
@@ -296,7 +296,6 @@ protected override Expression ApplyInferredTypeMappings(
protected class SqliteInferredTypeMappingApplier : RelationalInferredTypeMappingApplier
{
private readonly IRelationalTypeMappingSource _typeMappingSource;
- private readonly ISqlExpressionFactory _sqlExpressionFactory;
private Dictionary? _currentSelectInferredTypeMappings;
///
@@ -309,8 +308,8 @@ public SqliteInferredTypeMappingApplier(
IRelationalTypeMappingSource typeMappingSource,
ISqlExpressionFactory sqlExpressionFactory,
IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
- : base(inferredTypeMappings)
- => (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, sqlExpressionFactory);
+ : base(sqlExpressionFactory, inferredTypeMappings)
+ => _typeMappingSource = typeMappingSource;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore/Query/InlineQueryRootExpression.cs b/src/EFCore/Query/InlineQueryRootExpression.cs
index 3106f618c8a..dbbb0b470c2 100644
--- a/src/EFCore/Query/InlineQueryRootExpression.cs
+++ b/src/EFCore/Query/InlineQueryRootExpression.cs
@@ -45,6 +45,17 @@ public InlineQueryRootExpression(IReadOnlyList values, Type elementT
public override Expression DetachQueryProvider()
=> new InlineQueryRootExpression(Values, ElementType);
+ ///
+ /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
+ /// return this expression.
+ ///
+ /// The property of the result.
+ /// This expression if no children changed, or an expression with the updated children.
+ public virtual InlineQueryRootExpression Update(IReadOnlyList values)
+ => ReferenceEquals(values, Values) || values.SequenceEqual(Values)
+ ? this
+ : new InlineQueryRootExpression(values, ElementType);
+
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
@@ -70,7 +81,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
}
}
- return newValues is null ? this : new InlineQueryRootExpression(newValues, Type);
+ return newValues is null ? this : Update(newValues);
}
///
diff --git a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs
index 43ce2d91a3a..8a9fbe4eb7c 100644
--- a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs
@@ -515,7 +515,7 @@ public IDictionary Find(Expression expression)
var parentEvaluatable = _evaluatable;
var parentContainsClosure = _containsClosure;
- _evaluatable = IsEvaluatableNodeType(expression)
+ _evaluatable = IsEvaluatableNodeType(expression, out var preferNoEvaluation)
// Extension point to disable funcletization
&& _evaluatableExpressionFilter.IsEvaluatableExpression(expression, _model)
// Don't evaluate QueryableMethods if in compiled query
@@ -524,7 +524,7 @@ public IDictionary Find(Expression expression)
base.Visit(expression);
- if (_evaluatable)
+ if (_evaluatable && !preferNoEvaluation)
{
// Force parameterization when not in lambda
_evaluatableExpressions[expression] = _containsClosure || !_inLambda;
@@ -643,10 +643,23 @@ protected override Expression VisitConstant(ConstantExpression constantExpressio
return base.VisitConstant(constantExpression);
}
- private static bool IsEvaluatableNodeType(Expression expression)
- => expression.NodeType != ExpressionType.Extension
- || expression.CanReduce
- && IsEvaluatableNodeType(expression.ReduceAndCheck());
+ private static bool IsEvaluatableNodeType(Expression expression, out bool preferNoEvaluation)
+ {
+ switch (expression.NodeType)
+ {
+ case ExpressionType.NewArrayInit:
+ preferNoEvaluation = true;
+ return true;
+
+ case ExpressionType.Extension:
+ preferNoEvaluation = false;
+ return expression.CanReduce && IsEvaluatableNodeType(expression.ReduceAndCheck(), out preferNoEvaluation);
+
+ default:
+ preferNoEvaluation = false;
+ return true;
+ }
+ }
private static bool IsQueryableMethod(Expression expression)
=> expression is MethodCallExpression methodCallExpression
diff --git a/src/EFCore/Query/QueryRootProcessor.cs b/src/EFCore/Query/QueryRootProcessor.cs
index ac98c75b4f9..307f53fbee1 100644
--- a/src/EFCore/Query/QueryRootProcessor.cs
+++ b/src/EFCore/Query/QueryRootProcessor.cs
@@ -50,48 +50,18 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
var argument = methodCallExpression.Arguments[i];
var parameterType = parameters[i].ParameterType;
- Expression? visitedArgument = null;
-
// This converts collections over constants and parameters to query roots, for later translation of LINQ operators over them.
// The element type doesn't have to be directly mappable; we allow unknown CLR types in order to support value convertors
// (the precise type mapping - with the value converter - will be inferred later based on LINQ operators composed on the root).
// However, we do exclude element CLR types which are associated to entity types in our model, since Contains over entity
// collections isn't yet supported (#30712).
- if (parameterType.IsGenericType
+ var visitedArgument = parameterType.IsGenericType
&& (parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>)
|| parameterType.GetGenericTypeDefinition() == typeof(IQueryable<>))
&& parameterType.GetGenericArguments()[0] is var elementClrType
- && !_model.FindEntityTypes(elementClrType).Any())
- {
- switch (argument)
- {
- case ConstantExpression { Value: IEnumerable values } constantExpression
- when ShouldConvertToInlineQueryRoot(constantExpression):
-
- var valueExpressions = new List();
- foreach (var value in values)
- {
- valueExpressions.Add(Expression.Constant(value, elementClrType));
- }
- visitedArgument = new InlineQueryRootExpression(valueExpressions, elementClrType);
- break;
-
- // TODO: Support NewArrayExpression, see #30734.
-
- case ParameterExpression parameterExpression
- when parameterExpression.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal)
- == true
- && ShouldConvertToParameterQueryRoot(parameterExpression):
- visitedArgument = new ParameterQueryRootExpression(parameterExpression.Type.GetSequenceType(), parameterExpression);
- break;
-
- default:
- visitedArgument = null;
- break;
- }
- }
-
- visitedArgument ??= Visit(argument);
+ && !_model.FindEntityTypes(elementClrType).Any()
+ ? VisitQueryRootCandidate(argument, elementClrType)
+ : Visit(argument);
if (visitedArgument != argument)
{
@@ -117,12 +87,47 @@ when ShouldConvertToInlineQueryRoot(constantExpression):
: methodCallExpression.Update(methodCallExpression.Object, newArguments);
}
+ private Expression VisitQueryRootCandidate(Expression expression, Type elementClrType)
+ {
+ switch (expression)
+ {
+ // An array containing only constants is represented as a ConstantExpression with the array as the value.
+ // Convert that into a NewArrayExpression for use with InlineQueryRootExpression
+ case ConstantExpression { Value: IEnumerable values }:
+ var valueExpressions = new List();
+ foreach (var value in values)
+ {
+ valueExpressions.Add(Expression.Constant(value, elementClrType));
+ }
+
+ if (ShouldConvertToInlineQueryRoot(Expression.NewArrayInit(elementClrType, valueExpressions)))
+ {
+ return new InlineQueryRootExpression(valueExpressions, elementClrType);
+ }
+
+ goto default;
+
+ case NewArrayExpression newArrayExpression
+ when ShouldConvertToInlineQueryRoot(newArrayExpression):
+ return new InlineQueryRootExpression(newArrayExpression.Expressions, elementClrType);
+
+ case ParameterExpression parameterExpression
+ when parameterExpression.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal)
+ == true
+ && ShouldConvertToParameterQueryRoot(parameterExpression):
+ return new ParameterQueryRootExpression(parameterExpression.Type.GetSequenceType(), parameterExpression);
+
+ default:
+ return Visit(expression);
+ }
+ }
+
///
/// Determines whether a should be converted to a .
/// This handles cases inline expressions whose elements are all constants.
///
- /// The constant expression that's a candidate for conversion to a query root.
- protected virtual bool ShouldConvertToInlineQueryRoot(ConstantExpression constantExpression)
+ /// The new array expression that's a candidate for conversion to a query root.
+ protected virtual bool ShouldConvertToInlineQueryRoot(NewArrayExpression newArrayExpression)
=> false;
///
diff --git a/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs
index 980b287dfcd..24243773c64 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/UdfDbFunctionTestBase.cs
@@ -296,7 +296,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasTranslation(
args => new InExpression(
args.First(),
- new SqlConstantExpression(Expression.Constant(abc), typeMapping: null), // args.First().TypeMapping),
+ new[]
+ {
+ new SqlConstantExpression(Expression.Constant(abc[0]), typeMapping: null),
+ new SqlConstantExpression(Expression.Constant(abc[1]), typeMapping: null),
+ new SqlConstantExpression(Expression.Constant(abc[2]), typeMapping: null)
+ }, // args.First().TypeMapping)
negated: false,
typeMapping: null));
@@ -306,10 +311,19 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
args => new InExpression(
new InExpression(
args.First(),
- new SqlConstantExpression(Expression.Constant(abc), args.First().TypeMapping),
+ new[]
+ {
+ new SqlConstantExpression(Expression.Constant(abc[0]), args.First().TypeMapping),
+ new SqlConstantExpression(Expression.Constant(abc[1]), args.First().TypeMapping),
+ new SqlConstantExpression(Expression.Constant(abc[2]), args.First().TypeMapping)
+ },
negated: false,
typeMapping: null),
- new SqlConstantExpression(Expression.Constant(trueFalse), typeMapping: null),
+ new[]
+ {
+ new SqlConstantExpression(Expression.Constant(trueFalse[0]), typeMapping: null),
+ new SqlConstantExpression(Expression.Constant(trueFalse[1]), typeMapping: null)
+ },
negated: false,
typeMapping: null));
diff --git a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs
index c9870841924..b4304117daa 100644
--- a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs
@@ -1396,7 +1396,7 @@ public virtual Task Contains_over_scalar_with_null_should_rewrite_to_identity_eq
=> AssertQuery(
async,
ss => ss.Set().Where(
- o => ss.Set().Where(o => o.CustomerID == "VINET").Select(o => o.CustomerID).Contains(null)));
+ o => ss.Set().Where(o => o.CustomerID == "VINET").Select(o => o.EmployeeID).Contains(null)));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
@@ -1404,7 +1404,7 @@ public virtual Task Contains_over_entityType_with_null_should_rewrite_to_identit
=> AssertQuery(
async,
ss => ss.Set().Where(
- o => !ss.Set().Where(o => o.CustomerID == "VINET").Select(o => o.CustomerID).Contains(null)),
+ o => !ss.Set().Where(o => o.CustomerID == "VINET").Select(o => o.EmployeeID).Contains(null)),
entryCount: 830);
[ConditionalTheory]
@@ -1413,9 +1413,9 @@ public virtual Task Contains_over_entityType_with_null_should_rewrite_to_identit
=> AssertQuery(
async,
ss => ss.Set().Where(
- o => ss.Set().Where(o => o.CustomerID == "VINET").Select(o => o.CustomerID)
+ o => ss.Set().Where(o => o.CustomerID == "VINET").Select(o => o.EmployeeID)
.Contains(null)
- == ss.Set().Where(o => o.CustomerID != "VINET").Select(o => o.CustomerID)
+ == ss.Set().Where(o => o.CustomerID != "VINET").Select(o => o.EmployeeID)
.Contains(null)),
entryCount: 830);
@@ -1425,7 +1425,7 @@ public virtual Task Contains_over_nullable_scalar_with_null_in_subquery_translat
=> AssertQueryScalar(
async,
ss => ss.Set().Select(
- o => ss.Set().Where(o => o.CustomerID == "VINET").Select(o => o.CustomerID).Contains(null)));
+ o => ss.Set().Where(o => o.CustomerID == "VINET").Select(o => o.EmployeeID).Contains(null)));
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
index aff25e2494a..74a6ec49ab3 100644
--- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
@@ -115,6 +115,18 @@ public virtual Task Inline_collection_Contains_with_all_parameters(bool async)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_constant_and_parameter(bool async)
+ {
+ var j = 999;
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2, j }.Contains(c.Id)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory] // #30734
+ [MemberData(nameof(IsAsyncData))]
public virtual async Task Inline_collection_Contains_with_parameter_and_column_based_expression(bool async)
{
var i = 2;
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FiltersInheritanceQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FiltersInheritanceQuerySqlServerTest.cs
index a60cf4ff48a..dfcef05646e 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/FiltersInheritanceQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/FiltersInheritanceQuerySqlServerTest.cs
@@ -89,7 +89,7 @@ public override async Task Can_use_of_type_bird_predicate(bool async)
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[CountryId] = 1 AND [a].[CountryId] = 1
+WHERE [a].[CountryId] = 1
ORDER BY [a].[Species]
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/IncompleteMappingInheritanceQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/IncompleteMappingInheritanceQuerySqlServerTest.cs
index d1114119ee0..6191bd1fd84 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/IncompleteMappingInheritanceQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/IncompleteMappingInheritanceQuerySqlServerTest.cs
@@ -128,7 +128,7 @@ public override async Task Can_use_is_kiwi(bool async)
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND [a].[Discriminator] = N'Kiwi'
+WHERE [a].[Discriminator] = N'Kiwi'
""");
}
@@ -233,7 +233,7 @@ public override async Task Can_use_of_type_kiwi(bool async)
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND [a].[Discriminator] = N'Kiwi'
+WHERE [a].[Discriminator] = N'Kiwi'
""");
}
@@ -245,7 +245,7 @@ public override async Task Can_use_of_type_rose(bool async)
"""
SELECT [p].[Species], [p].[CountryId], [p].[Genus], [p].[Name], [p].[HasThorns]
FROM [Plants] AS [p]
-WHERE [p].[Genus] IN (1, 0) AND [p].[Genus] = 0
+WHERE [p].[Genus] = 0
""");
}
@@ -385,7 +385,7 @@ public override async Task Can_use_of_type_kiwi_where_north_on_derived_property(
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND [a].[Discriminator] = N'Kiwi' AND [a].[FoundOn] = CAST(0 AS tinyint)
+WHERE [a].[Discriminator] = N'Kiwi' AND [a].[FoundOn] = CAST(0 AS tinyint)
""");
}
@@ -397,7 +397,7 @@ public override async Task Can_use_of_type_kiwi_where_south_on_derived_property(
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND [a].[Discriminator] = N'Kiwi' AND [a].[FoundOn] = CAST(1 AS tinyint)
+WHERE [a].[Discriminator] = N'Kiwi' AND [a].[FoundOn] = CAST(1 AS tinyint)
""");
}
@@ -433,7 +433,7 @@ public override async Task Discriminator_used_when_projection_over_of_type(bool
"""
SELECT [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND [a].[Discriminator] = N'Kiwi'
+WHERE [a].[Discriminator] = N'Kiwi'
""");
}
@@ -641,7 +641,7 @@ public override async Task Discriminator_with_cast_in_shadow_property(bool async
"""
SELECT [a].[Name] AS [Predator]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND N'Kiwi' = [a].[Discriminator]
+WHERE [a].[Discriminator] = N'Kiwi'
""");
}
@@ -681,7 +681,7 @@ public override async Task Using_is_operator_on_multiple_type_with_no_result(boo
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND [a].[Discriminator] = N'Kiwi' AND [a].[Discriminator] = N'Eagle'
+WHERE 0 = 1
""");
}
@@ -693,7 +693,7 @@ public override async Task Using_is_operator_with_of_type_on_multiple_type_with_
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[Group]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND [a].[Discriminator] = N'Kiwi' AND [a].[Discriminator] = N'Eagle'
+WHERE 0 = 1
""");
}
@@ -736,7 +736,7 @@ public override async Task GetType_in_hierarchy_in_leaf_type_with_sibling(bool a
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND [a].[Discriminator] = N'Eagle'
+WHERE [a].[Discriminator] = N'Eagle'
""");
}
@@ -748,7 +748,7 @@ public override async Task GetType_in_hierarchy_in_leaf_type_with_sibling2(bool
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND [a].[Discriminator] = N'Kiwi'
+WHERE [a].[Discriminator] = N'Kiwi'
""");
}
@@ -760,7 +760,7 @@ public override async Task GetType_in_hierarchy_in_leaf_type_with_sibling2_rever
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] IN (N'Eagle', N'Kiwi') AND [a].[Discriminator] = N'Kiwi'
+WHERE [a].[Discriminator] = N'Kiwi'
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceQuerySqlServerTest.cs
index 7ef197c91da..68125e9cb73 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceQuerySqlServerTest.cs
@@ -652,7 +652,7 @@ public override async Task Using_is_operator_on_multiple_type_with_no_result(boo
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] = N'Kiwi' AND [a].[Discriminator] = N'Eagle'
+WHERE 0 = 1
""");
}
@@ -664,7 +664,7 @@ public override async Task Using_is_operator_with_of_type_on_multiple_type_with_
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[Group]
FROM [Animals] AS [a]
-WHERE [a].[Discriminator] = N'Kiwi' AND [a].[Discriminator] = N'Eagle'
+WHERE 0 = 1
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs
index 6265199c103..211ba45fcef 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs
@@ -2220,7 +2220,7 @@ FROM [Orders] AS [o]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o0]
- WHERE [o0].[CustomerID] = N'VINET' AND [o0].[CustomerID] IS NULL)
+ WHERE [o0].[CustomerID] = N'VINET' AND [o0].[EmployeeID] IS NULL)
""");
}
@@ -2235,7 +2235,7 @@ FROM [Orders] AS [o]
WHERE NOT (EXISTS (
SELECT 1
FROM [Orders] AS [o0]
- WHERE [o0].[CustomerID] = N'VINET' AND [o0].[CustomerID] IS NULL))
+ WHERE [o0].[CustomerID] = N'VINET' AND [o0].[EmployeeID] IS NULL))
""");
}
@@ -2251,13 +2251,13 @@ WHERE CASE
WHEN EXISTS (
SELECT 1
FROM [Orders] AS [o0]
- WHERE [o0].[CustomerID] = N'VINET' AND [o0].[CustomerID] IS NULL) THEN CAST(1 AS bit)
+ WHERE [o0].[CustomerID] = N'VINET' AND [o0].[EmployeeID] IS NULL) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END = CASE
WHEN EXISTS (
SELECT 1
FROM [Orders] AS [o1]
- WHERE ([o1].[CustomerID] <> N'VINET' OR [o1].[CustomerID] IS NULL) AND [o1].[CustomerID] IS NULL) THEN CAST(1 AS bit)
+ WHERE ([o1].[CustomerID] <> N'VINET' OR [o1].[CustomerID] IS NULL) AND [o1].[EmployeeID] IS NULL) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
""");
@@ -2273,7 +2273,7 @@ SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Orders] AS [o0]
- WHERE [o0].[CustomerID] = N'VINET' AND [o0].[CustomerID] IS NULL) THEN CAST(1 AS bit)
+ WHERE [o0].[CustomerID] = N'VINET' AND [o0].[EmployeeID] IS NULL) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Orders] AS [o]
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs
index 91b291451db..c0a68b615cc 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs
@@ -2518,7 +2518,7 @@ public override async Task Constant_array_Contains_OrElse_comparison_with_consta
"""
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
-WHERE [c].[CustomerID] IN (N'ALFKI', N'ANATR', N'ANTON')
+WHERE [c].[CustomerID] IN (N'ANTON', N'ALFKI', N'ANATR')
""");
}
@@ -2570,14 +2570,12 @@ public override async Task Array_of_parameters_Contains_OrElse_comparison_with_c
// issue #21462
AssertSql(
"""
-@__p_0='["ALFKI","ANATR"]' (Size = 4000)
+@__prm1_0='ALFKI' (Size = 5) (DbType = StringFixedLength)
+@__prm2_1='ANATR' (Size = 5) (DbType = StringFixedLength)
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
-WHERE EXISTS (
- SELECT 1
- FROM OpenJson(@__p_0) AS [p]
- WHERE CAST([p].[value] AS nchar(5)) = [c].[CustomerID]) OR [c].[CustomerID] = N'ANTON'
+WHERE [c].[CustomerID] IN (@__prm1_0, @__prm2_1, N'ANTON')
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs
index 79f3b958c09..9bd44d9196c 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs
@@ -2494,7 +2494,7 @@ public override async Task Negated_contains_with_comparison_without_null_get_com
"""
SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC]
FROM [Entities1] AS [e]
-WHERE [e].[NullableIntA] NOT IN (1, 2, 3)
+WHERE [e].[NullableIntA] NOT IN (3, 1, 2)
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
index ed3a176b359..bca920a69f5 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
@@ -151,9 +151,26 @@ public override async Task Inline_collection_Contains_with_all_parameters(bool a
AssertSql(
"""
+@__i_0='2'
+@__j_1='999'
+
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 [p].[Id] IN (2, 999)
+WHERE [p].[Id] IN (@__i_0, @__j_1)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_constant_and_parameter(bool async)
+ {
+ await base.Inline_collection_Contains_with_constant_and_parameter(async);
+
+ AssertSql(
+"""
+@__j_0='999'
+
+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 [p].[Id] IN (2, @__j_0)
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
index 6b9a1d8fcaa..cc3093bdcc4 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
@@ -144,18 +144,28 @@ public override async Task Inline_collection_Contains_with_all_parameters(bool a
{
await base.Inline_collection_Contains_with_all_parameters(async);
- // See #30732 for making this better
+ AssertSql(
+"""
+@__i_0='2'
+@__j_1='999'
+
+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 [p].[Id] IN (@__i_0, @__j_1)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_constant_and_parameter(bool async)
+ {
+ await base.Inline_collection_Contains_with_constant_and_parameter(async);
AssertSql(
"""
-@__p_0='[2,999]' (Size = 4000)
+@__j_0='999'
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 EXISTS (
- SELECT 1
- FROM OpenJson(@__p_0) AS [p0]
- WHERE CAST([p0].[value] AS int) = [p].[Id])
+WHERE [p].[Id] IN (2, @__j_0)
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCFiltersInheritanceQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCFiltersInheritanceQuerySqlServerTest.cs
index 472dbd42967..fbfde5ecaeb 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCFiltersInheritanceQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCFiltersInheritanceQuerySqlServerTest.cs
@@ -124,7 +124,7 @@ UNION ALL
SELECT [k].[Id], [k].[CountryId], [k].[Name], [k].[Species], [k].[EagleId], [k].[IsFlightless], NULL AS [Group], [k].[FoundOn], N'Kiwi' AS [Discriminator]
FROM [Kiwi] AS [k]
) AS [t]
-WHERE [t].[CountryId] = 1 AND [t].[CountryId] = 1
+WHERE [t].[CountryId] = 1
ORDER BY [t].[Species]
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTFiltersInheritanceQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTFiltersInheritanceQuerySqlServerTest.cs
index 4af39f70b41..ffab3a8a065 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTFiltersInheritanceQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTFiltersInheritanceQuerySqlServerTest.cs
@@ -122,7 +122,7 @@ FROM [Animals] AS [a]
LEFT JOIN [Birds] AS [b] ON [a].[Id] = [b].[Id]
LEFT JOIN [Eagle] AS [e] ON [a].[Id] = [e].[Id]
LEFT JOIN [Kiwi] AS [k] ON [a].[Id] = [k].[Id]
-WHERE [a].[CountryId] = 1 AND [a].[CountryId] = 1 AND ([k].[Id] IS NOT NULL OR [e].[Id] IS NOT NULL)
+WHERE [a].[CountryId] = 1 AND ([k].[Id] IS NOT NULL OR [e].[Id] IS NOT NULL)
ORDER BY [a].[Species]
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs
index 3c922201d74..539fc3a5195 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs
@@ -112,7 +112,7 @@ public override async Task Can_use_of_type_bird_predicate(bool async)
"""
SELECT [a].[Id], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[PeriodEnd], [a].[PeriodStart], [a].[Species], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn]
FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a]
-WHERE [a].[CountryId] = 1 AND [a].[CountryId] = 1
+WHERE [a].[CountryId] = 1
ORDER BY [a].[Species]
""");
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
index 8fc5012b386..e8e2d223eaa 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
@@ -12,7 +12,7 @@ public PrimitiveCollectionsQuerySqliteTest(PrimitiveCollectionsQuerySqlServerFix
: base(fixture)
{
Fixture.TestSqlLoggerFactory.Clear();
- // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
public override async Task Inline_collection_of_ints_Contains(bool async)
@@ -146,18 +146,28 @@ public override async Task Inline_collection_Contains_with_all_parameters(bool a
{
await base.Inline_collection_Contains_with_all_parameters(async);
- // See #30732 for making this better
+ AssertSql(
+"""
+@__i_0='2'
+@__j_1='999'
+
+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 "p"."Id" IN (@__i_0, @__j_1)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_constant_and_parameter(bool async)
+ {
+ await base.Inline_collection_Contains_with_constant_and_parameter(async);
AssertSql(
"""
-@__p_0='[2,999]' (Size = 7)
+@__j_0='999'
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 EXISTS (
- SELECT 1
- FROM json_each(@__p_0) AS "p0"
- WHERE "p0"."value" = "p"."Id")
+WHERE "p"."Id" IN (2, @__j_0)
""");
}