diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index 0fe96122904..eaede7ef8c2 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -622,12 +622,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,7 +1310,7 @@ 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)
=> string.Format(
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 938c9f6d455..d43e5124cd7 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -1,17 +1,17 @@
-
@@ -346,8 +346,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.
@@ -912,7 +912,7 @@
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..06537d10c0e 100644
--- a/src/EFCore.Relational/Query/ISqlExpressionFactory.cs
+++ b/src/EFCore.Relational/Query/ISqlExpressionFactory.cs
@@ -403,6 +403,15 @@ SqlFunctionExpression NiladicFunction(
/// An expression representing an EXISTS operation in a SQL tree.
ExistsExpression Exists(SelectExpression subquery, bool negated);
+ ///
+ /// 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);
+
///
/// 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);
///
/// 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);
///
/// 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..523a9405c6d 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,13 @@ public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
IReadOnlyList arguments,
IDiagnosticsLogger logger)
{
+ SqlExpression? itemExpression = null, valuesExpression = null;
+ // SqlExpression? values = null;
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 +53,34 @@ 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, negated: false);
+
+ case SqlConstantExpression { Value: IEnumerable values }:
+ var valuesExpressions = new List();
+
+ foreach (var value in values)
+ {
+ // TODO: Type mapping?
+ valuesExpressions.Add(_sqlExpressionFactory.Constant(value, itemExpression.TypeMapping));
+ }
+
+ return _sqlExpressionFactory.In(itemExpression, valuesExpressions, negated: false);
+ }
}
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/SqlExpressionSimplifyingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/SqlExpressionSimplifyingExpressionVisitor.cs
index 35f566a35e9..a5638e81505 100644
--- a/src/EFCore.Relational/Query/Internal/SqlExpressionSimplifyingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/Internal/SqlExpressionSimplifyingExpressionVisitor.cs
@@ -245,18 +245,11 @@ or ExpressionType.LessThan
&& leftCandidateInfo.ColumnExpression == rightCandidateInfo.ColumnExpression
&& leftCandidateInfo.OperationType == rightCandidateInfo.OperationType)
{
- var leftConstantIsEnumerable = leftCandidateInfo.ConstantValue is IEnumerable
- && !(leftCandidateInfo.ConstantValue is string)
- && !(leftCandidateInfo.ConstantValue is byte[]);
-
- var rightConstantIsEnumerable = rightCandidateInfo.ConstantValue is IEnumerable
- && !(rightCandidateInfo.ConstantValue is string)
- && !(rightCandidateInfo.ConstantValue is byte[]);
-
- if ((leftCandidateInfo.OperationType == ExpressionType.Equal
- && sqlBinaryExpression.OperatorType == ExpressionType.OrElse)
- || (leftCandidateInfo.OperationType == ExpressionType.NotEqual
- && sqlBinaryExpression.OperatorType == ExpressionType.AndAlso))
+ var leftConstantIsEnumerable = leftCandidateInfo.ConstantValue is IEnumerable and not string and not byte[];
+ var rightConstantIsEnumerable = rightCandidateInfo.ConstantValue is IEnumerable and not string and not byte[];
+
+ if ((leftCandidateInfo.OperationType, sqlBinaryExpression.OperatorType) is
+ (ExpressionType.Equal, ExpressionType.OrElse) or (ExpressionType.NotEqual, ExpressionType.AndAlso))
{
object leftValue;
object rightValue;
@@ -309,7 +302,7 @@ or ExpressionType.LessThan
return _sqlExpressionFactory.In(
leftCandidateInfo.ColumnExpression,
- _sqlExpressionFactory.Constant(resultArray, leftCandidateInfo.TypeMapping),
+ resultArray.Select(r => _sqlExpressionFactory.Constant(r, leftCandidateInfo.TypeMapping)).ToArray(),
leftCandidateInfo.OperationType == ExpressionType.NotEqual);
}
@@ -323,7 +316,7 @@ or ExpressionType.LessThan
return _sqlExpressionFactory.In(
leftCandidateInfo.ColumnExpression,
- _sqlExpressionFactory.Constant(resultArray, leftCandidateInfo.TypeMapping),
+ resultArray.Select(r => _sqlExpressionFactory.Constant(r, leftCandidateInfo.TypeMapping)).ToArray(),
leftCandidateInfo.OperationType == ExpressionType.NotEqual);
}
}
diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
index efac1cd435b..b42892cdebf 100644
--- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs
+++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
@@ -927,31 +927,29 @@ protected override Expression VisitExists(ExistsExpression existsExpression)
///
protected override Expression VisitIn(InExpression inExpression)
{
- if (inExpression.Values != null)
+ Check.DebugAssert(inExpression.ValuesParameter is null, "inExpression.ValuesParameter is null");
+
+ Visit(inExpression.Item);
+ _relationalCommandBuilder.Append(inExpression.IsNegated ? " NOT IN (" : " IN (");
+
+ if (inExpression.Values is not null)
{
- Visit(inExpression.Item);
- _relationalCommandBuilder.Append(inExpression.IsNegated ? " NOT IN " : " IN ");
- _relationalCommandBuilder.Append("(");
- var valuesConstant = (SqlConstantExpression)inExpression.Values;
- var valuesList = ((IEnumerable
- 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 536749f75ce..95d1032729c 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,24 @@ 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)
+ // TODO: Don't apply default type mapping
+ if (TranslateExpression(inlineQueryRootExpression.Values[i]) is not SqlExpression translatedValue)
{
- AddTranslationErrorDetails(RelationalStrings.OnlyConstantsSupportedInInlineCollectionQueryRoots);
return null;
}
- if (constantExpression.Value is null)
- {
- encounteredNull = true;
- }
+ // TODO: Poor man's null semantics - 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
}));
}
@@ -1802,7 +1801,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 +1869,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 +2742,7 @@ private void RegisterInferredTypeMapping(ColumnExpression columnExpression, Rela
///
protected class RelationalInferredTypeMappingApplier : ExpressionVisitor
{
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
private SelectExpression? _currentSelectExpression;
///
@@ -2761,10 +2753,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 +2828,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..2bddbab5af9 100644
--- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
@@ -1090,7 +1090,7 @@ SqlExpression GeneratePredicateTpt(EntityProjectionExpression entityProjectionEx
_sqlExpressionFactory.Constant(discriminatorValues[0]))
: _sqlExpressionFactory.In(
entityProjectionExpression.DiscriminatorExpression!,
- _sqlExpressionFactory.Constant(discriminatorValues),
+ discriminatorValues.Select(d => _sqlExpressionFactory.Constant(d)).ToArray(),
negated: false);
}
}
@@ -1110,8 +1110,7 @@ SqlExpression GeneratePredicateTpt(EntityProjectionExpression entityProjectionEx
_sqlExpressionFactory.Constant(concreteEntityTypes[0].GetDiscriminatorValue()))
: _sqlExpressionFactory.In(
discriminatorColumn,
- _sqlExpressionFactory.Constant(
- concreteEntityTypes.Select(et => et.GetDiscriminatorValue()).ToList()),
+ concreteEntityTypes.Select(et => _sqlExpressionFactory.Constant(et.GetDiscriminatorValue())).ToArray(),
negated: false);
}
}
diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs
index 6dc46ca1058..5567a3861fb 100644
--- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs
+++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs
@@ -231,26 +231,70 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(
private SqlExpression 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)
- ?? Dependencies.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;
+ }
+ }
- return item != inExpression.Item || values != inExpression.Values || inExpression.TypeMapping != _boolTypeMapping
- ? new InExpression(item, values, inExpression.IsNegated, _boolTypeMapping)
- : inExpression;
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
- return item != inExpression.Item || inExpression.TypeMapping != _boolTypeMapping
- ? new InExpression(item, inExpression.Subquery!, inExpression.IsNegated, _boolTypeMapping)
- : inExpression;
+ var item = ApplyTypeMapping(
+ inExpression.Item,
+ valuesTypeMapping ?? Dependencies.TypeMappingSource.FindMapping(inExpression.Item.Type, Dependencies.Model));
+
+ switch (inExpression)
+ {
+ case { Subquery: SelectExpression subquery }:
+ return inExpression.Update(item, subquery, values: null, valuesParameter: null);
+
+ case { ValuesParameter: SqlParameterExpression parameter }:
+ return inExpression.Update(
+ item, subquery: null, values: null, (SqlParameterExpression)ApplyTypeMapping(parameter, item.TypeMapping));
+
+ 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 inExpression.Update(item, subquery: null, newValues ?? values, valuesParameter: null);
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
}
///
@@ -541,24 +585,37 @@ public virtual ExistsExpression Exists(SelectExpression subquery, bool negated)
=> new(subquery, negated, _boolTypeMapping);
///
- public virtual InExpression In(SqlExpression item, SqlExpression values, bool negated)
+ public virtual InExpression In(SqlExpression item, SelectExpression subquery, bool negated)
+ {
+ var sqlExpression = subquery.Projection.Single().Expression;
+ var typeMapping = sqlExpression.TypeMapping;
+
+ item = ApplyTypeMapping(item, typeMapping);
+ return new InExpression(item, subquery, negated, _boolTypeMapping);
+ }
+
+ ///
+ public virtual InExpression In(SqlExpression item, IReadOnlyList values, bool negated)
{
var typeMapping = item.TypeMapping ?? Dependencies.TypeMappingSource.FindMapping(item.Type, Dependencies.Model);
item = ApplyTypeMapping(item, typeMapping);
- values = ApplyTypeMapping(values, typeMapping);
+ // TODO: Figure out type mappings here
+ // values = ApplyTypeMapping(values, typeMapping);
return new InExpression(item, values, negated, _boolTypeMapping);
}
///
- public virtual InExpression In(SqlExpression item, SelectExpression subquery, bool negated)
+ public virtual InExpression In(SqlExpression item, SqlParameterExpression valuesParameter, bool negated)
{
- var sqlExpression = subquery.Projection.Single().Expression;
- var typeMapping = sqlExpression.TypeMapping;
+ var typeMapping = item.TypeMapping ?? Dependencies.TypeMappingSource.FindMapping(item.Type, Dependencies.Model);
item = ApplyTypeMapping(item, typeMapping);
- return new InExpression(item, subquery, negated, _boolTypeMapping);
+ // TODO: Figure out type mappings here
+ // values = ApplyTypeMapping(values, typeMapping);
+
+ return new InExpression(item, valuesParameter, negated, _boolTypeMapping);
}
///
@@ -622,7 +679,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(), negated: false);
selectExpression.ApplyPredicate(predicate);
diff --git a/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs
index af0777b47bb..4e2b2439f20 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,32 @@ 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);
///
/// 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 +189,35 @@ protected override void Print(ExpressionPrinter expressionPrinter)
expressionPrinter.Append(IsNegated ? " NOT IN " : " IN ");
expressionPrinter.Append("(");
- if (Subquery != null)
- {
- using (expressionPrinter.Indent())
- {
- expressionPrinter.Visit(Subquery);
- }
- }
- else if (Values is SqlConstantExpression constantValuesExpression
- && constantValuesExpression.Value is IEnumerable constantValues)
+ switch (this)
{
- 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 +234,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/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..85d64bf1dae 100644
--- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
+++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
@@ -671,32 +671,36 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
&& subquery.Projection[0].Expression is ColumnExpression columnProjection
&& !columnProjection.IsNullable);
- return inExpression.Update(item, values: null, subquery);
+ return inExpression.Update(item, subquery, values: null, valuesParameter: null);
+ }
+
+ // First, if the InExpression has a parameterized value list, read the parameterized values and replace the InExpression with a
+ // regular InExpression containing constants.
+ if (inExpression.ValuesParameter is not null)
+ {
+ // Just convert to a regular Values InExpression
+ throw new NotImplementedException();
}
// 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 +716,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 +730,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 +742,82 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
simplifiedInExpression,
_sqlExpressionFactory.IsNull(item));
- (SqlConstantExpression ProcessedValuesExpression, List