diff --git a/All.sln.DotSettings b/All.sln.DotSettings
index 8229c429771..edd36d48c74 100644
--- a/All.sln.DotSettings
+++ b/All.sln.DotSettings
@@ -308,9 +308,12 @@ The .NET Foundation licenses this file to you under the MIT license.
True
True
True
+ True
True
True
+ True
True
+ True
True
True
True
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index fcc660d5a5f..0fe96122904 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -167,6 +167,14 @@ public static string ConflictingRowValuesSensitive(object? firstEntityType, obje
GetString("ConflictingRowValuesSensitive", nameof(firstEntityType), nameof(secondEntityType), nameof(keyValue), nameof(firstConflictingValue), nameof(secondConflictingValue), nameof(column)),
firstEntityType, secondEntityType, keyValue, firstConflictingValue, secondConflictingValue, column);
+ ///
+ /// Store type '{storeType1}' was inferred for a primitive collection, but that primitive collection was previously inferred to have store type '{storeType2}'.
+ ///
+ public static string ConflictingTypeMappingsForPrimitiveCollection(object? storeType1, object? storeType2)
+ => string.Format(
+ GetString("ConflictingTypeMappingsForPrimitiveCollection", nameof(storeType1), nameof(storeType2)),
+ storeType1, storeType2);
+
///
/// A seed entity for entity type '{entityType}' has the same key value as another seed entity mapped to the same table '{table}', but have different values for the column '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
///
@@ -621,6 +629,12 @@ public static string EitherOfTwoValuesMustBeNull(object? param1, object? param2)
GetString("EitherOfTwoValuesMustBeNull", nameof(param1), nameof(param2)),
param1, param2);
+ ///
+ /// Empty collections are not supported as constant query roots.
+ ///
+ public static string EmptyCollectionNotSupportedAsInlineQueryRoot
+ => GetString("EmptyCollectionNotSupportedAsInlineQueryRoot");
+
///
/// The short name for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type in the hierarchy must have a unique short name. Either rename one of the types or call modelBuilder.Entity<TEntity>().Metadata.SetDiscriminatorValue("NewShortName").
///
@@ -1409,6 +1423,12 @@ public static string OptionalDependentWithDependentWithoutIdentifyingProperty(ob
GetString("OptionalDependentWithDependentWithoutIdentifyingProperty", nameof(entityType)),
entityType);
+ ///
+ /// Only constants are supported inside inline collection query roots.
+ ///
+ public static string OnlyConstantsSupportedInInlineCollectionQueryRoots
+ => GetString("OnlyConstantsSupportedInInlineCollectionQueryRoots");
+
///
/// Cannot use the value provided for parameter '{parameter}' because it isn't assignable to type object[].
///
@@ -1966,7 +1986,7 @@ public static string UnsupportedOperatorForSqlExpression(object? nodeType, objec
nodeType, expressionType);
///
- /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'.
+ /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'.
///
public static string UnsupportedPropertyType(object? entity, object? property, object? clrType)
=> string.Format(
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 128be2b2177..938c9f6d455 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -175,6 +175,9 @@
Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different property values '{firstConflictingValue}' and '{secondConflictingValue}' for the column '{column}'.
+
+ Store type '{storeType1}' was inferred for a primitive collection, but that primitive collection was previously inferred to have store type '{storeType2}'.
+
A seed entity for entity type '{entityType}' has the same key value as another seed entity mapped to the same table '{table}', but have different values for the column '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
@@ -346,6 +349,9 @@
Either {param1} or {param2} must be null.
+
+ Empty collections are not supported as inline query roots.
+
The short name for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type in the hierarchy must have a unique short name. Either rename one of the types or call modelBuilder.Entity<TEntity>().Metadata.SetDiscriminatorValue("NewShortName").
@@ -950,6 +956,9 @@
Entity type '{entityType}' is an optional dependent using table sharing and containing other dependents without any required non shared property to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query causing nested dependent's values to be lost. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.
+
+ Only constants are supported inside inline collection query roots.
+
Cannot use the value provided for parameter '{parameter}' because it isn't assignable to type object[].
diff --git a/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs b/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs
index 69d98027268..ad58fbe7636 100644
--- a/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs
+++ b/src/EFCore.Relational/Query/Internal/EqualsTranslator.cs
@@ -60,8 +60,10 @@ public EqualsTranslator(ISqlExpressionFactory sqlExpressionFactory)
&& right != null)
{
if (left.Type == right.Type
- || (right.Type == typeof(object) && (right is SqlParameterExpression || right is SqlConstantExpression))
- || (left.Type == typeof(object) && (left is SqlParameterExpression || left is SqlConstantExpression)))
+ || (right.Type == typeof(object)
+ && right is SqlParameterExpression or SqlConstantExpression or ColumnExpression { TypeMapping: null })
+ || (left.Type == typeof(object)
+ && left is SqlParameterExpression or SqlConstantExpression or ColumnExpression { TypeMapping: null }))
{
return _sqlExpressionFactory.Equal(left, right);
}
diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
index 94ce376d396..efac1cd435b 100644
--- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs
+++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs
@@ -226,12 +226,7 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
subQueryIndent = _relationalCommandBuilder.Indent();
}
- if (IsNonComposedSetOperation(selectExpression))
- {
- // Naked set operation
- GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]);
- }
- else
+ if (!TryGenerateWithoutWrappingSelect(selectExpression))
{
_relationalCommandBuilder.Append("SELECT ");
@@ -300,6 +295,43 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
return selectExpression;
}
+ ///
+ /// If possible, generates the expression contained within the provided without the wrapping
+ /// SELECT. This can be done for set operations and VALUES, which can appear as top-level statements without needing to be wrapped
+ /// in SELECT.
+ ///
+ protected virtual bool TryGenerateWithoutWrappingSelect(SelectExpression selectExpression)
+ {
+ if (IsNonComposedSetOperation(selectExpression))
+ {
+ GenerateSetOperation((SetOperationBase)selectExpression.Tables[0]);
+ return true;
+ }
+
+ if (selectExpression is
+ {
+ Tables: [ValuesExpression valuesExpression],
+ Offset: null,
+ Limit: null,
+ IsDistinct: false,
+ Predicate: null,
+ Having: null,
+ Orderings.Count: 0,
+ GroupBy.Count: 0,
+ }
+ && selectExpression.Projection.Count == valuesExpression.ColumnNames.Count
+ && selectExpression.Projection.Select(
+ (pe, index) => pe.Expression is ColumnExpression column
+ && column.Name == valuesExpression.ColumnNames[index])
+ .All(e => e))
+ {
+ GenerateValues(valuesExpression);
+ return true;
+ }
+
+ return false;
+ }
+
///
/// Generates a pseudo FROM clause. Required by some providers when a query has no actual FROM clause.
///
@@ -371,16 +403,16 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
///
protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression)
{
- if (!string.IsNullOrEmpty(tableValuedFunctionExpression.StoreFunction.Schema))
+ if (!string.IsNullOrEmpty(tableValuedFunctionExpression.Schema))
{
_relationalCommandBuilder
- .Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Schema))
+ .Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Schema))
.Append(".");
}
- var name = tableValuedFunctionExpression.StoreFunction.IsBuiltIn
- ? tableValuedFunctionExpression.StoreFunction.Name
- : _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.StoreFunction.Name);
+ var name = tableValuedFunctionExpression.IsBuiltIn
+ ? tableValuedFunctionExpression.Name
+ : _sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Name);
_relationalCommandBuilder
.Append(name)
@@ -607,19 +639,22 @@ protected override Expression VisitSqlParameter(SqlParameterExpression sqlParame
{
var invariantName = sqlParameterExpression.Name;
var parameterName = sqlParameterExpression.Name;
+ var typeMapping = sqlParameterExpression.TypeMapping!;
// Try to see if a parameter already exists - if so, just integrate the same placeholder into the SQL instead of sending the same
// data twice.
// Note that if the type mapping differs, we do send the same data twice (e.g. the same string may be sent once as Unicode, once as
// non-Unicode).
+ // TODO: Note that we perform Equals comparison on the value converter. We should be able to do reference comparison, but for
+ // that we need to ensure that there's only ever one type mapping instance (i.e. no type mappings are ever instantiated out of the
+ // type mapping source). See #30677.
var parameter = _relationalCommandBuilder.Parameters.FirstOrDefault(
p =>
p.InvariantName == parameterName
- && p is TypeMappedRelationalParameter typeMappedRelationalParameter
- && string.Equals(
- typeMappedRelationalParameter.RelationalTypeMapping.StoreType, sqlParameterExpression.TypeMapping!.StoreType,
- StringComparison.OrdinalIgnoreCase)
- && typeMappedRelationalParameter.RelationalTypeMapping.Converter == sqlParameterExpression.TypeMapping!.Converter);
+ && p is TypeMappedRelationalParameter { RelationalTypeMapping: var existingTypeMapping }
+ && string.Equals(existingTypeMapping.StoreType, typeMapping.StoreType, StringComparison.OrdinalIgnoreCase)
+ && (existingTypeMapping.Converter is null && typeMapping.Converter is null
+ || existingTypeMapping.Converter is not null && existingTypeMapping.Converter.Equals(typeMapping.Converter)));
if (parameter is null)
{
@@ -1132,6 +1167,28 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres
return rowNumberExpression;
}
+ ///
+ protected override Expression VisitRowValue(RowValueExpression rowValueExpression)
+ {
+ Sql.Append("(");
+
+ var values = rowValueExpression.Values;
+ var count = values.Count;
+ for (var i = 0; i < count; i++)
+ {
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Visit(values[i]);
+ }
+
+ Sql.Append(")");
+
+ return rowValueExpression;
+ }
+
///
/// Generates a set operation in the relational command.
///
@@ -1311,6 +1368,65 @@ void LiftPredicate(TableExpressionBase joinTable)
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate)));
}
+ ///
+ protected override Expression VisitValues(ValuesExpression valuesExpression)
+ {
+ _relationalCommandBuilder.Append("(");
+
+ GenerateValues(valuesExpression);
+
+ _relationalCommandBuilder
+ .Append(")")
+ .Append(AliasSeparator)
+ .Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.Alias));
+
+ return valuesExpression;
+ }
+
+ ///
+ /// Generates a VALUES expression.
+ ///
+ protected virtual void GenerateValues(ValuesExpression valuesExpression)
+ {
+ var rowValues = valuesExpression.RowValues;
+
+ // Some databases support providing the names of columns projected out of VALUES, e.g.
+ // SQL Server/PG: (VALUES (1, 3), (2, 4)) AS x(a, b). Others unfortunately don't; so by default, we extract out the first row,
+ // and generate a SELECT for it with the names, and a UNION ALL over the rest of the values.
+ _relationalCommandBuilder.Append("SELECT ");
+
+ Check.DebugAssert(rowValues.Count > 0, "rowValues.Count > 0");
+ var firstRowValues = rowValues[0].Values;
+ for (var i = 0; i < firstRowValues.Count; i++)
+ {
+ if (i > 0)
+ {
+ _relationalCommandBuilder.Append(", ");
+ }
+
+ Visit(firstRowValues[i]);
+
+ _relationalCommandBuilder
+ .Append(AliasSeparator)
+ .Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i]));
+ }
+
+ if (rowValues.Count > 1)
+ {
+ _relationalCommandBuilder.Append(" UNION ALL VALUES ");
+
+ for (var i = 1; i < rowValues.Count; i++)
+ {
+ if (i > 1)
+ {
+ _relationalCommandBuilder.Append(", ");
+ }
+
+ Visit(valuesExpression.RowValues[i]);
+ }
+ }
+ }
+
///
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
=> throw new InvalidOperationException(
diff --git a/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs b/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs
new file mode 100644
index 00000000000..d2c218bb5c9
--- /dev/null
+++ b/src/EFCore.Relational/Query/RelationalQueryRootProcessor.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Query.Internal;
+using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+///
+public class RelationalQueryRootProcessor : QueryRootProcessor
+{
+ private readonly IModel _model;
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// Parameter object containing dependencies for this class.
+ /// Parameter object containing relational dependencies for this class.
+ /// The query compilation context object to use.
+ public RelationalQueryRootProcessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, queryCompilationContext)
+ {
+ _model = queryCompilationContext.Model;
+ }
+
+ ///
+ /// Indicates that a can be converted to a ;
+ /// this will later be translated to a SQL .
+ ///
+ protected override bool ShouldConvertToInlineQueryRoot(ConstantExpression constantExpression)
+ => true;
+
+ ///
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
+ {
+ // Create query root node for table-valued functions
+ if (_model.FindDbFunction(methodCallExpression.Method) is { IsScalar: false, StoreFunction: var storeFunction })
+ {
+ // See issue #19970
+ return new TableValuedFunctionQueryRootExpression(
+ storeFunction.EntityTypeMappings.Single().EntityType,
+ storeFunction,
+ methodCallExpression.Arguments);
+ }
+
+ return base.VisitMethodCall(methodCallExpression);
+ }
+
+ ///
+ protected override Expression VisitExtension(Expression node)
+ => node switch
+ {
+ // We skip FromSqlQueryRootExpression, since that contains the arguments as an object array parameter, and don't want to convert
+ // that to a query root
+ FromSqlQueryRootExpression e => e,
+
+ _ => base.VisitExtension(node)
+ };
+}
diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
index 6077a62c474..1d51f2f604b 100644
--- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs
@@ -35,10 +35,13 @@ public RelationalQueryTranslationPreprocessor(
public override Expression NormalizeQueryableMethod(Expression expression)
{
expression = new RelationalQueryMetadataExtractingExpressionVisitor(_relationalQueryCompilationContext).Visit(expression);
- expression = base.NormalizeQueryableMethod(expression);
- expression = new TableValuedFunctionToQueryRootConvertingExpressionVisitor(QueryCompilationContext.Model).Visit(expression);
expression = new CollectionIndexerToElementAtNormalizingExpressionVisitor().Visit(expression);
+ expression = base.NormalizeQueryableMethod(expression);
return expression;
}
+
+ ///
+ protected override Expression ProcessQueryRoots(Expression expression)
+ => new RelationalQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext).Visit(expression);
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs
index a8a505798c4..6bd82bdce6a 100644
--- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessorDependencies.cs
@@ -45,7 +45,13 @@ public sealed record RelationalQueryTranslationPreprocessorDependencies
/// the constructor at any point in this process.
///
[EntityFrameworkInternal]
- public RelationalQueryTranslationPreprocessorDependencies()
+ public RelationalQueryTranslationPreprocessorDependencies(IRelationalTypeMappingSource relationalTypeMappingSource)
{
+ RelationalTypeMappingSource = relationalTypeMappingSource;
}
+
+ ///
+ /// The type mapping source.
+ ///
+ public IRelationalTypeMappingSource RelationalTypeMappingSource { get; init; }
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index 9c6ee1a791a..536749f75ce 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
@@ -12,10 +11,14 @@ namespace Microsoft.EntityFrameworkCore.Query;
///
public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMethodTranslatingExpressionVisitor
{
+ private const string SqlQuerySingleColumnAlias = "Value";
+ private const string ValuesOrderingColumnName = "_ord", ValuesValueColumnName = "Value";
+
private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator;
private readonly SharedTypeEntityExpandingExpressionVisitor _sharedTypeEntityExpandingExpressionVisitor;
private readonly RelationalProjectionBindingExpressionVisitor _projectionBindingExpressionVisitor;
private readonly QueryCompilationContext _queryCompilationContext;
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly bool _subquery;
@@ -39,6 +42,7 @@ public RelationalQueryableMethodTranslatingExpressionVisitor(
_sharedTypeEntityExpandingExpressionVisitor =
new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, sqlExpressionFactory);
_projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator);
+ _typeMappingSource = relationalDependencies.TypeMappingSource;
_sqlExpressionFactory = sqlExpressionFactory;
_subquery = false;
}
@@ -63,10 +67,32 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor(
_sharedTypeEntityExpandingExpressionVisitor =
new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, parentVisitor._sqlExpressionFactory);
_projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator);
+ _typeMappingSource = parentVisitor._typeMappingSource;
_sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
_subquery = true;
}
+ ///
+ public override Expression Translate(Expression expression)
+ {
+ var visited = Visit(expression);
+
+ if (!_subquery)
+ {
+ // We've finished translating the entire query.
+
+ // If any constant/parameter query roots exist in the query, their columns don't yet have a type mapping.
+ // First, scan the query tree for inferred type mappings (e.g. based on a comparison of those columns to some regular column
+ // with a type mapping).
+ var inferredColumns = new ColumnTypeMappingScanner().Scan(visited);
+
+ // Then, apply those type mappings back on the constant/parameter tables (e.g. ValuesExpression).
+ visited = ApplyInferredTypeMappings(visited, inferredColumns);
+ }
+
+ return visited;
+ }
+
///
protected override Expression VisitExtension(Expression extensionExpression)
{
@@ -83,6 +109,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
fromSqlQueryRootExpression.Argument)));
case TableValuedFunctionQueryRootExpression tableValuedFunctionQueryRootExpression:
+ {
var function = tableValuedFunctionQueryRootExpression.Function;
var arguments = new List();
foreach (var arg in tableValuedFunctionQueryRootExpression.Arguments)
@@ -122,6 +149,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
var queryExpression = _sqlExpressionFactory.Select(entityType, translation);
return CreateShapedQueryExpression(entityType, queryExpression);
+ }
case EntityQueryRootExpression entityQueryRootExpression
when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
@@ -153,6 +181,7 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
.Visit(shapedQueryExpression.ShaperExpression));
case SqlQueryRootExpression sqlQueryRootExpression:
+ {
var typeMapping = RelationalDependencies.TypeMappingSource.FindMapping(sqlQueryRootExpression.ElementType);
if (typeMapping == null)
{
@@ -161,8 +190,7 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
}
var selectExpression = new SelectExpression(
- sqlQueryRootExpression.Type, typeMapping,
- new FromSqlExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument));
+ new FromSqlExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument), SqlQuerySingleColumnAlias, sqlQueryRootExpression.Type, typeMapping);
Expression shaperExpression = new ProjectionBindingExpression(
selectExpression, new ProjectionMember(), sqlQueryRootExpression.ElementType.MakeNullable());
@@ -177,6 +205,20 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)
}
return new ShapedQueryExpression(selectExpression, shaperExpression);
+ }
+
+ case InlineQueryRootExpression constantQueryRootExpression:
+ return VisitInlineQueryRoot(constantQueryRootExpression) ?? base.VisitExtension(extensionExpression);
+
+ case ParameterQueryRootExpression parameterQueryRootExpression:
+ var sqlParameterExpression =
+ _sqlTranslator.Visit(parameterQueryRootExpression.ParameterExpression) as SqlParameterExpression;
+ Check.DebugAssert(sqlParameterExpression is not null, "sqlParameterExpression is not null");
+ return TranslateCollection(
+ sqlParameterExpression,
+ elementTypeMapping: null,
+ char.ToLowerInvariant(sqlParameterExpression.Name.First(c => c != '_')).ToString())
+ ?? base.VisitExtension(extensionExpression);
default:
return base.VisitExtension(extensionExpression);
@@ -212,7 +254,133 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}
}
- return base.VisitMethodCall(methodCallExpression);
+ var translated = base.VisitMethodCall(methodCallExpression);
+
+ // Attempt to translate access into a primitive collection property
+ if (translated == QueryCompilationContext.NotTranslatedExpression
+ && _sqlTranslator.TryTranslatePropertyAccess(methodCallExpression, out var propertyAccessExpression)
+ && propertyAccessExpression is
+ {
+ TypeMapping.ElementTypeMapping: RelationalTypeMapping elementTypeMapping
+ } collectionPropertyAccessExpression)
+ {
+ var tableAlias = collectionPropertyAccessExpression switch
+ {
+ ColumnExpression c => c.Name[..1].ToLowerInvariant(),
+ JsonScalarExpression { Path: [.., { PropertyName: string propertyName }] } => propertyName[..1].ToLowerInvariant(),
+ _ => "j"
+ };
+
+ if (TranslateCollection(collectionPropertyAccessExpression, elementTypeMapping, tableAlias) is
+ { } primitiveCollectionTranslation)
+ {
+ return primitiveCollectionTranslation;
+ }
+ }
+
+ return translated;
+ }
+
+ ///
+ /// Translates a parameter or column collection. Providers can override this to translate e.g. int[] columns/parameters/constants to
+ /// a queryable table (OPENJSON on SQL Server, unnest on PostgreSQL...). The default implementation always returns
+ /// (no translation).
+ ///
+ ///
+ /// Inline collections aren't passed to this method; see for the translation of inline
+ /// collections.
+ ///
+ /// The expression to try to translate as a primitive collection expression.
+ ///
+ /// The type mapping of the collection's element, or when it's not known (i.e. for parameters).
+ ///
+ ///
+ /// Provides an alias to be used for the table returned from translation, which will represent the collection.
+ ///
+ /// A if the translation was successful, otherwise .
+ protected virtual ShapedQueryExpression? TranslateCollection(
+ SqlExpression sqlExpression,
+ RelationalTypeMapping? elementTypeMapping,
+ string tableAlias)
+ => null;
+
+ ///
+ /// Translates an inline collection into a queryable SQL VALUES expression.
+ ///
+ /// The inline collection to be translated.
+ /// A queryable SQL VALUES expression.
+ protected virtual ShapedQueryExpression? VisitInlineQueryRoot(InlineQueryRootExpression inlineQueryRootExpression)
+ {
+ var elementType = inlineQueryRootExpression.ElementType;
+
+ var rowExpressions = new List();
+ var encounteredNull = false;
+ var intTypeMapping = _typeMappingSource.FindMapping(typeof(int));
+
+ 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)
+ {
+ AddTranslationErrorDetails(RelationalStrings.OnlyConstantsSupportedInInlineCollectionQueryRoots);
+ return null;
+ }
+
+ if (constantExpression.Value is null)
+ {
+ encounteredNull = true;
+ }
+
+ 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)
+ }));
+ }
+
+ if (rowExpressions.Count == 0)
+ {
+ AddTranslationErrorDetails(RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot);
+ return null;
+ }
+
+ var valuesExpression = new ValuesExpression("v", rowExpressions, new[] { ValuesOrderingColumnName, ValuesValueColumnName });
+
+ // Note: we leave the element type mapping null, to allow it to get inferred based on queryable operators composed on top.
+ var selectExpression = new SelectExpression(
+ valuesExpression,
+ ValuesValueColumnName,
+ columnType: elementType.UnwrapNullableType(),
+ columnTypeMapping: null,
+ isColumnNullable: encounteredNull);
+
+ selectExpression.AppendOrdering(
+ new OrderingExpression(
+ selectExpression.CreateColumnExpression(
+ valuesExpression,
+ ValuesOrderingColumnName,
+ typeof(int),
+ intTypeMapping,
+ columnNullable: false),
+ ascending: true));
+
+ Expression shaperExpression = new ProjectionBindingExpression(
+ selectExpression, new ProjectionMember(), encounteredNull ? elementType.MakeNullable() : elementType);
+
+ if (elementType != shaperExpression.Type)
+ {
+ Check.DebugAssert(
+ elementType.MakeNullable() == shaperExpression.Type,
+ "expression.Type must be nullable of targetType");
+
+ shaperExpression = Expression.Convert(shaperExpression, elementType);
+ }
+
+ return new ShapedQueryExpression(selectExpression, shaperExpression);
}
///
@@ -244,7 +412,18 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
}
var selectExpression = (SelectExpression)source.QueryExpression;
- selectExpression.ApplyPredicate(_sqlExpressionFactory.Not(translation));
+
+ // Negate the predicate, unless it's already negated, in which case remove that.
+ selectExpression.ApplyPredicate(
+ translation is SqlUnaryExpression { OperatorType: ExpressionType.Not, Operand: var nestedOperand }
+ ? nestedOperand
+ : _sqlExpressionFactory.Not(translation));
+
+ if (TrySimplifyValuesToInExpression(source, isNegated: true, out var simplifiedQuery))
+ {
+ return simplifiedQuery;
+ }
+
selectExpression.ReplaceProjection(new List());
selectExpression.ApplyProjection();
if (selectExpression.Limit == null
@@ -273,6 +452,11 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
}
source = translatedSource;
+
+ if (TrySimplifyValuesToInExpression(source, isNegated: false, out var simplifiedQuery))
+ {
+ return simplifiedQuery;
+ }
}
var selectExpression = (SelectExpression)source.QueryExpression;
@@ -404,7 +588,7 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent
return null;
}
- if (selectExpression.Orderings.Count == 0)
+ if (!IsOrdered(selectExpression))
{
_queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
}
@@ -952,7 +1136,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
return null;
}
- if (selectExpression.Orderings.Count == 0)
+ if (!IsOrdered(selectExpression))
{
_queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
}
@@ -980,7 +1164,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
return null;
}
- if (selectExpression.Orderings.Count == 0)
+ if (!IsOrdered(selectExpression))
{
_queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
}
@@ -1583,9 +1767,13 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate(
protected virtual SqlExpression? TranslateExpression(Expression expression)
{
var translation = _sqlTranslator.Translate(expression);
- if (translation == null && _sqlTranslator.TranslationErrorDetails != null)
+
+ if (translation is null)
{
- AddTranslationErrorDetails(_sqlTranslator.TranslationErrorDetails);
+ if (_sqlTranslator.TranslationErrorDetails != null)
+ {
+ AddTranslationErrorDetails(_sqlTranslator.TranslationErrorDetails);
+ }
}
return translation;
@@ -1602,6 +1790,111 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate(
LambdaExpression lambdaExpression)
=> TranslateExpression(RemapLambdaBody(shapedQueryExpression, lambdaExpression));
+ ///
+ /// Invoked at the end of top-level translation, applies inferred type mappings for queryable constants/parameters and verifies that
+ /// all have a type mapping.
+ ///
+ /// The query expression to process.
+ ///
+ /// Inferred type mappings for queryable constants/parameters collected during translation. These will be applied to the appropriate
+ /// nodes in the tree.
+ ///
+ protected virtual Expression ApplyInferredTypeMappings(
+ Expression expression,
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ => new RelationalInferredTypeMappingApplier(inferredTypeMappings).Visit(expression);
+
+ ///
+ /// Determines whether the given is ordered, typically because orderings have been added to it.
+ ///
+ /// The to check for ordering.
+ /// Whether is ordered.
+ protected virtual bool IsOrdered(SelectExpression selectExpression)
+ => selectExpression.Orderings.Count > 0;
+
+ ///
+ /// Attempts to pattern-match for Contains over , which corresponds to
+ /// Where(b => new[] { 1, 2, 3 }.Contains(b.Id)). Simplifies this to the tighter [b].[Id] IN (1, 2, 3) instead of the
+ /// full subquery with VALUES.
+ ///
+ private bool TrySimplifyValuesToInExpression(
+ ShapedQueryExpression source,
+ bool isNegated,
+ [NotNullWhen(true)] out ShapedQueryExpression? simplifiedQuery)
+ {
+ if (source.QueryExpression is SelectExpression
+ {
+ Tables: [ValuesExpression
+ {
+ RowValues: [{ Values.Count: 2 }, ..],
+ ColumnNames: [ ValuesOrderingColumnName, ValuesValueColumnName ]
+ } valuesExpression],
+ GroupBy: [],
+ Having: null,
+ IsDistinct: false,
+ Limit: null,
+ Offset: null,
+ // Note that we don't care about orderings, they get elided anyway by Any/All
+ Predicate: SqlBinaryExpression { OperatorType: ExpressionType.Equal, Left: var left, Right: var right },
+ } selectExpression)
+ {
+ // The table is a ValuesExpression, and the predicate is an equality - this is a possible simplifiable Contains.
+ // Get the projection column pointing to the ValuesExpression, and check that it's compared to on one side of the predicate
+ // equality.
+ var shaperExpression = source.ShaperExpression;
+ if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression
+ && unaryExpression.Operand.Type.IsNullableType()
+ && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type)
+ {
+ shaperExpression = unaryExpression.Operand;
+ }
+
+ if (shaperExpression is ProjectionBindingExpression projectionBindingExpression
+ && selectExpression.GetProjection(projectionBindingExpression) is ColumnExpression projectionColumn)
+ {
+ SqlExpression item;
+
+ if (left is ColumnExpression leftColumn
+ && (leftColumn.Table, leftColumn.Name) == (projectionColumn.Table, projectionColumn.Name))
+ {
+ item = right;
+ }
+ else if (right is ColumnExpression rightColumn
+ && (rightColumn.Table, rightColumn.Name) == (projectionColumn.Table, projectionColumn.Name))
+ {
+ item = left;
+ }
+ else
+ {
+ simplifiedQuery = null;
+ return false;
+ }
+
+ var values = new object?[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;
+ }
+ }
+
+ var inExpression = _sqlExpressionFactory.In(item, _sqlExpressionFactory.Constant(values), isNegated);
+ simplifiedQuery = source.Update(_sqlExpressionFactory.Select(inExpression), source.ShaperExpression);
+ return true;
+ }
+ }
+
+ simplifiedQuery = null;
+ return false;
+ }
+
private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression)
{
var lambdaBody = ReplacingExpressionVisitor.Replace(
@@ -2275,4 +2568,299 @@ private static Expression MatchShaperNullabilityForSetOperation(Expression shape
return source.UpdateShaperExpression(shaper);
}
+
+ ///
+ /// A visitor which scans an expression tree and attempts to find columns for which we were missing type mappings (projected out
+ /// of queryable constant/parameter), and those type mappings have been inferred.
+ ///
+ ///
+ ///
+ /// This handles two cases: (1) an untyped column which type-inferred in the regular way, e.g. through comparison to a typed
+ /// column, and (2) set operations where on side is typed and the other is untyped.
+ ///
+ ///
+ /// Note that this visitor follows type columns across subquery projections. That is, if a root constant/parameter is buried
+ /// within subqueries, and somewhere above the column projected out of a subquery is inferred, this is picked up and propagated
+ /// all the way down.
+ ///
+ ///
+ /// The visitor dose not change the query tree in any way - it only populates the inferred type mappings it identified in
+ /// the given dictionary; actual application of the inferred type mappings happens later in
+ /// . We can't do this in a single pass since untyped roots
+ /// (e.g. may get visited before the type-inferred column referring to them (e.g. CROSS APPLY,
+ /// correlated subquery).
+ ///
+ ///
+ private sealed class ColumnTypeMappingScanner : ExpressionVisitor
+ {
+ private readonly Dictionary<(TableExpressionBase, string), RelationalTypeMapping> _inferredColumns = new();
+
+ private SelectExpression? _currentSelectExpression;
+ private ProjectionExpression? _currentProjectionExpression;
+
+ public IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> Scan(Expression expression)
+ {
+ _inferredColumns.Clear();
+
+ Visit(expression);
+
+ return _inferredColumns;
+ }
+
+ protected override Expression VisitExtension(Expression node)
+ {
+ switch (node)
+ {
+ // A column on a table which was possibly originally untyped (constant/parameter root or a subquery projection of one),
+ // which now does have a type mapping - this would mean in got inferred in the usual manner (comparison with typed column).
+ // Registered the inferred type mapping so it can be later applied back to its table, if it's untyped.
+ case ColumnExpression { TypeMapping: { } typeMapping } c when WasMaybeOriginallyUntyped(c):
+ {
+ RegisterInferredTypeMapping(c, typeMapping);
+
+ return base.VisitExtension(node);
+ }
+
+ // Similar to the above, but with ScalarSubqueryExpression the inferred type mapping is on the expression itself, while the
+ // ColumnExpression we need is on the subquery's projection.
+ case ScalarSubqueryExpression
+ {
+ TypeMapping: { } typeMapping,
+ Subquery.Projection: [{ Expression: ColumnExpression columnExpression }]
+ }
+ when WasMaybeOriginallyUntyped(columnExpression):
+ {
+ RegisterInferredTypeMapping(columnExpression, typeMapping);
+
+ return base.VisitExtension(node);
+ }
+
+ // For set operations involving a leg with a type mapping (e.g. some column) and a leg without one (queryable constant or
+ // parameter), we infer the missing type mapping from the other side.
+ case SetOperationBase
+ {
+ Source1.Projection: [{ Expression: var projection1 }],
+ Source2.Projection: [{ Expression: var projection2 }]
+ }
+ when UnwrapConvert(projection1) is ColumnExpression column1 && UnwrapConvert(projection2) is ColumnExpression column2:
+ {
+ if (projection1.TypeMapping is not null && WasMaybeOriginallyUntyped(column2))
+ {
+ RegisterInferredTypeMapping(column2, projection1.TypeMapping);
+ }
+
+ if (projection2.TypeMapping is not null && WasMaybeOriginallyUntyped(column1))
+ {
+ RegisterInferredTypeMapping(column1, projection2.TypeMapping);
+ }
+
+ return base.VisitExtension(node);
+ }
+
+ // Record state on the SelectExpression and ProjectionExpression so that we can associate ColumnExpressions to the
+ // projections they're in (see below).
+ case SelectExpression selectExpression:
+ {
+ var parentSelectExpression = _currentSelectExpression;
+ _currentSelectExpression = selectExpression;
+ var visited = base.VisitExtension(selectExpression);
+ _currentSelectExpression = parentSelectExpression;
+ return visited;
+ }
+
+ case ProjectionExpression projectionExpression:
+ {
+ var parentProjectionExpression = _currentProjectionExpression;
+ _currentProjectionExpression = projectionExpression;
+ var visited = base.VisitExtension(projectionExpression);
+ _currentProjectionExpression = parentProjectionExpression;
+ return visited;
+ }
+
+ // When visiting subqueries, we want to propagate the inferred type mappings from above into the subquery, recursively.
+ // So we record state above to know which subquery and projection we're visiting; when visiting columns inside a projection
+ // which has an inferred type mapping from above, we register the inferred type mapping for that column too.
+ case ColumnExpression { TypeMapping: null } columnExpression
+ when _currentSelectExpression is not null
+ && _currentProjectionExpression is not null
+ && _inferredColumns.TryGetValue(
+ (_currentSelectExpression, _currentProjectionExpression.Alias), out var inferredTypeMapping)
+ && WasMaybeOriginallyUntyped(columnExpression):
+ {
+ RegisterInferredTypeMapping(columnExpression, inferredTypeMapping);
+ return base.VisitExtension(node);
+ }
+
+ case ShapedQueryExpression shapedQueryExpression:
+ return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression));
+
+ default:
+ return base.VisitExtension(node);
+ }
+
+ bool WasMaybeOriginallyUntyped(ColumnExpression columnExpression)
+ {
+ var underlyingTable = columnExpression.Table is JoinExpressionBase joinExpression
+ ? joinExpression.Table
+ : columnExpression.Table;
+
+ return underlyingTable switch
+ {
+ TableExpression
+ => false,
+
+ SelectExpression subquery
+ => subquery.Projection.FirstOrDefault(p => p.Alias == columnExpression.Name) is { Expression.TypeMapping: null },
+
+ JoinExpressionBase
+ => throw new InvalidOperationException("Impossible: nested join"),
+
+ // Any other table expression is considered a root (TableValuedFunctionExpression, ValuesExpression...) which *may* be
+ // untyped, so we record the possible inference (note that TableValuedFunctionExpression may be typed, or not)
+ _ => true,
+ };
+ }
+
+ SqlExpression UnwrapConvert(SqlExpression expression)
+ => expression is SqlUnaryExpression { OperatorType: ExpressionType.Convert } convert
+ ? UnwrapConvert(convert.Operand)
+ : expression;
+ }
+
+ private void RegisterInferredTypeMapping(ColumnExpression columnExpression, RelationalTypeMapping inferredTypeMapping)
+ {
+ var underlyingTable = columnExpression.Table is JoinExpressionBase joinExpression
+ ? joinExpression.Table
+ : columnExpression.Table;
+
+ if (_inferredColumns.TryGetValue((underlyingTable, columnExpression.Name), out var knownTypeMapping)
+ && inferredTypeMapping != knownTypeMapping)
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.ConflictingTypeMappingsForPrimitiveCollection(
+ inferredTypeMapping.StoreType, knownTypeMapping.StoreType));
+ }
+
+ _inferredColumns[(underlyingTable, columnExpression.Name)] = inferredTypeMapping;
+ }
+ }
+
+ ///
+ /// A visitor executed at the end of translation, which verifies that all nodes have a type mapping,
+ /// and applies type mappings inferred for queryable constants (VALUES) and parameters (e.g. OPENJSON) back on their root tables.
+ ///
+ protected class RelationalInferredTypeMappingApplier : ExpressionVisitor
+ {
+ private SelectExpression? _currentSelectExpression;
+
+ ///
+ /// The inferred type mappings to be applied back on their query roots.
+ ///
+ protected IReadOnlyDictionary<(TableExpressionBase Table, string ColumnName), RelationalTypeMapping> InferredTypeMappings { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The inferred type mappings to be applied back on their query roots.
+ public RelationalInferredTypeMappingApplier(
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ => InferredTypeMappings = inferredTypeMappings;
+
+ ///
+ protected override Expression VisitExtension(Expression expression)
+ {
+ switch (expression)
+ {
+ case ColumnExpression { TypeMapping: null } columnExpression
+ when InferredTypeMappings.TryGetValue((columnExpression.Table, columnExpression.Name), out var typeMapping):
+ return columnExpression.ApplyTypeMapping(typeMapping);
+
+ case SelectExpression selectExpression:
+ var parentSelectExpression = _currentSelectExpression;
+ _currentSelectExpression = selectExpression;
+ var visited = base.VisitExtension(expression);
+ _currentSelectExpression = parentSelectExpression;
+ return visited;
+
+ // For ValueExpression, apply the inferred type mapping on all constants inside.
+ case ValuesExpression valuesExpression:
+ // By default, the ValuesExpression also contains an ordering by a synthetic increasing _ord. If the containing
+ // SelectExpression doesn't project it out or require it (limit/offset), strip that out.
+ // TODO: Strictly-speaking, this doesn't belong in this visitor which is about applying type mappings
+ return ApplyTypeMappingsOnValuesExpression(
+ valuesExpression,
+ stripOrdering: _currentSelectExpression is { Limit: null, Offset: null }
+ && !_currentSelectExpression.Projection.Any(
+ p => p.Expression is ColumnExpression { Name: ValuesOrderingColumnName } c && c.Table == valuesExpression));
+
+ // SqlExpressions without an inferred type mapping indicates a problem in EF - everything should have been inferred.
+ // One exception is SqlFragmentExpression, which never has a type mapping.
+ case SqlExpression { TypeMapping: null } sqlExpression and not SqlFragmentExpression and not ColumnExpression:
+ throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(sqlExpression.Print()));
+
+ case ShapedQueryExpression shapedQueryExpression:
+ return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression));
+
+ default:
+ return base.VisitExtension(expression);
+ }
+ }
+
+ ///
+ /// Applies the given type mappings to the values projected out by the given .
+ /// As an optimization, it can also strip the first _ord column if it's determined that it isn't needed (most cases).
+ ///
+ /// The to apply the mappings to.
+ /// Whether to strip the _ord column.
+ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExpression valuesExpression, bool stripOrdering)
+ {
+ var inferredTypeMappings = InferredTypeMappings.TryGetValue((valuesExpression, ValuesValueColumnName), out var typeMapping)
+ ? new[] { null, typeMapping }
+ : new RelationalTypeMapping?[] { null, null };
+
+ Check.DebugAssert(
+ valuesExpression.ColumnNames[0] == ValuesOrderingColumnName, "First ValuesExpression column isn't the ordering column");
+ var newColumnNames = stripOrdering
+ ? valuesExpression.ColumnNames.Skip(1).ToArray()
+ : valuesExpression.ColumnNames;
+
+ var newRowValues = new RowValueExpression[valuesExpression.RowValues.Count];
+ for (var i = 0; i < newRowValues.Length; i++)
+ {
+ var rowValue = valuesExpression.RowValues[i];
+ 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 inferredTypeMapping = inferredTypeMappings[j];
+ if (inferredTypeMapping is not null && value.TypeMapping is null)
+ {
+ newValue = new SqlConstantExpression(Expression.Constant(value.Value, value.Type), 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);
+ }
+ }
+
+ newValues[j - (stripOrdering ? 1 : 0)] = newValue;
+ }
+
+ newRowValues[i] = new RowValueExpression(newValues);
+ }
+
+ return new(valuesExpression.Alias, newRowValues, newColumnNames, valuesExpression.GetAnnotations());
+ }
+ }
}
diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
index 737d5fd13c3..38569b6d852 100644
--- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
@@ -66,7 +66,6 @@ private static readonly MethodInfo StringEqualsWithStringComparisonStatic
private readonly IModel _model;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly QueryableMethodTranslatingExpressionVisitor _queryableMethodTranslatingExpressionVisitor;
- private readonly SqlTypeMappingVerifyingExpressionVisitor _sqlTypeMappingVerifyingExpressionVisitor;
private bool _throwForNotTranslatedEfProperty;
@@ -86,7 +85,6 @@ public RelationalSqlTranslatingExpressionVisitor(
_queryCompilationContext = queryCompilationContext;
_model = queryCompilationContext.Model;
_queryableMethodTranslatingExpressionVisitor = queryableMethodTranslatingExpressionVisitor;
- _sqlTypeMappingVerifyingExpressionVisitor = new SqlTypeMappingVerifyingExpressionVisitor();
_throwForNotTranslatedEfProperty = true;
}
@@ -149,8 +147,6 @@ protected virtual void AddTranslationErrorDetails(string details)
return null;
}
- _sqlTypeMappingVerifyingExpressionVisitor.Visit(translation);
-
return translation;
}
@@ -704,6 +700,12 @@ protected override Expression VisitExtension(Expression extensionExpression)
return scalarSubqueryExpression;
+ // We have e.g. an array parameter inside a Where clause; this is represented as a QueryableParameterQueryRootExpression so
+ // that we can translate queryable operators over it (query root in subquery context), but in normal SQL translation context
+ // we just unwrap the query root expression to get the parameter out.
+ case ParameterQueryRootExpression queryableParameterQueryRootExpression:
+ return Visit(queryableParameterQueryRootExpression.ParameterExpression);
+
default:
return QueryCompilationContext.NotTranslatedExpression;
}
@@ -738,6 +740,36 @@ protected override Expression VisitMember(MemberExpression memberExpression)
protected override Expression VisitMemberInit(MemberInitExpression memberInitExpression)
=> GetConstantOrNotTranslated(memberInitExpression);
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ [EntityFrameworkInternal]
+ public virtual bool TryTranslatePropertyAccess(Expression expression, [NotNullWhen(true)] out SqlExpression? propertyAccessExpression)
+ {
+ if (expression is MethodCallExpression methodCallExpression)
+ {
+ if (methodCallExpression.TryGetEFPropertyArguments(out var source, out var propertyName)
+ && TryBindMember(Visit(source), MemberIdentity.Create(propertyName)) is { } result)
+ {
+ propertyAccessExpression = result;
+ return true;
+ }
+
+ if (methodCallExpression.TryGetIndexerArguments(_model, out source, out propertyName)
+ && TryBindMember(Visit(source), MemberIdentity.Create(propertyName)) is { } indexerResult)
+ {
+ propertyAccessExpression = indexerResult;
+ return true;
+ }
+ }
+
+ propertyAccessExpression = null;
+ return false;
+ }
+
///
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
@@ -1913,13 +1945,4 @@ public Expression Convert(Type type)
: new EntityReferenceExpression(this, derivedEntityType);
}
}
-
- private sealed class SqlTypeMappingVerifyingExpressionVisitor : ExpressionVisitor
- {
- protected override Expression VisitExtension(Expression extensionExpression)
- => extensionExpression is SqlExpression { TypeMapping: null } sqlExpression
- && extensionExpression is not SqlFragmentExpression
- ? throw new InvalidOperationException(RelationalStrings.NullTypeMappingInSqlTree(sqlExpression.Print()))
- : base.VisitExtension(extensionExpression);
- }
}
diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs
index 1da3ecfb20c..6dc46ca1058 100644
--- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs
+++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs
@@ -57,9 +57,11 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping),
CaseExpression e => ApplyTypeMappingOnCase(e, typeMapping),
CollateExpression e => ApplyTypeMappingOnCollate(e, typeMapping),
+ ColumnExpression e => e.ApplyTypeMapping(typeMapping),
DistinctExpression e => ApplyTypeMappingOnDistinct(e, typeMapping),
InExpression e => ApplyTypeMappingOnIn(e),
LikeExpression e => ApplyTypeMappingOnLike(e),
+ ScalarSubqueryExpression e => e.ApplyTypeMapping(typeMapping),
SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping),
SqlConstantExpression e => e.ApplyTypeMapping(typeMapping),
SqlFragmentExpression e => e,
@@ -641,8 +643,9 @@ private void AddConditions(SelectExpression selectExpression, IEntityType entity
return;
}
- var firstTable = selectExpression.Tables[0];
- var table = (firstTable as FromSqlExpression)?.Table ?? ((ITableBasedExpression)firstTable).Table;
+ var table = (selectExpression.Tables[0] as ITableBasedExpression)?.Table;
+ Check.DebugAssert(table is not null, "SelectExpression with unexpected missing table");
+
if (table.IsOptional(entityType))
{
SqlExpression? predicate = null;
diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
index 46fdcdb0e10..bb4c44c9307 100644
--- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs
@@ -43,6 +43,7 @@ ShapedQueryExpression shapedQueryExpression
ProjectionExpression projectionExpression => VisitProjection(projectionExpression),
TableValuedFunctionExpression tableValuedFunctionExpression => VisitTableValuedFunction(tableValuedFunctionExpression),
RowNumberExpression rowNumberExpression => VisitRowNumber(rowNumberExpression),
+ RowValueExpression rowValueExpression => VisitRowValue(rowValueExpression),
ScalarSubqueryExpression scalarSubqueryExpression => VisitScalarSubquery(scalarSubqueryExpression),
SelectExpression selectExpression => VisitSelect(selectExpression),
SqlBinaryExpression sqlBinaryExpression => VisitSqlBinary(sqlBinaryExpression),
@@ -55,6 +56,7 @@ ShapedQueryExpression shapedQueryExpression
UnionExpression unionExpression => VisitUnion(unionExpression),
UpdateExpression updateExpression => VisitUpdate(updateExpression),
JsonScalarExpression jsonScalarExpression => VisitJsonScalar(jsonScalarExpression),
+ ValuesExpression valuesExpression => VisitValues(valuesExpression),
_ => base.VisitExtension(extensionExpression),
};
@@ -205,6 +207,13 @@ ShapedQueryExpression shapedQueryExpression
/// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
protected abstract Expression VisitRowNumber(RowNumberExpression rowNumberExpression);
+ ///
+ /// Visits the children of the row value expression.
+ ///
+ /// The expression to visit.
+ /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
+ protected abstract Expression VisitRowValue(RowValueExpression rowValueExpression);
+
///
/// Visits the children of the scalar subquery expression.
///
@@ -288,4 +297,11 @@ ShapedQueryExpression shapedQueryExpression
/// The expression to visit.
/// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
protected abstract Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression);
+
+ ///
+ /// Visits the children of the values expression.
+ ///
+ /// The expression to visit.
+ /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
+ protected abstract Expression VisitValues(ValuesExpression valuesExpression);
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs
index 2a9a10cfb23..7d63dc2d58d 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs
@@ -51,6 +51,13 @@ protected ColumnExpression(Type type, RelationalTypeMapping? typeMapping)
/// A new expression which has property set to true.
public abstract ColumnExpression MakeNullable();
+ ///
+ /// Applies supplied type mapping to this expression.
+ ///
+ /// A relational type mapping to apply.
+ /// A new expression which has supplied type mapping.
+ public abstract SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping);
+
///
protected override void Print(ExpressionPrinter expressionPrinter)
{
diff --git a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
index f51942ea361..b82c4944f42 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs
@@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
/// not used in application code.
///
///
-public class FromSqlExpression : TableExpressionBase, IClonableTableExpressionBase
+public class FromSqlExpression : TableExpressionBase, ITableBasedExpression, IClonableTableExpressionBase
{
///
/// Creates a new instance of the class.
diff --git a/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs
index 06eecbdab27..7254b9c5f08 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/ITableBasedExpression.cs
@@ -5,7 +5,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
///
///
-/// An interface that gives access to associated with given table source.
+/// An interface that gives access to an optional associated with given table source.
///
///
/// This type is typically used by database providers (and other extensions). It is generally
@@ -15,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
public interface ITableBasedExpression
{
///
- /// The associated with given table source.
+ /// The associated with given table source, if any.
///
- ITableBase Table { get; }
+ ITableBase? Table { get; }
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs
new file mode 100644
index 00000000000..ecee8a57cff
--- /dev/null
+++ b/src/EFCore.Relational/Query/SqlExpressions/RowValueExpression.cs
@@ -0,0 +1,146 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Data;
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+
+///
+///
+/// An expression that represents a SQL row.
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally
+/// not used in application code.
+///
+///
+public class RowValueExpression : SqlExpression
+{
+ ///
+ /// The values of this row.
+ ///
+ public virtual IReadOnlyList Values { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The values of this row.
+ public RowValueExpression(IReadOnlyList values)
+ : base(typeof(ValueTuple
/// A subquery projecting single row with a single scalar projection.
public ScalarSubqueryExpression(SelectExpression subquery)
- : base(Verify(subquery).Projection[0].Type, subquery.Projection[0].Expression.TypeMapping)
+ : this(subquery, subquery.Projection[0].Expression.TypeMapping)
+ {
+ Subquery = subquery;
+ }
+
+ private ScalarSubqueryExpression(SelectExpression subquery, RelationalTypeMapping? typeMapping)
+ : base(Verify(subquery).Projection[0].Type, typeMapping)
{
Subquery = subquery;
}
@@ -46,6 +52,14 @@ private static SelectExpression Verify(SelectExpression selectExpression)
///
public virtual SelectExpression Subquery { get; }
+ ///
+ /// Applies supplied type mapping to this expression.
+ ///
+ /// A relational type mapping to apply.
+ /// A new expression which has supplied type mapping.
+ public virtual SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
+ => new ScalarSubqueryExpression(Subquery, typeMapping);
+
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
=> Update((SelectExpression)visitor.Visit(Subquery));
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
index 0af3351d300..defc4f156c1 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
@@ -605,7 +605,7 @@ public ConcreteColumnExpression(
string name,
TableReferenceExpression table,
Type type,
- RelationalTypeMapping typeMapping,
+ RelationalTypeMapping? typeMapping,
bool nullable)
: base(type, typeMapping)
{
@@ -629,7 +629,10 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
=> this;
public override ConcreteColumnExpression MakeNullable()
- => IsNullable ? this : new ConcreteColumnExpression(Name, _table, Type, TypeMapping!, true);
+ => IsNullable ? this : new ConcreteColumnExpression(Name, _table, Type, TypeMapping, true);
+
+ public override SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
+ => new ConcreteColumnExpression(Name, _table, Type, typeMapping, IsNullable);
public void UpdateTableReference(SelectExpression oldSelect, SelectExpression newSelect)
=> _table.UpdateTableReference(oldSelect, newSelect);
@@ -1019,12 +1022,11 @@ private sealed class CloningExpressionVisitor : ExpressionVisitor
newArguments[i] = (SqlExpression)Visit(tableValuedFunctionExpression.Arguments[i]);
}
- var newTableValuedFunctionExpression = new TableValuedFunctionExpression(
- tableValuedFunctionExpression.StoreFunction,
- newArguments)
- {
- Alias = tableValuedFunctionExpression.Alias
- };
+ var newTableValuedFunctionExpression = tableValuedFunctionExpression.StoreFunction is null
+ ? new TableValuedFunctionExpression(
+ tableValuedFunctionExpression.Alias, tableValuedFunctionExpression.Name, newArguments)
+ : new TableValuedFunctionExpression(
+ tableValuedFunctionExpression.StoreFunction, newArguments) { Alias = tableValuedFunctionExpression.Alias };
foreach (var annotation in tableValuedFunctionExpression.GetAnnotations())
{
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index e39a89f4ad5..547535b4da6 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -25,7 +25,6 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
public sealed partial class SelectExpression : TableExpressionBase
{
private const string DiscriminatorColumnAlias = "Discriminator";
- private const string SqlQuerySingleColumnAlias = "Value";
private static readonly IdentifierComparer IdentifierComparerInstance = new();
private static readonly Dictionary MirroredOperationMap =
@@ -112,14 +111,32 @@ internal SelectExpression(SqlExpression? projection)
}
}
- internal SelectExpression(Type type, RelationalTypeMapping typeMapping, FromSqlExpression fromSqlExpression)
+ ///
+ /// Creates a new instance of the class given a , with a single
+ /// column projection.
+ ///
+ /// The table expression.
+ /// The name of the column to add as the projection.
+ /// The type of the column to add as the projection.
+ /// The type mapping of the column to add as the projection.
+ /// Whether the column projected out is nullable.
+ public SelectExpression(
+ TableExpressionBase tableExpression,
+ string columnName,
+ Type columnType,
+ RelationalTypeMapping? columnTypeMapping,
+ bool? isColumnNullable = null)
: base(null)
{
- var tableReferenceExpression = new TableReferenceExpression(this, fromSqlExpression.Alias!);
- AddTable(fromSqlExpression, tableReferenceExpression);
+ var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!);
+ AddTable(tableExpression, tableReferenceExpression);
var columnExpression = new ConcreteColumnExpression(
- SqlQuerySingleColumnAlias, tableReferenceExpression, type, typeMapping, type.IsNullableType());
+ columnName,
+ tableReferenceExpression,
+ columnType.UnwrapNullableType(),
+ columnTypeMapping,
+ isColumnNullable ?? columnType.IsNullableType());
_projectionMapping[new ProjectionMember()] = columnExpression;
}
@@ -466,7 +483,9 @@ internal SelectExpression(IEntityType entityType, TableExpressionBase tableExpre
throw new InvalidOperationException(RelationalStrings.SelectExpressionNonTphWithCustomTable(entityType.DisplayName()));
}
- var table = (tableExpressionBase as FromSqlExpression)?.Table ?? ((ITableBasedExpression)tableExpressionBase).Table;
+ var table = (tableExpressionBase as ITableBasedExpression)?.Table;
+ Check.DebugAssert(table is not null, "SelectExpression with unexpected missing table");
+
var tableReferenceExpression = new TableReferenceExpression(this, tableExpressionBase.Alias!);
AddTable(tableExpressionBase, tableReferenceExpression);
@@ -3912,6 +3931,32 @@ public void PrepareForAggregate()
}
}
+ ///
+ /// Creates a that references a table on this .
+ ///
+ /// The table expression referenced by the column.
+ /// The column name.
+ /// The column CLR type.
+ /// The column's type mapping.
+ /// Whether the column is nullable.
+ public ColumnExpression CreateColumnExpression(
+ TableExpressionBase tableExpression,
+ string columnName,
+ Type type,
+ RelationalTypeMapping? typeMapping,
+ bool? columnNullable = null)
+ {
+ var tableIndex = _tables.FindIndex(teb => ReferenceEquals(teb, tableExpression));
+ var tableReferenceExpression = _tableReferences[tableIndex];
+
+ return new ConcreteColumnExpression(
+ columnName,
+ tableReferenceExpression,
+ type.UnwrapNullableType(),
+ typeMapping,
+ columnNullable ?? type.IsNullableType());
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
index 888e1bbe5ac..b4869e373dc 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs
@@ -24,20 +24,66 @@ public class TableValuedFunctionExpression : TableExpressionBase, ITableBasedExp
public TableValuedFunctionExpression(IStoreFunction storeFunction, IReadOnlyList arguments)
: this(
storeFunction.Name[..1].ToLowerInvariant(),
- storeFunction,
+ storeFunction.Name,
+ storeFunction.Schema,
+ storeFunction.IsBuiltIn,
arguments,
annotations: null)
+ {
+ StoreFunction = storeFunction;
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The name of the function.
+ /// The arguments of the function.
+ /// A collection of annotations associated with this expression.
+ public TableValuedFunctionExpression(
+ string name,
+ IReadOnlyList arguments,
+ IEnumerable? annotations = null)
+ : this(name[..1].ToLowerInvariant(), name, schema: null, builtIn: true, arguments, annotations)
{
}
- private TableValuedFunctionExpression(
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A string alias for the table source.
+ /// The name of the function.
+ /// The arguments of the function.
+ /// A collection of annotations associated with this expression.
+ public TableValuedFunctionExpression(
+ string alias,
+ string name,
+ IReadOnlyList arguments,
+ IEnumerable? annotations = null)
+ : this(alias, name, schema: null, builtIn: true, arguments, annotations)
+ {
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A string alias for the table source.
+ /// The name of the function.
+ /// The schema of the function.
+ /// Whether the function is built-in.
+ /// The arguments of the function.
+ /// A collection of annotations associated with this expression.
+ protected TableValuedFunctionExpression(
string alias,
- IStoreFunction storeFunction,
+ string name,
+ string? schema,
+ bool builtIn,
IReadOnlyList arguments,
- IEnumerable? annotations)
+ IEnumerable? annotations = null)
: base(alias, annotations)
{
- StoreFunction = storeFunction;
+ Name = name;
+ Schema = schema;
+ IsBuiltIn = builtIn;
Arguments = arguments;
}
@@ -54,17 +100,32 @@ public override string? Alias
///
/// The store function.
///
- public virtual IStoreFunction StoreFunction { get; }
+ public virtual IStoreFunction? StoreFunction { get; }
+
+ ///
+ ITableBase? ITableBasedExpression.Table
+ => StoreFunction;
+
+ ///
+ /// The name of the function.
+ ///
+ public virtual string Name { get; }
+
+ ///
+ /// The schema of the function.
+ ///
+ public virtual string? Schema { get; }
+
+ ///
+ /// Gets the value indicating whether the function is built-in.
+ ///
+ public virtual bool IsBuiltIn { get; }
///
/// The list of arguments of this function.
///
public virtual IReadOnlyList Arguments { get; }
- ///
- ITableBase ITableBasedExpression.Table
- => StoreFunction;
-
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
@@ -77,7 +138,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
}
return changed
- ? new TableValuedFunctionExpression(Alias, StoreFunction, arguments, GetAnnotations())
+ ? new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, arguments, GetAnnotations())
: this;
}
@@ -89,22 +150,22 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
/// This expression if no children changed, or an expression with the updated children.
public virtual TableValuedFunctionExpression Update(IReadOnlyList arguments)
=> !arguments.SequenceEqual(Arguments)
- ? new TableValuedFunctionExpression(Alias, StoreFunction, arguments, GetAnnotations())
+ ? new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, arguments, GetAnnotations())
: this;
///
protected override TableExpressionBase CreateWithAnnotations(IEnumerable annotations)
- => new TableValuedFunctionExpression(Alias, StoreFunction, Arguments, annotations);
+ => new TableValuedFunctionExpression(Alias, Name, Schema, IsBuiltIn, Arguments, annotations);
///
protected override void Print(ExpressionPrinter expressionPrinter)
{
- if (!string.IsNullOrEmpty(StoreFunction.Schema))
+ if (!string.IsNullOrEmpty(Schema))
{
- expressionPrinter.Append(StoreFunction.Schema).Append(".");
+ expressionPrinter.Append(Schema).Append(".");
}
- expressionPrinter.Append(StoreFunction.Name);
+ expressionPrinter.Append(Name);
expressionPrinter.Append("(");
expressionPrinter.VisitCollection(Arguments);
expressionPrinter.Append(")");
@@ -122,6 +183,9 @@ public override bool Equals(object? obj)
private bool Equals(TableValuedFunctionExpression tableValuedFunctionExpression)
=> base.Equals(tableValuedFunctionExpression)
+ && Name == tableValuedFunctionExpression.Name
+ && Schema == tableValuedFunctionExpression.Schema
+ && IsBuiltIn == tableValuedFunctionExpression.IsBuiltIn
&& StoreFunction == tableValuedFunctionExpression.StoreFunction
&& Arguments.SequenceEqual(tableValuedFunctionExpression.Arguments);
@@ -130,6 +194,9 @@ public override int GetHashCode()
{
var hash = new HashCode();
hash.Add(base.GetHashCode());
+ hash.Add(Name);
+ hash.Add(Schema);
+ hash.Add(IsBuiltIn);
hash.Add(StoreFunction);
for (var i = 0; i < Arguments.Count; i++)
{
diff --git a/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs
new file mode 100644
index 00000000000..56fcde09c15
--- /dev/null
+++ b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs
@@ -0,0 +1,181 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+
+///
+///
+/// An expression that represents a constant table in SQL, sometimes known as a table value constructor.
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally
+/// not used in application code.
+///
+///
+public class ValuesExpression : TableExpressionBase, IClonableTableExpressionBase
+{
+ ///
+ /// The row values for this table.
+ ///
+ public virtual IReadOnlyList RowValues { get; }
+
+ ///
+ /// The names of the columns contained in this table.
+ ///
+ public virtual IReadOnlyList ColumnNames { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A string alias for the table source.
+ /// The row values for this table.
+ /// The names of the columns contained in this table.
+ /// A collection of annotations associated with this expression.
+ public ValuesExpression(
+ string? alias,
+ IReadOnlyList rowValues,
+ IReadOnlyList columnNames,
+ IEnumerable? annotations = null)
+ : base(alias, annotations)
+ {
+ 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
+
+ RowValues = rowValues;
+ ColumnNames = columnNames;
+ }
+
+ ///
+ /// The alias assigned to this table source.
+ ///
+ [NotNull]
+ public override string? Alias
+ {
+ get => base.Alias!;
+ internal set => base.Alias = value;
+ }
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ Check.NotNull(visitor, nameof(visitor));
+
+ RowValueExpression[]? newRowValues = null;
+
+ for (var i = 0; i < RowValues.Count; i++)
+ {
+ var rowValue = RowValues[i];
+ var visited = (RowValueExpression)visitor.Visit(rowValue);
+ if (visited != rowValue && newRowValues is null)
+ {
+ newRowValues = new RowValueExpression[RowValues.Count];
+ for (var j = 0; j < i; j++)
+ {
+ newRowValues[j] = RowValues[j];
+ }
+ }
+
+ if (newRowValues is not null)
+ {
+ newRowValues[i] = visited;
+ }
+ }
+
+ return newRowValues is null ? this : new ValuesExpression(Alias, newRowValues, ColumnNames);
+ }
+
+ ///
+ /// 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.
+ ///
+ public virtual ValuesExpression Update(IReadOnlyList rowValues)
+ => rowValues.Count == RowValues.Count && rowValues.Zip(RowValues, (x, y) => (x, y)).All(tup => tup.x == tup.y)
+ ? this
+ : new ValuesExpression(Alias, rowValues, ColumnNames);
+
+ ///
+ protected override TableExpressionBase CreateWithAnnotations(IEnumerable annotations)
+ => new ValuesExpression(Alias, RowValues, ColumnNames, annotations);
+
+ ///
+ public virtual TableExpressionBase Clone()
+ => CreateWithAnnotations(GetAnnotations());
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append("VALUES (");
+
+ var count = RowValues.Count;
+ for (var i = 0; i < count; i++)
+ {
+ expressionPrinter.Visit(RowValues[i]);
+
+ if (i < count - 1)
+ {
+ expressionPrinter.Append(", ");
+ }
+ }
+
+ expressionPrinter.Append(")");
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => obj is ValuesExpression other && Equals(other);
+
+ private bool Equals(ValuesExpression? other)
+ {
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ if (other is null || !base.Equals(other) || other.RowValues.Count != RowValues.Count)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < RowValues.Count; i++)
+ {
+ if (!other.RowValues[i].Equals(RowValues[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+
+ foreach (var rowValue in RowValues)
+ {
+ hashCode.Add(rowValue);
+ }
+
+ return hashCode.ToHashCode();
+ }
+}
diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
index 1b413d0faa2..bf39f8f5947 100644
--- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
+++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
@@ -185,6 +185,33 @@ protected virtual TableExpressionBase Visit(TableExpressionBase tableExpressionB
case OuterApplyExpression outerApplyExpression:
return outerApplyExpression.Update(Visit(outerApplyExpression.Table));
+ case ValuesExpression valuesExpression:
+ {
+ RowValueExpression[]? newRowValues = null;
+
+ for (var i = 0; i < valuesExpression.RowValues.Count; i++)
+ {
+ var rowValue = valuesExpression.RowValues[i];
+ var newRowValue = (RowValueExpression)VisitRowValue(rowValue, allowOptimizedExpansion: false, out _);
+
+ if (newRowValue != rowValue && newRowValues is null)
+ {
+ newRowValues = new RowValueExpression[valuesExpression.RowValues.Count];
+ for (var j = 0; j < i; j++)
+ {
+ newRowValues[j] = valuesExpression.RowValues[j];
+ }
+ }
+
+ if (newRowValues is not null)
+ {
+ newRowValues[i] = newRowValue;
+ }
+ }
+
+ return newRowValues is null ? valuesExpression : valuesExpression.Update(newRowValues);
+ }
+
case SelectExpression selectExpression:
return Visit(selectExpression);
@@ -403,6 +430,8 @@ LikeExpression likeExpression
=> VisitLike(likeExpression, allowOptimizedExpansion, out nullable),
RowNumberExpression rowNumberExpression
=> VisitRowNumber(rowNumberExpression, allowOptimizedExpansion, out nullable),
+ RowValueExpression rowValueExpression
+ => VisitRowValue(rowValueExpression, allowOptimizedExpansion, out nullable),
ScalarSubqueryExpression scalarSubqueryExpression
=> VisitScalarSubquery(scalarSubqueryExpression, allowOptimizedExpansion, out nullable),
SqlBinaryExpression sqlBinaryExpression
@@ -733,7 +762,7 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt
case SqlParameterExpression sqlParameter:
DoNotCache();
typeMapping = sqlParameter.TypeMapping;
- values = (IEnumerable?)ParameterValues[sqlParameter.Name] ?? throw new NullReferenceException();
+ values = (IEnumerable?)ParameterValues[sqlParameter.Name] ?? Array.Empty();
break;
default:
@@ -826,6 +855,47 @@ protected virtual SqlExpression VisitRowNumber(
: rowNumberExpression;
}
+ ///
+ /// Visits a and computes its nullability.
+ ///
+ /// A row value expression to visit.
+ /// A bool value indicating if optimized expansion which considers null value as false value is allowed.
+ /// A bool value indicating whether the sql expression is nullable.
+ /// An optimized sql expression.
+ protected virtual SqlExpression VisitRowValue(
+ RowValueExpression rowValueExpression,
+ bool allowOptimizedExpansion,
+ out bool nullable)
+ {
+ SqlExpression[]? newValues = null;
+
+ for (var i = 0; i < rowValueExpression.Values.Count; i++)
+ {
+ var value = rowValueExpression.Values[i];
+
+ // Note that we disallow optimized expansion, since the null vs. false distinction does matter inside the row's values
+ var newValue = Visit(value, allowOptimizedExpansion: false, out _);
+ if (newValue != value && newValues is null)
+ {
+ newValues = new SqlExpression[rowValueExpression.Values.Count];
+ for (var j = 0; j < i; j++)
+ {
+ newValues[j] = rowValueExpression.Values[j];
+ }
+ }
+
+ if (newValues is not null)
+ {
+ newValues[i] = newValue;
+ }
+ }
+
+ // The row value expression itself can never be null
+ nullable = false;
+
+ return rowValueExpression.Update(newValues ?? rowValueExpression.Values);
+ }
+
///
/// Visits a and computes its nullability.
///
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs
index a994c313168..536f658af60 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs
@@ -120,6 +120,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec
.TryAdd()
.TryAdd()
.TryAdd()
+ .TryAdd()
.TryAdd()
.TryAdd()
.TryAdd()
diff --git a/src/EFCore.SqlServer/Infrastructure/SqlServerDbContextOptionsBuilder.cs b/src/EFCore.SqlServer/Infrastructure/SqlServerDbContextOptionsBuilder.cs
index 50513c6032a..066f6eccc03 100644
--- a/src/EFCore.SqlServer/Infrastructure/SqlServerDbContextOptionsBuilder.cs
+++ b/src/EFCore.SqlServer/Infrastructure/SqlServerDbContextOptionsBuilder.cs
@@ -106,7 +106,7 @@ public virtual SqlServerDbContextOptionsBuilder EnableRetryOnFailure(
///
/// Sets the SQL Server compatibility level that EF Core will use when interacting with the database. This allows configuring EF
- /// Core to work with older (or newer) versions of SQL Server. Defaults to 150 (SQL Server 2019).
+ /// Core to work with older (or newer) versions of SQL Server. Defaults to 160 (SQL Server 2022).
///
///
/// See Using DbContextOptions, and
@@ -114,7 +114,6 @@ public virtual SqlServerDbContextOptionsBuilder EnableRetryOnFailure(
/// documentation on compatibility level for more information and examples.
///
/// to have null resource
- // TODO: Naming; Cosmos doesn't have Use/Set, so just CompatibilityLevel? SetCompatibilityLevel?
public virtual SqlServerDbContextOptionsBuilder UseCompatibilityLevel(int compatibilityLevel)
=> WithOption(e => e.WithCompatibilityLevel(compatibilityLevel));
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
index 3e0407691a0..7ea65b2626a 100644
--- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
@@ -670,6 +670,27 @@ protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpres
return ApplyConversion(rowNumberExpression.Update(partitions, orderings), condition: false);
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitRowValue(RowValueExpression rowValueExpression)
+ {
+ var parentSearchCondition = _isSearchCondition;
+ _isSearchCondition = false;
+
+ var values = new SqlExpression[rowValueExpression.Values.Count];
+ for (var i = 0; i < values.Length; i++)
+ {
+ values[i] = (SqlExpression)Visit(rowValueExpression.Values[i]);
+ }
+
+ _isSearchCondition = parentSearchCondition;
+ return rowValueExpression.Update(values);
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -765,4 +786,25 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression)
///
protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression)
=> ApplyConversion(jsonScalarExpression, condition: false);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitValues(ValuesExpression valuesExpression)
+ {
+ var parentSearchCondition = _isSearchCondition;
+ _isSearchCondition = false;
+
+ var rowValues = new RowValueExpression[valuesExpression.RowValues.Count];
+ for (var i = 0; i < rowValues.Length; i++)
+ {
+ rowValues[i] = (RowValueExpression)Visit(valuesExpression.RowValues[i]);
+ }
+
+ _isSearchCondition = parentSearchCondition;
+ return valuesExpression.Update(rowValues);
+ }
}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
new file mode 100644
index 00000000000..c46b07f60ae
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerOpenJsonExpression.cs
@@ -0,0 +1,149 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
+
+///
+/// An expression that represents a SQL Server OPENJSON function call in a SQL tree.
+///
+///
+///
+/// See OPENJSON (Transact-SQL) for more
+/// information and examples.
+///
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+///
+public class SqlServerOpenJsonExpression : TableValuedFunctionExpression
+{
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual SqlExpression JsonExpression
+ => Arguments[0];
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual SqlExpression? Path
+ => Arguments.Count == 1 ? null : Arguments[1];
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual IReadOnlyList? ColumnInfos { get; }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqlServerOpenJsonExpression(
+ string alias,
+ SqlExpression jsonExpression,
+ SqlExpression? path = null,
+ IReadOnlyList? columnInfos = null)
+ : base(alias, "OpenJson", schema: null, builtIn: true, path is null ? new[] { jsonExpression } : new[] { jsonExpression, path })
+ {
+ ColumnInfos = columnInfos;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual SqlServerOpenJsonExpression Update(
+ SqlExpression jsonExpression,
+ SqlExpression? path,
+ IReadOnlyList? columnInfos = null)
+ => jsonExpression == JsonExpression
+ && path == Path
+ && (columnInfos is null ? ColumnInfos is null : ColumnInfos is not null && columnInfos.SequenceEqual(ColumnInfos))
+ ? this
+ : new SqlServerOpenJsonExpression(Alias, jsonExpression, path, columnInfos);
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append(Name);
+ expressionPrinter.Append("(");
+ expressionPrinter.VisitCollection(Arguments);
+ expressionPrinter.Append(")");
+
+ if (ColumnInfos is not null)
+ {
+ expressionPrinter.Append(" WITH (");
+
+ for (var i = 0; i < ColumnInfos.Count; i++)
+ {
+ var columnInfo = ColumnInfos[i];
+
+ if (i > 0)
+ {
+ expressionPrinter.Append(", ");
+ }
+
+ expressionPrinter
+ .Append(columnInfo.Name)
+ .Append(" ")
+ .Append(columnInfo.StoreType ?? "");
+
+ if (columnInfo.Path is not null)
+ {
+ expressionPrinter.Append(" ").Append("'" + columnInfo.Path + "'");
+ }
+
+ if (columnInfo.AsJson)
+ {
+ expressionPrinter.Append(" AS JSON");
+ }
+ }
+
+ expressionPrinter.Append(")");
+ }
+
+ PrintAnnotations(expressionPrinter);
+ expressionPrinter.Append(" AS ");
+ expressionPrinter.Append(Alias);
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => ReferenceEquals(this, obj) || (obj is SqlServerOpenJsonExpression openJsonExpression && Equals(openJsonExpression));
+
+ private bool Equals(SqlServerOpenJsonExpression openJsonExpression)
+ => base.Equals(openJsonExpression)
+ && (ColumnInfos is null
+ ? openJsonExpression.ColumnInfos is null
+ : openJsonExpression.ColumnInfos is not null && ColumnInfos.SequenceEqual(openJsonExpression.ColumnInfos));
+
+ ///
+ public override int GetHashCode()
+ => base.GetHashCode();
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public readonly record struct ColumnInfo(string Name, string? StoreType, string? Path = null, bool AsJson = false);
+}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootProcessor.cs
new file mode 100644
index 00000000000..eaff1e7eb80
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootProcessor.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class SqlServerQueryRootProcessor : RelationalQueryRootProcessor
+{
+ private readonly bool _supportsOpenJson;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqlServerQueryRootProcessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext,
+ ISqlServerSingletonOptions sqlServerSingletonOptions)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
+ => _supportsOpenJson = sqlServerSingletonOptions.CompatibilityLevel >= 130;
+
+ ///
+ /// Indicates that a can be converted to a , if the
+ /// configured SQL Server version supports OPENJSON.
+ ///
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override bool ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression)
+ => _supportsOpenJson;
+}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
index 3f7fd0bc9eb..227a5329192 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
@@ -16,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
public class SqlServerQuerySqlGenerator : QuerySqlGenerator
{
private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlGenerationHelper _sqlGenerationHelper;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -29,8 +30,22 @@ public SqlServerQuerySqlGenerator(
: base(dependencies)
{
_typeMappingSource = typeMappingSource;
+ _sqlGenerationHelper = dependencies.SqlGenerationHelper;
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override bool TryGenerateWithoutWrappingSelect(SelectExpression selectExpression)
+ // SQL Server doesn't support VALUES as a top-level statement, so we need to wrap the VALUES in a SELECT:
+ // SELECT 1 AS x UNION VALUES (2), (3) -- simple
+ // SELECT 1 AS x UNION SELECT * FROM (VALUES (2), (3)) AS f(x) -- SQL Server
+ => selectExpression.Tables is not [ValuesExpression]
+ && base.TryGenerateWithoutWrappingSelect(selectExpression);
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -138,6 +153,62 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression)
RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(RelationalQueryableExtensions.ExecuteUpdate)));
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitValues(ValuesExpression valuesExpression)
+ {
+ base.VisitValues(valuesExpression);
+
+ // SQL Server VALUES supports setting the projects column names: FROM (VALUES (1), (2)) AS v(foo)
+ Sql.Append("(");
+
+ for (var i = 0; i < valuesExpression.ColumnNames.Count; i++)
+ {
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Sql.Append(_sqlGenerationHelper.DelimitIdentifier(valuesExpression.ColumnNames[i]));
+ }
+
+ Sql.Append(")");
+
+ return valuesExpression;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override void GenerateValues(ValuesExpression valuesExpression)
+ {
+ // SQL Server supports providing the names of columns projected out of VALUES: (VALUES (1, 3), (2, 4)) AS x(a, b)
+ // (this is implemented in VisitValues above).
+ // But since other databases sometimes don't, the default relational implementation is complex, involving a SELECT for the first row
+ // and a UNION All on the rest. Override to do the nice simple thing.
+
+ var rowValues = valuesExpression.RowValues;
+
+ Sql.Append("VALUES ");
+
+ for (var i = 0; i < rowValues.Count; i++)
+ {
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Visit(valuesExpression.RowValues[i]);
+ }
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -305,6 +376,9 @@ when tableExpression.FindAnnotation(SqlServerAnnotationNames.TemporalOperationTy
case SqlServerAggregateFunctionExpression aggregateFunctionExpression:
return VisitSqlServerAggregateFunction(aggregateFunctionExpression);
+
+ case SqlServerOpenJsonExpression openJsonExpression:
+ return VisitOpenJsonExpression(openJsonExpression);
}
return base.VisitExtension(extensionExpression);
@@ -381,6 +455,65 @@ protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExp
return jsonScalarExpression;
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected virtual Expression VisitOpenJsonExpression(SqlServerOpenJsonExpression openJsonExpression)
+ {
+ // OPENJSON docs: https://learn.microsoft.com/sql/t-sql/functions/openjson-transact-sql
+
+ // OPENJSON is a regular table-valued function with a special WITH clause at the end
+ // Copy-paste from VisitTableValuedFunction, because that appends the 'AS ' but we need to insert WITH before that
+ Sql.Append("OpenJson(");
+
+ GenerateList(openJsonExpression.Arguments, e => Visit(e));
+
+ Sql.Append(")");
+
+ if (openJsonExpression.ColumnInfos is not null)
+ {
+ Sql.Append(" WITH (");
+
+ for (var i = 0; i < openJsonExpression.ColumnInfos.Count; i++)
+ {
+ var columnInfo = openJsonExpression.ColumnInfos[i];
+
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Check.DebugAssert(columnInfo.StoreType is not null, "Unset OpenJson column store type");
+
+ Sql
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(columnInfo.Name))
+ .Append(" ")
+ .Append(columnInfo.StoreType);
+
+ if (columnInfo.Path is not null)
+ {
+ Sql
+ .Append(" ")
+ .Append(_typeMappingSource.GetMapping("varchar(max)").GenerateSqlLiteral(columnInfo.Path));
+ }
+
+ if (columnInfo.AsJson)
+ {
+ Sql.Append(" AS JSON");
+ }
+ }
+
+ Sql.Append(")");
+ }
+
+ Sql.Append(AliasSeparator).Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(openJsonExpression.Alias));
+
+ return openJsonExpression;
+ }
+
///
protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql)
{
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs
new file mode 100644
index 00000000000..f22ddc0236a
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessor.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class SqlServerQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
+{
+ private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqlServerQueryTranslationPreprocessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ ISqlServerSingletonOptions sqlServerSingletonOptions,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
+ => _sqlServerSingletonOptions = sqlServerSingletonOptions;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression ProcessQueryRoots(Expression expression)
+ => new SqlServerQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext, _sqlServerSingletonOptions)
+ .Visit(expression);
+}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs
new file mode 100644
index 00000000000..ca3f6d16d68
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPreprocessorFactory.cs
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
+
+namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class SqlServerQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
+{
+ private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqlServerQueryTranslationPreprocessorFactory(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ ISqlServerSingletonOptions sqlServerSingletonOptions)
+ {
+ Dependencies = dependencies;
+ RelationalDependencies = relationalDependencies;
+ _sqlServerSingletonOptions = sqlServerSingletonOptions;
+ }
+
+ ///
+ /// Dependencies for this service.
+ ///
+ protected virtual QueryTranslationPreprocessorDependencies Dependencies { get; }
+
+ ///
+ /// Relational provider-specific dependencies for this service.
+ ///
+ protected virtual RelationalQueryTranslationPreprocessorDependencies RelationalDependencies { get; }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
+ => new SqlServerQueryTranslationPreprocessor(
+ Dependencies, RelationalDependencies, _sqlServerSingletonOptions, queryCompilationContext);
+}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
index ea225751457..604c95e619a 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs
@@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
@@ -15,6 +16,10 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
///
public class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor
{
+ private readonly QueryCompilationContext _queryCompilationContext;
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -27,6 +32,9 @@ public SqlServerQueryableMethodTranslatingExpressionVisitor(
QueryCompilationContext queryCompilationContext)
: base(dependencies, relationalDependencies, queryCompilationContext)
{
+ _queryCompilationContext = queryCompilationContext;
+ _typeMappingSource = relationalDependencies.TypeMappingSource;
+ _sqlExpressionFactory = relationalDependencies.SqlExpressionFactory;
}
///
@@ -39,6 +47,9 @@ protected SqlServerQueryableMethodTranslatingExpressionVisitor(
SqlServerQueryableMethodTranslatingExpressionVisitor parentVisitor)
: base(parentVisitor)
{
+ _queryCompilationContext = parentVisitor._queryCompilationContext;
+ _typeMappingSource = parentVisitor._typeMappingSource;
+ _sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
}
///
@@ -100,6 +111,86 @@ protected override Expression VisitExtension(Expression extensionExpression)
return base.VisitExtension(extensionExpression);
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override ShapedQueryExpression TranslateCollection(
+ SqlExpression sqlExpression,
+ RelationalTypeMapping? elementTypeMapping,
+ string tableAlias)
+ {
+ var elementClrType = sqlExpression.Type.GetSequenceType();
+
+ // Generate the OpenJson function expression, and wrap it in a SelectExpression.
+ // Note that we want to preserve the ordering of the element's, i.e. for the rows coming out of OpenJson to be the same as the
+ // element order in the original JSON array.
+ // Unfortunately, OpenJson with an explicit schema (with the WITH clause) doesn't support this; so we use the variant with the
+ // default schema, which returns a 'key' column containing the index, and order by that. This also means we need to explicitly
+ // apply a conversion from the values coming out of OpenJson (always NVARCHAR(MAX)) to the required relational store type.
+ var openJsonExpression = new TableValuedFunctionExpression(tableAlias, "OpenJson", new[] { sqlExpression });
+
+ // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression
+ var selectExpression = new SelectExpression(openJsonExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping, isColumnNullable: null);
+
+ if (elementTypeMapping is { StoreType: not "nvarchar(max)" })
+ {
+ // For columns (where we know the type mapping), we need to overwrite the projection in order to insert a CAST() to the actual
+ // relational store type we expect out of the JSON array (e.g. OpenJson returns strings, we want datetime2).
+ // For parameters (where we don't yet know the type mapping), we'll need to do that later, after the type mapping has been
+ // inferred.
+ // TODO: Need to pass through the type mapping API for converting the JSON value (nvarchar) to the relational store type (e.g.
+ // datetime2), see #30677
+ selectExpression.ReplaceProjection(
+ new Dictionary
+ {
+ {
+ new ProjectionMember(), _sqlExpressionFactory.Convert(
+ selectExpression.CreateColumnExpression(
+ openJsonExpression,
+ "value",
+ typeof(string),
+ _typeMappingSource.FindMapping("nvarchar(max)"),
+ // TODO: When we have metadata to determine if the element is nullable, pass that here to
+ // SelectExpression
+ columnNullable: null),
+ elementClrType,
+ elementTypeMapping)
+ }
+ });
+ }
+
+ // Append an ordering for the OpenJson 'key' column, converting it from nvarchar to int.
+ selectExpression.AppendOrdering(
+ new OrderingExpression(
+ _sqlExpressionFactory.Convert(
+ selectExpression.CreateColumnExpression(
+ openJsonExpression,
+ "key",
+ typeof(string),
+ typeMapping: _typeMappingSource.FindMapping("nvarchar(8000)"),
+ columnNullable: false),
+ typeof(int),
+ _typeMappingSource.FindMapping(typeof(int))),
+ ascending: true));
+
+ Expression shaperExpression = new ProjectionBindingExpression(
+ selectExpression, new ProjectionMember(), elementClrType.MakeNullable());
+
+ if (elementClrType != shaperExpression.Type)
+ {
+ Check.DebugAssert(
+ elementClrType.MakeNullable() == shaperExpression.Type,
+ "expression.Type must be nullable of targetType");
+
+ shaperExpression = Expression.Convert(shaperExpression, elementClrType);
+ }
+
+ return new ShapedQueryExpression(selectExpression, shaperExpression);
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -206,4 +297,148 @@ public TemporalAnnotationApplyingExpressionVisitor(Func
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression ApplyInferredTypeMappings(
+ Expression expression,
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ => new SqlServerInferredTypeMappingApplier(_typeMappingSource, _sqlExpressionFactory, inferredTypeMappings).Visit(expression);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected class SqlServerInferredTypeMappingApplier : RelationalInferredTypeMappingApplier
+ {
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+ private Dictionary? _currentSelectInferredTypeMappings;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqlServerInferredTypeMappingApplier(
+ IRelationalTypeMappingSource typeMappingSource,
+ ISqlExpressionFactory sqlExpressionFactory,
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ : base(inferredTypeMappings)
+ => (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, sqlExpressionFactory);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitExtension(Expression expression)
+ {
+ switch (expression)
+ {
+ case TableValuedFunctionExpression { Name: "OpenJson", Schema: null, IsBuiltIn: true } openJsonExpression
+ when InferredTypeMappings.TryGetValue((openJsonExpression, "value"), out var typeMapping):
+ return ApplyTypeMappingsOnOpenJsonExpression(openJsonExpression, new[] { typeMapping });
+
+ // Above, we applied the type mapping the the parameter that OpenJson accepts as an argument.
+ // But the inferred type mapping also needs to be applied as a SQL conversion on the column projections coming out of the
+ // SelectExpression containing the OpenJson call. So we set state to know about OpenJson tables and their type mappings
+ // in the immediate SelectExpression, and continue visiting down (see ColumnExpression visitation below).
+ case SelectExpression selectExpression:
+ {
+ Dictionary? previousSelectInferredTypeMappings = null;
+
+ foreach (var table in selectExpression.Tables)
+ {
+ if (table is TableValuedFunctionExpression { Name: "OpenJson", Schema: null, IsBuiltIn: true } openJsonExpression
+ && InferredTypeMappings.TryGetValue((openJsonExpression, "value"), out var inferredTypeMapping))
+ {
+ if (previousSelectInferredTypeMappings is null)
+ {
+ previousSelectInferredTypeMappings = _currentSelectInferredTypeMappings;
+ _currentSelectInferredTypeMappings = new();
+ }
+
+ _currentSelectInferredTypeMappings![openJsonExpression] = inferredTypeMapping;
+ }
+ }
+
+ var visited = base.VisitExtension(expression);
+
+ _currentSelectInferredTypeMappings = previousSelectInferredTypeMappings;
+
+ return visited;
+ }
+
+ case ColumnExpression { Name: "value" } columnExpression
+ when _currentSelectInferredTypeMappings is not null
+ && _currentSelectInferredTypeMappings.TryGetValue(columnExpression.Table, out var inferredTypeMapping):
+ return ApplyTypeMappingOnColumn(columnExpression, inferredTypeMapping);
+
+ default:
+ return base.VisitExtension(expression);
+ }
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected virtual TableValuedFunctionExpression ApplyTypeMappingsOnOpenJsonExpression(
+ TableValuedFunctionExpression openJsonExpression,
+ IReadOnlyList typeMappings)
+ {
+ Check.DebugAssert(typeMappings.Count == 1, "typeMappings.Count == 1");
+ var elementTypeMapping = typeMappings[0];
+
+ // Constant queryables are translated to VALUES, no need for JSON.
+ // Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them.
+ if (openJsonExpression.Arguments[0] is not SqlParameterExpression parameterExpression)
+ {
+ return openJsonExpression;
+ }
+
+ // TODO: We shouldn't need to manually construct the JSON string type mapping this way; we need to be able to provide the
+ // TODO: element's store type mapping as input to _typeMappingSource.FindMapping.
+ // TODO: When this is done, revert converter equality check in QuerySqlGenerator.VisitSqlParameter back to reference equality,
+ // since we'll always have the same instance of the type mapping returned from the type mapping source. Also remove
+ // CollectionToJsonStringConverter.Equals etc.
+ // TODO: Note: NpgsqlTypeMappingSource exposes FindContainerMapping() for this purpose.
+ if (_typeMappingSource.FindMapping(typeof(string)) is not SqlServerStringTypeMapping parameterTypeMapping)
+ {
+ throw new InvalidOperationException("Type mapping for 'string' could not be found or was not a SqlServerStringTypeMapping");
+ }
+
+ parameterTypeMapping = (SqlServerStringTypeMapping)parameterTypeMapping
+ .Clone(new CollectionToJsonStringConverter(parameterExpression.Type, elementTypeMapping));
+
+ parameterTypeMapping = (SqlServerStringTypeMapping)parameterTypeMapping.CloneWithElementTypeMapping(elementTypeMapping);
+
+ var arguments = openJsonExpression.Arguments.ToArray();
+ arguments[0] = parameterExpression.ApplyTypeMapping(parameterTypeMapping);
+ return openJsonExpression.Update(arguments);
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual SqlExpression ApplyTypeMappingOnColumn(ColumnExpression columnExpression, RelationalTypeMapping typeMapping)
+ // OpenJson's value column has type nvarchar(max); apply a CAST() unless that's the inferred element type mapping
+ => typeMapping.StoreType is "nvarchar(max)"
+ ? columnExpression
+ : _sqlExpressionFactory.Convert(columnExpression, typeMapping.ClrType, typeMapping);
+ }
}
diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
index 8d0d053cd62..751da51b988 100644
--- a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
+++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs
@@ -119,6 +119,17 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
return new SqlServerStringTypeMapping(parameters, _sqlDbType);
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual RelationalTypeMapping CloneWithElementTypeMapping(RelationalTypeMapping elementTypeMapping)
+ => new SqlServerStringTypeMapping(
+ Parameters.WithCoreParameters(Parameters.CoreParameters.WithElementTypeMapping(elementTypeMapping)),
+ _sqlDbType);
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs
index 21f5884a764..2394c2e3892 100644
--- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs
+++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs
@@ -4,6 +4,7 @@
using System.Collections;
using System.Data;
using System.Text.Json;
+using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
@@ -175,6 +176,8 @@ private readonly SqlServerJsonTypeMapping _json
private readonly Dictionary _storeTypeMappings;
+ private readonly bool _supportsOpenJson;
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -183,7 +186,8 @@ private readonly SqlServerJsonTypeMapping _json
///
public SqlServerTypeMappingSource(
TypeMappingSourceDependencies dependencies,
- RelationalTypeMappingSourceDependencies relationalDependencies)
+ RelationalTypeMappingSourceDependencies relationalDependencies,
+ ISqlServerSingletonOptions sqlServerSingletonOptions)
: base(dependencies, relationalDependencies)
{
_clrTypeMappings
@@ -270,6 +274,8 @@ public SqlServerTypeMappingSource(
{ "xml", new[] { _xml } }
};
// ReSharper restore CoVariantArrayConversion
+
+ _supportsOpenJson = sqlServerSingletonOptions.CompatibilityLevel >= 130;
}
///
@@ -279,7 +285,9 @@ public SqlServerTypeMappingSource(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
- => base.FindMapping(mappingInfo) ?? FindRawMapping(mappingInfo)?.Clone(mappingInfo);
+ => base.FindMapping(mappingInfo)
+ ?? FindRawMapping(mappingInfo)?.Clone(mappingInfo)
+ ?? FindCollectionMapping(mappingInfo)?.Clone(mappingInfo);
private RelationalTypeMapping? FindRawMapping(RelationalTypeMappingInfo mappingInfo)
{
@@ -415,6 +423,87 @@ public SqlServerTypeMappingSource(
return null;
}
+ private RelationalTypeMapping? FindCollectionMapping(RelationalTypeMappingInfo mappingInfo)
+ {
+ // Support mapping to a JSON array when the following is satisfied:
+ // 1. The ClrType is an IEnumerable.
+ // 2. The store type is either not given or a string type.
+ // 3. The element CLR type has a supported type mapping which isn't itself a collection (nested collections not yet supported).
+
+ // Note that e.g. Newtonsoft.Json's JToken is enumerable over itself, exclude that scenario to avoid stack overflow.
+ if (mappingInfo.ClrType?.TryGetElementType(typeof(IEnumerable<>)) is not { } elementClrType
+ || elementClrType == mappingInfo.ClrType)
+ {
+ return null;
+ }
+
+ switch (mappingInfo.StoreTypeNameBase)
+ {
+ case "char varying":
+ case "char":
+ case "character varying":
+ case "character":
+ case "national char varying":
+ case "national character varying":
+ case "national character":
+ case "varchar":
+ case null:
+ break;
+ default:
+ return null;
+ }
+
+ // TODO: need to allow the user to set the element store type
+
+ // Make sure the element type is mapped and isn't itself a collection (nested collections not supported)
+ if (FindMapping(elementClrType) is not { ElementTypeMapping: null } elementTypeMapping)
+ {
+ return null;
+ }
+
+ // Specifically exclude collections over Geometry, since there's a dedicated GeometryCollection type for that (see #30630)
+ if (elementClrType.Namespace == "NetTopologySuite.Geometries")
+ {
+ return null;
+ }
+
+ // TODO: This can be moved into a SQL Server implementation of ValueConverterSelector.. But it seems better for this method's logic
+ // to be in the type mapping source.
+ var stringMappingInfo = new RelationalTypeMappingInfo(
+ typeof(string),
+ mappingInfo.StoreTypeName,
+ mappingInfo.StoreTypeNameBase,
+ mappingInfo.IsKeyOrIndex,
+ mappingInfo.IsUnicode,
+ mappingInfo.Size,
+ mappingInfo.IsRowVersion,
+ mappingInfo.IsFixedLength,
+ mappingInfo.Precision,
+ mappingInfo.Scale);
+
+ if (FindMapping(stringMappingInfo) is not SqlServerStringTypeMapping stringTypeMapping)
+ {
+ return null;
+ }
+
+ stringTypeMapping = (SqlServerStringTypeMapping)stringTypeMapping
+ .Clone(new CollectionToJsonStringConverter(mappingInfo.ClrType, elementTypeMapping));
+
+ // OpenJson was introduced in SQL Server 2016 (compatibility level 130). If the user configures an older compatibility level,
+ // we allow mapping the column, but don't set the element type mapping on the mapping, so that it isn't queryable.
+ // This causes us to go into the old translation path for Contains over parameter via IN with constants.
+ if (_supportsOpenJson)
+ {
+ // The JSON representation for new[] { 1, 2 } is AQI= (base64?), this cannot simply be cast to varbinary(max) (0x0102)
+ if (elementTypeMapping is not SqlServerByteArrayTypeMapping)
+ {
+ stringTypeMapping = (SqlServerStringTypeMapping)stringTypeMapping.CloneWithElementTypeMapping(elementTypeMapping);
+ }
+ }
+
+ return stringTypeMapping;
+ }
+
private static readonly List NameBasesUsingPrecision = new()
{
"decimal",
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
index ed862f528a2..f88ec4741b3 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
@@ -110,6 +110,7 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio
.TryAdd()
.TryAdd()
.TryAdd()
+ .TryAdd()
.TryAdd()
.TryAdd()
.TryAdd()
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryRootProcessor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryRootProcessor.cs
new file mode 100644
index 00000000000..15abafb0867
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryRootProcessor.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Data.Sqlite;
+
+namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class SqliteQueryRootProcessor : RelationalQueryRootProcessor
+{
+ private readonly bool _areJsonFunctionsSupported;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqliteQueryRootProcessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
+ => _areJsonFunctionsSupported = new Version(new SqliteConnection().ServerVersion) >= new Version(3, 38);
+
+
+ ///
+ /// Indicates that a can be converted to a , if the
+ /// configured SQL Server version supports JSON functions (json_each).
+ ///
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override bool ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression)
+ => _areJsonFunctionsSupported;
+}
diff --git a/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessor.cs
similarity index 57%
rename from src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs
rename to src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessor.cs
index 7a433e8c5c5..214787bd6f2 100644
--- a/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessor.cs
@@ -1,7 +1,7 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.EntityFrameworkCore.Query.Internal;
+namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -9,19 +9,20 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal;
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
-public class TableValuedFunctionToQueryRootConvertingExpressionVisitor : ExpressionVisitor
+public class SqliteQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
{
- private readonly IModel _model;
-
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public TableValuedFunctionToQueryRootConvertingExpressionVisitor(IModel model)
+ public SqliteQueryTranslationPreprocessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
+ QueryCompilationContext queryCompilationContext)
+ : base(dependencies, relationalDependencies, queryCompilationContext)
{
- _model = model;
}
///
@@ -30,18 +31,7 @@ public TableValuedFunctionToQueryRootConvertingExpressionVisitor(IModel model)
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
- {
- var function = _model.FindDbFunction(methodCallExpression.Method);
-
- return function?.IsScalar == false
- ? CreateTableValuedFunctionQueryRootExpression(function.StoreFunction, methodCallExpression.Arguments)
- : base.VisitMethodCall(methodCallExpression);
- }
-
- private static Expression CreateTableValuedFunctionQueryRootExpression(
- IStoreFunction function,
- IReadOnlyCollection arguments)
- // See issue #19970
- => new TableValuedFunctionQueryRootExpression(function.EntityTypeMappings.Single().EntityType, function, arguments);
+ protected override Expression ProcessQueryRoots(Expression expression)
+ => new SqliteQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext)
+ .Visit(expression);
}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessorFactory.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessorFactory.cs
new file mode 100644
index 00000000000..e7a8c021558
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryTranslationPreprocessorFactory.cs
@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class SqliteQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
+{
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqliteQueryTranslationPreprocessorFactory(
+ QueryTranslationPreprocessorDependencies dependencies,
+ RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
+ {
+ Dependencies = dependencies;
+ RelationalDependencies = relationalDependencies;
+ }
+
+ ///
+ /// Dependencies for this service.
+ ///
+ protected virtual QueryTranslationPreprocessorDependencies Dependencies { get; }
+
+ ///
+ /// Relational provider-specific dependencies for this service.
+ ///
+ protected virtual RelationalQueryTranslationPreprocessorDependencies RelationalDependencies { get; }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
+ => new SqliteQueryTranslationPreprocessor(Dependencies, RelationalDependencies, queryCompilationContext);
+}
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
index 140244153c2..d0728006d3e 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs
@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Sqlite.Internal;
+using Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;
namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
@@ -14,6 +16,9 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
///
public class SqliteQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor
{
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -26,6 +31,8 @@ public SqliteQueryableMethodTranslatingExpressionVisitor(
QueryCompilationContext queryCompilationContext)
: base(dependencies, relationalDependencies, queryCompilationContext)
{
+ _typeMappingSource = relationalDependencies.TypeMappingSource;
+ _sqlExpressionFactory = relationalDependencies.SqlExpressionFactory;
}
///
@@ -38,6 +45,8 @@ protected SqliteQueryableMethodTranslatingExpressionVisitor(
SqliteQueryableMethodTranslatingExpressionVisitor parentVisitor)
: base(parentVisitor)
{
+ _typeMappingSource = parentVisitor._typeMappingSource;
+ _sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
}
///
@@ -111,8 +120,238 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
return translation;
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override ShapedQueryExpression? TranslateCount(ShapedQueryExpression source, LambdaExpression? predicate)
+ {
+ // Simplify x.Array.Count() => json_array_length(x.Array) instead of SELECT COUNT(*) FROM json_each(x.Array)
+ if (predicate is null && source.QueryExpression is SelectExpression
+ {
+ Tables: [TableValuedFunctionExpression { Name: "json_each", Schema: null, IsBuiltIn: true, Arguments: [var array] }],
+ GroupBy: [],
+ Having: null,
+ IsDistinct: false,
+ Limit: null,
+ Offset: null
+ })
+ {
+ var translation = _sqlExpressionFactory.Function(
+ "json_array_length",
+ new[] { array },
+ nullable: true,
+ argumentsPropagateNullability: new[] { true },
+ typeof(int));
+
+ return source.UpdateQueryExpression(_sqlExpressionFactory.Select(translation));
+ }
+
+ return base.TranslateCount(source, predicate);
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override ShapedQueryExpression TranslateCollection(
+ SqlExpression sqlExpression,
+ RelationalTypeMapping? elementTypeMapping,
+ string tableAlias)
+ {
+ var elementClrType = sqlExpression.Type.GetSequenceType();
+
+ var jsonEachExpression = new TableValuedFunctionExpression(tableAlias, "json_each", new[] { sqlExpression });
+
+ // TODO: When we have metadata to determine if the element is nullable, pass that here to SelectExpression
+ var selectExpression = new SelectExpression(
+ jsonEachExpression, columnName: "value", columnType: elementClrType, columnTypeMapping: elementTypeMapping,
+ isColumnNullable: null);
+
+ // TODO: SQLite does have REAL and BLOB types, which JSON does not. Need to possibly cast to that.
+ if (elementTypeMapping is not null)
+ {
+ // TODO: In any case, we still ned to pass through the type mapping API for doing any conversions (e.g. for datetime, from JSON
+ // ISO8601 to SQLite's format without the T), see #30677. Do this here.
+ }
+
+ // Append an ordering for the json_each 'key' column.
+ selectExpression.AppendOrdering(
+ new OrderingExpression(
+ selectExpression.CreateColumnExpression(
+ jsonEachExpression,
+ "key",
+ typeof(int),
+ typeMapping: _typeMappingSource.FindMapping(typeof(int)),
+ columnNullable: false),
+ ascending: true));
+
+ Expression shaperExpression = new ProjectionBindingExpression(
+ selectExpression, new ProjectionMember(), elementClrType.MakeNullable());
+
+ if (elementClrType != shaperExpression.Type)
+ {
+ Check.DebugAssert(
+ elementClrType.MakeNullable() == shaperExpression.Type,
+ "expression.Type must be nullable of targetType");
+
+ shaperExpression = Expression.Convert(shaperExpression, elementClrType);
+ }
+
+ return new ShapedQueryExpression(selectExpression, shaperExpression);
+ }
+
private static Type GetProviderType(SqlExpression expression)
=> expression.TypeMapping?.Converter?.ProviderClrType
?? expression.TypeMapping?.ClrType
?? expression.Type;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression ApplyInferredTypeMappings(
+ Expression expression,
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ => new SqliteInferredTypeMappingApplier(_typeMappingSource, _sqlExpressionFactory, inferredTypeMappings).Visit(expression);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected class SqliteInferredTypeMappingApplier : RelationalInferredTypeMappingApplier
+ {
+ private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+ private Dictionary? _currentSelectInferredTypeMappings;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqliteInferredTypeMappingApplier(
+ IRelationalTypeMappingSource typeMappingSource,
+ ISqlExpressionFactory sqlExpressionFactory,
+ IReadOnlyDictionary<(TableExpressionBase, string), RelationalTypeMapping> inferredTypeMappings)
+ : base(inferredTypeMappings)
+ => (_typeMappingSource, _sqlExpressionFactory) = (typeMappingSource, sqlExpressionFactory);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitExtension(Expression expression)
+ {
+ switch (expression)
+ {
+ case TableValuedFunctionExpression { Name: "json_each", Schema: null, IsBuiltIn: true } jsonEachExpression
+ when InferredTypeMappings.TryGetValue((jsonEachExpression, "value"), out var typeMapping):
+ return ApplyTypeMappingsOnJsonEachExpression(jsonEachExpression, typeMapping);
+
+ // Above, we applied the type mapping the the parameter that json_each accepts as an argument.
+ // But the inferred type mapping also needs to be applied as a SQL conversion on the column projections coming out of the
+ // SelectExpression containing the json_each call. So we set state to know about json_each tables and their type mappings
+ // in the immediate SelectExpression, and continue visiting down (see ColumnExpression visitation below).
+ case SelectExpression selectExpression:
+ {
+ Dictionary? previousSelectInferredTypeMappings = null;
+
+ foreach (var table in selectExpression.Tables)
+ {
+ if (table is TableValuedFunctionExpression { Name: "json_each", Schema: null, IsBuiltIn: true } jsonEachExpression
+ && InferredTypeMappings.TryGetValue((jsonEachExpression, "value"), out var inferredTypeMapping))
+ {
+ if (previousSelectInferredTypeMappings is null)
+ {
+ previousSelectInferredTypeMappings = _currentSelectInferredTypeMappings;
+ _currentSelectInferredTypeMappings = new();
+ }
+
+ _currentSelectInferredTypeMappings![jsonEachExpression] = inferredTypeMapping;
+ }
+ }
+
+ var visited = base.VisitExtension(expression);
+
+ _currentSelectInferredTypeMappings = previousSelectInferredTypeMappings;
+
+ return visited;
+ }
+
+ case ColumnExpression { Name: "value" } columnExpression
+ when _currentSelectInferredTypeMappings is not null
+ && _currentSelectInferredTypeMappings.TryGetValue(columnExpression.Table, out var inferredTypeMapping):
+ return ApplyTypeMappingOnColumn(columnExpression, inferredTypeMapping);
+
+ default:
+ return base.VisitExtension(expression);
+ }
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected virtual TableValuedFunctionExpression ApplyTypeMappingsOnJsonEachExpression(
+ TableValuedFunctionExpression jsonEachExpression,
+ RelationalTypeMapping inferredTypeMapping)
+ {
+ // Constant queryables are translated to VALUES, no need for JSON.
+ // Column queryables have their type mapping from the model, so we don't ever need to apply an inferred mapping on them.
+ if (jsonEachExpression.Arguments[0] is not SqlParameterExpression parameterExpression)
+ {
+ return jsonEachExpression;
+ }
+
+ // TODO: We shouldn't need to manually construct the JSON string type mapping this way; we need to be able to provide the
+ // TODO: element's store type mapping as input to _typeMappingSource.FindMapping.
+ if (_typeMappingSource.FindMapping(typeof(string)) is not SqliteStringTypeMapping parameterTypeMapping)
+ {
+ throw new InvalidOperationException("Type mapping for 'string' could not be found or was not a SqliteStringTypeMapping");
+ }
+
+ parameterTypeMapping = (SqliteStringTypeMapping)parameterTypeMapping
+ .Clone(new CollectionToJsonStringConverter(parameterExpression.Type, inferredTypeMapping));
+
+ parameterTypeMapping = (SqliteStringTypeMapping)parameterTypeMapping.CloneWithElementTypeMapping(inferredTypeMapping);
+
+ return jsonEachExpression.Update(new[] { parameterExpression.ApplyTypeMapping(parameterTypeMapping) });
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual SqlExpression ApplyTypeMappingOnColumn(ColumnExpression columnExpression, RelationalTypeMapping typeMapping)
+ => typeMapping switch
+ {
+ // TODO: These server-side conversions need to be managed on the type mapping
+
+ // The "standard" JSON timestamp representation is ISO8601, with a T between date and time; but SQLite's representation has
+ // no T. Apply a conversion on the value coming out of json_each.
+ SqliteDateTimeTypeMapping => _sqlExpressionFactory.Function(
+ "datetime", new[] { columnExpression }, nullable: true, new[] { true }, typeof(DateTime), typeMapping),
+
+ SqliteGuidTypeMapping => _sqlExpressionFactory.Function(
+ "upper", new[] { columnExpression }, nullable: true, new[] { true }, typeof(Guid), typeMapping),
+
+ _ => columnExpression
+ };
+ }
}
diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs
index 434c6766ad0..590417f511f 100644
--- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs
+++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs
@@ -47,6 +47,16 @@ protected SqliteStringTypeMapping(RelationalTypeMappingParameters parameters)
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new SqliteStringTypeMapping(parameters);
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual RelationalTypeMapping CloneWithElementTypeMapping(RelationalTypeMapping elementTypeMapping)
+ => new SqliteStringTypeMapping(
+ Parameters.WithCoreParameters(Parameters.CoreParameters.WithElementTypeMapping(elementTypeMapping)));
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs
index 08ec4f9e887..08b5cdc4b89 100644
--- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs
+++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
+using Microsoft.Data.Sqlite;
namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal;
@@ -94,6 +95,8 @@ private static readonly HashSet SpatialiteTypes
{ TextTypeName, Text }
};
+ private readonly bool _areJsonFunctionsSupported;
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -105,6 +108,9 @@ public SqliteTypeMappingSource(
RelationalTypeMappingSourceDependencies relationalDependencies)
: base(dependencies, relationalDependencies)
{
+ // Support for JSON functions was added in Sqlite 3.38.0 (2022-02-22, see https://www.sqlite.org/json1.html).
+ // This determines whether we have json_each, which is needed to query into JSON columns.
+ _areJsonFunctionsSupported = new Version(new SqliteConnection().ServerVersion) >= new Version(3, 38);
}
///
@@ -124,7 +130,9 @@ public static bool IsSpatialiteType(string columnType)
///
protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
{
- var mapping = base.FindMapping(mappingInfo) ?? FindRawMapping(mappingInfo);
+ var mapping = base.FindMapping(mappingInfo)
+ ?? FindRawMapping(mappingInfo)
+ ?? FindCollectionMapping(mappingInfo);
return mapping != null
&& mappingInfo.StoreTypeName != null
@@ -169,6 +177,69 @@ public static bool IsSpatialiteType(string columnType)
return null;
}
+ private RelationalTypeMapping? FindCollectionMapping(RelationalTypeMappingInfo mappingInfo)
+ {
+ // Make sure the element type is mapped and isn't itself a collection (nested collections not supported)
+ if (mappingInfo is { StoreTypeName: TextTypeName or null }
+ && mappingInfo.ClrType?.TryGetElementType(typeof(IEnumerable<>)) is { } elementClrType
+ && FindMapping(elementClrType) is { ElementTypeMapping: null } elementTypeMapping)
+ {
+ var stringMappingInfo = new RelationalTypeMappingInfo(
+ typeof(string),
+ mappingInfo.StoreTypeName,
+ mappingInfo.StoreTypeNameBase,
+ mappingInfo.IsKeyOrIndex,
+ mappingInfo.IsUnicode,
+ mappingInfo.Size,
+ mappingInfo.IsRowVersion,
+ mappingInfo.IsFixedLength,
+ mappingInfo.Precision,
+ mappingInfo.Scale);
+
+ if (FindMapping(stringMappingInfo) is not SqliteStringTypeMapping stringTypeMapping)
+ {
+ return null;
+ }
+
+ // Specifically exclude collections over Geometry, since there's a dedicated GeometryCollection type for that (see #30630)
+ if (elementClrType.Namespace == "NetTopologySuite.Geometries")
+ {
+ return null;
+ }
+
+ stringTypeMapping = (SqliteStringTypeMapping)stringTypeMapping
+ .Clone(new CollectionToJsonStringConverter(mappingInfo.ClrType, elementTypeMapping));
+
+ // json_each was introduced in SQLite 3.38.0; on older SQLite version we allow mapping the column, but don't set the element
+ // type mapping on the mapping, so that it isn't queryable. This causes us to go into the old translation path for Contains
+ // over parameter via IN with constants.
+ if (_areJsonFunctionsSupported)
+ {
+ switch (elementTypeMapping)
+ {
+ // The JSON representation for DateTimeOffset is ISO8601 (2023-01-01T12:30:00+02:00), but our SQL literal representation
+ // is 2023-01-01 12:30:00+02:00 (no T).
+ // datetime('2023-01-01T12:30:00+02:00') yields '2023-01-01 10:30:00' - converted to UTC, no timezone.
+ case SqliteDateTimeOffsetTypeMapping:
+ // The JSON representation for decimal is e.g. 1 (JSON int), whereas our literal representation is "1.0" (string)
+ case SqliteDecimalTypeMapping:
+ // The JSON representation for new[] { 1, 2 } is AQI= (base64?), our SQL literal representation is X'0102'
+ case ByteArrayTypeMapping:
+ break;
+
+
+ default:
+ stringTypeMapping = (SqliteStringTypeMapping)stringTypeMapping.CloneWithElementTypeMapping(elementTypeMapping);
+ break;
+ }
+ }
+
+ return stringTypeMapping;
+ }
+
+ return null;
+ }
+
private readonly Func[] _typeRules =
{
name => Contains(name, "INT")
diff --git a/src/EFCore/Query/InlineQueryRootExpression.cs b/src/EFCore/Query/InlineQueryRootExpression.cs
new file mode 100644
index 00000000000..3106f618c8a
--- /dev/null
+++ b/src/EFCore/Query/InlineQueryRootExpression.cs
@@ -0,0 +1,93 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+///
+///
+/// An expression that represents an inline query root within the query (e.g. new[] { 1, 2, 3 }).
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally not used in application code.
+///
+///
+public class InlineQueryRootExpression : QueryRootExpression
+{
+ ///
+ /// The values contained in this query root.
+ ///
+ public virtual IReadOnlyList Values { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The query provider associated with this query root.
+ /// The values contained in this query root.
+ /// The element type this query root represents.
+ public InlineQueryRootExpression(IAsyncQueryProvider asyncQueryProvider, IReadOnlyList values, Type elementType)
+ : base(asyncQueryProvider, elementType)
+ {
+ Values = values;
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// An expression containing the values that this query root represents.
+ /// The element type this query root represents.
+ public InlineQueryRootExpression(IReadOnlyList values, Type elementType)
+ : base(elementType)
+ {
+ Values = values;
+ }
+
+ ///
+ public override Expression DetachQueryProvider()
+ => new InlineQueryRootExpression(Values, ElementType);
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ Expression[]? newValues = null;
+
+ for (var i = 0; i < Values.Count; i++)
+ {
+ var value = Values[i];
+ var newValue = visitor.Visit(value);
+
+ if (newValue != value && newValues is null)
+ {
+ newValues = new Expression[Values.Count];
+ for (var j = 0; j < i; j++)
+ {
+ newValues[j] = Values[j];
+ }
+ }
+
+ if (newValues is not null)
+ {
+ newValues[i] = newValue;
+ }
+ }
+
+ return newValues is null ? this : new InlineQueryRootExpression(newValues, Type);
+ }
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.Append("[");
+
+ for (var i = 0; i < Values.Count; i++)
+ {
+ if (i > 0)
+ {
+ expressionPrinter.Append(",");
+ }
+
+ expressionPrinter.Visit(Values[i]);
+ }
+
+ expressionPrinter.Append("]");
+ }
+}
diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs
index a09f19db5d3..82c42039939 100644
--- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs
+++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs
@@ -104,22 +104,34 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}
}
- var navigation = memberIdentity.MemberInfo != null
+ var navigation = memberIdentity.MemberInfo is not null
? entityType.FindNavigation(memberIdentity.MemberInfo)
- : entityType.FindNavigation(memberIdentity.Name!);
- if (navigation != null)
+ : memberIdentity.Name is not null
+ ? entityType.FindNavigation(memberIdentity.Name)
+ : null;
+ if (navigation is not null)
{
- return ExpandNavigation(root, entityReference, navigation, convertedType != null);
+ return ExpandNavigation(root, entityReference, navigation, convertedType is not null);
}
- var skipNavigation = memberIdentity.MemberInfo != null
+ var skipNavigation = memberIdentity.MemberInfo is not null
? entityType.FindSkipNavigation(memberIdentity.MemberInfo)
: memberIdentity.Name is not null
? entityType.FindSkipNavigation(memberIdentity.Name)
: null;
- if (skipNavigation != null)
+ if (skipNavigation is not null)
+ {
+ return ExpandSkipNavigation(root, entityReference, skipNavigation, convertedType is not null);
+ }
+
+ var property = memberIdentity.MemberInfo != null
+ ? entityType.FindProperty(memberIdentity.MemberInfo)
+ : memberIdentity.Name is not null
+ ? entityType.FindProperty(memberIdentity.Name)
+ : null;
+ if (property?.GetTypeMapping().ElementTypeMapping != null)
{
- return ExpandSkipNavigation(root, entityReference, skipNavigation, convertedType != null);
+ return new PrimitiveCollectionReference(root, property);
}
}
@@ -1015,6 +1027,9 @@ private sealed class ReducingExpressionVisitor : ExpressionVisitor
case OwnedNavigationReference ownedNavigationReference:
return Visit(ownedNavigationReference.Parent).CreateEFPropertyExpression(ownedNavigationReference.Navigation);
+ case PrimitiveCollectionReference queryablePropertyReference:
+ return Visit(queryablePropertyReference.Parent).CreateEFPropertyExpression(queryablePropertyReference.Property);
+
case IncludeExpression includeExpression:
var entityExpression = Visit(includeExpression.EntityExpression);
var navigationExpression = ReplacingExpressionVisitor.Replace(
diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs
index 69d8229eb31..6e2ef8d64c6 100644
--- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs
+++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs
@@ -501,4 +501,44 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter)
}
}
}
+
+ ///
+ /// Queryable properties are not expanded (similar to .
+ ///
+ private sealed class PrimitiveCollectionReference : Expression, IPrintableExpression
+ {
+ public PrimitiveCollectionReference(Expression parent, IProperty property)
+ {
+ Parent = parent;
+ Property = property;
+ }
+
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ Parent = visitor.Visit(Parent);
+
+ return this;
+ }
+
+ public Expression Parent { get; private set; }
+ public new IProperty Property { get; }
+
+ public override Type Type
+ => Property.ClrType;
+
+ public override ExpressionType NodeType
+ => ExpressionType.Extension;
+
+ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.AppendLine(nameof(OwnedNavigationReference));
+ using (expressionPrinter.Indent())
+ {
+ expressionPrinter.Append("Parent: ");
+ expressionPrinter.Visit(Parent);
+ expressionPrinter.AppendLine();
+ expressionPrinter.Append("Property: " + Property.Name + " (QUERYABLE)");
+ }
+ }
+ }
}
diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
index f6fedc5ac46..923b9da71ce 100644
--- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
@@ -1954,17 +1954,6 @@ private NavigationExpansionExpression CreateNavigationExpansionExpression(
return new NavigationExpansionExpression(sourceExpression, currentTree, currentTree, parameterName);
}
- private NavigationExpansionExpression CreateNavigationExpansionExpression(
- Expression sourceExpression,
- OwnedNavigationReference ownedNavigationReference)
- {
- var parameterName = GetParameterName("o");
- var entityReference = ownedNavigationReference.EntityReference;
- var currentTree = new NavigationTreeExpression(entityReference);
-
- return new NavigationExpansionExpression(sourceExpression, currentTree, currentTree, parameterName);
- }
-
private Expression ExpandNavigationsForSource(NavigationExpansionExpression source, Expression expression)
{
expression = _removeRedundantNavigationComparisonExpressionVisitor.Visit(expression);
@@ -2048,14 +2037,37 @@ private Expression UnwrapCollectionMaterialization(Expression expression)
expression = materializeCollectionNavigationExpression.Subquery;
}
- return expression is OwnedNavigationReference ownedNavigationReference
- && ownedNavigationReference.Navigation.IsCollection
- ? CreateNavigationExpansionExpression(
+ switch (expression)
+ {
+ case OwnedNavigationReference { Navigation.IsCollection: true } ownedNavigationReference:
+ {
+ var currentTree = new NavigationTreeExpression(ownedNavigationReference.EntityReference);
+
+ return new NavigationExpansionExpression(
Expression.Call(
QueryableMethods.AsQueryable.MakeGenericMethod(ownedNavigationReference.Type.GetSequenceType()),
ownedNavigationReference),
- ownedNavigationReference)
- : expression;
+ currentTree,
+ currentTree,
+ GetParameterName("o"));
+ }
+
+ case PrimitiveCollectionReference primitiveCollectionReference:
+ {
+ var currentTree = new NavigationTreeExpression(Expression.Default(primitiveCollectionReference.Type.GetSequenceType()));
+
+ return new NavigationExpansionExpression(
+ Expression.Call(
+ QueryableMethods.AsQueryable.MakeGenericMethod(primitiveCollectionReference.Type.GetSequenceType()),
+ primitiveCollectionReference),
+ currentTree,
+ currentTree,
+ GetParameterName("p"));
+ }
+
+ default:
+ return expression;
+ }
}
private string GetParameterName(string prefix)
diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs
index b8ae88c3639..2f5dec852bc 100644
--- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs
@@ -252,9 +252,8 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa
}
if (methodCallExpression.Arguments.Count > 0
- && ClientSource(methodCallExpression.Arguments[0]))
+ && methodCallExpression.Arguments[0] is MemberInitExpression or NewExpression)
{
- // this is methodCall over closure variable or constant
return base.VisitMethodCall(methodCallExpression);
}
@@ -386,9 +385,8 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa
private Expression TryConvertListContainsToQueryableContains(MethodCallExpression methodCallExpression)
{
- if (ClientSource(methodCallExpression.Object))
+ if (methodCallExpression.Object is MemberInitExpression or NewExpression)
{
- // this is methodCall over closure variable or constant
return base.VisitMethodCall(methodCallExpression);
}
@@ -402,13 +400,6 @@ private Expression TryConvertListContainsToQueryableContains(MethodCallExpressio
methodCallExpression.Arguments[0]);
}
- private static bool ClientSource(Expression? expression)
- => expression is ConstantExpression
- || expression is MemberInitExpression
- || expression is NewExpression
- || expression is ParameterExpression parameter
- && parameter.Name?.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) == true;
-
private static bool CanConvertEnumerableToQueryable(Type enumerableType, Type queryableType)
{
if (enumerableType == typeof(IEnumerable)
diff --git a/src/EFCore/Query/ParameterQueryRootExpression.cs b/src/EFCore/Query/ParameterQueryRootExpression.cs
new file mode 100644
index 00000000000..81d9e44f966
--- /dev/null
+++ b/src/EFCore/Query/ParameterQueryRootExpression.cs
@@ -0,0 +1,63 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+///
+///
+/// An expression that represents a parameter query root within the query.
+///
+///
+/// This type is typically used by database providers (and other extensions). It is generally
+/// not used in application code.
+///
+///
+public class ParameterQueryRootExpression : QueryRootExpression
+{
+ ///
+ /// The parameter expression representing the values for this query root.
+ ///
+ public virtual ParameterExpression ParameterExpression { get; }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The query provider associated with this query root.
+ /// The values that this query root represents.
+ /// The parameter expression representing the values for this query root.
+ public ParameterQueryRootExpression(
+ IAsyncQueryProvider asyncQueryProvider, Type elementType, ParameterExpression parameterExpression)
+ : base(asyncQueryProvider, elementType)
+ {
+ ParameterExpression = parameterExpression;
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The values that this query root represents.
+ /// The parameter expression representing the values for this query root.
+ public ParameterQueryRootExpression(Type elementType, ParameterExpression parameterExpression)
+ : base(elementType)
+ {
+ ParameterExpression = parameterExpression;
+ }
+
+ ///
+ public override Expression DetachQueryProvider()
+ => new ParameterQueryRootExpression(ElementType, ParameterExpression);
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ {
+ var parameterExpression = (ParameterExpression)visitor.Visit(ParameterExpression);
+
+ return parameterExpression == ParameterExpression
+ ? this
+ : new ParameterQueryRootExpression(ElementType, parameterExpression);
+ }
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ => expressionPrinter.Visit(ParameterExpression);
+}
diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs
index c83a7c74910..e51a1ab42f4 100644
--- a/src/EFCore/Query/QueryCompilationContext.cs
+++ b/src/EFCore/Query/QueryCompilationContext.cs
@@ -163,7 +163,7 @@ public virtual Func CreateQueryExecutor(Expressi
query = _queryTranslationPreprocessorFactory.Create(this).Process(query);
// Convert EntityQueryable to ShapedQueryExpression
- query = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Visit(query);
+ query = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Translate(query);
query = _queryTranslationPostprocessorFactory.Create(this).Process(query);
// Inject actual entity materializer
diff --git a/src/EFCore/Query/QueryRootProcessor.cs b/src/EFCore/Query/QueryRootProcessor.cs
new file mode 100644
index 00000000000..ac98c75b4f9
--- /dev/null
+++ b/src/EFCore/Query/QueryRootProcessor.cs
@@ -0,0 +1,135 @@
+// 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.Internal;
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+///
+/// A visitor which adds additional query root nodes during preprocessing.
+///
+public class QueryRootProcessor : ExpressionVisitor
+{
+ private readonly IModel _model;
+
+ ///
+ /// Creates a new instance of the class with associated query provider.
+ ///
+ /// Parameter object containing dependencies for this class.
+ /// The query compilation context object to use.
+ public QueryRootProcessor(
+ QueryTranslationPreprocessorDependencies dependencies,
+ QueryCompilationContext queryCompilationContext)
+ {
+ _model = queryCompilationContext.Model;
+ }
+
+ ///
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
+ {
+ // We'll look for IEnumerable/IQueryable arguments to methods on Enumerable/Queryable, and convert these to constant/parameter query
+ // root nodes. These will later get translated to e.g. VALUES (constant) and OPENJSON (parameter) on SQL Server.
+ var method = methodCallExpression.Method;
+ if (method.DeclaringType != typeof(Queryable)
+ && method.DeclaringType != typeof(Enumerable)
+ && method.DeclaringType != typeof(QueryableExtensions)
+ && method.DeclaringType != typeof(EntityFrameworkQueryableExtensions))
+ {
+ return base.VisitMethodCall(methodCallExpression);
+ }
+
+ var parameters = method.GetParameters();
+
+ // Note that we don't need to look at methodCallExpression.Object, since IQueryable<> doesn't declare any methods.
+ // All methods over queryable are extensions.
+ Expression[]? newArguments = null;
+
+ for (var i = 0; i < methodCallExpression.Arguments.Count; i++)
+ {
+ 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
+ && (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);
+
+ if (visitedArgument != argument)
+ {
+ if (newArguments is null)
+ {
+ newArguments = new Expression[methodCallExpression.Arguments.Count];
+
+ for (var j = 0; j < i; j++)
+ {
+ newArguments[j] = methodCallExpression.Arguments[j];
+ }
+ }
+ }
+
+ if (newArguments is not null)
+ {
+ newArguments[i] = visitedArgument;
+ }
+ }
+
+ return newArguments is null
+ ? methodCallExpression
+ : methodCallExpression.Update(methodCallExpression.Object, newArguments);
+ }
+
+ ///
+ /// 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)
+ => false;
+
+ ///
+ /// Determines whether a should be converted to a .
+ ///
+ /// The parameter expression that's a candidate for conversion to a query root.
+ protected virtual bool ShouldConvertToParameterQueryRoot(ParameterExpression parameterExpression)
+ => false;
+}
+
diff --git a/src/EFCore/Query/QueryTranslationPreprocessor.cs b/src/EFCore/Query/QueryTranslationPreprocessor.cs
index 1f2a0cc307b..5fd863e6581 100644
--- a/src/EFCore/Query/QueryTranslationPreprocessor.cs
+++ b/src/EFCore/Query/QueryTranslationPreprocessor.cs
@@ -77,6 +77,18 @@ public virtual Expression Process(Expression query)
/// The query expression to normalize.
/// A query expression after normalization has been done.
public virtual Expression NormalizeQueryableMethod(Expression expression)
- => new QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext)
- .Normalize(expression);
+ {
+ expression = new QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext).Normalize(expression);
+ expression = ProcessQueryRoots(expression);
+
+ return expression;
+ }
+
+ ///
+ /// Adds additional query root nodes to the query.
+ ///
+ /// The query expression to process.
+ /// A query expression after query roots have been added.
+ protected virtual Expression ProcessQueryRoots(Expression expression)
+ => expression;
}
diff --git a/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs b/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs
index f308fd98fe6..53abf26c65f 100644
--- a/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs
+++ b/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs
@@ -46,13 +46,20 @@ public sealed record QueryTranslationPreprocessorDependencies
///
[EntityFrameworkInternal]
public QueryTranslationPreprocessorDependencies(
+ ITypeMappingSource typeMappingSource,
IEvaluatableExpressionFilter evaluatableExpressionFilter,
INavigationExpansionExtensibilityHelper navigationExpansionExtensibilityHelper)
{
+ TypeMappingSource = typeMappingSource;
EvaluatableExpressionFilter = evaluatableExpressionFilter;
NavigationExpansionExtensibilityHelper = navigationExpansionExtensibilityHelper;
}
+ ///
+ /// Type mapping source.
+ ///
+ public ITypeMappingSource TypeMappingSource { get; init; }
+
///
/// Evaluatable expression filter.
///
diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs
index 4b90c04448f..7d4ab4a455e 100644
--- a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs
@@ -50,6 +50,14 @@ protected QueryableMethodTranslatingExpressionVisitor(
///
public virtual string? TranslationErrorDetails { get; private set; }
+ ///
+ /// Translates an expression to an equivalent SQL representation.
+ ///
+ /// An expression to translate.
+ /// A SQL translation of the given expression.
+ public virtual Expression Translate(Expression expression)
+ => Visit(expression);
+
///
/// Adds detailed information about errors encountered during translation.
///
@@ -85,7 +93,9 @@ protected override Expression VisitExtension(Expression extensionExpression)
}
throw new InvalidOperationException(
- CoreStrings.QueryUnhandledQueryRootExpression(queryRootExpression.GetType().ShortDisplayName()));
+ TranslationErrorDetails is null
+ ? CoreStrings.QueryUnhandledQueryRootExpression(queryRootExpression.GetType().ShortDisplayName())
+ : CoreStrings.TranslationFailedWithDetails(queryRootExpression, TranslationErrorDetails));
}
return base.VisitExtension(extensionExpression);
@@ -508,7 +518,7 @@ protected virtual Expression MarkShaperNullable(Expression shaperExpression)
public virtual ShapedQueryExpression? TranslateSubquery(Expression expression)
{
var subqueryVisitor = CreateSubqueryVisitor();
- var translation = subqueryVisitor.Visit(expression) as ShapedQueryExpression;
+ var translation = subqueryVisitor.Translate(expression) as ShapedQueryExpression;
if (translation == null && subqueryVisitor.TranslationErrorDetails != null)
{
AddTranslationErrorDetails(subqueryVisitor.TranslationErrorDetails);
diff --git a/src/EFCore/Storage/CoreTypeMapping.cs b/src/EFCore/Storage/CoreTypeMapping.cs
index bc39feacff8..868cb139aea 100644
--- a/src/EFCore/Storage/CoreTypeMapping.cs
+++ b/src/EFCore/Storage/CoreTypeMapping.cs
@@ -35,13 +35,17 @@ protected readonly record struct CoreTypeMappingParameters
/// Supports custom comparisons between keys--e.g. PK to FK comparison.
/// Supports custom comparisons between converted provider values.
/// An optional factory for creating a specific .
+ ///
+ /// If this type mapping represents a primitive collection, this holds the element's type mapping.
+ ///
public CoreTypeMappingParameters(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type clrType,
ValueConverter? converter = null,
ValueComparer? comparer = null,
ValueComparer? keyComparer = null,
ValueComparer? providerValueComparer = null,
- Func? valueGeneratorFactory = null)
+ Func? valueGeneratorFactory = null,
+ CoreTypeMapping? elementTypeMapping = null)
{
ClrType = clrType;
Converter = converter;
@@ -49,6 +53,7 @@ public CoreTypeMappingParameters(
KeyComparer = keyComparer;
ProviderValueComparer = providerValueComparer;
ValueGeneratorFactory = valueGeneratorFactory;
+ ElementTypeMapping = elementTypeMapping;
}
///
@@ -83,6 +88,11 @@ public CoreTypeMappingParameters(
///
public Func? ValueGeneratorFactory { get; }
+ ///
+ /// If this type mapping represents a primitive collection, this holds the element's type mapping.
+ ///
+ public CoreTypeMapping? ElementTypeMapping { get; }
+
///
/// Creates a new parameter object with the given
/// converter composed with any existing converter and set on the new parameter object.
@@ -96,7 +106,24 @@ public CoreTypeMappingParameters WithComposedConverter(ValueConverter? converter
Comparer,
KeyComparer,
ProviderValueComparer,
- ValueGeneratorFactory);
+ ValueGeneratorFactory,
+ ElementTypeMapping);
+
+ ///
+ /// Creates a new parameter object with the given
+ /// element type mapping.
+ ///
+ /// The element type mapping.
+ /// The new parameter object.
+ public CoreTypeMappingParameters WithElementTypeMapping(CoreTypeMapping elementTypeMapping)
+ => new(
+ ClrType,
+ Converter,
+ Comparer,
+ KeyComparer,
+ ProviderValueComparer,
+ ValueGeneratorFactory,
+ elementTypeMapping);
}
private ValueComparer? _comparer;
@@ -224,4 +251,10 @@ public virtual ValueComparer ProviderValueComparer
/// An expression tree that can be used to generate code for the literal value.
public virtual Expression GenerateCodeLiteral(object value)
=> throw new NotSupportedException(CoreStrings.LiteralGenerationNotSupported(ClrType.ShortDisplayName()));
+
+ ///
+ /// If this type mapping represents a primitive collection, this holds the element's type mapping.
+ ///
+ public virtual CoreTypeMapping? ElementTypeMapping
+ => Parameters.ElementTypeMapping;
}
diff --git a/src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.cs b/src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.cs
new file mode 100644
index 00000000000..90d85bb48cb
--- /dev/null
+++ b/src/EFCore/Storage/ValueConversion/CollectionToJsonStringConverter.cs
@@ -0,0 +1,64 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+
+namespace Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+///
+/// A value converter that converts a .NET primitive collection into a JSON string.
+///
+// TODO: This currently just calls JsonSerialize.Serialize/Deserialize. It should go through the element type mapping's APIs for
+// serializing/deserializing JSON instead, when those APIs are introduced.
+// TODO: Nulls? Mapping hints? Customizable JsonSerializerOptions?
+public class CollectionToJsonStringConverter : ValueConverter
+{
+ private readonly CoreTypeMapping _elementTypeMapping;
+
+ ///
+ /// Creates a new instance of this converter.
+ ///
+ ///
+ /// See EF Core value converters for more information and examples.
+ ///
+ public CollectionToJsonStringConverter(Type modelClrType, CoreTypeMapping elementTypeMapping)
+ : base(
+ (Expression>)(x => JsonSerializer.Serialize(x, (JsonSerializerOptions?)null)),
+ (Expression>)(s => JsonSerializer.Deserialize(s, modelClrType, (JsonSerializerOptions?)null)!)) // TODO: Nullability
+ {
+ ModelClrType = modelClrType;
+ _elementTypeMapping = elementTypeMapping;
+
+ // TODO: Value converters on the element type mapping should be supported
+ // TODO: Full sanitization/nullability
+ ConvertToProvider = x => x is null ? "[]" : JsonSerializer.Serialize(x);
+ ConvertFromProvider = o
+ => o is string s
+ ? JsonSerializer.Deserialize(s, modelClrType)!
+ : throw new ArgumentException(); // TODO
+ }
+
+ ///
+ public override Func ConvertToProvider { get; }
+
+ ///
+ public override Func ConvertFromProvider { get; }
+
+ ///
+ public override Type ModelClrType { get; }
+
+ ///
+ public override Type ProviderClrType
+ => typeof(string);
+
+ ///
+ public override bool Equals(object? obj)
+ => ReferenceEquals(this, obj) || (obj is CollectionToJsonStringConverter other && Equals(other));
+
+ private bool Equals(CollectionToJsonStringConverter other)
+ => ModelClrType == other.ModelClrType && _elementTypeMapping.Equals(other._elementTypeMapping);
+
+ ///
+ public override int GetHashCode()
+ => ModelClrType.GetHashCode();
+}
diff --git a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs
index 3cabc98b4f3..d30276b6423 100644
--- a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs
+++ b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs
@@ -23,20 +23,6 @@ public override void Properties_can_be_made_concurrency_tokens()
Assert.Throws(
() => base.Properties_can_be_made_concurrency_tokens()).Message);
- protected override void Mapping_throws_for_non_ignored_array()
- {
- var modelBuilder = CreateModelBuilder();
-
- modelBuilder.Entity();
-
- var model = modelBuilder.FinalizeModel();
- var entityType = model.FindEntityType(typeof(OneDee));
-
- var property = entityType.FindProperty(nameof(OneDee.One));
- Assert.Null(property.GetProviderClrType());
- Assert.NotNull(property.FindTypeMapping());
- }
-
public override void Properties_can_have_provider_type_set_for_type()
{
var modelBuilder = CreateModelBuilder(c => c.Properties().HaveConversion());
diff --git a/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs b/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs
index f0dc6497a5b..5708ab1eefc 100644
--- a/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs
@@ -8,6 +8,8 @@ public class InMemoryComplianceTest : ComplianceTestBase
protected override ICollection IgnoredTestBases { get; } = new HashSet
{
// No in-memory tests
+ typeof(PrimitiveCollectionsQueryTestBase<>),
+ typeof(NonSharedPrimitiveCollectionsQueryTestBase),
typeof(FunkyDataQueryTestBase<>),
typeof(StoreGeneratedTestBase<>),
typeof(ConferencePlannerTestBase<>),
diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs
index 8902ca5c98d..25a49c3c5cb 100644
--- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindJoinQueryInMemoryTest.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.EntityFrameworkCore.TestModels.Northwind;
+
namespace Microsoft.EntityFrameworkCore.Query;
public class NorthwindJoinQueryInMemoryTest : NorthwindJoinQueryTestBase>
@@ -30,4 +32,24 @@ public override Task SelectMany_with_client_eval_with_collection_shaper_ignored(
public override Task SelectMany_with_client_eval_with_constructor(bool async)
// Joins between sources with client eval. Issue #21200.
=> Assert.ThrowsAsync(() => base.SelectMany_with_client_eval_with_constructor(async));
+
+ public override async Task Join_local_collection_int_closure_is_cached_correctly(bool async)
+ {
+ var ids = new uint[] { 1, 2 };
+
+ await AssertTranslationFailed(
+ () => AssertQueryScalar(
+ async,
+ ss => from e in ss.Set()
+ join id in ids on e.EmployeeID equals id
+ select e.EmployeeID));
+
+ ids = new uint[] { 3 };
+ await AssertTranslationFailed(
+ () => AssertQueryScalar(
+ async,
+ ss => from e in ss.Set()
+ join id in ids on e.EmployeeID equals id
+ select e.EmployeeID));
+ }
}
diff --git a/test/EFCore.InMemory.FunctionalTests/Query/QueryFilterFuncletizationInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/QueryFilterFuncletizationInMemoryTest.cs
index 67ce682a73e..180f02494c2 100644
--- a/test/EFCore.InMemory.FunctionalTests/Query/QueryFilterFuncletizationInMemoryTest.cs
+++ b/test/EFCore.InMemory.FunctionalTests/Query/QueryFilterFuncletizationInMemoryTest.cs
@@ -13,6 +13,25 @@ public QueryFilterFuncletizationInMemoryTest(
{
}
+ public override void DbContext_list_is_parameterized()
+ {
+ using var context = CreateContext();
+ // Default value of TenantIds is null InExpression over null values throws
+ Assert.Throws(() => context.Set().ToList());
+
+ context.TenantIds = new List();
+ var query = context.Set().ToList();
+ Assert.Empty(query);
+
+ context.TenantIds = new List { 1 };
+ query = context.Set().ToList();
+ Assert.Single(query);
+
+ context.TenantIds = new List { 2, 3 };
+ query = context.Set().ToList();
+ Assert.Equal(2, query.Count);
+ }
+
public class QueryFilterFuncletizationInMemoryFixture : QueryFilterFuncletizationFixtureBase
{
protected override ITestStoreFactory TestStoreFactory
diff --git a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs
new file mode 100644
index 00000000000..19d33e77f10
--- /dev/null
+++ b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public abstract class NonSharedPrimitiveCollectionsQueryRelationalTestBase : NonSharedPrimitiveCollectionsQueryTestBase
+{
+ // On relational databases, byte[] gets mapped to a special binary data type, which isn't queryable as a regular primitive collection.
+ [ConditionalFact]
+ public override Task Array_of_byte()
+ => AssertTranslationFailed(() => TestArray((byte)1, (byte)2));
+
+ [ConditionalFact]
+ public virtual async Task Column_collection_inside_json_owned_entity()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity().OwnsOne(t => t.Owned, b => b.ToJson()),
+ seed: context =>
+ {
+ context.AddRange(
+ new TestOwner { Owned = new TestOwned { Strings = new[] { "foo", "bar" } } },
+ new TestOwner { Owned = new TestOwned { Strings = new[] { "baz" } } });
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var result = await context.Set().SingleAsync(o => o.Owned.Strings.Count() == 2);
+ Assert.Equivalent(new[] { "foo", "bar" }, result.Owned.Strings);
+
+ result = await context.Set().SingleAsync(o => o.Owned.Strings[1] == "bar");
+ Assert.Equivalent(new[] { "foo", "bar" }, result.Owned.Strings);
+ }
+
+ protected class TestOwner
+ {
+ public int Id { get; set; }
+ public TestOwned Owned { get; set; }
+ }
+
+ [Owned]
+ protected class TestOwned
+ {
+ public string[] Strings { get; set; }
+ }
+
+ protected TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
+ protected void ClearLog()
+ => TestSqlLoggerFactory.Clear();
+
+ protected void AssertSql(params string[] expected)
+ => TestSqlLoggerFactory.AssertBaseline(expected);
+}
diff --git a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs
index e63dffbd772..c779cb7ed2f 100644
--- a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs
@@ -108,29 +108,6 @@ public virtual void Throws_when_subquery_main_from_clause()
CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer)));
}
- [ConditionalFact]
- public virtual void Throws_when_select_many()
- {
- using var context = CreateContext();
-
- AssertTranslationFailed(
- () => (from c1 in context.Customers
- from i in new[] { 1, 2, 3 }
- select c1)
- .ToList());
- }
-
- [ConditionalFact]
- public virtual void Throws_when_join()
- {
- using var context = CreateContext();
- AssertTranslationFailed(
- () => (from e1 in context.Employees
- join i in new uint[] { 1, 2, 3 } on e1.EmployeeID equals i
- select e1)
- .ToList());
- }
-
[ConditionalFact]
public virtual void Does_not_throws_when_group_join()
{
diff --git a/test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs
new file mode 100644
index 00000000000..4780f98ee64
--- /dev/null
+++ b/test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs
@@ -0,0 +1,292 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public abstract class NonSharedPrimitiveCollectionsQueryTestBase : NonSharedModelTestBase
+{
+ #region Support for specific element types
+
+ [ConditionalFact]
+ public virtual Task Array_of_string()
+ => TestArray("a", "b");
+
+ [ConditionalFact]
+ public virtual Task Array_of_int()
+ => TestArray(1, 2);
+
+ [ConditionalFact]
+ public virtual Task Array_of_long()
+ => TestArray(1L, 2L);
+
+ [ConditionalFact]
+ public virtual Task Array_of_short()
+ => TestArray((short)1, (short)2);
+
+ [ConditionalFact]
+ public virtual Task Array_of_byte()
+ => TestArray((byte)1, (byte)2);
+
+ [ConditionalFact]
+ public virtual Task Array_of_double()
+ => TestArray(1d, 2d);
+
+ [ConditionalFact]
+ public virtual Task Array_of_float()
+ => TestArray(1f, 2f);
+
+ [ConditionalFact]
+ public virtual Task Array_of_decimal()
+ => TestArray(1m, 2m);
+
+ [ConditionalFact]
+ public virtual Task Array_of_DateTime()
+ => TestArray(new DateTime(2023, 1, 1, 12, 30, 0), new DateTime(2023, 1, 2, 12, 30, 0));
+
+ [ConditionalFact]
+ public virtual Task Array_of_DateOnly()
+ => TestArray(new DateOnly(2023, 1, 1), new DateOnly(2023, 1, 2));
+
+ [ConditionalFact]
+ public virtual Task Array_of_TimeOnly()
+ => TestArray(new TimeOnly(12, 30, 0), new TimeOnly(12, 30, 1));
+
+ [ConditionalFact]
+ public virtual Task Array_of_DateTimeOffset()
+ => TestArray(
+ new DateTimeOffset(2023, 1, 1, 12, 30, 0, TimeSpan.FromHours(2)),
+ new DateTimeOffset(2023, 1, 2, 12, 30, 0, TimeSpan.FromHours(2)));
+
+ [ConditionalFact]
+ public virtual Task Array_of_bool()
+ => TestArray(true, false);
+
+ [ConditionalFact]
+ public virtual Task Array_of_Guid()
+ => TestArray(
+ new Guid("dc8c903d-d655-4144-a0fd-358099d40ae1"),
+ new Guid("008719a5-1999-4798-9cf3-92a78ffa94a2"));
+
+ [ConditionalFact]
+ public virtual Task Array_of_byte_array()
+ => TestArray(new byte[] { 1, 2 }, new byte[] { 3, 4 });
+
+ [ConditionalFact]
+ public virtual Task Array_of_enum()
+ => TestArray(MyEnum.Label1, MyEnum.Label2);
+
+ enum MyEnum { Label1, Label2 }
+
+ // This ensures that collections of Geometry (e.g. Geometry[]) aren't mapped; NTS has GeometryCollection for that.
+ // See SQL Server/SQLite for a sample implementation.
+ [ConditionalFact] // #30630
+ public abstract Task Array_of_geometry_is_not_supported();
+
+ [ConditionalFact]
+ public virtual async Task Array_of_array_is_not_supported()
+ {
+ var exception = await Assert.ThrowsAsync(() => TestArray(new[] { 1, 2, 3 }, new[] { 4, 5, 6 }));
+ Assert.Equal(CoreStrings.PropertyNotMapped("int[][]", "TestEntity", "SomeArray"), exception.Message);
+ }
+
+ [ConditionalFact]
+ public virtual async Task Multidimensional_array_is_not_supported()
+ {
+ var exception = await Assert.ThrowsAsync(() => InitializeAsync(
+ onModelCreating: mb => mb.Entity().Property(typeof(int[,]), "MultidimensionalArray")));
+ Assert.Equal(CoreStrings.PropertyNotMapped("int[,]", "TestEntity", "MultidimensionalArray"), exception.Message);
+ }
+
+ #endregion Support for specific element types
+
+ [ConditionalFact]
+ public virtual async Task Column_with_custom_converter()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity()
+ .Property(m => m.Ints)
+ .HasConversion(
+ i => string.Join(",", i),
+ s => s.Split(",", StringSplitOptions.None).Select(int.Parse).ToArray(),
+ new ValueComparer(favorStructuralComparisons: true)),
+ seed: context =>
+ {
+ context.AddRange(
+ new TestEntity { Id = 1, Ints = new[] { 1, 2, 3 } },
+ new TestEntity { Id = 2, Ints = new[] { 1, 2, 4 } });
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var ints = new[] { 1, 2, 3 };
+ var result = await context.Set().SingleAsync(m => m.Ints == ints);
+ Assert.Equal(1, result.Id);
+
+ // Custom converters allow reading/writing, but not querying, as we have no idea about the internal representation
+ await AssertTranslationFailed(() => context.Set().SingleAsync(m => m.Ints.Length == 2));
+ }
+
+ [ConditionalFact(Skip = "Currently fails because we don't use the element mapping when serializing to JSON, but just do JsonSerializer.Serialize, #30677")]
+ public virtual async Task Parameter_with_inferred_value_converter()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity(
+ b =>
+ {
+ b.Property("PropertyWithValueConverter")
+ .HasConversion(w => w.Value, i => new IntWrapper(i));
+ }),
+ seed: context =>
+ {
+ var entry1 = context.Add(new TestEntity { Id = 1 });
+ entry1.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(8);
+ var entry2 = context.Add(new TestEntity { Id = 2 });
+ entry2.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(9);
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var ints = new IntWrapper[] { new(1), new(8) };
+ var result = await context.Set()
+ .SingleAsync(m => ints.Count(i => i == EF.Property(m, "PropertyWithValueConverter")) == 1);
+ Assert.Equal(1, result.Id);
+ }
+
+ [ConditionalFact]
+ public virtual async Task Constant_with_inferred_value_converter()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity(
+ b =>
+ {
+ b.Property("PropertyWithValueConverter")
+ .HasConversion(w => w.Value, i => new IntWrapper(i));
+ }),
+ seed: context =>
+ {
+ var entry1 = context.Add(new TestEntity { Id = 1 });
+ entry1.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(8);
+ var entry2 = context.Add(new TestEntity { Id = 2 });
+ entry2.Property("PropertyWithValueConverter").CurrentValue = new IntWrapper(9);
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var result = await context.Set()
+ .SingleAsync(
+ m => new IntWrapper[] { new(1), new(8) }.Count(i => i == EF.Property(m, "PropertyWithValueConverter")) == 1);
+ Assert.Equal(1, result.Id);
+ }
+
+ class IntWrapper
+ {
+ public IntWrapper(int value)
+ => Value = value;
+
+ public int Value { get; set; }
+ }
+
+ [ConditionalFact]
+ public virtual async Task Inline_collection_in_query_filter()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity().HasQueryFilter(t => new[] { 1, 2, 3 }.Count(i => i > t.Id) == 1),
+ seed: context =>
+ {
+ context.AddRange(
+ new TestEntity { Id = 1 },
+ new TestEntity { Id = 2 });
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var result = await context.Set().SingleAsync();
+ Assert.Equal(2, result.Id);
+ }
+
+ ///
+ /// A utility that allows easy testing of querying out arbitrary element types from a primitive collection, provided two distinct
+ /// element values.
+ ///
+ protected async Task TestArray(
+ TElement value1,
+ TElement value2,
+ Action onModelCreating = null)
+ {
+ var arrayClrType = typeof(TElement).MakeArrayType();
+
+ var contextFactory = await InitializeAsync(
+ onModelCreating: onModelCreating ?? (mb => mb.Entity().Property(arrayClrType, "SomeArray")),
+ seed: context =>
+ {
+ var instance1 = new TestEntity { Id = 1 };
+ context.Add(instance1);
+ var array1 = new TElement[2];
+ array1.SetValue(value1, 0);
+ array1.SetValue(value1, 1);
+ context.Entry(instance1).Property("SomeArray").CurrentValue = array1;
+
+ var instance2 = new TestEntity { Id = 2 };
+ context.Add(instance2);
+ var array2 = new TElement[2];
+ array2.SetValue(value1, 0);
+ array2.SetValue(value2, 1);
+ context.Entry(instance2).Property("SomeArray").CurrentValue = array2;
+
+ context.SaveChanges();
+ });
+
+ await using var context = contextFactory.CreateContext();
+
+ var entityParam = Expression.Parameter(typeof(TestEntity), "m");
+ var efPropertyCall = Expression.Call(
+ typeof(EF).GetMethod(nameof(EF.Property), BindingFlags.Public | BindingFlags.Static)!.MakeGenericMethod(arrayClrType),
+ entityParam,
+ Expression.Constant("SomeArray"));
+
+ var elementParam = Expression.Parameter(typeof(TElement), "a");
+ var predicate = Expression.Lambda>(
+ Expression.Equal(
+ Expression.Call(
+ EnumerableMethods.CountWithPredicate.MakeGenericMethod(typeof(TElement)),
+ efPropertyCall,
+ Expression.Lambda(
+ Expression.Equal(elementParam, Expression.Constant(value1)),
+ elementParam)),
+ Expression.Constant(2)),
+ entityParam);
+
+ var result = await context.Set().SingleAsync(predicate);
+ Assert.Equal(1, result.Id);
+ }
+
+ protected class TestContext : DbContext
+ {
+ public TestContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ => modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever();
+ }
+
+ protected class TestEntity
+ {
+ public int Id { get; set; }
+ public int[] Ints { get; set; }
+ }
+
+ protected override string StoreName
+ => "NonSharedPrimitiveCollectionsTest";
+
+ protected static async Task AssertTranslationFailed(Func query)
+ => Assert.Contains(
+ CoreStrings.TranslationFailed("")[48..],
+ (await Assert.ThrowsAsync(query))
+ .Message);
+}
diff --git a/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs
index a8514731ae7..9b30b0cbf21 100644
--- a/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/NorthwindCompiledQueryTestBase.cs
@@ -208,12 +208,12 @@ public virtual void Query_with_array_parameter()
using (var context = CreateContext())
{
- query(context, new[] { "ALFKI" });
+ Assert.Equal(1, query(context, new[] { "ALFKI" }).Count());
}
using (var context = CreateContext())
{
- query(context, new[] { "ANATR" });
+ Assert.Equal(1, query(context, new[] { "ANATR" }).Count());
}
}
@@ -466,12 +466,12 @@ public virtual async Task Query_with_array_parameter_async()
using (var context = CreateContext())
{
- await Enumerate(query(context, new[] { "ALFKI" }));
+ Assert.Equal(1, await CountAsync(query(context, new[] { "ALFKI" })));
}
using (var context = CreateContext())
{
- await Enumerate(query(context, new[] { "ANATR" }));
+ Assert.Equal(1, await CountAsync(query(context, new[] { "ANATR" })));
}
}
@@ -847,27 +847,18 @@ await asyncSingleResultQueryWithCancellationToken(
"CHOPS", "CONSH", default));
}
- [ConditionalFact]
- public virtual void MakeBinary_does_not_throw_for_unsupported_operator()
- {
- var query = EF.CompileQuery(
- (NorthwindContext context, object[] parameters)
- => context.Customers.Where(c => c.CustomerID == (string)parameters[0]));
-
- using var context = CreateContext();
-
- var result = query(context, new[] { "ALFKI" }).ToList();
-
- Assert.Single(result);
- }
-
- protected async Task Enumerate(IAsyncEnumerable source)
+ protected async Task CountAsync(IAsyncEnumerable source)
{
+ var count = 0;
await foreach (var _ in source)
{
+ count++;
}
+ return count;
}
protected NorthwindContext CreateContext()
=> Fixture.CreateContext();
+
+ public static IEnumerable IsAsyncData = new[] { new object[] { false }, new object[] { true } };
}
diff --git a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs
index ca678bdefb3..8fa586cf48b 100644
--- a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs
@@ -230,30 +230,25 @@ join o in ss.Set().Where(o => o.OrderID < 10250) on true equals true
public virtual async Task Join_local_collection_int_closure_is_cached_correctly(bool async)
{
var ids = new uint[] { 1, 2 };
- // Join with local collection using TVP. Issue #19016.
- await AssertTranslationFailed(
- () => AssertQueryScalar(
- async,
- ss => from e in ss.Set()
- join id in ids on e.EmployeeID equals id
- select e.EmployeeID));
+ await AssertQueryScalar(
+ async,
+ ss => from e in ss.Set()
+ join id in ids on e.EmployeeID equals id
+ select e.EmployeeID);
ids = new uint[] { 3 };
- // Join with local collection using TVP. Issue #19016.
- await AssertTranslationFailed(
- () => AssertQueryScalar(
- async,
- ss => from e in ss.Set()
- join id in ids on e.EmployeeID equals id
- select e.EmployeeID));
+ await AssertQueryScalar(
+ async,
+ ss => from e in ss.Set()
+ join id in ids on e.EmployeeID equals id
+ select e.EmployeeID);
}
- [ConditionalTheory]
+ [ConditionalTheory(Skip = "#30677")]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Join_local_string_closure_is_cached_correctly(bool async)
{
var ids = "12";
- // Join with local collection using TVP. Issue #19016.
await AssertTranslationFailed(
() => AssertQueryScalar(
async,
@@ -262,7 +257,6 @@ join id in ids on e.EmployeeID equals id
select e.EmployeeID));
ids = "3";
- // Join with local collection using TVP. Issue #19016.
await AssertTranslationFailed(
() => AssertQueryScalar(
async,
@@ -271,13 +265,12 @@ join id in ids on e.EmployeeID equals id
select e.EmployeeID));
}
- [ConditionalTheory]
+ [ConditionalTheory(Skip = "#30677")]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Join_local_bytes_closure_is_cached_correctly(bool async)
{
var ids = new byte[] { 1, 2 };
- // Join with local collection using TVP. Issue #19016.
await AssertTranslationFailed(
() => AssertQueryScalar(
async,
@@ -286,7 +279,6 @@ join id in ids on e.EmployeeID equals id
select e.EmployeeID));
ids = new byte[] { 3 };
- // Join with local collection using TVP. Issue #19016.
await AssertTranslationFailed(
() => AssertQueryScalar(
async,
diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
new file mode 100644
index 00000000000..199ae52ce07
--- /dev/null
+++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
@@ -0,0 +1,734 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public class PrimitiveCollectionsQueryTestBase : QueryTestBase
+ where TFixture : PrimitiveCollectionsQueryTestBase.PrimitiveCollectionsQueryFixtureBase, new()
+{
+ protected PrimitiveCollectionsQueryTestBase(TFixture fixture)
+ : base(fixture)
+ {
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_of_ints_Contains(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 10, 999 }.Contains(c.Int)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_of_nullable_ints_Contains(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new int?[] { 10, 999 }.Contains(c.NullableInt)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_of_nullable_ints_Contains_null(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new int?[] { null, 999 }.Contains(c.NullableInt)),
+ entryCount: 2);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Count_with_zero_values(bool async)
+ => AssertQuery(
+ async,
+ // ReSharper disable once UseArrayEmptyMethod
+ ss => ss.Set().Where(c => new int[0].Count(i => i > c.Id) == 1),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Count_with_one_value(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2 }.Count(i => i > c.Id) == 1),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Count_with_two_values(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2, 999 }.Count(i => i > c.Id) == 1),
+ entryCount: 2);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Count_with_three_values(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2, 999, 1000 }.Count(i => i > c.Id) == 2),
+ entryCount: 2);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_zero_values(bool async)
+ => AssertQuery(
+ async,
+ // ReSharper disable once UseArrayEmptyMethod
+ ss => ss.Set().Where(c => new int[0].Contains(c.Id)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_one_value(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2 }.Contains(c.Id)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_two_values(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2, 999 }.Contains(c.Id)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_three_values(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 2, 999, 1000 }.Contains(c.Id)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Contains_with_all_parameters(bool async)
+ {
+ var (i, j) = (2, 999);
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { i, j }.Contains(c.Id)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Inline_collection_Contains_with_parameter_and_column_based_expression(bool async)
+ {
+ var i = 2;
+
+ await AssertTranslationFailed(
+ () => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { i, c.Int }.Contains(c.Id)),
+ entryCount: 1));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_Count(bool async)
+ {
+ var ids = new[] { 2, 999 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ids.Count(i => i > c.Id) == 1),
+ entryCount: 2);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_ints_Contains(bool async)
+ {
+ var ints = new[] { 10, 999 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ints.Contains(c.Int)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_nullable_ints_Contains(bool async)
+ {
+ var nullableInts = new int?[] { 10, 999 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => nullableInts.Contains(c.NullableInt)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_nullable_ints_Contains_null(bool async)
+ {
+ var nullableInts = new int?[] { null, 999 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => nullableInts.Contains(c.NullableInt)),
+ entryCount: 2);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_strings_Contains(bool async)
+ {
+ var strings = new[] { "10", "999" };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => strings.Contains(c.String)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_DateTimes_Contains(bool async)
+ {
+ var dateTimes = new[]
+ {
+ new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc),
+ new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => dateTimes.Contains(c.DateTime)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_bools_Contains(bool async)
+ {
+ var bools = new[] { true };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => bools.Contains(c.Bool)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_of_enums_Contains(bool async)
+ {
+ var enums = new[] { MyEnum.Value1, MyEnum.Value4 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => enums.Contains(c.Enum)),
+ entryCount: 2);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Parameter_collection_null_Contains(bool async)
+ {
+ int[] ints = null;
+
+ await AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ints.Contains(c.Int)),
+ ss => ss.Set().Where(c => false));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_of_ints_Contains(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Contains(10)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_of_nullable_ints_Contains(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.NullableInts.Contains(10)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_of_nullable_ints_Contains_null(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.NullableInts.Contains(null)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_of_bools_Contains(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Bools.Contains(true)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Count_method(bool async)
+ => AssertQuery(
+ async,
+ // ReSharper disable once UseCollectionCountProperty
+ ss => ss.Set().Where(c => c.Ints.Count() == 2),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Length(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Length == 2),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_index_int(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints[1] == 10),
+ ss => ss.Set().Where(c => (c.Ints.Length >= 2 ? c.Ints[1] : -1) == 10),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_index_string(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Strings[1] == "10"),
+ ss => ss.Set().Where(c => (c.Strings.Length >= 2 ? c.Strings[1] : "-1") == "10"),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_index_datetime(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(
+ c => c.DateTimes[1] == new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc)),
+ ss => ss.Set().Where(
+ c => (c.DateTimes.Length >= 2 ? c.DateTimes[1] : default) == new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_index_beyond_end(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints[999] == 10),
+ ss => ss.Set().Where(c => false),
+ entryCount: 0);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_index_Column(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => new[] { 1, 2, 3 }[c.Int] == 1),
+ ss => ss.Set().Where(c => (c.Int <= 2 ? new[] { 1, 2, 3 }[c.Int] : -1) == 1),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_index_Column(bool async)
+ {
+ var ints = new[] { 1, 2, 3 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ints[c.Int] == 1),
+ ss => ss.Set().Where(c => (c.Int <= 2 ? ints[c.Int] : -1) == 1),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_ElementAt(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.ElementAt(1) == 10),
+ ss => ss.Set().Where(c => (c.Ints.Length >= 2 ? c.Ints.ElementAt(1) : -1) == 10),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Skip(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Skip(1).Count() == 2),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Take(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Take(2).Contains(11)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Skip_Take(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Skip(1).Take(2).Contains(11)),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Any(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Any()),
+ entryCount: 2);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_projection_from_top_level(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().OrderBy(c => c.Id).Select(c => c.Ints),
+ elementAsserter: (a, b) => Assert.Equivalent(a, b),
+ assertOrder: true);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_and_parameter_collection_Join(bool async)
+ {
+ var ints = new[] { 11, 111 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Join(ints, i => i, j => j, (i, j) => new { I = i, J = j }).Count() == 2),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Parameter_collection_Concat_column_collection(bool async)
+ {
+ var ints = new[] { 11, 111 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => ints.Concat(c.Ints).Count() == 2),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Union_parameter_collection(bool async)
+ {
+ var ints = new[] { 11, 111 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Union(ints).Count() == 2),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_Intersect_inline_collection(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Intersect(new[] { 11, 111 }).Count() == 2),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Inline_collection_Except_column_collection(bool async)
+ // Note that since the VALUES is on the left side of the set operation, it must assign column names, otherwise the column coming
+ // out of the set operation has undetermined naming.
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(
+ c => new[] { 11, 111 }.Except(c.Ints).Count(i => i % 2 == 1) == 2),
+ entryCount: 2);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_equality_parameter_collection(bool async)
+ {
+ var ints = new[] { 1, 10 };
+
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints == ints),
+ ss => ss.Set().Where(c => c.Ints.SequenceEqual(ints)),
+ entryCount: 1);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(bool async)
+ {
+ var ints = new[] { 1, 10 };
+
+ await AssertTranslationFailed(
+ () => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Concat(ints) == new[] { 1, 11, 111, 1, 10 })));
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_equality_inline_collection(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints == new[] { 1, 10 }),
+ ss => ss.Set().Where(c => c.Ints.SequenceEqual(new[] { 1, 10 })),
+ entryCount: 1);
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async)
+ {
+ // The Skip causes a pushdown into a subquery before the Union, and so the projection on the left side of the union points to the
+ // subquery as its table, and not directly to the parameter's table.
+ // This creates an initially untyped ColumnExpression referencing the pushed-down subquery; it must also be inferred.
+ // Note that this must be a compiled query, since with normal queries the Skip(1) gets client-evaluated.
+ // TODO:
+ var compiledQuery = EF.CompileQuery(
+ (PrimitiveCollectionsContext context, int[] ints)
+ => context.Set().Where(p => ints.Skip(1).Count(i => i > p.Id) == 1).Count());
+
+ await using var context = Fixture.CreateContext();
+ var ints = new[] { 10, 111 };
+
+ // TODO: Complete
+ var results = compiledQuery(context, ints);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async)
+ {
+ // The Skip causes a pushdown into a subquery before the Union, and so the projection on the left side of the union points to the
+ // subquery as its table, and not directly to the parameter's table.
+ // This creates an initially untyped ColumnExpression referencing the pushed-down subquery; it must also be inferred.
+ // Note that this must be a compiled query, since with normal queries the Skip(1) gets client-evaluated.
+ var compiledQuery = EF.CompileQuery(
+ (PrimitiveCollectionsContext context, int[] ints)
+ => context.Set().Where(p => ints.Skip(1).Union(p.Ints).Count() == 3));
+
+ await using var context = Fixture.CreateContext();
+ var ints = new[] { 10, 111 };
+
+ // TODO: Complete
+ var results = compiledQuery(context, ints).ToList();
+ }
+
+ [ConditionalFact]
+ public virtual void Parameter_collection_in_subquery_and_Convert_as_compiled_query()
+ {
+ // The array indexing is translated as a subquery over e.g. OpenJson with LIMIT/OFFSET.
+ // Since there's a CAST over that, the type mapping inference from the other side (p.String) doesn't propagate inside to the
+ // subquery. In this case, the CAST operand gets the default CLR type mapping, but that's object in this case.
+ // We should apply the default type mapping to the parameter, but need to figure out the exact rules when to do this.
+ var query = EF.CompileQuery(
+ (PrimitiveCollectionsContext context, object[] parameters)
+ => context.Set().Where(p => p.String == (string)parameters[0]));
+
+ using var context = Fixture.CreateContext();
+
+ var exception = Assert.Throws(() => query(context, new[] { "foo" }).ToList());
+
+ Assert.Contains("in the SQL tree does not have a type mapping assigned", exception.Message);
+ }
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_in_subquery_Union_parameter_collection(bool async)
+ {
+ var ints = new[] { 10, 111 };
+
+ // The Skip causes a pushdown into a subquery before the Union. This creates an initially untyped ColumnExpression referencing the
+ // pushed-down subquery; it must also be inferred
+ return AssertQuery(
+ async,
+ ss => ss.Set().Where(c => c.Ints.Skip(1).Union(ints).Count() == 3),
+ entryCount: 1);
+ }
+
+ public abstract class PrimitiveCollectionsQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase
+ {
+ private PrimitiveArrayData _expectedData;
+
+ protected override string StoreName
+ => "PrimitiveCollectionsTest";
+
+ public Func GetContextCreator()
+ => () => CreateContext();
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(c => c.Log(CoreEventId.DistinctAfterOrderByWithoutRowLimitingOperatorWarning));
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
+ => modelBuilder.Entity().Property(p => p.Id).ValueGeneratedNever();
+
+ protected override void Seed(PrimitiveCollectionsContext context)
+ => new PrimitiveArrayData(context);
+
+ public virtual ISetSource GetExpectedData()
+ => _expectedData ??= new PrimitiveArrayData();
+
+ public IReadOnlyDictionary EntitySorters { get; } = new Dictionary>
+ {
+ { typeof(PrimitiveCollectionsEntity), e => ((PrimitiveCollectionsEntity)e)?.Id }
+ }.ToDictionary(e => e.Key, e => (object)e.Value);
+
+ public IReadOnlyDictionary EntityAsserters { get; } = new Dictionary>
+ {
+ {
+ typeof(PrimitiveCollectionsEntity), (e, a) =>
+ {
+ Assert.Equal(e == null, a == null);
+
+ if (a != null)
+ {
+ var ee = (PrimitiveCollectionsEntity)e;
+ var aa = (PrimitiveCollectionsEntity)a;
+
+ Assert.Equal(ee.Id, aa.Id);
+ Assert.Equivalent(ee.Ints, aa.Ints, strict: true);
+ Assert.Equivalent(ee.Strings, aa.Strings, strict: true);
+ Assert.Equivalent(ee.DateTimes, aa.DateTimes, strict: true);
+ Assert.Equivalent(ee.Bools, aa.Bools, strict: true);
+ // TODO: Complete
+ }
+ }
+ }
+ }.ToDictionary(e => e.Key, e => (object)e.Value);
+ }
+
+ public class PrimitiveCollectionsContext : PoolableDbContext
+ {
+ public PrimitiveCollectionsContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+ }
+
+ public class PrimitiveCollectionsEntity
+ {
+ public int Id { get; set; }
+
+ public string String { get; set; }
+ public int Int { get; set; }
+ public DateTime DateTime { get; set; }
+ public bool Bool { get; set; }
+ public MyEnum Enum { get; set; }
+ public int? NullableInt { get; set; }
+
+ public string[] Strings { get; set; }
+ public int[] Ints { get; set; }
+ public DateTime[] DateTimes { get; set; }
+ public bool[] Bools { get; set; }
+ public MyEnum[] Enums { get; set; }
+ public int?[] NullableInts { get; set; }
+ }
+
+ public enum MyEnum { Value1, Value2, Value3, Value4 }
+
+ public class PrimitiveArrayData : ISetSource
+ {
+ public IReadOnlyList PrimitiveArrayEntities { get; }
+
+ public PrimitiveArrayData(PrimitiveCollectionsContext context = null)
+ {
+ PrimitiveArrayEntities = CreatePrimitiveArrayEntities();
+
+ if (context != null)
+ {
+ context.AddRange(PrimitiveArrayEntities);
+ context.SaveChanges();
+ }
+ }
+
+ public IQueryable Set()
+ where TEntity : class
+ {
+ if (typeof(TEntity) == typeof(PrimitiveCollectionsEntity))
+ {
+ return (IQueryable)PrimitiveArrayEntities.AsQueryable();
+ }
+
+ throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity));
+ }
+
+ private static IReadOnlyList CreatePrimitiveArrayEntities()
+ => new List
+ {
+ new()
+ {
+ Id = 1,
+
+ Int = 10,
+ String = "10",
+ DateTime = new DateTime(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc),
+ Bool = true,
+ Enum = MyEnum.Value1,
+ NullableInt = 10,
+
+ Ints = new[] { 1, 10 },
+ Strings = new[] { "1", "10" },
+ DateTimes = new DateTime[]
+ {
+ new(2020, 1, 1, 12, 30, 0, DateTimeKind.Utc),
+ new(2020, 1, 10, 12, 30, 0, DateTimeKind.Utc)
+ },
+ Bools = new[] { true, false },
+ Enums = new[] { MyEnum.Value1, MyEnum.Value2 },
+ NullableInts = new int?[] { 1, 10 },
+ },
+ new()
+ {
+ Id = 2,
+
+ Int = 11,
+ String = "11",
+ DateTime = new DateTime(2020, 1, 11, 12, 30, 0, DateTimeKind.Utc),
+ Bool = false,
+ Enum = MyEnum.Value2,
+ NullableInt = null,
+
+ Ints = new[] { 1, 11, 111 },
+ Strings = new[] { "1", "11", "111" },
+ DateTimes = new DateTime[]
+ {
+ new(2020, 1, 1, 12, 30, 0, DateTimeKind.Utc),
+ new(2020, 1, 11, 12, 30, 0, DateTimeKind.Utc),
+ new(2020, 1, 31, 12, 30, 0, DateTimeKind.Utc)
+ },
+ Bools = new[] { false },
+ Enums = new[] { MyEnum.Value2, MyEnum.Value3 },
+ NullableInts = new int?[] { 1, 11, null },
+ },
+ new()
+ {
+ Id = 3,
+
+ Int = 0,
+ String = "",
+ DateTime = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ Bool = false,
+ Enum = MyEnum.Value1,
+ NullableInt = null,
+
+ Ints = Array.Empty(),
+ Strings = Array.Empty(),
+ DateTimes = Array.Empty(),
+ Bools = Array.Empty(),
+ Enums = Array.Empty(),
+ NullableInts = Array.Empty(),
+ }
+ };
+ }
+}
diff --git a/test/EFCore.Specification.Tests/Query/QueryTestBase.cs b/test/EFCore.Specification.Tests/Query/QueryTestBase.cs
index 45fc352f56a..822cacbd7e8 100644
--- a/test/EFCore.Specification.Tests/Query/QueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/QueryTestBase.cs
@@ -1165,6 +1165,12 @@ protected static async Task AssertTranslationFailed(Func query)
(await Assert.ThrowsAsync(query))
.Message);
+ protected static void AssertTranslationFailed(Action query)
+ => Assert.Contains(
+ CoreStrings.TranslationFailed("")[48..],
+ Assert.Throws(query)
+ .Message);
+
protected static async Task AssertTranslationFailedWithDetails(Func query, string details)
=> Assert.Contains(
CoreStrings.TranslationFailedWithDetails("", details)[21..],
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs
index e9c32a9894f..ea1a611bc6e 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs
@@ -1214,6 +1214,8 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT CASE
WHEN [l0].[Id] IS NULL THEN 0
ELSE [l0].[Id]
@@ -1221,7 +1223,10 @@ ELSE [l0].[Id]
FROM [LevelOne] AS [l]
LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id]
LEFT JOIN [LevelThree] AS [l1] ON [l0].[Id] = [l1].[OneToMany_Required_Inverse3Id]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
ORDER BY [l].[Id], [l0].[Id]
""");
}
@@ -2325,17 +2330,25 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [t].[Date], [t0].[Id]
FROM (
SELECT [l].[Date]
FROM [LevelOne] AS [l]
- WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
GROUP BY [l].[Date]
) AS [t]
LEFT JOIN (
SELECT [l0].[Id], [l0].[Date]
FROM [LevelOne] AS [l0]
- WHERE [l0].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v0]
+ WHERE [v0].[value] = [l0].[Name] OR ([v0].[value] IS NULL AND [l0].[Name] IS NULL))
) AS [t0] ON [t].[Date] = [t0].[Date]
ORDER BY [t].[Date]
""");
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs
index f7041ff82ca..fd5290ec280 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs
@@ -2848,6 +2848,8 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT CASE
WHEN [t0].[OneToOne_Required_PK_Date] IS NULL OR [t0].[Level1_Required_Id] IS NULL OR [t0].[OneToMany_Required_Inverse2Id] IS NULL THEN 0
WHEN [t0].[OneToOne_Required_PK_Date] IS NOT NULL AND [t0].[Level1_Required_Id] IS NOT NULL AND [t0].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t0].[Id0]
@@ -2872,7 +2874,10 @@ WHERE [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse
) AS [t1] ON CASE
WHEN [t0].[OneToOne_Required_PK_Date] IS NOT NULL AND [t0].[Level1_Required_Id] IS NOT NULL AND [t0].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t0].[Id0]
END = [t1].[OneToMany_Required_Inverse3Id]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
ORDER BY [l].[Id], [t0].[Id], [t0].[Id0]
""");
}
@@ -3009,17 +3014,25 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [t].[Date], [t0].[Id]
FROM (
SELECT [l].[Date]
FROM [Level1] AS [l]
- WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
GROUP BY [l].[Date]
) AS [t]
LEFT JOIN (
SELECT [l0].[Id], [l0].[Date]
FROM [Level1] AS [l0]
- WHERE [l0].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v0]
+ WHERE [v0].[value] = [l0].[Name] OR ([v0].[value] IS NULL AND [l0].[Name] IS NULL))
) AS [t0] ON [t].[Date] = [t0].[Date]
ORDER BY [t].[Date]
""");
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs
index ffc5d625c4d..8cb7b155829 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs
@@ -3187,22 +3187,32 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT CASE
WHEN [l0].[Id] IS NULL THEN 0
ELSE [l0].[Id]
END, [l].[Id], [l0].[Id]
FROM [LevelOne] AS [l]
LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
ORDER BY [l].[Id], [l0].[Id]
""",
//
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [l1].[Id], [l1].[Level2_Optional_Id], [l1].[Level2_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse3Id], [l1].[OneToMany_Optional_Self_Inverse3Id], [l1].[OneToMany_Required_Inverse3Id], [l1].[OneToMany_Required_Self_Inverse3Id], [l1].[OneToOne_Optional_PK_Inverse3Id], [l1].[OneToOne_Optional_Self3Id], [l].[Id], [l0].[Id]
FROM [LevelOne] AS [l]
LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id]
INNER JOIN [LevelThree] AS [l1] ON [l0].[Id] = [l1].[OneToMany_Required_Inverse3Id]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
ORDER BY [l].[Id], [l0].[Id]
""");
}
@@ -3734,25 +3744,38 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [l].[Date]
FROM [LevelOne] AS [l]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
GROUP BY [l].[Date]
ORDER BY [l].[Date]
""",
//
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [t0].[Id], [t].[Date]
FROM (
SELECT [l].[Date]
FROM [LevelOne] AS [l]
- WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
GROUP BY [l].[Date]
) AS [t]
INNER JOIN (
SELECT [l0].[Id], [l0].[Date]
FROM [LevelOne] AS [l0]
- WHERE [l0].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v0]
+ WHERE [v0].[value] = [l0].[Name] OR ([v0].[value] IS NULL AND [l0].[Name] IS NULL))
) AS [t0] ON [t].[Date] = [t0].[Date]
ORDER BY [t].[Date]
""");
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs
index d55fd460371..02910614534 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs
@@ -3076,10 +3076,15 @@ public override async Task Accessing_optional_property_inside_result_operator_su
AssertSql(
"""
+@__names_0='["Name1","Name2"]' (Size = 4000)
+
SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id]
FROM [LevelOne] AS [l]
LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Optional_Id]
-WHERE [l0].[Name] NOT IN (N'Name1', N'Name2') OR [l0].[Name] IS NULL
+WHERE NOT EXISTS (
+ SELECT 1
+ FROM OpenJson(@__names_0) AS [n]
+ WHERE ([l0].[Name] = [n].[value] AND [l0].[Name] IS NOT NULL AND [n].[value] IS NOT NULL) OR ([l0].[Name] IS NULL AND [n].[value] IS NULL))
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs
index 1c2cbc0bf24..35913c190f3 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs
@@ -5403,6 +5403,8 @@ public override async Task Accessing_optional_property_inside_result_operator_su
AssertSql(
"""
+@__names_0='["Name1","Name2"]' (Size = 4000)
+
SELECT [l].[Id], [l].[Date], [l].[Name]
FROM [Level1] AS [l]
LEFT JOIN (
@@ -5410,7 +5412,10 @@ LEFT JOIN (
FROM [Level1] AS [l0]
WHERE [l0].[OneToOne_Required_PK_Date] IS NOT NULL AND [l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToMany_Required_Inverse2Id] IS NOT NULL
) AS [t] ON [l].[Id] = [t].[Level1_Optional_Id]
-WHERE [t].[Level2_Name] NOT IN (N'Name1', N'Name2') OR [t].[Level2_Name] IS NULL
+WHERE NOT EXISTS (
+ SELECT 1
+ FROM OpenJson(@__names_0) AS [n]
+ WHERE ([t].[Level2_Name] = [n].[value] AND [t].[Level2_Name] IS NOT NULL AND [n].[value] IS NOT NULL) OR ([t].[Level2_Name] IS NULL AND [n].[value] IS NULL))
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
index ce029b62c64..3dbb7320e7f 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs
@@ -211,10 +211,15 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Gears] AS [g]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE [t].[Id] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [t].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -229,11 +234,16 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Gears] AS [g]
INNER JOIN [Cities] AS [c] ON [g].[CityOfBirthName] = [c].[Name]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE [c].[Location] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [c].[Location] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -248,10 +258,15 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE [t].[Id] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [t].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -2085,9 +2100,14 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n
AssertSql(
"""
+@__cities_0='["Unknown","Jacinto\u0027s location","Ephyra\u0027s location"]' (Size = 4000)
+
SELECT [c].[Name], [c].[Location], [c].[Nation]
FROM [Cities] AS [c]
-WHERE [c].[Location] IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS varchar(100)) = [c].[Location] OR ([c0].[value] IS NULL AND [c].[Location] IS NULL))
""");
}
@@ -3082,9 +3102,14 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a
AssertSql(
"""
+@__ids_0='["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]' (Size = 4000)
+
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Tags] AS [t]
-WHERE [t].[Id] IN ('d2c26679-562b-44d1-ab96-23d1775e0926', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS uniqueidentifier) = [t].[Id])
""");
}
@@ -3600,10 +3625,15 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool
AssertSql(
"""
+@__cities_0='["Ephyra",null]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
LEFT JOIN [Cities] AS [c] ON [g].[AssignedCityName] = [c].[Name]
-WHERE [g].[SquadId] < 2 AND ([c].[Name] = N'Ephyra' OR [c].[Name] IS NULL)
+WHERE [g].[SquadId] < 2 AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS nvarchar(450)) = [c].[Name] OR ([c0].[value] IS NULL AND [c].[Name] IS NULL))
""");
}
@@ -5886,10 +5916,18 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz
AssertSql(
"""
+@__nicknames_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [w].[Name], [w].[Id]
FROM [Gears] AS [g]
LEFT JOIN [Weapons] AS [w] ON [g].[FullName] = [w].[OwnerFullName]
-ORDER BY [g].[Nickname], [g].[SquadId]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__nicknames_0) AS [n]
+ WHERE CAST([n].[value] AS nvarchar(450)) = [g].[Nickname]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END DESC, [g].[Nickname], [g].[SquadId]
""");
}
@@ -6625,10 +6663,14 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a
"""
@__start_0='1902-01-01T10:00:00.1234567+01:30'
@__end_1='1902-01-03T10:00:00.1234567+01:30'
+@__dates_2='["1902-01-02T10:00:00.1234567+01:30"]' (Size = 4000)
SELECT [m].[Id], [m].[BriefingDocument], [m].[BriefingDocumentFileExtension], [m].[CodeName], [m].[Date], [m].[Duration], [m].[Rating], [m].[Time], [m].[Timeline]
FROM [Missions] AS [m]
-WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30'
+WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dates_2) AS [d]
+ WHERE CAST([d].[value] AS datetimeoffset) = [m].[Timeline])
""");
}
@@ -7335,8 +7377,17 @@ public override async Task OrderBy_Contains_empty_list(bool async)
AssertSql(
"""
+@__ids_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [g].[SquadId]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""");
}
@@ -8087,10 +8138,15 @@ public override async Task Enum_array_contains(bool async)
AssertSql(
"""
+@__types_0='[null,1]' (Size = 4000)
+
SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId]
FROM [Weapons] AS [w]
LEFT JOIN [Weapons] AS [w0] ON [w].[SynergyWithId] = [w0].[Id]
-WHERE [w0].[Id] IS NOT NULL AND ([w0].[AmmunitionType] = 1 OR [w0].[AmmunitionType] IS NULL)
+WHERE [w0].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__types_0) AS [t]
+ WHERE CAST([t].[value] AS int) = [w0].[AmmunitionType] OR ([t].[value] IS NULL AND [w0].[AmmunitionType] IS NULL))
""");
}
@@ -9319,9 +9375,14 @@ public override async Task Where_bool_column_and_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
-WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit))
+WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__values_0) AS [v]
+ WHERE CAST([v].[value] AS bit) = [g].[HasSoulPatch])
""");
}
@@ -9331,9 +9392,14 @@ public override async Task Where_bool_column_or_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
-WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit))
+WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__values_0) AS [v]
+ WHERE CAST([v].[value] AS bit) = [g].[HasSoulPatch])
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs
new file mode 100644
index 00000000000..947b7f192c6
--- /dev/null
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs
@@ -0,0 +1,348 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using NetTopologySuite.Geometries;
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public class NonSharedPrimitiveCollectionsQuerySqlServerTest : NonSharedPrimitiveCollectionsQueryRelationalTestBase
+{
+ #region Support for specific element types
+
+ public override async Task Array_of_int()
+ {
+ await base.Array_of_int();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS int) = 1) = 2
+""");
+ }
+
+ public override async Task Array_of_long()
+ {
+ await base.Array_of_long();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS bigint) = CAST(1 AS bigint)) = 2
+""");
+ }
+
+ public override async Task Array_of_short()
+ {
+ await base.Array_of_short();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS smallint) = CAST(1 AS smallint)) = 2
+""");
+ }
+
+ public override async Task Array_of_double()
+ {
+ await base.Array_of_double();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS float) = 1.0E0) = 2
+""");
+ }
+
+ public override async Task Array_of_float()
+ {
+ await base.Array_of_float();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS real) = CAST(1 AS real)) = 2
+""");
+ }
+
+ public override async Task Array_of_decimal()
+ {
+ await base.Array_of_decimal();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS decimal(18,2)) = 1.0) = 2
+""");
+ }
+
+ public override async Task Array_of_DateTime()
+ {
+ await base.Array_of_DateTime();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS datetime2) = '2023-01-01T12:30:00.0000000') = 2
+""");
+ }
+
+ public override async Task Array_of_DateOnly()
+ {
+ await base.Array_of_DateOnly();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS date) = '2023-01-01') = 2
+""");
+ }
+
+ public override async Task Array_of_TimeOnly()
+ {
+ await base.Array_of_TimeOnly();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS time) = '12:30:00') = 2
+""");
+ }
+
+ public override async Task Array_of_DateTimeOffset()
+ {
+ await base.Array_of_DateTimeOffset();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS datetimeoffset) = '2023-01-01T12:30:00.0000000+02:00') = 2
+""");
+ }
+
+ public override async Task Array_of_bool()
+ {
+ await base.Array_of_bool();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS bit) = CAST(1 AS bit)) = 2
+""");
+ }
+
+ public override async Task Array_of_Guid()
+ {
+ await base.Array_of_Guid();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS uniqueidentifier) = 'dc8c903d-d655-4144-a0fd-358099d40ae1') = 2
+""");
+ }
+
+ // The JSON representation for new[] { 1, 2 } is AQI= (base64), this cannot simply be cast to varbinary(max) (0x0102). See #30727.
+ public override Task Array_of_byte_array()
+ => AssertTranslationFailed(() => base.Array_of_byte_array());
+
+ public override async Task Array_of_enum()
+ {
+ await base.Array_of_enum();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[SomeArray]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([t].[SomeArray]) AS [s]
+ WHERE CAST([s].[value] AS int) = 0) = 2
+""");
+ }
+
+ [ConditionalFact] // #30630
+ public override async Task Array_of_geometry_is_not_supported()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => InitializeAsync(
+ onConfiguring: options => options.UseSqlServer(o => o.UseNetTopologySuite()),
+ addServices: s => s.AddEntityFrameworkSqlServerNetTopologySuite(),
+ onModelCreating: mb => mb.Entity().Property("Points")));
+
+ Assert.Equal(CoreStrings.PropertyNotMapped("Point[]", "TestEntity", "Points"), exception.Message);
+ }
+
+ #endregion Support for specific element types
+
+ #region Type mapping inference
+
+ public override async Task Constant_with_inferred_value_converter()
+ {
+ await base.Constant_with_inferred_value_converter();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints], [t].[PropertyWithValueConverter]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM (VALUES (CAST(1 AS int)), (8)) AS [v]([Value])
+ WHERE [v].[Value] = [t].[PropertyWithValueConverter]) = 1
+""");
+ }
+
+ public override async Task Inline_collection_in_query_filter()
+ {
+ await base.Inline_collection_in_query_filter();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Ints]
+FROM [TestEntity] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM (VALUES (CAST(1 AS int)), (2), (3)) AS [v]([Value])
+ WHERE [v].[Value] > [t].[Id]) = 1
+""");
+ }
+
+ public override async Task Column_collection_inside_json_owned_entity()
+ {
+ await base.Column_collection_inside_json_owned_entity();
+
+ AssertSql(
+"""
+SELECT TOP(2) [t].[Id], [t].[Owned]
+FROM [TestOwner] AS [t]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson(JSON_VALUE([t].[Owned], '$.Strings')) AS [s]) = 2
+""",
+ //
+"""
+SELECT TOP(2) [t].[Id], [t].[Owned]
+FROM [TestOwner] AS [t]
+WHERE (
+ SELECT [s].[value]
+ FROM OpenJson(JSON_VALUE([t].[Owned], '$.Strings')) AS [s]
+ ORDER BY CAST([s].[key] AS int)
+ OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY) = N'bar'
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Same_parameter_with_different_type_mappings()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity(
+ b =>
+ {
+ b.Property(typeof(DateTime), "DateTime").HasColumnType("datetime");
+ b.Property(typeof(DateTime), "DateTime2").HasColumnType("datetime2");
+ }));
+
+ await using var context = contextFactory.CreateContext();
+
+ var dateTimes = new[] { new DateTime(2020, 1, 1, 12, 30, 00), new DateTime(2020, 1, 2, 12, 30, 00) };
+
+ _ = await context.Set()
+ .Where(
+ m =>
+ dateTimes.Contains(EF.Property(m, "DateTime"))
+ && dateTimes.Contains(EF.Property(m, "DateTime2")))
+ .ToArrayAsync();
+
+ AssertSql(
+"""
+@__dateTimes_0='["2020-01-01T12:30:00","2020-01-02T12:30:00"]' (Size = 4000)
+@__dateTimes_0_1='["2020-01-01T12:30:00","2020-01-02T12:30:00"]' (Size = 4000)
+
+SELECT [t].[Id], [t].[DateTime], [t].[DateTime2], [t].[Ints]
+FROM [TestEntity] AS [t]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0) AS [d]
+ WHERE CAST([d].[value] AS datetime) = [t].[DateTime]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_1) AS [d0]
+ WHERE CAST([d0].[value] AS datetime2) = [t].[DateTime2])
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Same_collection_with_conflicting_type_mappings_not_supported()
+ {
+ var contextFactory = await InitializeAsync(
+ onModelCreating: mb => mb.Entity(
+ b =>
+ {
+ b.Property(typeof(DateTime), "DateTime").HasColumnType("datetime");
+ b.Property(typeof(DateTime), "DateTime2").HasColumnType("datetime2");
+ }));
+
+ await using var context = contextFactory.CreateContext();
+
+ var dateTimes = new[] { new DateTime(2020, 1, 1, 12, 30, 00), new DateTime(2020, 1, 2, 12, 30, 00) };
+
+ var exception = await Assert.ThrowsAsync(
+ () => context.Set()
+ .Where(
+ m => dateTimes
+ .Any(d => d == EF.Property(m, "DateTime") && d == EF.Property(m, "DateTime2")))
+ .ToArrayAsync());
+ Assert.Equal(RelationalStrings.ConflictingTypeMappingsForPrimitiveCollection("datetime2", "datetime"), exception.Message);
+ }
+
+ #endregion Type mapping inference
+
+ protected override ITestStoreFactory TestStoreFactory
+ => SqlServerTestStoreFactory.Instance;
+}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs
index e12bc821892..6265199c103 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs
@@ -14,7 +14,7 @@ public NorthwindAggregateOperatorsQuerySqlServerTest(
: base(fixture)
{
ClearLog();
- //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
protected override bool CanExecuteQueryString
@@ -1550,15 +1550,25 @@ public override async Task Contains_with_local_array_closure(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]' (Size = 4000)
+
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'ABCDE', N'ALFKI')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""",
//
"""
+@__ids_0='["ABCDE"]' (Size = 4000)
+
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] = N'ABCDE'
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -1568,21 +1578,31 @@ public override async Task Contains_with_subquery_and_local_array_closure(bool a
AssertSql(
"""
+@__ids_0='["London","Buenos Aires"]' (Size = 4000)
+
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 [Customers] AS [c0]
- WHERE [c0].[City] IN (N'London', N'Buenos Aires') AND [c0].[CustomerID] = [c].[CustomerID])
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nvarchar(15)) = [c0].[City] OR ([i].[value] IS NULL AND [c0].[City] IS NULL)) AND [c0].[CustomerID] = [c].[CustomerID])
""",
//
"""
+@__ids_0='["London"]' (Size = 4000)
+
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 [Customers] AS [c0]
- WHERE [c0].[City] = N'London' AND [c0].[CustomerID] = [c].[CustomerID])
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nvarchar(15)) = [c0].[City] OR ([i].[value] IS NULL AND [c0].[City] IS NULL)) AND [c0].[CustomerID] = [c].[CustomerID])
""");
}
@@ -1592,15 +1612,25 @@ public override async Task Contains_with_local_uint_array_closure(bool async)
AssertSql(
"""
+@__ids_0='[0,1]' (Size = 4000)
+
SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title]
FROM [Employees] AS [e]
-WHERE [e].[EmployeeID] IN (0, 1)
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[EmployeeID])
""",
//
"""
+@__ids_0='[0]' (Size = 4000)
+
SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title]
FROM [Employees] AS [e]
-WHERE [e].[EmployeeID] = 0
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[EmployeeID])
""");
}
@@ -1610,15 +1640,25 @@ public override async Task Contains_with_local_nullable_uint_array_closure(bool
AssertSql(
"""
+@__ids_0='[0,1]' (Size = 4000)
+
SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title]
FROM [Employees] AS [e]
-WHERE [e].[EmployeeID] IN (0, 1)
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[EmployeeID])
""",
//
"""
+@__ids_0='[0]' (Size = 4000)
+
SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title]
FROM [Employees] AS [e]
-WHERE [e].[EmployeeID] = 0
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[EmployeeID])
""");
}
@@ -1640,9 +1680,14 @@ public override async Task Contains_with_local_list_closure(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]' (Size = 4000)
+
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'ABCDE', N'ALFKI')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -1652,9 +1697,14 @@ public override async Task Contains_with_local_object_list_closure(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]' (Size = 4000)
+
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'ABCDE', N'ALFKI')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -1664,9 +1714,14 @@ public override async Task Contains_with_local_list_closure_all_null(bool async)
AssertSql(
"""
+@__ids_0='[null,null]' (Size = 4000)
+
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 0 = 1
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -1688,15 +1743,25 @@ public override async Task Contains_with_local_list_inline_closure_mix(bool asyn
AssertSql(
"""
+@__p_0='["ABCDE","ALFKI"]' (Size = 4000)
+
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'ABCDE', N'ALFKI')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__p_0) AS [p]
+ WHERE CAST([p].[value] AS nchar(5)) = [c].[CustomerID])
""",
//
"""
+@__p_0='["ABCDE","ANATR"]' (Size = 4000)
+
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'ABCDE', N'ANATR')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__p_0) AS [p]
+ WHERE CAST([p].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -1706,15 +1771,25 @@ public override async Task Contains_with_local_non_primitive_list_inline_closure
AssertSql(
"""
+@__Select_0='["ABCDE","ALFKI"]' (Size = 4000)
+
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'ABCDE', N'ALFKI')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__Select_0) AS [s]
+ WHERE CAST([s].[value] AS nchar(5)) = [c].[CustomerID])
""",
//
"""
+@__Select_0='["ABCDE","ANATR"]' (Size = 4000)
+
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'ABCDE', N'ANATR')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__Select_0) AS [s]
+ WHERE CAST([s].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -1724,9 +1799,14 @@ public override async Task Contains_with_local_non_primitive_list_closure_mix(bo
AssertSql(
"""
+@__Select_0='["ABCDE","ALFKI"]' (Size = 4000)
+
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'ABCDE', N'ALFKI')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__Select_0) AS [s]
+ WHERE CAST([s].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -1736,9 +1816,14 @@ public override async Task Contains_with_local_collection_false(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]' (Size = 4000)
+
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] NOT IN (N'ABCDE', N'ALFKI')
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]))
""");
}
@@ -1748,9 +1833,14 @@ public override async Task Contains_with_local_collection_complex_predicate_and(
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI"]' (Size = 4000)
+
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'ABCDE') AND [c].[CustomerID] IN (N'ABCDE', N'ALFKI')
+WHERE [c].[CustomerID] IN (N'ALFKI', N'ABCDE') AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -1784,9 +1874,14 @@ public override async Task Contains_with_local_collection_sql_injection(bool asy
AssertSql(
"""
+@__ids_0='["ALFKI","ABC\u0027)); GO; DROP TABLE Orders; GO; --"]' (Size = 4000)
+
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'ABC'')); GO; DROP TABLE Orders; GO; --') OR [c].[CustomerID] IN (N'ALFKI', N'ABCDE')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID]) OR [c].[CustomerID] IN (N'ALFKI', N'ABCDE')
""");
}
@@ -1796,9 +1891,14 @@ public override async Task Contains_with_local_collection_empty_closure(bool asy
AssertSql(
"""
+@__ids_0='[]' (Size = 4000)
+
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 0 = 1
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -2249,9 +2349,14 @@ public override async Task Where_subquery_any_equals_operator(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000)
+
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'ABCDE', N'ALFKI', N'ANATR')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -2273,9 +2378,14 @@ public override async Task Where_subquery_any_equals_static(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000)
+
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'ABCDE', N'ALFKI', N'ANATR')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -2285,15 +2395,25 @@ public override async Task Where_subquery_where_any(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000)
+
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].[City] = N'México D.F.' AND [c].[CustomerID] IN (N'ABCDE', N'ALFKI', N'ANATR')
+WHERE [c].[City] = N'México D.F.' AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""",
//
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000)
+
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].[City] = N'México D.F.' AND [c].[CustomerID] IN (N'ABCDE', N'ALFKI', N'ANATR')
+WHERE [c].[City] = N'México D.F.' AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE [c].[CustomerID] = CAST([i].[value] AS nchar(5)))
""");
}
@@ -2303,9 +2423,14 @@ public override async Task Where_subquery_all_not_equals_operator(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000)
+
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] NOT IN (N'ABCDE', N'ALFKI', N'ANATR')
+WHERE NOT EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID] AND [i].[value] IS NOT NULL)
""");
}
@@ -2327,9 +2452,14 @@ public override async Task Where_subquery_all_not_equals_static(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000)
+
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] NOT IN (N'ABCDE', N'ALFKI', N'ANATR')
+WHERE NOT EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -2339,15 +2469,25 @@ public override async Task Where_subquery_where_all(bool async)
AssertSql(
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000)
+
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].[City] = N'México D.F.' AND [c].[CustomerID] NOT IN (N'ABCDE', N'ALFKI', N'ANATR')
+WHERE [c].[City] = N'México D.F.' AND NOT EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS nchar(5)) = [c].[CustomerID] AND [i].[value] IS NOT NULL)
""",
//
"""
+@__ids_0='["ABCDE","ALFKI","ANATR"]' (Size = 4000)
+
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].[City] = N'México D.F.' AND [c].[CustomerID] NOT IN (N'ABCDE', N'ALFKI', N'ANATR')
+WHERE [c].[City] = N'México D.F.' AND NOT EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE [c].[CustomerID] = CAST([i].[value] AS nchar(5)) AND [i].[value] IS NOT NULL)
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs
index b6d6df1c188..ed8e8e79695 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindCompiledQuerySqlServerTest.cs
@@ -13,7 +13,7 @@ public NorthwindCompiledQuerySqlServerTest(
: base(fixture)
{
fixture.TestSqlLoggerFactory.Clear();
- //fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
[ConditionalFact]
@@ -178,15 +178,25 @@ public override void Query_with_contains()
AssertSql(
"""
+@__args='["ALFKI"]' (Size = 4000)
+
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] = N'ALFKI'
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__args) AS [a]
+ WHERE CAST([a].[value] AS nchar(5)) = [c].[CustomerID])
""",
//
"""
+@__args='["ANATR"]' (Size = 4000)
+
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] = N'ANATR'
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__args) AS [a]
+ WHERE CAST([a].[value] AS nchar(5)) = [c].[CustomerID])
""");
}
@@ -385,66 +395,64 @@ FROM [Customers] AS [c]
""");
}
- public override void MakeBinary_does_not_throw_for_unsupported_operator()
- => Assert.Equal(
- CoreStrings.TranslationFailedWithDetails(
- "DbSet() .Where(c => c.CustomerID == (string)__parameters .ElementAt(0))",
- CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))),
- Assert.Throws(
- () => base.MakeBinary_does_not_throw_for_unsupported_operator()).Message.Replace("\r", "").Replace("\n", ""));
-
public override void Query_with_array_parameter()
{
- var query = EF.CompileQuery(
- (NorthwindContext context, string[] args)
- => context.Customers.Where(c => c.CustomerID == args[0]));
-
- using (var context = CreateContext())
- {
- Assert.Equal(
- CoreStrings.TranslationFailedWithDetails(
- "DbSet() .Where(c => c.CustomerID == __args .ElementAt(0))",
- CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))),
- Assert.Throws(
- () => query(context, new[] { "ALFKI" }).First().CustomerID).Message.Replace("\r", "").Replace("\n", ""));
- }
-
- using (var context = CreateContext())
- {
- Assert.Equal(
- CoreStrings.TranslationFailedWithDetails(
- "DbSet() .Where(c => c.CustomerID == __args .ElementAt(0))",
- CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))),
- Assert.Throws(
- () => query(context, new[] { "ANATR" }).First().CustomerID).Message.Replace("\r", "").Replace("\n", ""));
- }
+ base.Query_with_array_parameter();
+
+ AssertSql(
+"""
+@__args='["ALFKI"]' (Size = 4000)
+
+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] = (
+ SELECT CAST([a].[value] AS nchar(5)) AS [value]
+ FROM OpenJson(@__args) AS [a]
+ ORDER BY CAST([a].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY)
+""",
+ //
+"""
+@__args='["ANATR"]' (Size = 4000)
+
+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] = (
+ SELECT CAST([a].[value] AS nchar(5)) AS [value]
+ FROM OpenJson(@__args) AS [a]
+ ORDER BY CAST([a].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY)
+""");
}
public override async Task Query_with_array_parameter_async()
{
- var query = EF.CompileAsyncQuery(
- (NorthwindContext context, string[] args)
- => context.Customers.Where(c => c.CustomerID == args[0]));
-
- using (var context = CreateContext())
- {
- Assert.Equal(
- CoreStrings.TranslationFailedWithDetails(
- "DbSet() .Where(c => c.CustomerID == __args .ElementAt(0))",
- CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))),
- (await Assert.ThrowsAsync(
- () => Enumerate(query(context, new[] { "ALFKI" })))).Message.Replace("\r", "").Replace("\n", ""));
- }
-
- using (var context = CreateContext())
- {
- Assert.Equal(
- CoreStrings.TranslationFailedWithDetails(
- "DbSet() .Where(c => c.CustomerID == __args .ElementAt(0))",
- CoreStrings.QueryUnableToTranslateMethod("System.Linq.Enumerable", nameof(Enumerable.ElementAt))),
- (await Assert.ThrowsAsync(
- () => Enumerate(query(context, new[] { "ANATR" })))).Message.Replace("\r", "").Replace("\n", ""));
- }
+ await base.Query_with_array_parameter_async();
+
+ AssertSql(
+"""
+@__args='["ALFKI"]' (Size = 4000)
+
+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] = (
+ SELECT CAST([a].[value] AS nchar(5)) AS [value]
+ FROM OpenJson(@__args) AS [a]
+ ORDER BY CAST([a].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY)
+""",
+ //
+"""
+@__args='["ANATR"]' (Size = 4000)
+
+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] = (
+ SELECT CAST([a].[value] AS nchar(5)) AS [value]
+ FROM OpenJson(@__args) AS [a]
+ ORDER BY CAST([a].[key] AS int)
+ OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY)
+""");
}
public override void Multiple_queries()
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs
index e9cb3da6941..520ad2ce690 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindEFPropertyIncludeQuerySqlServerTest.cs
@@ -616,18 +616,25 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
@@ -957,14 +964,27 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
- SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(0 AS bit) AS [c]
+ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
@@ -1340,18 +1360,25 @@ public override async Task Include_collection_OrderBy_list_contains(bool async)
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
@@ -1812,14 +1839,27 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
- SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(1 AS bit) AS [c]
+ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs
index b50fb07400b..193b4a52678 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeNoTrackingQuerySqlServerTest.cs
@@ -170,18 +170,25 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
@@ -445,18 +452,25 @@ public override async Task Include_collection_OrderBy_list_contains(bool async)
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
@@ -2039,14 +2053,27 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
- SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(0 AS bit) AS [c]
+ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
@@ -2060,14 +2087,27 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
- SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(1 AS bit) AS [c]
+ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs
index 5cbbb69b7b8..333392d6b4f 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs
@@ -1503,14 +1503,27 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
- SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(0 AS bit) AS [c]
+ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
@@ -1524,14 +1537,27 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
- SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(1 AS bit) AS [c]
+ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
@@ -1545,18 +1571,25 @@ public override async Task Include_collection_OrderBy_list_contains(bool async)
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
@@ -1572,18 +1605,25 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs
index e333574f79e..653dae72de0 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs
@@ -901,7 +901,22 @@ public override async Task Join_local_collection_int_closure_is_cached_correctly
{
await base.Join_local_collection_int_closure_is_cached_correctly(async);
- AssertSql();
+ AssertSql(
+"""
+@__p_0='[1,2]' (Size = 4000)
+
+SELECT [e].[EmployeeID]
+FROM [Employees] AS [e]
+INNER JOIN OpenJson(@__p_0) AS [p] ON [e].[EmployeeID] = [p].[value]
+""",
+ //
+"""
+@__p_0='[3]' (Size = 4000)
+
+SELECT [e].[EmployeeID]
+FROM [Employees] AS [e]
+INNER JOIN OpenJson(@__p_0) AS [p] ON [e].[EmployeeID] = [p].[value]
+""");
}
public override async Task Join_local_string_closure_is_cached_correctly(bool async)
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs
index 1d68ed3f0a1..22ddc3fac29 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs
@@ -4035,15 +4035,25 @@ public override async Task Contains_with_DateTime_Date(bool async)
AssertSql(
"""
+@__dates_0='["1996-07-04T00:00:00","1996-07-16T00:00:00"]' (Size = 4000)
+
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM [Orders] AS [o]
-WHERE CONVERT(date, [o].[OrderDate]) IN ('1996-07-04T00:00:00.000', '1996-07-16T00:00:00.000')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dates_0) AS [d]
+ WHERE CAST([d].[value] AS datetime) = CONVERT(date, [o].[OrderDate]))
""",
//
"""
+@__dates_0='["1996-07-04T00:00:00"]' (Size = 4000)
+
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM [Orders] AS [o]
-WHERE CONVERT(date, [o].[OrderDate]) = '1996-07-04T00:00:00.000'
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dates_0) AS [d]
+ WHERE CAST([d].[value] AS datetime) = CONVERT(date, [o].[OrderDate]))
""");
}
@@ -5070,8 +5080,17 @@ public override async Task OrderBy_empty_list_contains(bool async)
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
+
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]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""");
}
@@ -5081,8 +5100,17 @@ public override async Task OrderBy_empty_list_does_not_contains(bool async)
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
+
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]
+ORDER BY CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""");
}
@@ -6241,10 +6269,15 @@ FROM [Customers] AS [c]
""",
//
"""
+@__orderIds_0='[10643,10692,10702,10835,10952,11011]' (Size = 4000)
+
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Orders] AS [o]
LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID]
-WHERE [o].[OrderID] IN (10643, 10692, 10702, 10835, 10952, 11011)
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__orderIds_0) AS [o0]
+ WHERE CAST([o0].[value] AS int) = [o].[OrderID])
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs
index 9caea5b48a4..32b023f2cf5 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs
@@ -12,6 +12,7 @@ public NorthwindNavigationsQuerySqlServerTest(
: base(fixture)
{
fixture.TestSqlLoggerFactory.Clear();
+ //fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
protected override bool CanExecuteQueryString
@@ -773,6 +774,8 @@ public override async Task Collection_select_nav_prop_first_or_default_then_nav_
AssertSql(
"""
+@__orderIds_0='[10643,10692,10702,10835,10952,11011]' (Size = 4000)
+
SELECT [t0].[CustomerID], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region]
FROM [Customers] AS [c]
LEFT JOIN (
@@ -781,7 +784,10 @@ LEFT JOIN (
SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region], [o].[CustomerID] AS [CustomerID0], ROW_NUMBER() OVER(PARTITION BY [o].[CustomerID] ORDER BY [o].[OrderID], [c0].[CustomerID]) AS [row]
FROM [Orders] AS [o]
LEFT JOIN [Customers] AS [c0] ON [o].[CustomerID] = [c0].[CustomerID]
- WHERE [o].[OrderID] IN (10643, 10692, 10702, 10835, 10952, 11011)
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__orderIds_0) AS [o0]
+ WHERE CAST([o0].[value] AS int) = [o].[OrderID])
) AS [t]
WHERE [t].[row] <= 1
) AS [t0] ON [c].[CustomerID] = [t0].[CustomerID0]
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs
index 646c2411e1e..9502a1c4211 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs
@@ -2078,6 +2078,8 @@ public override async Task Projecting_after_navigation_and_distinct(bool async)
AssertSql(
"""
+@__filteredOrderIds_0='[10248,10249,10250]' (Size = 4000)
+
SELECT [t].[CustomerID], [t0].[CustomerID], [t0].[OrderID], [t0].[OrderDate]
FROM (
SELECT DISTINCT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
@@ -2087,7 +2089,10 @@ FROM [Orders] AS [o]
OUTER APPLY (
SELECT [t].[CustomerID], [o0].[OrderID], [o0].[OrderDate]
FROM [Orders] AS [o0]
- WHERE [t].[CustomerID] IS NOT NULL AND [t].[CustomerID] = [o0].[CustomerID] AND [o0].[OrderID] IN (10248, 10249, 10250)
+ WHERE [t].[CustomerID] IS NOT NULL AND [t].[CustomerID] = [o0].[CustomerID] AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__filteredOrderIds_0) AS [f]
+ WHERE CAST([f].[value] AS int) = [o0].[OrderID])
) AS [t0]
ORDER BY [t].[CustomerID], [t0].[OrderID]
""");
@@ -2099,6 +2104,8 @@ public override async Task Correlated_collection_after_distinct_with_complex_pro
AssertSql(
"""
+@__filteredOrderIds_0='[10248,10249,10250]' (Size = 4000)
+
SELECT [t].[OrderID], [t].[Complex], [t0].[Outer], [t0].[Inner], [t0].[OrderDate]
FROM (
SELECT DISTINCT [o].[OrderID], DATEPART(month, [o].[OrderDate]) AS [Complex]
@@ -2107,7 +2114,10 @@ FROM [Orders] AS [o]
OUTER APPLY (
SELECT [t].[OrderID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate]
FROM [Orders] AS [o0]
- WHERE [o0].[OrderID] = [t].[OrderID] AND [o0].[OrderID] IN (10248, 10249, 10250)
+ WHERE [o0].[OrderID] = [t].[OrderID] AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__filteredOrderIds_0) AS [f]
+ WHERE CAST([f].[value] AS int) = [o0].[OrderID])
) AS [t0]
ORDER BY [t].[OrderID]
""");
@@ -2119,6 +2129,8 @@ public override async Task Correlated_collection_after_distinct_not_containing_o
AssertSql(
"""
+@__filteredOrderIds_0='[10248,10249,10250]' (Size = 4000)
+
SELECT [t].[OrderDate], [t].[CustomerID], [t0].[Outer1], [t0].[Outer2], [t0].[Inner], [t0].[OrderDate]
FROM (
SELECT DISTINCT [o].[OrderDate], [o].[CustomerID]
@@ -2127,7 +2139,10 @@ FROM [Orders] AS [o]
OUTER APPLY (
SELECT [t].[OrderDate] AS [Outer1], [t].[CustomerID] AS [Outer2], [o0].[OrderID] AS [Inner], [o0].[OrderDate]
FROM [Orders] AS [o0]
- WHERE ([o0].[CustomerID] = [t].[CustomerID] OR ([o0].[CustomerID] IS NULL AND [t].[CustomerID] IS NULL)) AND [o0].[OrderID] IN (10248, 10249, 10250)
+ WHERE ([o0].[CustomerID] = [t].[CustomerID] OR ([o0].[CustomerID] IS NULL AND [t].[CustomerID] IS NULL)) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__filteredOrderIds_0) AS [f]
+ WHERE CAST([f].[value] AS int) = [o0].[OrderID])
) AS [t0]
ORDER BY [t].[OrderDate], [t].[CustomerID]
""");
@@ -2151,6 +2166,8 @@ public override async Task Correlated_collection_after_groupby_with_complex_proj
AssertSql(
"""
+@__filteredOrderIds_0='[10248,10249,10250]' (Size = 4000)
+
SELECT [t0].[OrderID], [t0].[Complex], [t1].[Outer], [t1].[Inner], [t1].[OrderDate]
FROM (
SELECT [t].[OrderID], [t].[Complex]
@@ -2163,7 +2180,10 @@ FROM [Orders] AS [o]
OUTER APPLY (
SELECT [t0].[OrderID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate]
FROM [Orders] AS [o0]
- WHERE [o0].[OrderID] = [t0].[OrderID] AND [o0].[OrderID] IN (10248, 10249, 10250)
+ WHERE [o0].[OrderID] = [t0].[OrderID] AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__filteredOrderIds_0) AS [f]
+ WHERE CAST([f].[value] AS int) = [o0].[OrderID])
) AS [t1]
ORDER BY [t0].[OrderID]
""");
@@ -2586,6 +2606,8 @@ public override async Task Correlated_collection_after_groupby_with_complex_proj
AssertSql(
"""
+@__filteredOrderIds_0='[10248,10249,10250]' (Size = 4000)
+
SELECT [t0].[CustomerID], [t0].[Complex], [t1].[Outer], [t1].[Inner], [t1].[OrderDate]
FROM (
SELECT [t].[CustomerID], [t].[Complex]
@@ -2598,7 +2620,10 @@ FROM [Orders] AS [o]
OUTER APPLY (
SELECT [t0].[CustomerID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate]
FROM [Orders] AS [o0]
- WHERE ([o0].[CustomerID] = [t0].[CustomerID] OR ([o0].[CustomerID] IS NULL AND [t0].[CustomerID] IS NULL)) AND [o0].[OrderID] IN (10248, 10249, 10250)
+ WHERE ([o0].[CustomerID] = [t0].[CustomerID] OR ([o0].[CustomerID] IS NULL AND [t0].[CustomerID] IS NULL)) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__filteredOrderIds_0) AS [f]
+ WHERE CAST([f].[value] AS int) = [o0].[OrderID])
) AS [t1]
ORDER BY [t0].[CustomerID], [t0].[Complex]
""");
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs
index cf8f9e0e9ba..5c79cfbef7f 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeNoTrackingQuerySqlServerTest.cs
@@ -107,31 +107,42 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
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] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, [c].[CustomerID]
OFFSET @__p_1 ROWS
""",
//
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID]
FROM (
SELECT [c].[CustomerID], CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
@@ -1093,31 +1104,42 @@ public override async Task Include_collection_OrderBy_list_contains(bool async)
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
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] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, [c].[CustomerID]
OFFSET @__p_1 ROWS
""",
//
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID]
FROM (
SELECT [c].[CustomerID], CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
@@ -1529,24 +1551,44 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
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] LIKE N'A%'
-ORDER BY (SELECT 1), [c].[CustomerID]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END, [c].[CustomerID]
OFFSET @__p_1 ROWS
""",
//
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID]
FROM (
- SELECT [c].[CustomerID], CAST(0 AS bit) AS [c]
+ SELECT [c].[CustomerID], CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
INNER JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
@@ -1688,24 +1730,44 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
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] LIKE N'A%'
-ORDER BY (SELECT 1), [c].[CustomerID]
+ORDER BY CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END, [c].[CustomerID]
OFFSET @__p_1 ROWS
""",
//
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID]
FROM (
- SELECT [c].[CustomerID], CAST(1 AS bit) AS [c]
+ SELECT [c].[CustomerID], CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
INNER JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs
index da9ff8928ea..e68ab3cb97d 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs
@@ -2046,24 +2046,44 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
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] LIKE N'A%'
-ORDER BY (SELECT 1), [c].[CustomerID]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END, [c].[CustomerID]
OFFSET @__p_1 ROWS
""",
//
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID]
FROM (
- SELECT [c].[CustomerID], CAST(0 AS bit) AS [c]
+ SELECT [c].[CustomerID], CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
INNER JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
@@ -2077,24 +2097,44 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
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] LIKE N'A%'
-ORDER BY (SELECT 1), [c].[CustomerID]
+ORDER BY CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END, [c].[CustomerID]
OFFSET @__p_1 ROWS
""",
//
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID]
FROM (
- SELECT [c].[CustomerID], CAST(1 AS bit) AS [c]
+ SELECT [c].[CustomerID], CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
INNER JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
@@ -2108,31 +2148,42 @@ public override async Task Include_collection_OrderBy_list_contains(bool async)
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
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] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, [c].[CustomerID]
OFFSET @__p_1 ROWS
""",
//
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID]
FROM (
SELECT [c].[CustomerID], CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
@@ -2148,31 +2199,42 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
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] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, [c].[CustomerID]
OFFSET @__p_1 ROWS
""",
//
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[CustomerID]
FROM (
SELECT [c].[CustomerID], CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs
index 9dfc2f41cbd..9091ace0ea5 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindStringIncludeQuerySqlServerTest.cs
@@ -616,18 +616,25 @@ public override async Task Include_collection_OrderBy_list_does_not_contains(boo
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] <> N'ALFKI' THEN CAST(1 AS bit)
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
@@ -957,14 +964,27 @@ public override async Task Include_collection_OrderBy_empty_list_contains(bool a
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
- SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(0 AS bit) AS [c]
+ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
@@ -1340,18 +1360,25 @@ public override async Task Include_collection_OrderBy_list_contains(bool async)
AssertSql(
"""
+@__list_0='["ALFKI"]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
ORDER BY CASE
- WHEN [c].[CustomerID] = N'ALFKI' THEN CAST(1 AS bit)
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
OFFSET @__p_1 ROWS
@@ -1812,14 +1839,27 @@ public override async Task Include_collection_OrderBy_empty_list_does_not_contai
AssertSql(
"""
+@__list_0='[]' (Size = 4000)
@__p_1='1'
SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM (
- SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CAST(1 AS bit) AS [c]
+ SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END AS [c]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'A%'
- ORDER BY (SELECT 1)
+ ORDER BY CASE
+ WHEN NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE CAST([l].[value] AS nchar(5)) = [c].[CustomerID])) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+ END
OFFSET @__p_1 ROWS
) AS [t]
LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID]
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs
index 8d9fb99f2a0..91b291451db 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs
@@ -12,7 +12,7 @@ public NorthwindWhereQuerySqlServerTest(
: base(fixture)
{
ClearLog();
- //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
protected override bool CanExecuteQueryString
@@ -2081,9 +2081,14 @@ public override async Task Generic_Ilist_contains_translates_to_server(bool asyn
AssertSql(
"""
+@__cities_0='["Seattle"]' (Size = 4000)
+
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].[City] = N'Seattle'
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS nvarchar(15)) = [c].[City] OR ([c0].[value] IS NULL AND [c].[City] IS NULL))
""");
}
@@ -2441,9 +2446,14 @@ public override async Task Where_list_object_contains_over_value_type(bool async
AssertSql(
"""
+@__orderIds_0='[10248,10249]' (Size = 4000)
+
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM [Orders] AS [o]
-WHERE [o].[OrderID] IN (10248, 10249)
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__orderIds_0) AS [o0]
+ WHERE CAST([o0].[value] AS int) = [o].[OrderID])
""");
}
@@ -2453,9 +2463,14 @@ public override async Task Where_array_of_object_contains_over_value_type(bool a
AssertSql(
"""
+@__orderIds_0='[10248,10249]' (Size = 4000)
+
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate]
FROM [Orders] AS [o]
-WHERE [o].[OrderID] IN (10248, 10249)
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__orderIds_0) AS [o0]
+ WHERE CAST([o0].[value] AS int) = [o].[OrderID])
""");
}
@@ -2555,9 +2570,14 @@ public override async Task Array_of_parameters_Contains_OrElse_comparison_with_c
// issue #21462
AssertSql(
"""
+@__p_0='["ALFKI","ANATR"]' (Size = 4000)
+
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') OR [c].[CustomerID] = N'ANTON'
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__p_0) AS [p]
+ WHERE CAST([p].[value] AS nchar(5)) = [c].[CustomerID]) OR [c].[CustomerID] = N'ANTON'
""");
}
@@ -2580,9 +2600,14 @@ public override async Task Parameter_array_Contains_OrElse_comparison_with_const
AssertSql(
"""
+@__array_0='["ALFKI","ANATR"]' (Size = 4000)
+
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') OR [c].[CustomerID] = N'ANTON'
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__array_0) AS [a]
+ WHERE CAST([a].[value] AS nchar(5)) = [c].[CustomerID]) OR [c].[CustomerID] = N'ANTON'
""");
}
@@ -2593,11 +2618,15 @@ public override async Task Parameter_array_Contains_OrElse_comparison_with_param
AssertSql(
"""
@__prm1_0='ANTON' (Size = 5) (DbType = StringFixedLength)
+@__array_1='["ALFKI","ANATR"]' (Size = 4000)
@__prm2_2='ALFKI' (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 [c].[CustomerID] = @__prm1_0 OR [c].[CustomerID] IN (N'ALFKI', N'ANATR') OR [c].[CustomerID] = @__prm2_2
+WHERE [c].[CustomerID] = @__prm1_0 OR EXISTS (
+ SELECT 1
+ FROM OpenJson(@__array_1) AS [a]
+ WHERE CAST([a].[value] AS nchar(5)) = [c].[CustomerID]) OR [c].[CustomerID] = @__prm2_2
""");
}
@@ -2899,9 +2928,14 @@ public override async Task Where_Contains_and_comparison(bool async)
AssertSql(
"""
+@__customerIds_0='["ALFKI","FISSA"]' (Size = 4000)
+
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'FISSA') AND [c].[City] = N'Seattle'
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__customerIds_0) AS [c0]
+ WHERE CAST([c0].[value] AS nchar(5)) = [c].[CustomerID]) AND [c].[City] = N'Seattle'
""");
}
@@ -2911,9 +2945,14 @@ public override async Task Where_Contains_or_comparison(bool async)
AssertSql(
"""
+@__customerIds_0='["ALFKI","FISSA"]' (Size = 4000)
+
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'FISSA') OR [c].[City] = N'Seattle'
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__customerIds_0) AS [c0]
+ WHERE CAST([c0].[value] AS nchar(5)) = [c].[CustomerID]) OR [c].[City] = N'Seattle'
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs
index e302fe90f3d..79f3b958c09 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs
@@ -911,9 +911,14 @@ public override async Task Contains_with_local_array_closure_with_null(bool asyn
AssertSql(
"""
+@__ids_0='["Foo",null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[NullableStringA] = N'Foo' OR [e].[NullableStringA] IS NULL
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE [i].[value] = [e].[NullableStringA] OR ([i].[value] IS NULL AND [e].[NullableStringA] IS NULL))
""");
}
@@ -923,9 +928,14 @@ public override async Task Contains_with_local_array_closure_false_with_null(boo
AssertSql(
"""
+@__ids_0='["Foo",null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[NullableStringA] <> N'Foo' AND [e].[NullableStringA] IS NOT NULL
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE [i].[value] = [e].[NullableStringA] OR ([i].[value] IS NULL AND [e].[NullableStringA] IS NULL)))
""");
}
@@ -935,9 +945,14 @@ public override async Task Contains_with_local_nullable_array_closure_negated(bo
AssertSql(
"""
+@__ids_0='["Foo"]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[NullableStringA] <> N'Foo' OR [e].[NullableStringA] IS NULL
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE [i].[value] = [e].[NullableStringA] OR ([i].[value] IS NULL AND [e].[NullableStringA] IS NULL)))
""");
}
@@ -947,9 +962,14 @@ public override async Task Contains_with_local_array_closure_with_multiple_nulls
AssertSql(
"""
+@__ids_0='[null,"Foo",null,null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[NullableStringA] = N'Foo' OR [e].[NullableStringA] IS NULL
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE [i].[value] = [e].[NullableStringA] OR ([i].[value] IS NULL AND [e].[NullableStringA] IS NULL))
""");
}
@@ -1223,9 +1243,14 @@ public override async Task Where_conditional_search_condition_in_result(bool asy
AssertSql(
"""
+@__list_0='["Foo","Bar"]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[StringA] IN (N'Foo', N'Bar')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__list_0) AS [l]
+ WHERE [l].[value] = [e].[StringA])
""",
//
"""
@@ -1264,9 +1289,14 @@ public override void Where_contains_on_parameter_array_with_relational_null_sema
AssertSql(
"""
+@__names_0='["Foo","Bar"]' (Size = 4000)
+
SELECT [e].[NullableStringA]
FROM [Entities1] AS [e]
-WHERE [e].[NullableStringA] IN (N'Foo', N'Bar')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__names_0) AS [n]
+ WHERE [n].[value] = [e].[NullableStringA])
""");
}
@@ -1276,9 +1306,14 @@ public override void Where_contains_on_parameter_empty_array_with_relational_nul
AssertSql(
"""
+@__names_0='[]' (Size = 4000)
+
SELECT [e].[NullableStringA]
FROM [Entities1] AS [e]
-WHERE 0 = 1
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__names_0) AS [n]
+ WHERE [n].[value] = [e].[NullableStringA])
""");
}
@@ -1288,9 +1323,14 @@ public override void Where_contains_on_parameter_array_with_just_null_with_relat
AssertSql(
"""
+@__names_0='[null]' (Size = 4000)
+
SELECT [e].[NullableStringA]
FROM [Entities1] AS [e]
-WHERE [e].[NullableStringA] = NULL
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__names_0) AS [n]
+ WHERE [n].[value] = [e].[NullableStringA])
""");
}
@@ -1740,27 +1780,47 @@ public override async Task Null_semantics_contains(bool async)
AssertSql(
"""
+@__ids_0='[1,2]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[NullableIntA] IN (1, 2)
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[value] IS NULL AND [e].[NullableIntA] IS NULL))
""",
//
"""
+@__ids_0='[1,2]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[NullableIntA] NOT IN (1, 2) OR [e].[NullableIntA] IS NULL
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[value] IS NULL AND [e].[NullableIntA] IS NULL)))
""",
//
"""
+@__ids2_0='[1,2,null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[NullableIntA] IN (1, 2) OR [e].[NullableIntA] IS NULL
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids2_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[value] IS NULL AND [e].[NullableIntA] IS NULL))
""",
//
"""
+@__ids2_0='[1,2,null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[NullableIntA] NOT IN (1, 2) AND [e].[NullableIntA] IS NOT NULL
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids2_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[value] IS NULL AND [e].[NullableIntA] IS NULL)))
""",
//
"""
@@ -1794,26 +1854,47 @@ public override async Task Null_semantics_contains_array_with_no_values(bool asy
AssertSql(
"""
+@__ids_0='[]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE 0 = 1
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[value] IS NULL AND [e].[NullableIntA] IS NULL))
""",
//
"""
+@__ids_0='[]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[value] IS NULL AND [e].[NullableIntA] IS NULL)))
""",
//
"""
+@__ids2_0='[null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[NullableIntA] IS NULL
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids2_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[value] IS NULL AND [e].[NullableIntA] IS NULL))
""",
//
"""
+@__ids2_0='[null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[NullableIntA] IS NOT NULL
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids2_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[NullableIntA] OR ([i].[value] IS NULL AND [e].[NullableIntA] IS NULL)))
""",
//
"""
@@ -1846,49 +1927,91 @@ public override async Task Null_semantics_contains_non_nullable_argument(bool as
AssertSql(
"""
+@__ids_0='[1,2,null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[IntA] IN (1, 2)
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[IntA])
""",
//
"""
+@__ids_0='[1,2,null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[IntA] NOT IN (1, 2)
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[IntA]))
""",
//
"""
+@__ids2_0='[1,2]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[IntA] IN (1, 2)
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids2_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[IntA])
""",
//
"""
+@__ids2_0='[1,2]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE [e].[IntA] NOT IN (1, 2)
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids2_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[IntA]))
""",
//
"""
+@__ids3_0='[]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE 0 = 1
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids3_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[IntA])
""",
//
"""
+@__ids3_0='[]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids3_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[IntA]))
""",
//
"""
+@__ids4_0='[null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
-WHERE 0 = 1
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids4_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[IntA])
""",
//
"""
+@__ids4_0='[null]' (Size = 4000)
+
SELECT [e].[Id]
FROM [Entities1] AS [e]
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids4_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [e].[IntA]))
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
new file mode 100644
index 00000000000..5003eeb4c76
--- /dev/null
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
@@ -0,0 +1,440 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+///
+/// Runs all primitive collection tests with SQL Server compatibility level 120 (SQL Server 2014), which doesn't support OPENJSON.
+/// This exercises the older translation paths for e.g. Contains, to make sure things work for providers with no queryable constant/
+/// parameter support.
+///
+public class PrimitiveCollectionsQueryOldSqlServerTest : PrimitiveCollectionsQueryTestBase<
+ PrimitiveCollectionsQueryOldSqlServerTest.PrimitiveCollectionsQueryOldSqlServerFixture>
+{
+ public PrimitiveCollectionsQueryOldSqlServerTest(PrimitiveCollectionsQueryOldSqlServerFixture fixture, ITestOutputHelper testOutputHelper)
+ : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ }
+
+ public override async Task Inline_collection_of_ints_Contains(bool async)
+ {
+ await base.Inline_collection_of_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Int] IN (10, 999)
+""");
+ }
+
+ public override async Task Inline_collection_of_nullable_ints_Contains(bool async)
+ {
+ await base.Inline_collection_of_nullable_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[NullableInt] IN (10, 999)
+""");
+ }
+
+ public override async Task Inline_collection_of_nullable_ints_Contains_null(bool async)
+ {
+ await base.Inline_collection_of_nullable_ints_Contains_null(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[NullableInt] = 999 OR [p].[NullableInt] IS NULL
+""");
+ }
+
+ public override Task Inline_collection_Count_with_zero_values(bool async)
+ => AssertTranslationFailedWithDetails(
+ () => base.Inline_collection_Count_with_zero_values(async),
+ RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot);
+
+ public override async Task Inline_collection_Count_with_one_value(bool async)
+ {
+ await base.Inline_collection_Count_with_one_value(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (VALUES (CAST(2 AS int))) AS [v]([Value])
+ WHERE [v].[Value] > [p].[Id]) = 1
+""");
+ }
+
+ public override async Task Inline_collection_Count_with_two_values(bool async)
+ {
+ await base.Inline_collection_Count_with_two_values(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (VALUES (CAST(2 AS int)), (999)) AS [v]([Value])
+ WHERE [v].[Value] > [p].[Id]) = 1
+""");
+ }
+
+ public override async Task Inline_collection_Count_with_three_values(bool async)
+ {
+ await base.Inline_collection_Count_with_three_values(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (VALUES (CAST(2 AS int)), (999), (1000)) AS [v]([Value])
+ WHERE [v].[Value] > [p].[Id]) = 2
+""");
+ }
+
+ public override Task Inline_collection_Contains_with_zero_values(bool async)
+ => AssertTranslationFailedWithDetails(
+ () => base.Inline_collection_Contains_with_zero_values(async),
+ RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot);
+
+ public override async Task Inline_collection_Contains_with_one_value(bool async)
+ {
+ await base.Inline_collection_Contains_with_one_value(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Id] = 2
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_two_values(bool async)
+ {
+ await base.Inline_collection_Contains_with_two_values(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Id] IN (2, 999)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_three_values(bool async)
+ {
+ await base.Inline_collection_Contains_with_three_values(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Id] IN (2, 999, 1000)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_all_parameters(bool async)
+ {
+ await base.Inline_collection_Contains_with_all_parameters(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Id] IN (2, 999)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_parameter_and_column_based_expression(bool async)
+ {
+ await base.Inline_collection_Contains_with_parameter_and_column_based_expression(async);
+
+ AssertSql();
+ }
+
+ public override Task Parameter_collection_Count(bool async)
+ => AssertTranslationFailed(() => base.Parameter_collection_Count(async));
+
+ public override async Task Parameter_collection_of_ints_Contains(bool async)
+ {
+ await base.Parameter_collection_of_nullable_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[NullableInt] IN (10, 999)
+""");
+ }
+
+ public override async Task Parameter_collection_of_nullable_ints_Contains(bool async)
+ {
+ await base.Parameter_collection_of_nullable_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[NullableInt] IN (10, 999)
+""");
+ }
+
+ public override async Task Parameter_collection_of_nullable_ints_Contains_null(bool async)
+ {
+ await base.Parameter_collection_of_nullable_ints_Contains_null(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[NullableInt] = 999 OR [p].[NullableInt] IS NULL
+""");
+ }
+
+ public override async Task Parameter_collection_of_strings_Contains(bool async)
+ {
+ await base.Parameter_collection_of_strings_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[String] IN (N'10', N'999')
+""");
+ }
+
+ public override async Task Parameter_collection_of_DateTimes_Contains(bool async)
+ {
+ await base.Parameter_collection_of_DateTimes_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[DateTime] IN ('2020-01-10T12:30:00.0000000Z', '9999-01-01T00:00:00.0000000Z')
+""");
+ }
+
+ public override async Task Parameter_collection_of_bools_Contains(bool async)
+ {
+ await base.Parameter_collection_of_bools_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Bool] = CAST(1 AS bit)
+""");
+ }
+
+ public override async Task Parameter_collection_of_enums_Contains(bool async)
+ {
+ await base.Parameter_collection_of_enums_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Enum] IN (0, 3)
+""");
+ }
+
+ public override async Task Parameter_collection_null_Contains(bool async)
+ {
+ await base.Parameter_collection_null_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE 0 = 1
+""");
+ }
+
+ public override Task Column_collection_of_ints_Contains(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_of_ints_Contains(async));
+
+ public override Task Column_collection_of_nullable_ints_Contains(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_of_nullable_ints_Contains(async));
+
+ public override Task Column_collection_of_nullable_ints_Contains_null(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_of_nullable_ints_Contains_null(async));
+
+ public override Task Column_collection_of_bools_Contains(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_of_bools_Contains(async));
+
+ [ConditionalFact]
+ public virtual async Task Json_representation_of_bool_array()
+ {
+ await using var context = CreateContext();
+
+ Assert.Equal(
+ "[true,false]",
+ await context.Database.SqlQuery($"SELECT [Bools] AS [Value] FROM [PrimitiveCollectionsEntity] WHERE [Id] = 1").SingleAsync());
+ }
+
+ public override Task Column_collection_Count_method(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Count_method(async));
+
+ public override Task Column_collection_Length(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Length(async));
+
+ public override Task Column_collection_index_int(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_index_int(async));
+
+ public override Task Column_collection_index_string(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_index_string(async));
+
+ public override Task Column_collection_index_datetime(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_index_datetime(async));
+
+ public override Task Column_collection_index_beyond_end(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_index_beyond_end(async));
+
+ public override async Task Inline_collection_index_Column(bool async)
+ {
+ await base.Inline_collection_index_Column(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT [v].[Value]
+ FROM (VALUES (0, CAST(1 AS int)), (1, 2), (2, 3)) AS [v]([_ord], [Value])
+ ORDER BY [v].[_ord]
+ OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1
+""");
+ }
+
+ public override Task Parameter_collection_index_Column(bool async)
+ => AssertTranslationFailed(() => base.Parameter_collection_index_Column(async));
+
+ public override Task Column_collection_ElementAt(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_ElementAt(async));
+
+ public override Task Column_collection_Skip(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Skip(async));
+
+ public override Task Column_collection_Take(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Take(async));
+
+ public override Task Column_collection_Skip_Take(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Skip_Take(async));
+
+ public override Task Column_collection_Any(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Any(async));
+
+ public override async Task Column_collection_projection_from_top_level(bool async)
+ {
+ await base.Column_collection_projection_from_top_level(async);
+
+ AssertSql(
+"""
+SELECT [p].[Ints]
+FROM [PrimitiveCollectionsEntity] AS [p]
+ORDER BY [p].[Id]
+""");
+ }
+
+ public override Task Column_collection_and_parameter_collection_Join(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_and_parameter_collection_Join(async));
+
+ public override Task Parameter_collection_Concat_column_collection(bool async)
+ => AssertTranslationFailed(() => base.Parameter_collection_Concat_column_collection(async));
+
+ public override Task Column_collection_Union_parameter_collection(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Union_parameter_collection(async));
+
+ public override Task Column_collection_Intersect_inline_collection(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_Intersect_inline_collection(async));
+
+ public override Task Inline_collection_Except_column_collection(bool async)
+ => AssertTranslationFailed(() => base.Inline_collection_Except_column_collection(async));
+
+ public override async Task Column_collection_equality_parameter_collection(bool async)
+ {
+ await base.Column_collection_equality_parameter_collection(async);
+
+ AssertSql(
+"""
+@__ints_0='[1,10]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Ints] = @__ints_0
+""");
+ }
+
+ public override async Task Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(bool async)
+ {
+ await base.Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(async);
+
+ AssertSql();
+ }
+
+ public override async Task Column_collection_equality_inline_collection(bool async)
+ {
+ await base.Column_collection_equality_inline_collection(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Ints] = N'[1,10]'
+""");
+ }
+
+ public override Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async)
+ => AssertTranslationFailed(() => base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async));
+
+ public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query()
+ {
+ // Base implementation asserts that a different exception is thrown
+ }
+
+ public override Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async)
+ => AssertTranslationFailed(() => base.Parameter_collection_in_subquery_Count_as_compiled_query(async));
+
+ public override Task Column_collection_in_subquery_Union_parameter_collection(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_in_subquery_Union_parameter_collection(async));
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+
+ private PrimitiveCollectionsContext CreateContext()
+ => Fixture.CreateContext();
+
+ public class PrimitiveCollectionsQueryOldSqlServerFixture : PrimitiveCollectionsQueryFixtureBase
+ {
+ // Use a different store name to prevent concurrency issues with the non-old PrimitiveCollectionsQuerySqlServerTest
+ protected override string StoreName
+ => "OldPrimitiveCollectionsTest";
+
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
+ protected override ITestStoreFactory TestStoreFactory
+ => SqlServerTestStoreFactory.Instance;
+
+ // Compatibility level 120 (SQL Server 2014) doesn't support OPENJSON
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).UseSqlServer(o => o.UseCompatibilityLevel(120));
+ }
+}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
new file mode 100644
index 00000000000..8a06980d06e
--- /dev/null
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
@@ -0,0 +1,861 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public class PrimitiveCollectionsQuerySqlServerTest : PrimitiveCollectionsQueryTestBase<
+ PrimitiveCollectionsQuerySqlServerTest.PrimitiveCollectionsQuerySqlServerFixture>
+{
+ public PrimitiveCollectionsQuerySqlServerTest(PrimitiveCollectionsQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper)
+ : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ // Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ }
+
+ public override async Task Inline_collection_of_ints_Contains(bool async)
+ {
+ await base.Inline_collection_of_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Int] IN (10, 999)
+""");
+ }
+
+ public override async Task Inline_collection_of_nullable_ints_Contains(bool async)
+ {
+ await base.Inline_collection_of_nullable_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[NullableInt] IN (10, 999)
+""");
+ }
+
+ public override async Task Inline_collection_of_nullable_ints_Contains_null(bool async)
+ {
+ await base.Inline_collection_of_nullable_ints_Contains_null(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[NullableInt] = 999 OR [p].[NullableInt] IS NULL
+""");
+ }
+
+ public override Task Inline_collection_Count_with_zero_values(bool async)
+ => AssertTranslationFailedWithDetails(
+ () => base.Inline_collection_Count_with_zero_values(async),
+ RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot);
+
+ public override async Task Inline_collection_Count_with_one_value(bool async)
+ {
+ await base.Inline_collection_Count_with_one_value(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (VALUES (CAST(2 AS int))) AS [v]([Value])
+ WHERE [v].[Value] > [p].[Id]) = 1
+""");
+ }
+
+ public override async Task Inline_collection_Count_with_two_values(bool async)
+ {
+ await base.Inline_collection_Count_with_two_values(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (VALUES (CAST(2 AS int)), (999)) AS [v]([Value])
+ WHERE [v].[Value] > [p].[Id]) = 1
+""");
+ }
+
+ public override async Task Inline_collection_Count_with_three_values(bool async)
+ {
+ await base.Inline_collection_Count_with_three_values(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (VALUES (CAST(2 AS int)), (999), (1000)) AS [v]([Value])
+ WHERE [v].[Value] > [p].[Id]) = 2
+""");
+ }
+
+ public override Task Inline_collection_Contains_with_zero_values(bool async)
+ => AssertTranslationFailedWithDetails(
+ () => base.Inline_collection_Contains_with_zero_values(async),
+ RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot);
+
+ public override async Task Inline_collection_Contains_with_one_value(bool async)
+ {
+ await base.Inline_collection_Contains_with_one_value(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Id] = 2
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_two_values(bool async)
+ {
+ await base.Inline_collection_Contains_with_two_values(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Id] IN (2, 999)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_three_values(bool async)
+ {
+ await base.Inline_collection_Contains_with_three_values(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Id] IN (2, 999, 1000)
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_all_parameters(bool async)
+ {
+ await base.Inline_collection_Contains_with_all_parameters(async);
+
+ // See #30732 for making this better
+
+ AssertSql(
+"""
+@__p_0='[2,999]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__p_0) AS [p0]
+ WHERE CAST([p0].[value] AS int) = [p].[Id])
+""");
+ }
+
+ public override async Task Inline_collection_Contains_with_parameter_and_column_based_expression(bool async)
+ {
+ await base.Inline_collection_Contains_with_parameter_and_column_based_expression(async);
+
+ AssertSql();
+ }
+
+ public override async Task Parameter_collection_Count(bool async)
+ {
+ await base.Parameter_collection_Count(async);
+
+ AssertSql(
+"""
+@__ids_0='[2,999]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) > [p].[Id]) = 1
+""");
+ }
+
+ public override async Task Parameter_collection_of_ints_Contains(bool async)
+ {
+ await base.Parameter_collection_of_ints_Contains(async);
+
+ AssertSql(
+"""
+@__ints_0='[10,999]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ints_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [p].[Int])
+""");
+ }
+
+ public override async Task Parameter_collection_of_nullable_ints_Contains(bool async)
+ {
+ await base.Parameter_collection_of_nullable_ints_Contains(async);
+
+ AssertSql(
+"""
+@__nullableInts_0='[10,999]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__nullableInts_0) AS [n]
+ WHERE CAST([n].[value] AS int) = [p].[NullableInt] OR ([n].[value] IS NULL AND [p].[NullableInt] IS NULL))
+""");
+ }
+
+ public override async Task Parameter_collection_of_nullable_ints_Contains_null(bool async)
+ {
+ await base.Parameter_collection_of_nullable_ints_Contains_null(async);
+
+ AssertSql(
+"""
+@__nullableInts_0='[null,999]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__nullableInts_0) AS [n]
+ WHERE CAST([n].[value] AS int) = [p].[NullableInt] OR ([n].[value] IS NULL AND [p].[NullableInt] IS NULL))
+""");
+ }
+
+ public override async Task Parameter_collection_of_strings_Contains(bool async)
+ {
+ await base.Parameter_collection_of_strings_Contains(async);
+
+ AssertSql(
+"""
+@__strings_0='["10","999"]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__strings_0) AS [s]
+ WHERE [s].[value] = [p].[String] OR ([s].[value] IS NULL AND [p].[String] IS NULL))
+""");
+ }
+
+ public override async Task Parameter_collection_of_DateTimes_Contains(bool async)
+ {
+ await base.Parameter_collection_of_DateTimes_Contains(async);
+
+ AssertSql(
+"""
+@__dateTimes_0='["2020-01-10T12:30:00Z","9999-01-01T00:00:00Z"]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0) AS [d]
+ WHERE CAST([d].[value] AS datetime) = [p].[DateTime])
+""");
+ }
+
+ public override async Task Parameter_collection_of_bools_Contains(bool async)
+ {
+ await base.Parameter_collection_of_bools_Contains(async);
+
+ AssertSql(
+"""
+@__bools_0='[true]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__bools_0) AS [b]
+ WHERE CAST([b].[value] AS bit) = [p].[Bool])
+""");
+ }
+
+ public override async Task Parameter_collection_of_enums_Contains(bool async)
+ {
+ await base.Parameter_collection_of_enums_Contains(async);
+
+ AssertSql(
+"""
+@__enums_0='[0,3]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__enums_0) AS [e]
+ WHERE CAST([e].[value] AS int) = [p].[Enum])
+""");
+ }
+
+ public override async Task Parameter_collection_null_Contains(bool async)
+ {
+ await base.Parameter_collection_null_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(N'[]') AS [i]
+ WHERE CAST([i].[value] AS int) = [p].[Int])
+""");
+ }
+
+ public override async Task Column_collection_of_ints_Contains(bool async)
+ {
+ await base.Column_collection_of_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson([p].[Ints]) AS [i]
+ WHERE CAST([i].[value] AS int) = 10)
+""");
+ }
+
+ public override async Task Column_collection_of_nullable_ints_Contains(bool async)
+ {
+ await base.Column_collection_of_nullable_ints_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson([p].[NullableInts]) AS [n]
+ WHERE CAST([n].[value] AS int) = 10)
+""");
+ }
+
+ public override async Task Column_collection_of_nullable_ints_Contains_null(bool async)
+ {
+ await base.Column_collection_of_nullable_ints_Contains_null(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson([p].[NullableInts]) AS [n]
+ WHERE [n].[value] IS NULL)
+""");
+ }
+
+ public override async Task Column_collection_of_bools_Contains(bool async)
+ {
+ await base.Column_collection_of_bools_Contains(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson([p].[Bools]) AS [b]
+ WHERE CAST([b].[value] AS bit) = CAST(1 AS bit))
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Json_representation_of_bool_array()
+ {
+ await using var context = CreateContext();
+
+ Assert.Equal(
+ "[true,false]",
+ await context.Database.SqlQuery($"SELECT [Bools] AS [Value] FROM [PrimitiveCollectionsEntity] WHERE [Id] = 1").SingleAsync());
+ }
+
+ public override async Task Column_collection_Count_method(bool async)
+ {
+ await base.Column_collection_Count_method(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([p].[Ints]) AS [i]) = 2
+""");
+ }
+
+ public override async Task Column_collection_Length(bool async)
+ {
+ await base.Column_collection_Length(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([p].[Ints]) AS [i]) = 2
+""");
+ }
+
+ public override async Task Column_collection_index_int(bool async)
+ {
+ await base.Column_collection_index_int(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT CAST([i].[value] AS int)
+ FROM OpenJson([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY) = 10
+""");
+ }
+
+ public override async Task Column_collection_index_string(bool async)
+ {
+ await base.Column_collection_index_string(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT [s].[value]
+ FROM OpenJson([p].[Strings]) AS [s]
+ ORDER BY CAST([s].[key] AS int)
+ OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY) = N'10'
+""");
+ }
+
+ public override async Task Column_collection_index_datetime(bool async)
+ {
+ await base.Column_collection_index_datetime(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT CAST([d].[value] AS datetime2)
+ FROM OpenJson([p].[DateTimes]) AS [d]
+ ORDER BY CAST([d].[key] AS int)
+ OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY) = '2020-01-10T12:30:00.0000000Z'
+""");
+ }
+
+ public override async Task Column_collection_index_beyond_end(bool async)
+ {
+ await base.Column_collection_index_beyond_end(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT CAST([i].[value] AS int)
+ FROM OpenJson([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 999 ROWS FETCH NEXT 1 ROWS ONLY) = 10
+""");
+ }
+
+ public override async Task Inline_collection_index_Column(bool async)
+ {
+ await base.Inline_collection_index_Column(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT [v].[Value]
+ FROM (VALUES (0, CAST(1 AS int)), (1, 2), (2, 3)) AS [v]([_ord], [Value])
+ ORDER BY [v].[_ord]
+ OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1
+""");
+ }
+
+ public override async Task Parameter_collection_index_Column(bool async)
+ {
+ await base.Parameter_collection_index_Column(async);
+
+ AssertSql(
+"""
+@__ints_0='[1,2,3]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT CAST([i].[value] AS int) AS [value]
+ FROM OpenJson(@__ints_0) AS [i]
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET [p].[Int] ROWS FETCH NEXT 1 ROWS ONLY) = 1
+""");
+ }
+
+ public override async Task Column_collection_ElementAt(bool async)
+ {
+ await base.Column_collection_ElementAt(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT CAST([i].[value] AS int)
+ FROM OpenJson([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY) = 10
+""");
+ }
+
+ public override async Task Column_collection_Skip(bool async)
+ {
+ await base.Column_collection_Skip(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT CAST([i].[key] AS int) AS [c]
+ FROM OpenJson([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 1 ROWS
+ ) AS [t]) = 2
+""");
+ }
+
+ public override async Task Column_collection_Take(bool async)
+ {
+ await base.Column_collection_Take(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM (
+ SELECT TOP(2) CAST([i].[value] AS int) AS [c], CAST([i].[key] AS int) AS [c0]
+ FROM OpenJson([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)
+ ) AS [t]
+ WHERE [t].[c] = 11)
+""");
+ }
+
+ public override async Task Column_collection_Skip_Take(bool async)
+ {
+ await base.Column_collection_Skip_Take(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM (
+ SELECT CAST([i].[value] AS int) AS [c], CAST([i].[key] AS int) AS [c0]
+ FROM OpenJson([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 1 ROWS FETCH NEXT 2 ROWS ONLY
+ ) AS [t]
+ WHERE [t].[c] = 11)
+""");
+ }
+
+ public override async Task Column_collection_Any(bool async)
+ {
+ await base.Column_collection_Any(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson([p].[Ints]) AS [i])
+""");
+ }
+
+ public override async Task Column_collection_projection_from_top_level(bool async)
+ {
+ await base.Column_collection_projection_from_top_level(async);
+
+ AssertSql(
+"""
+SELECT [p].[Ints]
+FROM [PrimitiveCollectionsEntity] AS [p]
+ORDER BY [p].[Id]
+""");
+ }
+
+ public override async Task Column_collection_and_parameter_collection_Join(bool async)
+ {
+ await base.Column_collection_and_parameter_collection_Join(async);
+
+ AssertSql(
+"""
+@__ints_0='[11,111]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM OpenJson([p].[Ints]) AS [i]
+ INNER JOIN OpenJson(@__ints_0) AS [i0] ON CAST([i].[value] AS int) = [i0].[value]) = 2
+""");
+ }
+
+ public override async Task Parameter_collection_Concat_column_collection(bool async)
+ {
+ await base.Parameter_collection_Concat_column_collection(async);
+
+ AssertSql(
+"""
+@__ints_0='[11,111]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT CAST([i].[value] AS int) AS [value]
+ FROM OpenJson(@__ints_0) AS [i]
+ UNION ALL
+ SELECT CAST([i0].[value] AS int) AS [value]
+ FROM OpenJson([p].[Ints]) AS [i0]
+ ) AS [t]) = 2
+""");
+ }
+
+ public override async Task Column_collection_Union_parameter_collection(bool async)
+ {
+ await base.Column_collection_Union_parameter_collection(async);
+
+ AssertSql(
+"""
+@__ints_0='[11,111]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT CAST([i].[value] AS int) AS [c]
+ FROM OpenJson([p].[Ints]) AS [i]
+ UNION
+ SELECT CAST([i0].[value] AS int) AS [c]
+ FROM OpenJson(@__ints_0) AS [i0]
+ ) AS [t]) = 2
+""");
+ }
+
+ public override async Task Column_collection_Intersect_inline_collection(bool async)
+ {
+ await base.Column_collection_Intersect_inline_collection(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT CAST([i].[value] AS int) AS [c]
+ FROM OpenJson([p].[Ints]) AS [i]
+ INTERSECT
+ SELECT [v].[Value] AS [c]
+ FROM (VALUES (CAST(11 AS int)), (111)) AS [v]([Value])
+ ) AS [t]) = 2
+""");
+ }
+
+ public override async Task Inline_collection_Except_column_collection(bool async)
+ {
+ await base.Inline_collection_Except_column_collection(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT [v].[Value]
+ FROM (VALUES (CAST(11 AS int)), (111)) AS [v]([Value])
+ EXCEPT
+ SELECT CAST([i].[value] AS int) AS [Value]
+ FROM OpenJson([p].[Ints]) AS [i]
+ ) AS [t]
+ WHERE [t].[Value] % 2 = 1) = 2
+""");
+ }
+
+ public override async Task Column_collection_equality_parameter_collection(bool async)
+ {
+ await base.Column_collection_equality_parameter_collection(async);
+
+ AssertSql(
+"""
+@__ints_0='[1,10]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Ints] = @__ints_0
+""");
+ }
+
+ public override async Task Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(bool async)
+ {
+ await base.Column_collection_Concat_parameter_collection_equality_inline_collection_not_supported(async);
+
+ AssertSql();
+ }
+
+ public override async Task Column_collection_equality_inline_collection(bool async)
+ {
+ await base.Column_collection_equality_inline_collection(async);
+
+ AssertSql(
+"""
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE [p].[Ints] = N'[1,10]'
+""");
+ }
+
+ public override async Task Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(bool async)
+ {
+ await base.Parameter_collection_in_subquery_Union_column_collection_as_compiled_query(async);
+
+ AssertSql(
+"""
+@__ints='[10,111]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT [t].[value]
+ FROM (
+ SELECT CAST([i].[value] AS int) AS [value]
+ FROM OpenJson(@__ints) AS [i]
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 1 ROWS
+ ) AS [t]
+ UNION
+ SELECT CAST([i0].[value] AS int) AS [value]
+ FROM OpenJson([p].[Ints]) AS [i0]
+ ) AS [t0]) = 3
+""");
+ }
+
+ public override void Parameter_collection_in_subquery_and_Convert_as_compiled_query()
+ {
+ base.Parameter_collection_in_subquery_and_Convert_as_compiled_query();
+
+ AssertSql();
+ }
+
+ public override async Task Parameter_collection_in_subquery_Count_as_compiled_query(bool async)
+ {
+ await base.Parameter_collection_in_subquery_Count_as_compiled_query(async);
+
+ // TODO: the subquery projection contains two extra columns which we should remove
+ AssertSql(
+"""
+@__ints='[10,111]' (Size = 4000)
+
+SELECT COUNT(*)
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT CAST([i].[value] AS int) AS [value], CAST([i].[key] AS int) AS [c], CAST([i].[value] AS int) AS [value0]
+ FROM OpenJson(@__ints) AS [i]
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 1 ROWS
+ ) AS [t]
+ WHERE [t].[value0] > [p].[Id]) = 1
+""");
+ }
+
+ public override async Task Column_collection_in_subquery_Union_parameter_collection(bool async)
+ {
+ await base.Column_collection_in_subquery_Union_parameter_collection(async);
+
+ AssertSql(
+"""
+@__ints_0='[10,111]' (Size = 4000)
+
+SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[String], [p].[Strings]
+FROM [PrimitiveCollectionsEntity] AS [p]
+WHERE (
+ SELECT COUNT(*)
+ FROM (
+ SELECT [t].[c]
+ FROM (
+ SELECT CAST([i].[value] AS int) AS [c]
+ FROM OpenJson([p].[Ints]) AS [i]
+ ORDER BY CAST([i].[key] AS int)
+ OFFSET 1 ROWS
+ ) AS [t]
+ UNION
+ SELECT CAST([i0].[value] AS int) AS [c]
+ FROM OpenJson(@__ints_0) AS [i0]
+ ) AS [t0]) = 3
+""");
+ }
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+
+ private PrimitiveCollectionsContext CreateContext()
+ => Fixture.CreateContext();
+
+ public class PrimitiveCollectionsQuerySqlServerFixture : PrimitiveCollectionsQueryFixtureBase
+ {
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
+ protected override ITestStoreFactory TestStoreFactory
+ => SqlServerTestStoreFactory.Instance;
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
+ {
+ base.OnModelCreating(modelBuilder, context);
+
+ // Map DateTime to non-default datetime instead of the default datetime2 to exercise type mapping inference
+ modelBuilder.Entity().Property(p => p.DateTime).HasColumnType("datetime");
+ }
+ }
+}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs
index bfeefd43a8c..60a0192c1a2 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs
@@ -168,11 +168,58 @@ public async Task Where_contains_DateTime_literals(bool async)
Assert.Single(results);
+ // TODO: The parameters values below are incorrect, since we currently don't take the element type mapping into account when
+ // generating the JSON representation (#30677)
AssertSql(
"""
+@__dateTimes_0='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+@__dateTimes_0_1='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+@__dateTimes_0_2='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+@__dateTimes_0_3='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+@__dateTimes_0_4='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+@__dateTimes_0_5='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+@__dateTimes_0_6='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+@__dateTimes_0_7='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+@__dateTimes_0_8='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+@__dateTimes_0_9='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+@__dateTimes_0_10='["1970-09-03T12:00:00","1971-09-03T12:00:10.22","1972-09-03T12:00:10.333","1973-09-03T12:00:10","1974-09-03T12:00:10.5","1975-09-03T12:00:10.66","1976-09-03T12:00:10.777","1977-09-03T12:00:10.888","1978-09-03T12:00:10.999","1979-09-03T12:00:10.111","1980-09-03T12:00:10.222"]' (Size = 4000)
+
SELECT [d].[Id], [d].[DateTime], [d].[DateTime2], [d].[DateTime2_0], [d].[DateTime2_1], [d].[DateTime2_2], [d].[DateTime2_3], [d].[DateTime2_4], [d].[DateTime2_5], [d].[DateTime2_6], [d].[DateTime2_7], [d].[SmallDateTime]
FROM [Dates] AS [d]
-WHERE [d].[SmallDateTime] IN ('1970-09-03T12:00:00', '1971-09-03T12:00:10', '1972-09-03T12:00:10', '1973-09-03T12:00:10', '1974-09-03T12:00:10', '1975-09-03T12:00:10', '1976-09-03T12:00:10', '1977-09-03T12:00:10', '1978-09-03T12:00:10', '1979-09-03T12:00:10', '1980-09-03T12:00:10') AND [d].[DateTime] IN ('1970-09-03T12:00:00.000', '1971-09-03T12:00:10.220', '1972-09-03T12:00:10.333', '1973-09-03T12:00:10.000', '1974-09-03T12:00:10.500', '1975-09-03T12:00:10.660', '1976-09-03T12:00:10.777', '1977-09-03T12:00:10.888', '1978-09-03T12:00:10.999', '1979-09-03T12:00:10.111', '1980-09-03T12:00:10.222') AND [d].[DateTime2] IN ('1970-09-03T12:00:00.0000000', '1971-09-03T12:00:10.2200000', '1972-09-03T12:00:10.3330000', '1973-09-03T12:00:10.0000000', '1974-09-03T12:00:10.5000000', '1975-09-03T12:00:10.6600000', '1976-09-03T12:00:10.7770000', '1977-09-03T12:00:10.8880000', '1978-09-03T12:00:10.9990000', '1979-09-03T12:00:10.1110000', '1980-09-03T12:00:10.2220000') AND [d].[DateTime2_0] IN ('1970-09-03T12:00:00', '1971-09-03T12:00:10', '1972-09-03T12:00:10', '1973-09-03T12:00:10', '1974-09-03T12:00:10', '1975-09-03T12:00:10', '1976-09-03T12:00:10', '1977-09-03T12:00:10', '1978-09-03T12:00:10', '1979-09-03T12:00:10', '1980-09-03T12:00:10') AND [d].[DateTime2_1] IN ('1970-09-03T12:00:00.0', '1971-09-03T12:00:10.2', '1972-09-03T12:00:10.3', '1973-09-03T12:00:10.0', '1974-09-03T12:00:10.5', '1975-09-03T12:00:10.6', '1976-09-03T12:00:10.7', '1977-09-03T12:00:10.8', '1978-09-03T12:00:10.9', '1979-09-03T12:00:10.1', '1980-09-03T12:00:10.2') AND [d].[DateTime2_2] IN ('1970-09-03T12:00:00.00', '1971-09-03T12:00:10.22', '1972-09-03T12:00:10.33', '1973-09-03T12:00:10.00', '1974-09-03T12:00:10.50', '1975-09-03T12:00:10.66', '1976-09-03T12:00:10.77', '1977-09-03T12:00:10.88', '1978-09-03T12:00:10.99', '1979-09-03T12:00:10.11', '1980-09-03T12:00:10.22') AND [d].[DateTime2_3] IN ('1970-09-03T12:00:00.000', '1971-09-03T12:00:10.220', '1972-09-03T12:00:10.333', '1973-09-03T12:00:10.000', '1974-09-03T12:00:10.500', '1975-09-03T12:00:10.660', '1976-09-03T12:00:10.777', '1977-09-03T12:00:10.888', '1978-09-03T12:00:10.999', '1979-09-03T12:00:10.111', '1980-09-03T12:00:10.222') AND [d].[DateTime2_4] IN ('1970-09-03T12:00:00.0000', '1971-09-03T12:00:10.2200', '1972-09-03T12:00:10.3330', '1973-09-03T12:00:10.0000', '1974-09-03T12:00:10.5000', '1975-09-03T12:00:10.6600', '1976-09-03T12:00:10.7770', '1977-09-03T12:00:10.8880', '1978-09-03T12:00:10.9990', '1979-09-03T12:00:10.1110', '1980-09-03T12:00:10.2220') AND [d].[DateTime2_5] IN ('1970-09-03T12:00:00.00000', '1971-09-03T12:00:10.22000', '1972-09-03T12:00:10.33300', '1973-09-03T12:00:10.00000', '1974-09-03T12:00:10.50000', '1975-09-03T12:00:10.66000', '1976-09-03T12:00:10.77700', '1977-09-03T12:00:10.88800', '1978-09-03T12:00:10.99900', '1979-09-03T12:00:10.11100', '1980-09-03T12:00:10.22200') AND [d].[DateTime2_6] IN ('1970-09-03T12:00:00.000000', '1971-09-03T12:00:10.220000', '1972-09-03T12:00:10.333000', '1973-09-03T12:00:10.000000', '1974-09-03T12:00:10.500000', '1975-09-03T12:00:10.660000', '1976-09-03T12:00:10.777000', '1977-09-03T12:00:10.888000', '1978-09-03T12:00:10.999000', '1979-09-03T12:00:10.111000', '1980-09-03T12:00:10.222000') AND [d].[DateTime2_7] IN ('1970-09-03T12:00:00.0000000', '1971-09-03T12:00:10.2200000', '1972-09-03T12:00:10.3330000', '1973-09-03T12:00:10.0000000', '1974-09-03T12:00:10.5000000', '1975-09-03T12:00:10.6600000', '1976-09-03T12:00:10.7770000', '1977-09-03T12:00:10.8880000', '1978-09-03T12:00:10.9990000', '1979-09-03T12:00:10.1110000', '1980-09-03T12:00:10.2220000')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0) AS [d0]
+ WHERE CAST([d0].[value] AS smalldatetime) = [d].[SmallDateTime]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_1) AS [d1]
+ WHERE CAST([d1].[value] AS datetime) = [d].[DateTime]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_2) AS [d2]
+ WHERE CAST([d2].[value] AS datetime2) = [d].[DateTime2]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_3) AS [d3]
+ WHERE CAST([d3].[value] AS datetime2(0)) = [d].[DateTime2_0]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_4) AS [d4]
+ WHERE CAST([d4].[value] AS datetime2(1)) = [d].[DateTime2_1]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_5) AS [d5]
+ WHERE CAST([d5].[value] AS datetime2(2)) = [d].[DateTime2_2]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_6) AS [d6]
+ WHERE CAST([d6].[value] AS datetime2(3)) = [d].[DateTime2_3]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_7) AS [d7]
+ WHERE CAST([d7].[value] AS datetime2(4)) = [d].[DateTime2_4]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_8) AS [d8]
+ WHERE CAST([d8].[value] AS datetime2(5)) = [d].[DateTime2_5]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_9) AS [d9]
+ WHERE CAST([d9].[value] AS datetime2(6)) = [d].[DateTime2_6]) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dateTimes_0_10) AS [d10]
+ WHERE CAST([d10].[value] AS datetime2(7)) = [d].[DateTime2_7])
""");
}
@@ -3871,9 +3918,14 @@ public virtual async Task DateTime_Contains_with_smalldatetime_generates_correct
AssertSql(
"""
+@__testDateList_0='["2018-10-07T00:00:00"]' (Size = 4000)
+
SELECT [r].[Id], [r].[MyTime]
FROM [ReproEntity] AS [r]
-WHERE [r].[MyTime] = '2018-10-07T00:00:00'
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__testDateList_0) AS [t]
+ WHERE CAST([t].[value] AS smalldatetime) = [r].[MyTime])
""");
}
}
@@ -3929,14 +3981,18 @@ public virtual async Task Nested_contains_with_enum()
AssertSql(
"""
+@__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000)
@__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7'
SELECT [t].[Id], [t].[Type]
FROM [Todos] AS [t]
-WHERE CASE
- WHEN [t].[Type] = 0 THEN @__key_2
- ELSE @__key_2
-END IN ('0a47bcb7-a1cb-4345-8944-c58f82d6aac7', '5f221fb9-66f4-442a-92c9-d97ed5989cc7')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__keys_0) AS [k]
+ WHERE CAST([k].[value] AS uniqueidentifier) = CASE
+ WHEN [t].[Type] = 0 THEN @__key_2
+ ELSE @__key_2
+ END)
""");
}
}
@@ -8834,9 +8890,14 @@ public virtual async Task Query_filter_with_contains_evaluates_correctly()
AssertSql(
"""
+@__ef_filter___ids_0='[1,7]' (Size = 4000)
+
SELECT [e].[Id], [e].[Name]
FROM [Entities] AS [e]
-WHERE [e].[Id] NOT IN (1, 7)
+WHERE NOT (EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ef_filter___ids_0) AS [e0]
+ WHERE CAST([e0].[value] AS int) = [e].[Id]))
""");
}
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs
index 4bd1c669950..136e8066818 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs
@@ -90,39 +90,49 @@ FROM [MethodCallFilter] AS [m]
public override void DbContext_list_is_parameterized()
{
- using var context = CreateContext();
- // Default value of TenantIds is null InExpression over null values throws
- Assert.Throws(() => context.Set().ToList());
-
- context.TenantIds = new List();
- var query = context.Set().ToList();
- Assert.Empty(query);
-
- context.TenantIds = new List { 1 };
- query = context.Set().ToList();
- Assert.Single(query);
-
- context.TenantIds = new List { 2, 3 };
- query = context.Set().ToList();
- Assert.Equal(2, query.Count);
+ base.DbContext_list_is_parameterized();
AssertSql(
"""
SELECT [l].[Id], [l].[Tenant]
FROM [ListFilter] AS [l]
-WHERE 0 = 1
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(N'[]') AS [e]
+ WHERE CAST([e].[value] AS int) = [l].[Tenant])
""",
//
"""
+@__ef_filter__TenantIds_0='[]' (Size = 4000)
+
SELECT [l].[Id], [l].[Tenant]
FROM [ListFilter] AS [l]
-WHERE [l].[Tenant] = 1
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ef_filter__TenantIds_0) AS [e]
+ WHERE CAST([e].[value] AS int) = [l].[Tenant])
""",
//
"""
+@__ef_filter__TenantIds_0='[1]' (Size = 4000)
+
SELECT [l].[Id], [l].[Tenant]
FROM [ListFilter] AS [l]
-WHERE [l].[Tenant] IN (2, 3)
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ef_filter__TenantIds_0) AS [e]
+ WHERE CAST([e].[value] AS int) = [l].[Tenant])
+""",
+ //
+"""
+@__ef_filter__TenantIds_0='[2,3]' (Size = 4000)
+
+SELECT [l].[Id], [l].[Tenant]
+FROM [ListFilter] AS [l]
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ef_filter__TenantIds_0) AS [e]
+ WHERE CAST([e].[value] AS int) = [l].[Tenant])
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyFixture.cs
index 42d0b34643f..2a108b2e6f6 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyFixture.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyFixture.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 Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
using Microsoft.EntityFrameworkCore.TestModels.SpatialModel;
using NetTopologySuite;
@@ -39,8 +40,9 @@ protected class ReplacementTypeMappingSource : SqlServerTypeMappingSource
{
public ReplacementTypeMappingSource(
TypeMappingSourceDependencies dependencies,
- RelationalTypeMappingSourceDependencies relationalDependencies)
- : base(dependencies, relationalDependencies)
+ RelationalTypeMappingSourceDependencies relationalDependencies,
+ ISqlServerSingletonOptions sqlServerSingletonOptions)
+ : base(dependencies, relationalDependencies, sqlServerSingletonOptions)
{
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryFixture.cs
index 935d63f5168..8084b613b7d 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryFixture.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryFixture.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 Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
using Microsoft.EntityFrameworkCore.TestModels.SpatialModel;
using NetTopologySuite.Geometries;
@@ -36,8 +37,9 @@ protected class ReplacementTypeMappingSource : SqlServerTypeMappingSource
{
public ReplacementTypeMappingSource(
TypeMappingSourceDependencies dependencies,
- RelationalTypeMappingSourceDependencies relationalDependencies)
- : base(dependencies, relationalDependencies)
+ RelationalTypeMappingSourceDependencies relationalDependencies,
+ ISqlServerSingletonOptions sqlServerSingletonOptions)
+ : base(dependencies, relationalDependencies, sqlServerSingletonOptions)
{
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs
index 10608526023..8aa9b03fddb 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs
@@ -305,6 +305,8 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator], [t0].[Id], [t0].[GearNickName], [t0].[GearSquadId], [t0].[IssueDate], [t0].[Note]
FROM (
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator]
@@ -314,7 +316,10 @@ UNION ALL
FROM [Officers] AS [o]
) AS [t]
LEFT JOIN [Tags] AS [t0] ON [t].[Nickname] = [t0].[GearNickName] AND [t].[SquadId] = [t0].[GearSquadId]
-WHERE [t0].[Id] IS NOT NULL AND [t0].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [t0].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t1]
+ WHERE CAST([t1].[value] AS uniqueidentifier) = [t0].[Id] OR ([t1].[value] IS NULL AND [t0].[Id] IS NULL))
""");
}
@@ -329,6 +334,8 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator], [t0].[Id], [t0].[GearNickName], [t0].[GearSquadId], [t0].[IssueDate], [t0].[Note]
FROM (
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator]
@@ -339,7 +346,10 @@ FROM [Officers] AS [o]
) AS [t]
INNER JOIN [Cities] AS [c] ON [t].[CityOfBirthName] = [c].[Name]
LEFT JOIN [Tags] AS [t0] ON [t].[Nickname] = [t0].[GearNickName] AND [t].[SquadId] = [t0].[GearSquadId]
-WHERE [c].[Location] IS NOT NULL AND [t0].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [c].[Location] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t1]
+ WHERE CAST([t1].[value] AS uniqueidentifier) = [t0].[Id] OR ([t1].[value] IS NULL AND [t0].[Id] IS NULL))
""");
}
@@ -354,6 +364,8 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator]
FROM (
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator]
@@ -363,7 +375,10 @@ UNION ALL
FROM [Officers] AS [o]
) AS [t]
LEFT JOIN [Tags] AS [t0] ON [t].[Nickname] = [t0].[GearNickName] AND [t].[SquadId] = [t0].[GearSquadId]
-WHERE [t0].[Id] IS NOT NULL AND [t0].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [t0].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t1]
+ WHERE CAST([t1].[value] AS uniqueidentifier) = [t0].[Id] OR ([t1].[value] IS NULL AND [t0].[Id] IS NULL))
""");
}
@@ -2907,9 +2922,14 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n
AssertSql(
"""
+@__cities_0='["Unknown","Jacinto\u0027s location","Ephyra\u0027s location"]' (Size = 4000)
+
SELECT [c].[Name], [c].[Location], [c].[Nation]
FROM [Cities] AS [c]
-WHERE [c].[Location] IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS varchar(100)) = [c].[Location] OR ([c0].[value] IS NULL AND [c].[Location] IS NULL))
""");
}
@@ -4124,9 +4144,14 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a
AssertSql(
"""
+@__ids_0='["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]' (Size = 4000)
+
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Tags] AS [t]
-WHERE [t].[Id] IN ('d2c26679-562b-44d1-ab96-23d1775e0926', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS uniqueidentifier) = [t].[Id])
""");
}
@@ -4772,6 +4797,8 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool
AssertSql(
"""
+@__cities_0='["Ephyra",null]' (Size = 4000)
+
SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator]
FROM (
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator]
@@ -4781,7 +4808,10 @@ UNION ALL
FROM [Officers] AS [o]
) AS [t]
LEFT JOIN [Cities] AS [c] ON [t].[AssignedCityName] = [c].[Name]
-WHERE [t].[SquadId] < 2 AND ([c].[Name] = N'Ephyra' OR [c].[Name] IS NULL)
+WHERE [t].[SquadId] < 2 AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS nvarchar(450)) = [c].[Name] OR ([c0].[value] IS NULL AND [c].[Name] IS NULL))
""");
}
@@ -8042,6 +8072,8 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz
AssertSql(
"""
+@__nicknames_0='[]' (Size = 4000)
+
SELECT [t].[Nickname], [t].[SquadId], [w].[Name], [w].[Id]
FROM (
SELECT [g].[Nickname], [g].[SquadId], [g].[FullName]
@@ -8051,7 +8083,13 @@ UNION ALL
FROM [Officers] AS [o]
) AS [t]
LEFT JOIN [Weapons] AS [w] ON [t].[FullName] = [w].[OwnerFullName]
-ORDER BY [t].[Nickname], [t].[SquadId]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__nicknames_0) AS [n]
+ WHERE CAST([n].[value] AS nvarchar(450)) = [t].[Nickname]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END DESC, [t].[Nickname], [t].[SquadId]
""");
}
@@ -8921,10 +8959,14 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a
"""
@__start_0='1902-01-01T10:00:00.1234567+01:30'
@__end_1='1902-01-03T10:00:00.1234567+01:30'
+@__dates_2='["1902-01-02T10:00:00.1234567+01:30"]' (Size = 4000)
SELECT [m].[Id], [m].[CodeName], [m].[Date], [m].[Duration], [m].[Rating], [m].[Time], [m].[Timeline]
FROM [Missions] AS [m]
-WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30'
+WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dates_2) AS [d]
+ WHERE CAST([d].[value] AS datetimeoffset) = [m].[Timeline])
""");
}
@@ -9791,6 +9833,8 @@ public override async Task OrderBy_Contains_empty_list(bool async)
AssertSql(
"""
+@__ids_0='[]' (Size = 4000)
+
SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator]
FROM (
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator]
@@ -9799,6 +9843,13 @@ UNION ALL
SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator]
FROM [Officers] AS [o]
) AS [t]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [t].[SquadId]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""");
}
@@ -10806,10 +10857,15 @@ public override async Task Enum_array_contains(bool async)
AssertSql(
"""
+@__types_0='[null,1]' (Size = 4000)
+
SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId]
FROM [Weapons] AS [w]
LEFT JOIN [Weapons] AS [w0] ON [w].[SynergyWithId] = [w0].[Id]
-WHERE [w0].[Id] IS NOT NULL AND ([w0].[AmmunitionType] = 1 OR [w0].[AmmunitionType] IS NULL)
+WHERE [w0].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__types_0) AS [t]
+ WHERE CAST([t].[value] AS int) = [w0].[AmmunitionType] OR ([t].[value] IS NULL AND [w0].[AmmunitionType] IS NULL))
""");
}
@@ -11909,6 +11965,8 @@ public override async Task Where_bool_column_and_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 4000)
+
SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator]
FROM (
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator]
@@ -11917,7 +11975,10 @@ UNION ALL
SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator]
FROM [Officers] AS [o]
) AS [t]
-WHERE [t].[HasSoulPatch] = CAST(1 AS bit) AND [t].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit))
+WHERE [t].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__values_0) AS [v]
+ WHERE CAST([v].[value] AS bit) = [t].[HasSoulPatch])
""");
}
@@ -11927,6 +11988,8 @@ public override async Task Where_bool_column_or_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 4000)
+
SELECT [t].[Nickname], [t].[SquadId], [t].[AssignedCityName], [t].[CityOfBirthName], [t].[FullName], [t].[HasSoulPatch], [t].[LeaderNickname], [t].[LeaderSquadId], [t].[Rank], [t].[Discriminator]
FROM (
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], N'Gear' AS [Discriminator]
@@ -11935,7 +11998,10 @@ UNION ALL
SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator]
FROM [Officers] AS [o]
) AS [t]
-WHERE [t].[HasSoulPatch] = CAST(1 AS bit) AND [t].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit))
+WHERE [t].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__values_0) AS [v]
+ WHERE CAST([v].[value] AS bit) = [t].[HasSoulPatch])
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs
index 9584725abb2..bc90b417e6e 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs
@@ -289,13 +289,18 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE
WHEN [o].[Nickname] IS NOT NULL THEN N'Officer'
END AS [Discriminator], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Gears] AS [g]
LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE [t].[Id] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [t].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -310,6 +315,8 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE
WHEN [o].[Nickname] IS NOT NULL THEN N'Officer'
END AS [Discriminator], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
@@ -317,7 +324,10 @@ FROM [Gears] AS [g]
LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId]
INNER JOIN [Cities] AS [c] ON [g].[CityOfBirthName] = [c].[Name]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE [c].[Location] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [c].[Location] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -332,13 +342,18 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","df36f493-463f-4123-83f9-6b135deeb7ba","a8ad98f9-e023-4e2a-9a70-c2728455bd34","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","b39a6fba-9026-4d69-828e-fd7068673e57"]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE
WHEN [o].[Nickname] IS NOT NULL THEN N'Officer'
END AS [Discriminator]
FROM [Gears] AS [g]
LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE [t].[Id] IS NOT NULL AND [t].[Id] IN ('34c8d86e-a4ac-4be5-827f-584dda348a07', 'df36f493-463f-4123-83f9-6b135deeb7ba', 'a8ad98f9-e023-4e2a-9a70-c2728455bd34', '70534e05-782c-4052-8720-c2c54481ce5f', 'a7be028a-0cf2-448f-ab55-ce8bc5d8cf69', 'b39a6fba-9026-4d69-828e-fd7068673e57')
+WHERE [t].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -2465,9 +2480,14 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n
AssertSql(
"""
+@__cities_0='["Unknown","Jacinto\u0027s location","Ephyra\u0027s location"]' (Size = 4000)
+
SELECT [c].[Name], [c].[Location], [c].[Nation]
FROM [Cities] AS [c]
-WHERE [c].[Location] IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS varchar(100)) = [c].[Location] OR ([c0].[value] IS NULL AND [c].[Location] IS NULL))
""");
}
@@ -3545,9 +3565,14 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a
AssertSql(
"""
+@__ids_0='["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]' (Size = 4000)
+
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note]
FROM [Tags] AS [t]
-WHERE [t].[Id] IN ('d2c26679-562b-44d1-ab96-23d1775e0926', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS uniqueidentifier) = [t].[Id])
""");
}
@@ -4126,13 +4151,18 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool
AssertSql(
"""
+@__cities_0='["Ephyra",null]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE
WHEN [o].[Nickname] IS NOT NULL THEN N'Officer'
END AS [Discriminator]
FROM [Gears] AS [g]
LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId]
LEFT JOIN [Cities] AS [c] ON [g].[AssignedCityName] = [c].[Name]
-WHERE [g].[SquadId] < 2 AND ([c].[Name] = N'Ephyra' OR [c].[Name] IS NULL)
+WHERE [g].[SquadId] < 2 AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS nvarchar(450)) = [c].[Name] OR ([c0].[value] IS NULL AND [c].[Name] IS NULL))
""");
}
@@ -6794,10 +6824,18 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz
AssertSql(
"""
+@__nicknames_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [w].[Name], [w].[Id]
FROM [Gears] AS [g]
LEFT JOIN [Weapons] AS [w] ON [g].[FullName] = [w].[OwnerFullName]
-ORDER BY [g].[Nickname], [g].[SquadId]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__nicknames_0) AS [n]
+ WHERE CAST([n].[value] AS nvarchar(450)) = [g].[Nickname]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END DESC, [g].[Nickname], [g].[SquadId]
""");
}
@@ -7584,10 +7622,14 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a
"""
@__start_0='1902-01-01T10:00:00.1234567+01:30'
@__end_1='1902-01-03T10:00:00.1234567+01:30'
+@__dates_2='["1902-01-02T10:00:00.1234567+01:30"]' (Size = 4000)
SELECT [m].[Id], [m].[CodeName], [m].[Date], [m].[Duration], [m].[Rating], [m].[Time], [m].[Timeline]
FROM [Missions] AS [m]
-WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30'
+WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dates_2) AS [d]
+ WHERE CAST([d].[value] AS datetimeoffset) = [m].[Timeline])
""");
}
@@ -8386,11 +8428,20 @@ public override async Task OrderBy_Contains_empty_list(bool async)
AssertSql(
"""
+@__ids_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE
WHEN [o].[Nickname] IS NOT NULL THEN N'Officer'
END AS [Discriminator]
FROM [Gears] AS [g]
LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [g].[SquadId]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""");
}
@@ -9229,10 +9280,15 @@ public override async Task Enum_array_contains(bool async)
AssertSql(
"""
+@__types_0='[null,1]' (Size = 4000)
+
SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId]
FROM [Weapons] AS [w]
LEFT JOIN [Weapons] AS [w0] ON [w].[SynergyWithId] = [w0].[Id]
-WHERE [w0].[Id] IS NOT NULL AND ([w0].[AmmunitionType] = 1 OR [w0].[AmmunitionType] IS NULL)
+WHERE [w0].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__types_0) AS [t]
+ WHERE CAST([t].[value] AS int) = [w0].[AmmunitionType] OR ([t].[value] IS NULL AND [w0].[AmmunitionType] IS NULL))
""");
}
@@ -10201,12 +10257,17 @@ public override async Task Where_bool_column_and_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE
WHEN [o].[Nickname] IS NOT NULL THEN N'Officer'
END AS [Discriminator]
FROM [Gears] AS [g]
LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId]
-WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit))
+WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__values_0) AS [v]
+ WHERE CAST([v].[value] AS bit) = [g].[HasSoulPatch])
""");
}
@@ -10216,12 +10277,17 @@ public override async Task Where_bool_column_or_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE
WHEN [o].[Nickname] IS NOT NULL THEN N'Officer'
END AS [Discriminator]
FROM [Gears] AS [g]
LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId]
-WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit))
+WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__values_0) AS [v]
+ WHERE CAST([v].[value] AS bit) = [g].[HasSoulPatch])
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs
index bea5f56d43f..8dbf5a595e2 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs
@@ -2301,17 +2301,25 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [t].[Date], [t0].[Id]
FROM (
SELECT [l].[Date]
FROM [LevelOne] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l]
- WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
GROUP BY [l].[Date]
) AS [t]
LEFT JOIN (
SELECT [l0].[Id], [l0].[Date]
FROM [LevelOne] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l0]
- WHERE [l0].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v0]
+ WHERE [v0].[value] = [l0].[Name] OR ([v0].[value] IS NULL AND [l0].[Name] IS NULL))
) AS [t0] ON [t].[Date] = [t0].[Date]
ORDER BY [t].[Date]
""");
@@ -2610,6 +2618,8 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT CASE
WHEN [l0].[Id] IS NULL THEN 0
ELSE [l0].[Id]
@@ -2617,7 +2627,10 @@ ELSE [l0].[Id]
FROM [LevelOne] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l]
LEFT JOIN [LevelTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id]
LEFT JOIN [LevelThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l1] ON [l0].[Id] = [l1].[OneToMany_Required_Inverse3Id]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
ORDER BY [l].[Id], [l0].[Id]
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs
index 4550597b143..331a4b9972d 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs
@@ -1842,6 +1842,8 @@ public override async Task LeftJoin_with_Any_on_outer_source_and_projecting_coll
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT CASE
WHEN [t0].[OneToOne_Required_PK_Date] IS NULL OR [t0].[Level1_Required_Id] IS NULL OR [t0].[OneToMany_Required_Inverse2Id] IS NULL OR CASE
WHEN [t0].[PeriodEnd0] IS NOT NULL AND [t0].[PeriodStart0] IS NOT NULL THEN [t0].[PeriodEnd0]
@@ -1874,7 +1876,10 @@ WHERE [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse
) AS [t1] ON CASE
WHEN [t0].[OneToOne_Required_PK_Date] IS NOT NULL AND [t0].[Level1_Required_Id] IS NOT NULL AND [t0].[OneToMany_Required_Inverse2Id] IS NOT NULL AND [t0].[PeriodEnd0] IS NOT NULL AND [t0].[PeriodStart0] IS NOT NULL THEN [t0].[Id0]
END = [t1].[OneToMany_Required_Inverse3Id]
-WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
ORDER BY [l].[Id], [t0].[Id], [t0].[Id0]
""");
}
@@ -3029,17 +3034,25 @@ public override async Task Collection_projection_over_GroupBy_over_parameter(boo
AssertSql(
"""
+@__validIds_0='["L1 01","L1 02"]' (Size = 4000)
+
SELECT [t].[Date], [t0].[Id]
FROM (
SELECT [l].[Date]
FROM [Level1] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l]
- WHERE [l].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v]
+ WHERE [v].[value] = [l].[Name] OR ([v].[value] IS NULL AND [l].[Name] IS NULL))
GROUP BY [l].[Date]
) AS [t]
LEFT JOIN (
SELECT [l0].[Id], [l0].[Date]
FROM [Level1] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l0]
- WHERE [l0].[Name] IN (N'L1 01', N'L1 02')
+ WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__validIds_0) AS [v0]
+ WHERE [v0].[value] = [l0].[Name] OR ([v0].[value] IS NULL AND [l0].[Name] IS NULL))
) AS [t0] ON [t].[Date] = [t0].[Date]
ORDER BY [t].[Date]
""");
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs
index 2ec49776d41..3d70e089d77 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs
@@ -63,10 +63,15 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note], [t].[PeriodEnd], [t].[PeriodStart]
FROM [Gears] AS [g]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE 0 = 1
+WHERE [t].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -84,11 +89,16 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank], [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note], [t].[PeriodEnd], [t].[PeriodStart]
FROM [Gears] AS [g]
INNER JOIN [Cities] AS [c] ON [g].[CityOfBirthName] = [c].[Name]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE 0 = 1
+WHERE [c].[Location] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -106,10 +116,15 @@ FROM [Tags] AS [t]
""",
//
"""
+@__tags_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank]
FROM [Gears] AS [g]
LEFT JOIN [Tags] AS [t] ON [g].[Nickname] = [t].[GearNickName] AND [g].[SquadId] = [t].[GearSquadId]
-WHERE 0 = 1
+WHERE [t].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__tags_0) AS [t0]
+ WHERE CAST([t0].[value] AS uniqueidentifier) = [t].[Id] OR ([t0].[value] IS NULL AND [t].[Id] IS NULL))
""");
}
@@ -1613,9 +1628,14 @@ public override async Task Where_bool_column_or_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank]
FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g]
-WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit))
+WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__values_0) AS [v]
+ WHERE CAST([v].[value] AS bit) = [g].[HasSoulPatch])
""");
}
@@ -1719,10 +1739,15 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool
AssertSql(
"""
+@__cities_0='["Ephyra",null]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank]
FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g]
LEFT JOIN [Cities] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [c] ON [g].[AssignedCityName] = [c].[Name]
-WHERE [g].[SquadId] < 2 AND ([c].[Name] = N'Ephyra' OR [c].[Name] IS NULL)
+WHERE [g].[SquadId] < 2 AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS nvarchar(450)) = [c].[Name] OR ([c0].[value] IS NULL AND [c].[Name] IS NULL))
""");
}
@@ -5558,10 +5583,15 @@ public override async Task Enum_array_contains(bool async)
AssertSql(
"""
+@__types_0='[null,1]' (Size = 4000)
+
SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[PeriodEnd], [w].[PeriodStart], [w].[SynergyWithId]
FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w]
LEFT JOIN [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w0] ON [w].[SynergyWithId] = [w0].[Id]
-WHERE [w0].[Id] IS NOT NULL AND ([w0].[AmmunitionType] = 1 OR [w0].[AmmunitionType] IS NULL)
+WHERE [w0].[Id] IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__types_0) AS [t]
+ WHERE CAST([t].[value] AS int) = [w0].[AmmunitionType] OR ([t].[value] IS NULL AND [w0].[AmmunitionType] IS NULL))
""");
}
@@ -6128,8 +6158,17 @@ public override async Task OrderBy_Contains_empty_list(bool async)
AssertSql(
"""
+@__ids_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank]
FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS int) = [g].[SquadId]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
""");
}
@@ -6240,10 +6279,14 @@ public override async Task DateTimeOffset_Contains_Less_than_Greater_than(bool a
"""
@__start_0='1902-01-01T10:00:00.1234567+01:30'
@__end_1='1902-01-03T10:00:00.1234567+01:30'
+@__dates_2='["1902-01-02T10:00:00.1234567+01:30"]' (Size = 4000)
SELECT [m].[Id], [m].[BriefingDocument], [m].[BriefingDocumentFileExtension], [m].[CodeName], [m].[Date], [m].[Duration], [m].[PeriodEnd], [m].[PeriodStart], [m].[Rating], [m].[Time], [m].[Timeline]
FROM [Missions] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [m]
-WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND [m].[Timeline] = '1902-01-02T10:00:00.1234567+01:30'
+WHERE @__start_0 <= CAST(CONVERT(date, [m].[Timeline]) AS datetimeoffset) AND [m].[Timeline] < @__end_1 AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__dates_2) AS [d]
+ WHERE CAST([d].[value] AS datetimeoffset) = [m].[Timeline])
""");
}
@@ -6620,9 +6663,14 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n
AssertSql(
"""
+@__cities_0='["Unknown","Jacinto\u0027s location","Ephyra\u0027s location"]' (Size = 4000)
+
SELECT [c].[Name], [c].[Location], [c].[Nation], [c].[PeriodEnd], [c].[PeriodStart]
FROM [Cities] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [c]
-WHERE [c].[Location] IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__cities_0) AS [c0]
+ WHERE CAST([c0].[value] AS varchar(100)) = [c].[Location] OR ([c0].[value] IS NULL AND [c].[Location] IS NULL))
""");
}
@@ -7927,10 +7975,18 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz
AssertSql(
"""
+@__nicknames_0='[]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [w].[Name], [w].[Id]
FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g]
LEFT JOIN [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] ON [g].[FullName] = [w].[OwnerFullName]
-ORDER BY [g].[Nickname], [g].[SquadId]
+ORDER BY CASE
+ WHEN EXISTS (
+ SELECT 1
+ FROM OpenJson(@__nicknames_0) AS [n]
+ WHERE CAST([n].[value] AS nvarchar(450)) = [g].[Nickname]) THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END DESC, [g].[Nickname], [g].[SquadId]
""");
}
@@ -8243,9 +8299,14 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a
AssertSql(
"""
+@__ids_0='["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]' (Size = 4000)
+
SELECT [t].[Id], [t].[GearNickName], [t].[GearSquadId], [t].[IssueDate], [t].[Note], [t].[PeriodEnd], [t].[PeriodStart]
FROM [Tags] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [t]
-WHERE [t].[Id] IN ('d2c26679-562b-44d1-ab96-23d1775e0926', '23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3', 'ab1b82d7-88db-42bd-a132-7eef9aa68af4')
+WHERE EXISTS (
+ SELECT 1
+ FROM OpenJson(@__ids_0) AS [i]
+ WHERE CAST([i].[value] AS uniqueidentifier) = [t].[Id])
""");
}
@@ -8896,9 +8957,14 @@ public override async Task Where_bool_column_and_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 4000)
+
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank]
FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g]
-WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND [g].[HasSoulPatch] IN (CAST(0 AS bit), CAST(1 AS bit))
+WHERE [g].[HasSoulPatch] = CAST(1 AS bit) AND EXISTS (
+ SELECT 1
+ FROM OpenJson(@__values_0) AS [v]
+ WHERE CAST([v].[value] AS bit) = [g].[HasSoulPatch])
""");
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerUpdateSqlGeneratorTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerUpdateSqlGeneratorTest.cs
index df55f71991d..7de26bdcb11 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerUpdateSqlGeneratorTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Update/SqlServerUpdateSqlGeneratorTest.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 Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Update.Internal;
@@ -10,13 +11,19 @@ namespace Microsoft.EntityFrameworkCore.Update;
public class SqlServerUpdateSqlGeneratorTest : UpdateSqlGeneratorTestBase
{
protected override IUpdateSqlGenerator CreateSqlGenerator()
- => new SqlServerUpdateSqlGenerator(
+ {
+ var optionsBuilder = new DbContextOptionsBuilder();
+ optionsBuilder.UseSqlServer("Database=Foo");
+
+ return new SqlServerUpdateSqlGenerator(
new UpdateSqlGeneratorDependencies(
new SqlServerSqlGenerationHelper(
new RelationalSqlGenerationHelperDependencies()),
new SqlServerTypeMappingSource(
TestServiceFactory.Instance.Create(),
- TestServiceFactory.Instance.Create())));
+ TestServiceFactory.Instance.Create(),
+ new SqlServerSingletonOptions())));
+ }
protected override TestHelpers TestHelpers
=> SqlServerTestHelpers.Instance;
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs
index 268b4a5c4da..59a8e40e39d 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs
@@ -804,13 +804,18 @@ public override async Task Include_where_list_contains_navigation2(bool async)
SELECT "t"."Id"
FROM "Tags" AS "t"
""",
- //
- """
+ //
+"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","a8ad98f9-e023-4e2a-9a70-c2728455bd34","b39a6fba-9026-4d69-828e-fd7068673e57","df36f493-463f-4123-83f9-6b135deeb7ba"]' (Size = 235)
+
SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."Discriminator", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank", "t"."Id", "t"."GearNickName", "t"."GearSquadId", "t"."IssueDate", "t"."Note"
FROM "Gears" AS "g"
INNER JOIN "Cities" AS "c" ON "g"."CityOfBirthName" = "c"."Name"
LEFT JOIN "Tags" AS "t" ON "g"."Nickname" = "t"."GearNickName" AND "g"."SquadId" = "t"."GearSquadId"
-WHERE "c"."Location" IS NOT NULL AND "t"."Id" IN ('34C8D86E-A4AC-4BE5-827F-584DDA348A07', '70534E05-782C-4052-8720-C2C54481CE5F', 'A7BE028A-0CF2-448F-AB55-CE8BC5D8CF69', 'A8AD98F9-E023-4E2A-9A70-C2728455BD34', 'B39A6FBA-9026-4D69-828E-FD7068673E57', 'DF36F493-463F-4123-83F9-6B135DEEB7BA')
+WHERE "c"."Location" IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM json_each(@__tags_0) AS "t0"
+ WHERE upper("t0"."value") = "t"."Id" OR ("t0"."value" IS NULL AND "t"."Id" IS NULL))
""");
}
@@ -1171,9 +1176,14 @@ public override async Task Non_unicode_string_literals_in_contains_is_used_for_n
AssertSql(
"""
+@__cities_0='["Unknown","Jacinto\u0027s location","Ephyra\u0027s location"]' (Size = 62)
+
SELECT "c"."Name", "c"."Location", "c"."Nation"
FROM "Cities" AS "c"
-WHERE "c"."Location" IN ('Unknown', 'Jacinto''s location', 'Ephyra''s location')
+WHERE EXISTS (
+ SELECT 1
+ FROM json_each(@__cities_0) AS "c0"
+ WHERE "c0"."value" = "c"."Location" OR ("c0"."value" IS NULL AND "c"."Location" IS NULL))
""");
}
@@ -1889,10 +1899,15 @@ public override async Task Correlated_collection_with_complex_order_by_funcletiz
AssertSql(
"""
+@__nicknames_0='[]' (Size = 2)
+
SELECT "g"."Nickname", "g"."SquadId", "w"."Name", "w"."Id"
FROM "Gears" AS "g"
LEFT JOIN "Weapons" AS "w" ON "g"."FullName" = "w"."OwnerFullName"
-ORDER BY "g"."Nickname", "g"."SquadId"
+ORDER BY EXISTS (
+ SELECT 1
+ FROM json_each(@__nicknames_0) AS "n"
+ WHERE "n"."value" = "g"."Nickname") DESC, "g"."Nickname", "g"."SquadId"
""");
}
@@ -2149,9 +2164,14 @@ public override async Task Where_bool_column_and_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 12)
+
SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."Discriminator", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank"
FROM "Gears" AS "g"
-WHERE "g"."HasSoulPatch" AND "g"."HasSoulPatch" IN (0, 1)
+WHERE "g"."HasSoulPatch" AND EXISTS (
+ SELECT 1
+ FROM json_each(@__values_0) AS "v"
+ WHERE "v"."value" = "g"."HasSoulPatch")
""");
}
@@ -2726,10 +2746,15 @@ public override async Task Contains_on_nullable_array_produces_correct_sql(bool
AssertSql(
"""
+@__cities_0='["Ephyra",null]' (Size = 15)
+
SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."Discriminator", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank"
FROM "Gears" AS "g"
LEFT JOIN "Cities" AS "c" ON "g"."AssignedCityName" = "c"."Name"
-WHERE "g"."SquadId" < 2 AND ("c"."Name" = 'Ephyra' OR "c"."Name" IS NULL)
+WHERE "g"."SquadId" < 2 AND EXISTS (
+ SELECT 1
+ FROM json_each(@__cities_0) AS "c0"
+ WHERE "c0"."value" = "c"."Name" OR ("c0"."value" IS NULL AND "c"."Name" IS NULL))
""");
}
@@ -3010,10 +3035,15 @@ public override async Task Enum_array_contains(bool async)
AssertSql(
"""
+@__types_0='[null,1]' (Size = 8)
+
SELECT "w"."Id", "w"."AmmunitionType", "w"."IsAutomatic", "w"."Name", "w"."OwnerFullName", "w"."SynergyWithId"
FROM "Weapons" AS "w"
LEFT JOIN "Weapons" AS "w0" ON "w"."SynergyWithId" = "w0"."Id"
-WHERE "w0"."Id" IS NOT NULL AND ("w0"."AmmunitionType" = 1 OR "w0"."AmmunitionType" IS NULL)
+WHERE "w0"."Id" IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM json_each(@__types_0) AS "t"
+ WHERE "t"."value" = "w0"."AmmunitionType" OR ("t"."value" IS NULL AND "w0"."AmmunitionType" IS NULL))
""");
}
@@ -3633,12 +3663,17 @@ public override async Task Navigation_accessed_twice_outside_and_inside_subquery
SELECT "t"."Id"
FROM "Tags" AS "t"
""",
- //
- """
+ //
+"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","a8ad98f9-e023-4e2a-9a70-c2728455bd34","b39a6fba-9026-4d69-828e-fd7068673e57","df36f493-463f-4123-83f9-6b135deeb7ba"]' (Size = 235)
+
SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."Discriminator", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank"
FROM "Gears" AS "g"
LEFT JOIN "Tags" AS "t" ON "g"."Nickname" = "t"."GearNickName" AND "g"."SquadId" = "t"."GearSquadId"
-WHERE "t"."Id" IS NOT NULL AND "t"."Id" IN ('34C8D86E-A4AC-4BE5-827F-584DDA348A07', '70534E05-782C-4052-8720-C2C54481CE5F', 'A7BE028A-0CF2-448F-AB55-CE8BC5D8CF69', 'A8AD98F9-E023-4E2A-9A70-C2728455BD34', 'B39A6FBA-9026-4D69-828E-FD7068673E57', 'DF36F493-463F-4123-83F9-6B135DEEB7BA')
+WHERE "t"."Id" IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM json_each(@__tags_0) AS "t0"
+ WHERE upper("t0"."value") = "t"."Id" OR ("t0"."value" IS NULL AND "t"."Id" IS NULL))
""");
}
@@ -4466,9 +4501,14 @@ public override async Task Where_bool_column_or_Contains(bool async)
AssertSql(
"""
+@__values_0='[false,true]' (Size = 12)
+
SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."Discriminator", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank"
FROM "Gears" AS "g"
-WHERE "g"."HasSoulPatch" AND "g"."HasSoulPatch" IN (0, 1)
+WHERE "g"."HasSoulPatch" AND EXISTS (
+ SELECT 1
+ FROM json_each(@__values_0) AS "v"
+ WHERE "v"."value" = "g"."HasSoulPatch")
""");
}
@@ -4790,12 +4830,17 @@ public override async Task Include_where_list_contains_navigation(bool async)
SELECT "t"."Id"
FROM "Tags" AS "t"
""",
- //
- """
+ //
+"""
+@__tags_0='["34c8d86e-a4ac-4be5-827f-584dda348a07","70534e05-782c-4052-8720-c2c54481ce5f","a7be028a-0cf2-448f-ab55-ce8bc5d8cf69","a8ad98f9-e023-4e2a-9a70-c2728455bd34","b39a6fba-9026-4d69-828e-fd7068673e57","df36f493-463f-4123-83f9-6b135deeb7ba"]' (Size = 235)
+
SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."Discriminator", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank", "t"."Id", "t"."GearNickName", "t"."GearSquadId", "t"."IssueDate", "t"."Note"
FROM "Gears" AS "g"
LEFT JOIN "Tags" AS "t" ON "g"."Nickname" = "t"."GearNickName" AND "g"."SquadId" = "t"."GearSquadId"
-WHERE "t"."Id" IS NOT NULL AND "t"."Id" IN ('34C8D86E-A4AC-4BE5-827F-584DDA348A07', '70534E05-782C-4052-8720-C2C54481CE5F', 'A7BE028A-0CF2-448F-AB55-CE8BC5D8CF69', 'A8AD98F9-E023-4E2A-9A70-C2728455BD34', 'B39A6FBA-9026-4D69-828E-FD7068673E57', 'DF36F493-463F-4123-83F9-6B135DEEB7BA')
+WHERE "t"."Id" IS NOT NULL AND EXISTS (
+ SELECT 1
+ FROM json_each(@__tags_0) AS "t0"
+ WHERE upper("t0"."value") = "t"."Id" OR ("t0"."value" IS NULL AND "t"."Id" IS NULL))
""");
}
@@ -5545,8 +5590,14 @@ public override async Task OrderBy_Contains_empty_list(bool async)
AssertSql(
"""
+@__ids_0='[]' (Size = 2)
+
SELECT "g"."Nickname", "g"."SquadId", "g"."AssignedCityName", "g"."CityOfBirthName", "g"."Discriminator", "g"."FullName", "g"."HasSoulPatch", "g"."LeaderNickname", "g"."LeaderSquadId", "g"."Rank"
FROM "Gears" AS "g"
+ORDER BY EXISTS (
+ SELECT 1
+ FROM json_each(@__ids_0) AS "i"
+ WHERE "i"."value" = "g"."SquadId")
""");
}
@@ -7921,9 +7972,14 @@ public override async Task Contains_with_local_nullable_guid_list_closure(bool a
AssertSql(
"""
+@__ids_0='["d2c26679-562b-44d1-ab96-23d1775e0926","23cbcf9b-ce14-45cf-aafa-2c2667ebfdd3","ab1b82d7-88db-42bd-a132-7eef9aa68af4"]' (Size = 118)
+
SELECT "t"."Id", "t"."GearNickName", "t"."GearSquadId", "t"."IssueDate", "t"."Note"
FROM "Tags" AS "t"
-WHERE "t"."Id" IN ('D2C26679-562B-44D1-AB96-23D1775E0926', '23CBCF9B-CE14-45CF-AAFA-2C2667EBFDD3', 'AB1B82D7-88DB-42BD-A132-7EEF9AA68AF4')
+WHERE EXISTS (
+ SELECT 1
+ FROM json_each(@__ids_0) AS "i"
+ WHERE upper("i"."value") = "t"."Id")
""");
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs
new file mode 100644
index 00000000000..1d4ff06a3e2
--- /dev/null
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs
@@ -0,0 +1,243 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using NetTopologySuite.Geometries;
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public class NonSharedPrimitiveCollectionsQuerySqliteTest : NonSharedPrimitiveCollectionsQueryRelationalTestBase
+{
+ #region Support for specific element types
+
+ public override async Task Array_of_int()
+ {
+ await base.Array_of_int();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE "s"."value" = 1) = 2
+LIMIT 2
+""");
+ }
+
+ public override async Task Array_of_long()
+ {
+ await base.Array_of_long();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE "s"."value" = 1) = 2
+LIMIT 2
+""");
+ }
+
+ public override async Task Array_of_short()
+ {
+ await base.Array_of_short();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE "s"."value" = 1) = 2
+LIMIT 2
+""");
+ }
+
+ public override async Task Array_of_double()
+ {
+ await base.Array_of_double();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE "s"."value" = 1.0) = 2
+LIMIT 2
+""");
+ }
+
+ public override async Task Array_of_float()
+ {
+ await base.Array_of_float();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE "s"."value" = 1) = 2
+LIMIT 2
+""");
+ }
+
+ // The JSON representation for decimal is e.g. 1 (JSON int), whereas our literal representation is "1.0" (string). See #30727.
+ public override Task Array_of_decimal()
+ => AssertTranslationFailed(() => base.Array_of_decimal());
+
+ public override async Task Array_of_DateTime()
+ {
+ await base.Array_of_DateTime();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE datetime("s"."value") = '2023-01-01 12:30:00') = 2
+LIMIT 2
+""");
+ }
+
+ public override async Task Array_of_DateOnly()
+ {
+ await base.Array_of_DateOnly();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE "s"."value" = '2023-01-01') = 2
+LIMIT 2
+""");
+ }
+
+ public override async Task Array_of_TimeOnly()
+ {
+ await base.Array_of_TimeOnly();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE "s"."value" = '12:30:00') = 2
+LIMIT 2
+""");
+ }
+
+ // The JSON representation for DateTimeOffset is ISO8601 (2023-01-01T12:30:00+02:00), but our SQL literal representation is
+ // 2023-01-01 12:30:00+02:00 (no T).
+ // datetime('2023-01-01T12:30:00+02:00') yields '2023-01-01 10:30:00' - converted to UTC, no timezone.
+ // See #30727.
+ public override Task Array_of_DateTimeOffset()
+ => AssertTranslationFailed(() => base.Array_of_DateTimeOffset());
+
+ public override async Task Array_of_bool()
+ {
+ await base.Array_of_bool();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE "s"."value") = 2
+LIMIT 2
+""");
+ }
+
+ public override async Task Array_of_Guid()
+ {
+ await base.Array_of_Guid();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE upper("s"."value") = 'DC8C903D-D655-4144-A0FD-358099D40AE1') = 2
+LIMIT 2
+""");
+ }
+
+ // The JSON representation for new[] { 1, 2 } is AQI= (base64), our SQL literal representation is X'0102'. See #30727.
+ public override Task Array_of_byte_array()
+ => AssertTranslationFailed(() => base.Array_of_byte_array());
+
+ public override async Task Array_of_enum()
+ {
+ await base.Array_of_enum();
+
+ AssertSql(
+"""
+SELECT "t"."Id", "t"."Ints", "t"."SomeArray"
+FROM "TestEntity" AS "t"
+WHERE (
+ SELECT COUNT(*)
+ FROM json_each("t"."SomeArray") AS "s"
+ WHERE "s"."value" = 0) = 2
+LIMIT 2
+""");
+ }
+
+ [ConditionalFact] // #30630
+ public override async Task Array_of_geometry_is_not_supported()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => InitializeAsync(
+ onConfiguring: options => options.UseSqlite(o => o.UseNetTopologySuite()),
+ addServices: s => s.AddEntityFrameworkSqliteNetTopologySuite(),
+ onModelCreating: mb => mb.Entity().Property