diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/InExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/InExpression.cs index 74cde489d33..0b43d7ab8cf 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/InExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/InExpression.cs @@ -49,6 +49,12 @@ private InExpression( CoreTypeMapping? typeMapping) : base(typeof(bool), typeMapping) { + if ((values is null ? 0 : 1) + (valuesParameter is null ? 0 : 1) != 1) + { + throw new ArgumentException( + CosmosStrings.OneOfTwoValuesMustBeSet(nameof(values), nameof(valuesParameter))); + } + Item = item; Values = values; ValuesParameter = valuesParameter; @@ -154,17 +160,9 @@ public virtual InExpression Update( SqlExpression item, IReadOnlyList? values, SqlParameterExpression? valuesParameter) - { - if (!(values is null ^ valuesParameter is null)) - { - throw new ArgumentException( - CosmosStrings.OneOfTwoValuesMustBeSet(nameof(values), nameof(valuesParameter))); - } - - return item == Item && values == Values && valuesParameter == ValuesParameter + => item == Item && values == Values && valuesParameter == ValuesParameter ? this : new InExpression(item, values, valuesParameter, TypeMapping); - } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryTranslationPreprocessor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryTranslationPreprocessor.cs index 14d4bd6d74e..5988d837c39 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryTranslationPreprocessor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryTranslationPreprocessor.cs @@ -46,4 +46,8 @@ public override Expression Process(Expression query) return result; } + + /// + protected override bool IsEfConstantSupported + => true; } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 7da52ebd44e..3b7b9c4c49e 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1513,6 +1513,14 @@ public static string OneOfThreeValuesMustBeSet(object? param1, object? param2, o GetString("OneOfThreeValuesMustBeSet", nameof(param1), nameof(param2), nameof(param3)), param1, param2, param3); + /// + /// Exactly one of '{param1}' or '{param2}' must be set. + /// + public static string OneOfTwoValuesMustBeSet(object? param1, object? param2) + => string.Format( + GetString("OneOfTwoValuesMustBeSet", nameof(param1), nameof(param2)), + param1, param2); + /// /// Only constants are supported inside inline collection query roots. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index b5d0e191417..7099418d2e3 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -1004,6 +1004,9 @@ Exactly one of '{param1}', '{param2}' or '{param3}' must be set. + + Exactly one of '{param1}' or '{param2}' must be set. + Only constants are supported inside inline collection query roots. diff --git a/src/EFCore.Relational/Query/IRelationalParameterBasedSqlProcessorFactory.cs b/src/EFCore.Relational/Query/IRelationalParameterBasedSqlProcessorFactory.cs index 6422893fe7f..276527fe9f1 100644 --- a/src/EFCore.Relational/Query/IRelationalParameterBasedSqlProcessorFactory.cs +++ b/src/EFCore.Relational/Query/IRelationalParameterBasedSqlProcessorFactory.cs @@ -17,7 +17,7 @@ public interface IRelationalParameterBasedSqlProcessorFactory /// /// Creates a new . /// - /// A bool value indicating if relational nulls should be used. + /// Parameters for . /// A relational parameter based sql processor. - RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls); + RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters); } diff --git a/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs b/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs index 774a31d2585..19a8c30bdb3 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs @@ -35,12 +35,13 @@ public RelationalCommandCache( IQuerySqlGeneratorFactory querySqlGeneratorFactory, IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory, Expression queryExpression, - bool useRelationalNulls) + bool useRelationalNulls, + HashSet parametersToConstantize) { _memoryCache = memoryCache; _querySqlGeneratorFactory = querySqlGeneratorFactory; _queryExpression = queryExpression; - _relationalParameterBasedSqlProcessor = relationalParameterBasedSqlProcessorFactory.Create(useRelationalNulls); + _relationalParameterBasedSqlProcessor = relationalParameterBasedSqlProcessorFactory.Create(new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls, parametersToConstantize)); } /// diff --git a/src/EFCore.Relational/Query/Internal/RelationalParameterBasedSqlProcessorFactory.cs b/src/EFCore.Relational/Query/Internal/RelationalParameterBasedSqlProcessorFactory.cs index 87efd188a1f..ece419ebaf1 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalParameterBasedSqlProcessorFactory.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalParameterBasedSqlProcessorFactory.cs @@ -34,6 +34,6 @@ public RelationalParameterBasedSqlProcessorFactory( /// 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 RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls) - => new(Dependencies, useRelationalNulls); + public virtual RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) + => new(Dependencies, parameters); } diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 8d2eb15e4ec..1906d587a40 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -1570,6 +1570,10 @@ protected override Expression VisitValues(ValuesExpression valuesExpression) /// The for which to generate SQL. protected virtual void GenerateValues(ValuesExpression valuesExpression) { + Check.DebugAssert( + valuesExpression.RowValues is not null, + "ValuesExpression.RowValues has to be set before SQL generation (i.e. in SqlNullabilityProcessor)"); + if (valuesExpression.RowValues.Count == 0) { throw new InvalidOperationException(RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); diff --git a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs index dc0df133a80..45a0bff14a5 100644 --- a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs +++ b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs @@ -21,13 +21,13 @@ public class RelationalParameterBasedSqlProcessor /// Creates a new instance of the class. /// /// Parameter object containing dependencies for this class. - /// A bool value indicating if relational nulls should be used. + /// Parameter object containing parameters for this class. public RelationalParameterBasedSqlProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) { Dependencies = dependencies; - UseRelationalNulls = useRelationalNulls; + Parameters = parameters; } /// @@ -36,9 +36,9 @@ public RelationalParameterBasedSqlProcessor( protected virtual RelationalParameterBasedSqlProcessorDependencies Dependencies { get; } /// - /// A bool value indicating if relational nulls should be used. + /// Parameter object containing parameters for this class. /// - protected virtual bool UseRelationalNulls { get; } + protected virtual RelationalParameterBasedSqlProcessorParameters Parameters { get; } /// /// Optimizes the query expression for given parameter values. @@ -74,7 +74,7 @@ protected virtual Expression ProcessSqlNullability( Expression queryExpression, IReadOnlyDictionary parametersValues, out bool canCache) - => new SqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process(queryExpression, parametersValues, out canCache); + => new SqlNullabilityProcessor(Dependencies, Parameters).Process(queryExpression, parametersValues, out canCache); /// /// Expands the parameters to inside the query expression for given parameter values. diff --git a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs new file mode 100644 index 00000000000..bbd689d9ab9 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs @@ -0,0 +1,32 @@ +// 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; + +/// +/// Parameters for . +/// +public sealed record RelationalParameterBasedSqlProcessorParameters +{ + /// + /// A value indicating if relational nulls should be used. + /// + public bool UseRelationalNulls { get; init; } + + /// + /// A collection of parameter names to constantize. + /// + public HashSet ParametersToConstantize { get; init; } + + /// + /// Creates a new instance of . + /// + /// A value indicating if relational nulls should be used. + /// A collection of parameter names to constantize. + [EntityFrameworkInternal] + public RelationalParameterBasedSqlProcessorParameters(bool useRelationalNulls, HashSet parametersToConstantize) + { + UseRelationalNulls = useRelationalNulls; + ParametersToConstantize = parametersToConstantize; + } +} diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs index 88f1c0be282..d56bbe2d7b5 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs @@ -43,4 +43,8 @@ public override Expression NormalizeQueryableMethod(Expression expression) /// protected override Expression ProcessQueryRoots(Expression expression) => new RelationalQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext).Visit(expression); + + /// + protected override bool IsEfConstantSupported + => true; } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 22b4ce1443d..a98c0d070c1 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -2,8 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Query; @@ -292,6 +295,21 @@ JsonScalarExpression jsonScalar Check.DebugAssert(sqlParameterExpression is not null, "sqlParameterExpression is not null"); var tableAlias = _sqlAliasManager.GenerateTableAlias(sqlParameterExpression.Name.TrimStart('_')); + + if (QueryCompilationContext.ParametersToConstantize.Contains(sqlParameterExpression.Name)) + { + var valuesExpression = new ValuesExpression( + tableAlias, + sqlParameterExpression, + [ValuesOrderingColumnName, ValuesValueColumnName]); + return CreateShapedQueryExpressionForValuesExpression( + valuesExpression, + tableAlias, + parameterQueryRootExpression.ElementType, + sqlParameterExpression.TypeMapping, + sqlParameterExpression.IsNullable); + } + return TranslatePrimitiveCollection(sqlParameterExpression, property: null, tableAlias); } @@ -378,7 +396,6 @@ JsonScalarExpression jsonScalar for (var i = 0; i < sqlExpressions.Length; i++) { var sqlExpression = sqlExpressions[i]; - rowExpressions[i] = new RowValueExpression( new[] @@ -394,45 +411,15 @@ sqlExpression.TypeMapping is null && inferredTypeMaping is not null : sqlExpression }); } - var alias = _sqlAliasManager.GenerateTableAlias("values"); var valuesExpression = new ValuesExpression(alias, 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 valueColumn = new ColumnExpression( - ValuesValueColumnName, + return CreateShapedQueryExpressionForValuesExpression( + valuesExpression, alias, - elementType.UnwrapNullableType(), - typeMapping: inferredTypeMaping, - nullable: encounteredNull); - var orderingColumn = new ColumnExpression( - ValuesOrderingColumnName, - alias, - typeof(int), - typeMapping: intTypeMapping, - nullable: false); - - var selectExpression = new SelectExpression( - [valuesExpression], - valueColumn, - identifier: [(orderingColumn, orderingColumn.TypeMapping!.Comparer)], - _sqlAliasManager); - - selectExpression.AppendOrdering(new OrderingExpression(orderingColumn, 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); + elementType, + inferredTypeMaping, + encounteredNull); } /// @@ -577,9 +564,14 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s } // Pattern-match Contains over ValuesExpression, translating to simplified 'item IN (1, 2, 3)' with constant elements - if (TryExtractBareInlineCollectionValues(source, out var values)) + if (TryExtractBareInlineCollectionValues(source, out var values, out var valuesParameter)) { - var inExpression = _sqlExpressionFactory.In(translatedItem, values); + var inExpression = (values, valuesParameter) switch + { + (not null, null) => _sqlExpressionFactory.In(translatedItem, values), + (null, not null) => _sqlExpressionFactory.In(translatedItem, valuesParameter), + _ => throw new UnreachableException(), + }; return source.Update(new SelectExpression(inExpression, _sqlAliasManager), source.ShaperExpression); } @@ -2074,8 +2066,10 @@ private bool TryGetProjection(ShapedQueryExpression shapedQueryExpression, [NotN projection = null; return false; } - private bool TryExtractBareInlineCollectionValues(ShapedQueryExpression shapedQuery, [NotNullWhen(true)] out SqlExpression[]? values) + => TryExtractBareInlineCollectionValues(shapedQuery, out values, out _); + + private bool TryExtractBareInlineCollectionValues(ShapedQueryExpression shapedQuery, out SqlExpression[]? values, out SqlParameterExpression? valuesParameter) { if (TryGetProjection(shapedQuery, out var projection) && shapedQuery.QueryExpression is SelectExpression @@ -2096,21 +2090,76 @@ private bool TryExtractBareInlineCollectionValues(ShapedQueryExpression shapedQu && projection is ColumnExpression { TableAlias: var tableAlias } && tableAlias == valuesExpression.Alias) { - values = new SqlExpression[valuesExpression.RowValues.Count]; - - for (var i = 0; i < values.Length; i++) + switch (valuesExpression) { - // Skip the first value (_ord) - this function assumes ordering doesn't matter - values[i] = valuesExpression.RowValues[i].Values[1]; - } + case { RowValues: not null }: + values = new SqlExpression[valuesExpression.RowValues.Count]; - return true; + for (var i = 0; i < values.Length; i++) + { + // Skip the first value (_ord) - this function assumes ordering doesn't matter + values[i] = valuesExpression.RowValues[i].Values[1]; + } + + valuesParameter = null; + return true; + + case { ValuesParameter: not null }: + valuesParameter = valuesExpression.ValuesParameter; + values = null; + return true; + } } values = null; + valuesParameter = null; return false; } + private ShapedQueryExpression CreateShapedQueryExpressionForValuesExpression( + ValuesExpression valuesExpression, + string tableAlias, + Type elementType, + RelationalTypeMapping? inferredTypeMapping, + bool encounteredNull) + { + // Note: we leave the element type mapping null, to allow it to get inferred based on queryable operators composed on top. + var valueColumn = new ColumnExpression( + ValuesValueColumnName, + tableAlias, + elementType.UnwrapNullableType(), + typeMapping: inferredTypeMapping, + nullable: encounteredNull); + var orderingColumn = new ColumnExpression( + ValuesOrderingColumnName, + tableAlias, + typeof(int), + typeMapping: _typeMappingSource.FindMapping(typeof(int), RelationalDependencies.Model), + nullable: false); + + var selectExpression = new SelectExpression( + [valuesExpression], + valueColumn, + identifier: [(orderingColumn, orderingColumn.TypeMapping!.Comparer)], + _sqlAliasManager); + + selectExpression.AppendOrdering(new OrderingExpression(orderingColumn, 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); + } + /// /// This visitor has been obsoleted; Extend RelationalTypeMappingPostprocessor instead, and invoke it from /// . diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index 1a760d7cff7..494399d8d0b 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -14,6 +14,7 @@ namespace Microsoft.EntityFrameworkCore.Query; /// public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQueryCompilingExpressionVisitor { + private readonly HashSet _parametersToConstantize; private readonly Type _contextType; private readonly ISet _tags; private readonly bool _threadSafetyChecksEnabled; @@ -29,6 +30,15 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQue private static PropertyInfo? _commandBuilderDependenciesProperty; private static MethodInfo? _getRelationalCommandTemplateMethod; + private static ConstructorInfo? _hashSetConstructor; + private static PropertyInfo? _stringComparerOrdinalProperty; + private static ConstructorInfo? _relationalCommandCacheConstructor; + private static PropertyInfo? _dependenciesProperty; + private static PropertyInfo? _dependenciesMemoryCacheProperty; + private static PropertyInfo? _relationalDependenciesProperty; + private static PropertyInfo? _relationalDependenciesQuerySqlGeneratorFactoryProperty; + private static PropertyInfo? _relationalDependenciesRelationalParameterBasedSqlProcessorFactoryProperty; + /// /// Creates a new instance of the class. /// @@ -42,8 +52,11 @@ public RelationalShapedQueryCompilingExpressionVisitor( : base(dependencies, queryCompilationContext) { RelationalDependencies = relationalDependencies; + + _parametersToConstantize = QueryCompilationContext.ParametersToConstantize; + _relationalParameterBasedSqlProcessor = - relationalDependencies.RelationalParameterBasedSqlProcessorFactory.Create(_useRelationalNulls); + relationalDependencies.RelationalParameterBasedSqlProcessorFactory.Create(new RelationalParameterBasedSqlProcessorParameters(_useRelationalNulls, _parametersToConstantize)); _querySqlGeneratorFactory = relationalDependencies.QuerySqlGeneratorFactory; _contextType = queryCompilationContext.ContextType; @@ -498,16 +511,12 @@ private Expression CreateRelationalCommandResolverExpression(Expression queryExp RelationalDependencies.QuerySqlGeneratorFactory, RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, queryExpression, - _useRelationalNulls); + _useRelationalNulls, + _parametersToConstantize); var commandLiftableConstant = RelationalDependencies.RelationalLiftableConstantFactory.CreateLiftableConstant( relationalCommandCache, - c => new RelationalCommandCache( - c.Dependencies.MemoryCache, - c.RelationalDependencies.QuerySqlGeneratorFactory, - c.RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, - queryExpression, - _useRelationalNulls), + GenerateRelationalCommandCacheExpression(), "relationalCommandCache", typeof(RelationalCommandCache)); @@ -714,6 +723,42 @@ Expression GenerateRelationalCommandExpression(IReadOnlyDictionary> GenerateRelationalCommandCacheExpression() + { + _hashSetConstructor ??= typeof(HashSet).GetConstructor([typeof(IEnumerable), typeof(StringComparer)])!; + _stringComparerOrdinalProperty ??= typeof(StringComparer).GetProperty(nameof(StringComparer.Ordinal))!; + _relationalCommandCacheConstructor ??= typeof(RelationalCommandCache).GetConstructors().Single(); + _dependenciesProperty ??= typeof(RelationalMaterializerLiftableConstantContext).GetProperty(nameof(RelationalMaterializerLiftableConstantContext.Dependencies))!; + _dependenciesMemoryCacheProperty ??= typeof(ShapedQueryCompilingExpressionVisitorDependencies).GetProperty(nameof(ShapedQueryCompilingExpressionVisitorDependencies.MemoryCache))!; + _relationalDependenciesProperty ??= typeof(RelationalMaterializerLiftableConstantContext).GetProperty(nameof(RelationalMaterializerLiftableConstantContext.RelationalDependencies))!; + _relationalDependenciesQuerySqlGeneratorFactoryProperty ??= typeof(RelationalShapedQueryCompilingExpressionVisitorDependencies).GetProperty(nameof(RelationalShapedQueryCompilingExpressionVisitorDependencies.QuerySqlGeneratorFactory))!; + _relationalDependenciesRelationalParameterBasedSqlProcessorFactoryProperty ??= typeof(RelationalShapedQueryCompilingExpressionVisitorDependencies).GetProperty(nameof(RelationalShapedQueryCompilingExpressionVisitorDependencies.RelationalParameterBasedSqlProcessorFactory))!; + + var newHashSetExpression = New( + _hashSetConstructor, + NewArrayInit(typeof(string), _parametersToConstantize.Select(Constant)), + MakeMemberAccess(null, _stringComparerOrdinalProperty)); + var contextParameter = Parameter(typeof(RelationalMaterializerLiftableConstantContext), "c"); + return + Lambda>( + New( + _relationalCommandCacheConstructor, + MakeMemberAccess( + MakeMemberAccess(contextParameter, _dependenciesProperty), + _dependenciesMemoryCacheProperty), + MakeMemberAccess( + MakeMemberAccess(contextParameter, _relationalDependenciesProperty), + _relationalDependenciesQuerySqlGeneratorFactoryProperty), + MakeMemberAccess( + MakeMemberAccess(contextParameter, _relationalDependenciesProperty), + _relationalDependenciesRelationalParameterBasedSqlProcessorFactoryProperty), + Constant(queryExpression), + Constant(_useRelationalNulls), + newHashSetExpression), + contextParameter); + } } private sealed class SqlParameterLocator : ExpressionVisitor diff --git a/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs b/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs index 03eaaefeb14..578d9812047 100644 --- a/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalTypeMappingPostprocessor.cs @@ -162,40 +162,65 @@ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExp ? valuesExpression.ColumnNames.Skip(1).ToArray() : valuesExpression.ColumnNames; - var newRowValues = new RowValueExpression[valuesExpression.RowValues.Count]; - for (var i = 0; i < newRowValues.Length; i++) + switch (valuesExpression) { - var rowValue = valuesExpression.RowValues[i]; - var newValues = new SqlExpression[newColumnNames.Count]; - for (var j = 0; j < valuesExpression.ColumnNames.Count; j++) + case { RowValues: not null }: { - if (j == 0 && stripOrdering) + var newRowValues = new RowValueExpression[valuesExpression.RowValues.Count]; + for (var i = 0; i < newRowValues.Length; i++) { - continue; - } - - var value = rowValue.Values[j]; + var rowValue = valuesExpression.RowValues[i]; + var newValues = new SqlExpression[newColumnNames.Count]; + for (var j = 0; j < valuesExpression.ColumnNames.Count; j++) + { + if (j == 0 && stripOrdering) + { + continue; + } + + var value = rowValue.Values[j]; + + if (value.TypeMapping is null + && inferredTypeMappings[j] is RelationalTypeMapping inferredTypeMapping) + { + value = _sqlExpressionFactory.ApplyTypeMapping(value, inferredTypeMapping); + } + + // We currently add explicit conversions on the first row (but not to the _ord column), to ensure that the inferred types + // are properly typed. See #30605 for removing that when not needed. + if (i == 0 && j > 0 && value is not ColumnExpression) + { + value = new SqlUnaryExpression(ExpressionType.Convert, value, value.Type, value.TypeMapping); + } + + newValues[j - (stripOrdering ? 1 : 0)] = value; + } - if (value.TypeMapping is null - && inferredTypeMappings[j] is RelationalTypeMapping inferredTypeMapping) - { - value = _sqlExpressionFactory.ApplyTypeMapping(value, inferredTypeMapping); + newRowValues[i] = new RowValueExpression(newValues); } + return new ValuesExpression(valuesExpression.Alias, newRowValues, null, newColumnNames); + } - // We currently add explicit conversions on the first row (but not to the _ord column), to ensure that the inferred types - // are properly typed. See #30605 for removing that when not needed. - if (i == 0 && j > 0 && value is not ColumnExpression) + case { ValuesParameter: not null }: + { + var valuesParameter = valuesExpression.ValuesParameter; + if (valuesParameter.TypeMapping is null + && inferredTypeMappings[1] is RelationalTypeMapping elementTypeMapping) { - value = new SqlUnaryExpression(ExpressionType.Convert, value, value.Type, value.TypeMapping); + if (RelationalDependencies.TypeMappingSource.FindMapping(valuesParameter.Type, QueryCompilationContext.Model, elementTypeMapping) is not RelationalTypeMapping { ElementTypeMapping: not null } parameterTypeMapping) + { + throw new UnreachableException("A RelationalTypeMapping collection type mapping could not be found"); + } + + valuesParameter = (SqlParameterExpression)valuesParameter.ApplyTypeMapping(parameterTypeMapping); } - newValues[j - (stripOrdering ? 1 : 0)] = value; + return new ValuesExpression(valuesExpression.Alias, null, valuesParameter, newColumnNames); } - newRowValues[i] = new RowValueExpression(newValues); - } - - return new ValuesExpression(valuesExpression.Alias, newRowValues, newColumnNames); + default: + throw new UnreachableException(); + }; } /// diff --git a/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs index a07f3bc7c76..b84e1471e38 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs @@ -72,6 +72,12 @@ private InExpression( { Check.DebugAssert(subquery?.IsMutable != true, "Mutable subquery provided to ExistsExpression"); + if ((subquery is null ? 0 : 1) + (values is null ? 0 : 1) + (valuesParameter is null ? 0 : 1) != 1) + { + throw new ArgumentException( + RelationalStrings.OneOfThreeValuesMustBeSet(nameof(subquery), nameof(values), nameof(valuesParameter))); + } + Item = item; Subquery = subquery; Values = values; @@ -186,17 +192,9 @@ public virtual InExpression Update( SelectExpression? subquery, IReadOnlyList? values, SqlParameterExpression? valuesParameter) - { - if ((subquery is null ? 0 : 1) + (values is null ? 0 : 1) + (valuesParameter is null ? 0 : 1) != 1) - { - throw new ArgumentException( - RelationalStrings.OneOfThreeValuesMustBeSet(nameof(subquery), nameof(values), nameof(valuesParameter))); - } - - return item == Item && subquery == Subquery && values == Values && valuesParameter == ValuesParameter + => item == Item && subquery == Subquery && values == Values && valuesParameter == ValuesParameter ? this : new InExpression(item, subquery, values, valuesParameter, TypeMapping); - } /// public override Expression Quote() diff --git a/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs index f8a03726bff..50b006b9c3c 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs @@ -1,8 +1,6 @@ // 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; /// @@ -21,7 +19,13 @@ public class ValuesExpression : TableExpressionBase /// /// The row values for this table. /// - public virtual IReadOnlyList RowValues { get; } + public virtual IReadOnlyList? RowValues { get; } + + /// + /// A parameter containing the list of values. The parameterized list get expanded to the actual value + /// before the query SQL is generated. + /// + public virtual SqlParameterExpression? ValuesParameter { get; } /// /// The names of the columns contained in this table. @@ -38,14 +42,22 @@ public ValuesExpression( string? alias, IReadOnlyList rowValues, IReadOnlyList columnNames) - : base(alias, annotations: (IReadOnlyDictionary?)null) + : this(alias, rowValues: rowValues, valuesParameter: null, columnNames: columnNames) { - Check.DebugAssert( - rowValues.All(rv => rv.Values.Count == columnNames.Count), - "All row values must have a value count matching the number of column names"); + } - RowValues = rowValues; - ColumnNames = columnNames; + /// + /// Creates a new instance of the class. + /// + /// A string alias for the table source. + /// A parameterized list of values. + /// The names of the columns contained in this table. + public ValuesExpression( + string? alias, + SqlParameterExpression valuesParameter, + IReadOnlyList columnNames) + : this(alias, rowValues: null, valuesParameter: valuesParameter, columnNames: columnNames) + { } /// @@ -57,16 +69,33 @@ public ValuesExpression( [EntityFrameworkInternal] public ValuesExpression( string? alias, - IReadOnlyList rowValues, + IReadOnlyList? rowValues, + SqlParameterExpression? valuesParameter, IReadOnlyList columnNames, - IReadOnlyDictionary? annotations) + IReadOnlyDictionary? annotations = null) : base(alias, annotations) { - Check.DebugAssert( - rowValues.All(rv => rv.Values.Count == columnNames.Count), - "All row values must have a value count matching the number of column names"); + if (rowValues is not null) + { + Check.DebugAssert( + rowValues.All(rv => rv.Values.Count == columnNames.Count), + "All row values must have a value count matching the number of column names"); + } + + if (valuesParameter is not null) + { + Check.DebugAssert( + columnNames.Count is 1 or 2, + $"Column names do not match usage of {nameof(ValuesParameter)}"); + } + if (!(rowValues is null ^ valuesParameter is null)) + { + throw new ArgumentException( + RelationalStrings.OneOfTwoValuesMustBeSet(nameof(rowValues), nameof(valuesParameter))); + } RowValues = rowValues; + ValuesParameter = valuesParameter; ColumnNames = columnNames; } @@ -78,27 +107,50 @@ public override string Alias /// protected override Expression VisitChildren(ExpressionVisitor visitor) - => visitor.VisitAndConvert(RowValues) is var newRowValues - && ReferenceEquals(newRowValues, RowValues) - ? this - : new ValuesExpression(Alias, newRowValues, ColumnNames); + => this switch + { + { RowValues: not null } => Update(visitor.VisitAndConvert(RowValues)), + { ValuesParameter: not null } => Update((SqlParameterExpression)visitor.Visit(ValuesParameter)), + _ => throw new UnreachableException() + }; /// /// 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) + => Update(rowValues: rowValues, valuesParameter: null); + + /// + /// 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(SqlParameterExpression valuesParameter) + => Update(rowValues: null, valuesParameter: valuesParameter); + + /// + /// 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, + SqlParameterExpression? valuesParameter) + => ((rowValues is not null + && RowValues is not null + && rowValues.Count == RowValues.Count + && rowValues.Zip(RowValues, (x, y) => (x, y)).All(tup => tup.x == tup.y)) + || (rowValues is null && RowValues is null)) + && valuesParameter == ValuesParameter ? this - : new ValuesExpression(Alias, rowValues, ColumnNames); + : new ValuesExpression(Alias, rowValues, valuesParameter, ColumnNames, Annotations); /// protected override ValuesExpression WithAnnotations(IReadOnlyDictionary annotations) - => new(Alias, RowValues, ColumnNames, annotations); + => new(Alias, RowValues, ValuesParameter, ColumnNames, annotations); /// public override ValuesExpression WithAlias(string newAlias) - => new(newAlias, RowValues, ColumnNames, Annotations); + => new(newAlias, RowValues, ValuesParameter, ColumnNames, Annotations); /// public override Expression Quote() @@ -107,25 +159,36 @@ public override Expression Quote() [ typeof(string), typeof(IReadOnlyList), + typeof(SqlParameterExpression), typeof(IReadOnlyList), typeof(IReadOnlyDictionary) ])!, Constant(Alias, typeof(string)), - NewArrayInit(typeof(RowValueExpression), RowValues.Select(rv => rv.Quote())), + RowValues is not null ? NewArrayInit(typeof(RowValueExpression), RowValues.Select(rv => rv.Quote())) : Constant(null, typeof(RowValueExpression)), + RelationalExpressionQuotingUtilities.QuoteOrNull(ValuesParameter), NewArrayInit(typeof(string), ColumnNames.Select(Constant)), RelationalExpressionQuotingUtilities.QuoteAnnotations(Annotations)); /// public override TableExpressionBase Clone(string? alias, ExpressionVisitor cloningExpressionVisitor) { - var newRowValues = new RowValueExpression[RowValues.Count]; - - for (var i = 0; i < newRowValues.Length; i++) + switch (this) { - newRowValues[i] = (RowValueExpression)cloningExpressionVisitor.Visit(RowValues[i]); + case { RowValues: not null }: + var newRowValues = new RowValueExpression[RowValues.Count]; + for (var i = 0; i < newRowValues.Length; i++) + { + newRowValues[i] = (RowValueExpression)cloningExpressionVisitor.Visit(RowValues[i]); + } + return new ValuesExpression(alias, newRowValues, null, ColumnNames, Annotations); + + case { ValuesParameter: not null }: + var newValuesParameter = (SqlParameterExpression)cloningExpressionVisitor.Visit(ValuesParameter); + return new ValuesExpression(alias, null, newValuesParameter, ColumnNames, Annotations); + + default: + throw new UnreachableException(); } - - return new ValuesExpression(alias, newRowValues, ColumnNames, Annotations); } /// @@ -133,15 +196,27 @@ protected override void Print(ExpressionPrinter expressionPrinter) { expressionPrinter.Append("VALUES ("); - var count = RowValues.Count; - for (var i = 0; i < count; i++) + switch (this) { - expressionPrinter.Visit(RowValues[i]); - - if (i < count - 1) - { - expressionPrinter.Append(", "); - } + case { RowValues: not null }: + var count = RowValues.Count; + for (var i = 0; i < count; i++) + { + expressionPrinter.Visit(RowValues[i]); + + if (i < count - 1) + { + expressionPrinter.Append(", "); + } + } + break; + + case { ValuesParameter: not null }: + expressionPrinter.Visit(ValuesParameter); + break; + + default: + throw new ArgumentOutOfRangeException(); } expressionPrinter.Append(")"); @@ -149,39 +224,30 @@ protected override void Print(ExpressionPrinter expressionPrinter) /// public override bool Equals(object? obj) - => obj is ValuesExpression other && Equals(other); + => obj != null + && (ReferenceEquals(this, obj) + || obj is ValuesExpression valuesExpression + && Equals(valuesExpression)); - 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; - } + private bool Equals(ValuesExpression? valuesExpression) + => base.Equals(valuesExpression) + && (ValuesParameter?.Equals(valuesExpression.ValuesParameter) ?? valuesExpression.ValuesParameter == null) + && (ReferenceEquals(RowValues, valuesExpression.RowValues) + || (RowValues is not null && valuesExpression.RowValues is not null && RowValues.SequenceEqual(valuesExpression.RowValues))); /// public override int GetHashCode() { var hashCode = new HashCode(); + hashCode.Add(base.GetHashCode()); + hashCode.Add(ValuesParameter); - foreach (var rowValue in RowValues) + if (RowValues is not null) { - hashCode.Add(rowValue); + for (var i = 0; i < RowValues.Count; i++) + { + hashCode.Add(RowValues[i]); + } } return hashCode.ToHashCode(); diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index e9dc788484d..3b740ca419e 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -28,15 +28,16 @@ public class SqlNullabilityProcessor /// Creates a new instance of the class. /// /// Parameter object containing dependencies for this class. - /// A bool value indicating whether relational null semantics are in use. + /// Parameter object containing parameters for this class. public SqlNullabilityProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) { Dependencies = dependencies; + UseRelationalNulls = parameters.UseRelationalNulls; + ParametersToConstantize = parameters.ParametersToConstantize; _sqlExpressionFactory = dependencies.SqlExpressionFactory; - UseRelationalNulls = useRelationalNulls; _nonNullableColumns = []; _nullValueColumns = []; ParameterValues = null!; @@ -52,6 +53,11 @@ public SqlNullabilityProcessor( /// protected virtual bool UseRelationalNulls { get; } + /// + /// A collection of parameter names to constantize. + /// + protected virtual HashSet ParametersToConstantize { get; } + /// /// Dictionary of current parameter values in use. /// @@ -187,29 +193,54 @@ protected virtual TableExpressionBase Visit(TableExpressionBase tableExpressionB case ValuesExpression valuesExpression: { - RowValueExpression[]? newRowValues = null; - - for (var i = 0; i < valuesExpression.RowValues.Count; i++) + switch (valuesExpression) { - var rowValue = valuesExpression.RowValues[i]; - var newRowValue = (RowValueExpression)VisitRowValue(rowValue, allowOptimizedExpansion: false, out _); + case { RowValues: not null }: + 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++) + 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 not null + ? valuesExpression.Update(newRowValues) + : valuesExpression; + + case { ValuesParameter: SqlParameterExpression valuesParameter }: + DoNotCache(); + Check.DebugAssert(valuesParameter.TypeMapping is not null, "valuesParameter.TypeMapping is not null"); + Check.DebugAssert(valuesParameter.TypeMapping.ElementTypeMapping is not null, "valuesParameter.TypeMapping.ElementTypeMapping is not null"); + var typeMapping = (RelationalTypeMapping)valuesParameter.TypeMapping.ElementTypeMapping; + var values = (IEnumerable?)ParameterValues[valuesParameter.Name] ?? Array.Empty(); + + var processedValues = new List(); + foreach (var value in values) { - newRowValues[j] = valuesExpression.RowValues[j]; + processedValues.Add( + new RowValueExpression([ + _sqlExpressionFactory.Constant(value, value?.GetType() ?? typeof(object), typeMapping)])); } - } + return processedValues is not [] + ? valuesExpression.Update(processedValues) + : valuesExpression; - if (newRowValues is not null) - { - newRowValues[i] = newRowValue; - } + default: + throw new UnreachableException(); } - - return newRowValues is null ? valuesExpression : valuesExpression.Update(newRowValues); } case SelectExpression selectExpression: @@ -1496,11 +1527,27 @@ protected virtual SqlExpression VisitSqlParameter( bool allowOptimizedExpansion, out bool nullable) { - nullable = ParameterValues[sqlParameterExpression.Name] == null; + var parameterValue = ParameterValues[sqlParameterExpression.Name]; + nullable = parameterValue == null; + + if (nullable) + { + return _sqlExpressionFactory.Constant( + null, + sqlParameterExpression.Type, + sqlParameterExpression.TypeMapping); + } + + if (ParametersToConstantize.Contains(sqlParameterExpression.Name)) + { + DoNotCache(); + return _sqlExpressionFactory.Constant( + parameterValue, + sqlParameterExpression.Type, + sqlParameterExpression.TypeMapping); + } - return nullable - ? _sqlExpressionFactory.Constant(null, sqlParameterExpression.Type, sqlParameterExpression.TypeMapping) - : sqlParameterExpression; + return sqlParameterExpression; } /// diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index bbfaf4b7b78..0aeca7c0527 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -842,14 +842,24 @@ 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++) + switch (valuesExpression) { - rowValues[i] = (RowValueExpression)Visit(valuesExpression.RowValues[i]); - } + case { RowValues: not null }: + 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); - _isSearchCondition = parentSearchCondition; - return valuesExpression.Update(rowValues); + case { ValuesParameter: not null }: + var valuesParameter = (SqlParameterExpression)Visit(valuesExpression.ValuesParameter); + _isSearchCondition = parentSearchCondition; + return valuesExpression.Update(valuesParameter); + + default: + throw new UnreachableException(); + } } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs index 17b97fdb119..3a379fdfe82 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs @@ -19,8 +19,8 @@ public class SqlServerParameterBasedSqlProcessor : RelationalParameterBasedSqlPr /// public SqlServerParameterBasedSqlProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) { } @@ -54,7 +54,7 @@ protected override Expression ProcessSqlNullability( Check.NotNull(selectExpression, nameof(selectExpression)); Check.NotNull(parametersValues, nameof(parametersValues)); - return new SqlServerSqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process( + return new SqlServerSqlNullabilityProcessor(Dependencies, Parameters).Process( selectExpression, parametersValues, out canCache); } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessorFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessorFactory.cs index 0a887046f23..2f9a897ecf5 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessorFactory.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessorFactory.cs @@ -34,6 +34,6 @@ public SqlServerParameterBasedSqlProcessorFactory( /// 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 RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls) - => new SqlServerParameterBasedSqlProcessor(Dependencies, useRelationalNulls); + public virtual RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) + => new SqlServerParameterBasedSqlProcessor(Dependencies, parameters); } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index 67c7726df4a..6e5693a4670 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -210,6 +210,11 @@ protected override Expression VisitValues(ValuesExpression valuesExpression) /// protected override void GenerateValues(ValuesExpression valuesExpression) { + if (valuesExpression.RowValues is null) + { + throw new UnreachableException(); + } + if (valuesExpression.RowValues.Count == 0) { throw new InvalidOperationException(RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs index 1696821a4e3..9a41d9caa7b 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -23,8 +23,8 @@ public class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor /// public SqlServerSqlNullabilityProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) { } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessor.cs index 7d04f8c8716..122c90235ee 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessor.cs @@ -19,8 +19,8 @@ public class SqliteParameterBasedSqlProcessor : RelationalParameterBasedSqlProce /// public SqliteParameterBasedSqlProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) { } @@ -34,5 +34,5 @@ protected override Expression ProcessSqlNullability( Expression queryExpression, IReadOnlyDictionary parametersValues, out bool canCache) - => new SqliteSqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process(queryExpression, parametersValues, out canCache); + => new SqliteSqlNullabilityProcessor(Dependencies, Parameters).Process(queryExpression, parametersValues, out canCache); } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessorFactory.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessorFactory.cs index 667ba2c6e35..58fdf8869f2 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessorFactory.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessorFactory.cs @@ -30,6 +30,6 @@ public SqliteParameterBasedSqlProcessorFactory(RelationalParameterBasedSqlProces /// 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 RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls) - => new SqliteParameterBasedSqlProcessor(_dependencies, useRelationalNulls); + public virtual RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) + => new SqliteParameterBasedSqlProcessor(_dependencies, parameters); } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlNullabilityProcessor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlNullabilityProcessor.cs index 5ecfd0d51e5..653b963d70d 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlNullabilityProcessor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlNullabilityProcessor.cs @@ -22,8 +22,8 @@ public class SqliteSqlNullabilityProcessor : SqlNullabilityProcessor /// public SqliteSqlNullabilityProcessor( RelationalParameterBasedSqlProcessorDependencies dependencies, - bool useRelationalNulls) - : base(dependencies, useRelationalNulls) + RelationalParameterBasedSqlProcessorParameters parameters) + : base(dependencies, parameters) { } diff --git a/src/EFCore/EFCore.csproj b/src/EFCore/EFCore.csproj index 836ce35028f..f8ba3e739d9 100644 --- a/src/EFCore/EFCore.csproj +++ b/src/EFCore/EFCore.csproj @@ -13,6 +13,7 @@ Microsoft.EntityFrameworkCore.DbSet Microsoft.EntityFrameworkCore true true + $(NoWarn);EF9002 $(NoWarn);EF9100 $(NoWarn);EF9101 diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index ed27431a234..a71d12b602f 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -994,6 +994,12 @@ public static string DuplicateTrigger(object? trigger, object? entityType, objec public static string EFConstantInvoked => GetString("EFConstantInvoked"); + /// + /// 'EF.Constant()' isn't supported your by provider. + /// + public static string EFConstantNotSupported + => GetString("EFConstantNotSupported"); + /// /// The EF.Constant<T> method is not supported when using precompiled queries. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index dd90b7d71b3..34cac8e6dd1 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -486,6 +486,9 @@ The EF.Constant<T> method may only be used within Entity Framework LINQ queries. + + 'EF.Constant()' isn't supported by your provider. + The EF.Constant<T> method is not supported when using precompiled queries. diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index 97db2294863..492635b383d 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.Internal; using static System.Linq.Expressions.Expression; namespace Microsoft.EntityFrameworkCore.Query.Internal; @@ -925,13 +926,12 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCall) throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluatableArgument); } - argumentState = argumentState with - { - StateType = StateType.EvaluatableWithoutCapturedVariable, ForceConstantization = true - }; + // Even EF.Constant will be parameter here. + // To have a query cache hit, the constantization will happen later in pipeline. + argumentState = argumentState with { StateType = StateType.EvaluatableWithCapturedVariable }; var evaluatedArgument = ProcessEvaluatableRoot(argument, ref argumentState); _state = argumentState; - return evaluatedArgument; + return Call(method, evaluatedArgument); } case nameof(EF.Parameter): diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index cba57489143..140637f03cf 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -16,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal; public class QueryableMethodNormalizingExpressionVisitor : ExpressionVisitor { private readonly QueryCompilationContext _queryCompilationContext; + private readonly bool _isEfConstantSupported; private readonly SelectManyVerifyingExpressionVisitor _selectManyVerifyingExpressionVisitor = new(); private readonly GroupJoinConvertingExpressionVisitor _groupJoinConvertingExpressionVisitor = new(); @@ -25,9 +26,10 @@ public class QueryableMethodNormalizingExpressionVisitor : ExpressionVisitor /// 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 QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext queryCompilationContext) + public QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext queryCompilationContext, bool isEfConstantSupported) { _queryCompilationContext = queryCompilationContext; + _isEfConstantSupported = isEfConstantSupported; } /// @@ -110,6 +112,19 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return expression; } + if (method.DeclaringType == typeof(EF) + && method.Name == nameof(EF.Constant)) + { + if (!_isEfConstantSupported) + { + throw new InvalidOperationException(CoreStrings.EFConstantNotSupported); + } + + var parameterExpression = (ParameterExpression)Visit(methodCallExpression.Arguments[0]); + _queryCompilationContext.ParametersToConstantize.Add(parameterExpression.Name!); + return parameterExpression; + } + // Normalize list[x] to list.ElementAt(x) if (methodCallExpression is { diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index c30e04ff448..4c1784f9721 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -53,6 +53,17 @@ public class QueryCompilationContext /// public static readonly Expression NotTranslatedExpression = new NotTranslatedExpressionType(); + /// + /// + /// Names of parameters on which was used. Such parameters are later transformed into constants. + /// + /// + /// This property is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + public virtual HashSet ParametersToConstantize { get; } = new(StringComparer.Ordinal); + private static readonly IReadOnlySet EmptySet = new HashSet(); private readonly IQueryTranslationPreprocessorFactory _queryTranslationPreprocessorFactory; diff --git a/src/EFCore/Query/QueryRootProcessor.cs b/src/EFCore/Query/QueryRootProcessor.cs index 84167213994..eecb4f70089 100644 --- a/src/EFCore/Query/QueryRootProcessor.cs +++ b/src/EFCore/Query/QueryRootProcessor.cs @@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Query; /// public class QueryRootProcessor : ExpressionVisitor { - private readonly IModel _model; + private readonly QueryCompilationContext _queryCompilationContext; /// /// Creates a new instance of the class with associated query provider. @@ -22,7 +22,7 @@ public QueryRootProcessor( QueryTranslationPreprocessorDependencies dependencies, QueryCompilationContext queryCompilationContext) { - _model = queryCompilationContext.Model; + _queryCompilationContext = queryCompilationContext; } /// @@ -59,7 +59,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp && (parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>) || parameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)) && parameterType.GetGenericArguments()[0] is var elementClrType - && !_model.FindEntityTypes(elementClrType).Any() + && !_queryCompilationContext.Model.FindEntityTypes(elementClrType).Any() ? VisitQueryRootCandidate(argument, elementClrType) : Visit(argument); diff --git a/src/EFCore/Query/QueryTranslationPreprocessor.cs b/src/EFCore/Query/QueryTranslationPreprocessor.cs index a9860384aa0..1047b540020 100644 --- a/src/EFCore/Query/QueryTranslationPreprocessor.cs +++ b/src/EFCore/Query/QueryTranslationPreprocessor.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Query.Internal; namespace Microsoft.EntityFrameworkCore.Query; @@ -79,7 +80,7 @@ public virtual Expression Process(Expression query) /// A query expression after normalization has been done. public virtual Expression NormalizeQueryableMethod(Expression expression) { - expression = new QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext).Normalize(expression); + expression = new QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext, IsEfConstantSupported).Normalize(expression); expression = ProcessQueryRoots(expression); return expression; @@ -92,4 +93,11 @@ public virtual Expression NormalizeQueryableMethod(Expression expression) /// A query expression after query roots have been added. protected virtual Expression ProcessQueryRoots(Expression expression) => new QueryRootProcessor(Dependencies, QueryCompilationContext).Visit(expression); + + /// + /// A value indicating whether 'EF.Constant' are handled appropriately in postprocessing of query. + /// + [Experimental(EFDiagnostics.ProviderExperimentalApi)] + protected virtual bool IsEfConstantSupported + => false; } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs index 462d9fdbc14..bbf6d938b00 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs @@ -3157,36 +3157,29 @@ FROM root c """); }); - public override Task EF_Constant(bool async) - => Fixture.NoSyncTest( - async, async a => - { - await base.EF_Constant(a); - - AssertSql("ReadItem(None, ALFKI)"); - }); - - public override Task EF_Constant_with_subtree(bool async) - => Fixture.NoSyncTest( - async, async a => - { - await base.EF_Constant_with_subtree(a); + public override async Task EF_Constant(bool async) + { + // #34327 + var exception = await Assert.ThrowsAsync( + () => base.EF_Constant(async)); + Assert.Equal(CoreStrings.EFConstantNotSupported, exception.Message); + } - AssertSql("ReadItem(None, ALFKI)"); - }); + public override async Task EF_Constant_with_subtree(bool async) + { + // #34327 + var exception = await Assert.ThrowsAsync( + () => base.EF_Constant_with_subtree(async)); + Assert.Equal(CoreStrings.EFConstantNotSupported, exception.Message); + } - public override Task EF_Constant_does_not_parameterized_as_part_of_bigger_subtree(bool async) - => Fixture.NoSyncTest( - async, async a => - { - await base.EF_Constant_does_not_parameterized_as_part_of_bigger_subtree(a); -AssertSql( - """ -SELECT VALUE c -FROM root c -WHERE (c["id"] = ("ALF" || "KI")) -"""); - }); + public override async Task EF_Constant_does_not_parameterized_as_part_of_bigger_subtree(bool async) + { + // #34327 + var exception = await Assert.ThrowsAsync( + () => base.EF_Constant_does_not_parameterized_as_part_of_bigger_subtree(async)); + Assert.Equal(CoreStrings.EFConstantNotSupported, exception.Message); + } public override async Task EF_Constant_with_non_evaluatable_argument_throws(bool async) { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 5a1d15393f2..a931d3dccc6 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -177,20 +177,6 @@ public override Task Inline_collection_Contains_with_three_values(bool async) SELECT VALUE c FROM root c WHERE c["Id"] IN (2, 999, 1000) -"""); - }); - - public override Task Inline_collection_Contains_with_EF_Constant(bool async) - => CosmosTestHelpers.Instance.NoSyncTest( - async, async a => - { - await base.Inline_collection_Contains_with_EF_Constant(a); - - AssertSql( - """ -SELECT VALUE c -FROM root c -WHERE c["Id"] IN (2, 999, 1000) """); }); @@ -500,6 +486,37 @@ FROM a IN (SELECT VALUE [30, c["NullableInt"], @__i_0])) = 30) """); }); + public override Task Inline_collection_with_single_parameter_element_Contains(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Inline_collection_with_single_parameter_element_Contains(a); + + AssertSql( + """ +ReadItem(None, 2) +"""); + }); + + public override Task Inline_collection_with_single_parameter_element_Count(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Inline_collection_with_single_parameter_element_Count(a); + + AssertSql( + """ +@__i_0='2' + +SELECT VALUE c +FROM root c +WHERE (( + SELECT VALUE COUNT(1) + FROM a IN (SELECT VALUE [@__i_0]) + WHERE (a > c["Id"])) = 1) +"""); + }); + public override Task Parameter_collection_Count(bool async) => CosmosTestHelpers.Instance.NoSyncTest( async, async a => @@ -799,6 +816,30 @@ WHERE ARRAY_CONTAINS(@__ints_0, c["Int"]) """); }); + public override async Task Parameter_collection_Contains_with_EF_Constant(bool async) + { + // #34327 + var exception = await Assert.ThrowsAsync( + () => base.Parameter_collection_Contains_with_EF_Constant(async)); + Assert.Equal(CoreStrings.EFConstantNotSupported, exception.Message); + } + + public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any(bool async) + { + // #34327 + var exception = await Assert.ThrowsAsync( + () => base.Parameter_collection_Where_with_EF_Constant_Where_Any(async)); + Assert.Equal(CoreStrings.EFConstantNotSupported, exception.Message); + } + + public override async Task Parameter_collection_Count_with_column_predicate_with_EF_Constant(bool async) + { + // #34327 + var exception = await Assert.ThrowsAsync( + () => base.Parameter_collection_Count_with_column_predicate_with_EF_Constant(async)); + Assert.Equal(CoreStrings.EFConstantNotSupported, exception.Message); + } + public override Task Column_collection_of_ints_Contains(bool async) => CosmosTestHelpers.Instance.NoSyncTest( async, async a => diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 4fa6eaed80a..5b041cbfc25 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -87,18 +87,6 @@ public virtual Task Inline_collection_Contains_with_three_values(bool async) async, ss => ss.Set().Where(c => new[] { 2, 999, 1000 }.Contains(c.Id))); - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Inline_collection_Contains_with_EF_Constant(bool async) - { - var ids = new[] { 2, 999, 1000 }; - - return AssertQuery( - async, - ss => ss.Set().Where(c => EF.Constant(ids).Contains(c.Id)), - ss => ss.Set().Where(c => new[] { 2, 99, 1000 }.Contains(c.Id))); - } - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Inline_collection_Contains_with_all_parameters(bool async) @@ -275,6 +263,30 @@ await AssertQuery( ss => ss.Set().Where(c => new[] { 30, c.NullableInt, i }.Max() == 30)); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inline_collection_with_single_parameter_element_Contains(bool async) + { + var i = 2; + + return AssertQuery( + async, + ss => ss.Set().Where(c => new[] { i }.Contains(c.Id)), + ss => ss.Set().Where(c => new[] { i }.Contains(c.Id))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inline_collection_with_single_parameter_element_Count(bool async) + { + var i = 2; + + return AssertQuery( + async, + ss => ss.Set().Where(c => new[] { i }.Count(i => i > c.Id) == 1), + ss => ss.Set().Where(c => new[] { i }.Count(i => i > c.Id) == 1)); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Parameter_collection_Count(bool async) @@ -463,6 +475,42 @@ await AssertQuery( assertEmpty: true); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_collection_Contains_with_EF_Constant(bool async) + { + var ids = new[] { 2, 999, 1000 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => EF.Constant(ids).Contains(c.Id)), + ss => ss.Set().Where(c => ids.Contains(c.Id))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_collection_Where_with_EF_Constant_Where_Any(bool async) + { + var ids = new[] { 2, 999, 1000 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => EF.Constant(ids).Where(x => x > 0).Any()), + ss => ss.Set().Where(c => ids.Where(x => x > 0).Any())); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Parameter_collection_Count_with_column_predicate_with_EF_Constant(bool async) + { + var ids = new[] { 2, 999, 1000 }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => EF.Constant(ids).Count(i => i > c.Id) == 2), + ss => ss.Set().Where(c => ids.Count(i => i > c.Id) == 2)); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Column_collection_of_ints_Contains(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index 5877a8c7c14..077c136ab9a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -161,18 +161,6 @@ WHERE [p].[Id] IN (2, 999, 1000) """); } - public override async Task Inline_collection_Contains_with_EF_Constant(bool async) - { - await base.Inline_collection_Contains_with_EF_Constant(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].[NullableString], [p].[NullableStrings], [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); @@ -434,6 +422,37 @@ SELECT MAX([v].[Value]) """); } + public override async Task Inline_collection_with_single_parameter_element_Contains(bool async) + { + await base.Inline_collection_with_single_parameter_element_Contains(async); + + AssertSql( + """ +@__i_0='2' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] = @__i_0 +"""); + } + + public override async Task Inline_collection_with_single_parameter_element_Count(bool async) + { + await base.Inline_collection_with_single_parameter_element_Count(async); + + AssertSql( + """ +@__i_0='2' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (CAST(@__i_0 AS int))) AS [v]([Value]) + WHERE [v].[Value] > [p].[Id]) = 1 +"""); + } + public override Task Parameter_collection_Count(bool async) => AssertCompatibilityLevelTooLow(() => base.Parameter_collection_Count(async)); @@ -647,6 +666,48 @@ FROM [PrimitiveCollectionsEntity] AS [p] """); } + public override async Task Parameter_collection_Contains_with_EF_Constant(bool async) + { + await base.Parameter_collection_Contains_with_EF_Constant(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].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] IN (2, 999, 1000) +"""); + } + + public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any(bool async) + { + await base.Parameter_collection_Where_with_EF_Constant_Where_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].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + WHERE [i].[Value] > 0) +"""); + } + + public override async Task Parameter_collection_Count_with_column_predicate_with_EF_Constant(bool async) + { + await base.Parameter_collection_Count_with_column_predicate_with_EF_Constant(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].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + WHERE [i].[Value] > [p].[Id]) = 2 +"""); + } + public override Task Column_collection_of_ints_Contains(bool async) => AssertCompatibilityLevelTooLow(() => base.Column_collection_of_ints_Contains(async)); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index 09878bccedd..b1159553a81 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs @@ -150,18 +150,6 @@ WHERE [p].[Id] IN (2, 999, 1000) """); } - public override async Task Inline_collection_Contains_with_EF_Constant(bool async) - { - await base.Inline_collection_Contains_with_EF_Constant(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].[NullableString], [p].[NullableStrings], [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); @@ -399,6 +387,37 @@ WHERE GREATEST(30, [p].[NullableInt], NULL) = 30 """); } + public override async Task Inline_collection_with_single_parameter_element_Contains(bool async) + { + await base.Inline_collection_with_single_parameter_element_Contains(async); + + AssertSql( + """ +@__i_0='2' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] = @__i_0 +"""); + } + + public override async Task Inline_collection_with_single_parameter_element_Count(bool async) + { + await base.Inline_collection_with_single_parameter_element_Count(async); + + AssertSql( + """ +@__i_0='2' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (CAST(@__i_0 AS int))) AS [v]([Value]) + WHERE [v].[Value] > [p].[Id]) = 1 +"""); + } + public override async Task Parameter_collection_Count(bool async) { await base.Parameter_collection_Count(async); @@ -734,6 +753,48 @@ FROM OPENJSON(NULL) AS [i] """); } + public override async Task Parameter_collection_Contains_with_EF_Constant(bool async) + { + await base.Parameter_collection_Contains_with_EF_Constant(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].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] IN (2, 999, 1000) +"""); + } + + public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any(bool async) + { + await base.Parameter_collection_Where_with_EF_Constant_Where_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].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + WHERE [i].[Value] > 0) +"""); + } + + public override async Task Parameter_collection_Count_with_column_predicate_with_EF_Constant(bool async) + { + await base.Parameter_collection_Count_with_column_predicate_with_EF_Constant(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].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + WHERE [i].[Value] > [p].[Id]) = 2 +"""); + } + public override async Task Column_collection_of_ints_Contains(bool async) { await base.Column_collection_of_ints_Contains(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 09d2a392c88..d99fb9ea774 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -149,18 +149,6 @@ WHERE [p].[Id] IN (2, 999, 1000) """); } - public override async Task Inline_collection_Contains_with_EF_Constant(bool async) - { - await base.Inline_collection_Contains_with_EF_Constant(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].[NullableString], [p].[NullableStrings], [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); @@ -422,6 +410,37 @@ SELECT MAX([v].[Value]) """); } + public override async Task Inline_collection_with_single_parameter_element_Contains(bool async) + { + await base.Inline_collection_with_single_parameter_element_Contains(async); + + AssertSql( + """ +@__i_0='2' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] = @__i_0 +"""); + } + + public override async Task Inline_collection_with_single_parameter_element_Count(bool async) + { + await base.Inline_collection_with_single_parameter_element_Count(async); + + AssertSql( + """ +@__i_0='2' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (CAST(@__i_0 AS int))) AS [v]([Value]) + WHERE [v].[Value] > [p].[Id]) = 1 +"""); + } + public override async Task Parameter_collection_Count(bool async) { await base.Parameter_collection_Count(async); @@ -757,6 +776,48 @@ FROM OPENJSON(NULL) AS [i] """); } + public override async Task Parameter_collection_Contains_with_EF_Constant(bool async) + { + await base.Parameter_collection_Contains_with_EF_Constant(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].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Id] IN (2, 999, 1000) +"""); + } + + public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any(bool async) + { + await base.Parameter_collection_Where_with_EF_Constant_Where_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].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + WHERE [i].[Value] > 0) +"""); + } + + public override async Task Parameter_collection_Count_with_column_predicate_with_EF_Constant(bool async) + { + await base.Parameter_collection_Count_with_column_predicate_with_EF_Constant(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].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT COUNT(*) + FROM (VALUES (2), (999), (1000)) AS [i]([Value]) + WHERE [i].[Value] > [p].[Id]) = 2 +"""); + } + public override async Task Column_collection_of_ints_Contains(bool async) { await base.Column_collection_of_ints_Contains(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index ae4f0870f76..641a258fd1b 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -155,18 +155,6 @@ public override async Task Inline_collection_Contains_with_three_values(bool asy """); } - public override async Task Inline_collection_Contains_with_EF_Constant(bool async) - { - await base.Inline_collection_Contains_with_EF_Constant(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"."NullableString", "p"."NullableStrings", "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); @@ -412,6 +400,37 @@ SELECT MAX("v"."Value") """); } + public override async Task Inline_collection_with_single_parameter_element_Contains(bool async) + { + await base.Inline_collection_with_single_parameter_element_Contains(async); + + AssertSql( + """ +@__i_0='2' + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Id" = @__i_0 +"""); + } + + public override async Task Inline_collection_with_single_parameter_element_Count(bool async) + { + await base.Inline_collection_with_single_parameter_element_Count(async); + + AssertSql( + """ +@__i_0='2' + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM (SELECT CAST(@__i_0 AS INTEGER) AS "Value") AS "v" + WHERE "v"."Value" > "p"."Id") = 1 +"""); + } + public override async Task Parameter_collection_Count(bool async) { await base.Parameter_collection_Count(async); @@ -747,6 +766,48 @@ FROM json_each(NULL) AS "i" """); } + public override async Task Parameter_collection_Contains_with_EF_Constant(bool async) + { + await base.Parameter_collection_Contains_with_EF_Constant(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"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Id" IN (2, 999, 1000) +"""); + } + + public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any(bool async) + { + await base.Parameter_collection_Where_with_EF_Constant_Where_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"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE EXISTS ( + SELECT 1 + FROM (SELECT 2 AS "Value" UNION ALL VALUES (999), (1000)) AS "i" + WHERE "i"."Value" > 0) +"""); + } + + public override async Task Parameter_collection_Count_with_column_predicate_with_EF_Constant(bool async) + { + await base.Parameter_collection_Count_with_column_predicate_with_EF_Constant(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"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE ( + SELECT COUNT(*) + FROM (SELECT 2 AS "Value" UNION ALL VALUES (999), (1000)) AS "i" + WHERE "i"."Value" > "p"."Id") = 2 +"""); + } + public override async Task Column_collection_of_ints_Contains(bool async) { await base.Column_collection_of_ints_Contains(async); diff --git a/test/EFCore.Tests/Infrastructure/EntityFrameworkMetricsTest.cs b/test/EFCore.Tests/Infrastructure/EntityFrameworkMetricsTest.cs index c2f3bfffb53..9efee770e7d 100644 --- a/test/EFCore.Tests/Infrastructure/EntityFrameworkMetricsTest.cs +++ b/test/EFCore.Tests/Infrastructure/EntityFrameworkMetricsTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using OpenTelemetry; using OpenTelemetry.Metrics; @@ -116,7 +117,7 @@ public async Task Validate_query_cache_hits(bool async) { using var context = new SomeDbContext(); - var query = context.Foos.Where(e => e.Id == Guids[0]); + var query = context.Foos.Where(e => e.Id == new Guid("BB833808-1ADC-4FC2-ACB2-AA6EA31A7DBF")); _ = async ? await query.ToListAsync() : query.ToList(); @@ -142,8 +143,13 @@ public async Task Validate_query_cache_misses(bool async) { using var context = new SomeDbContext(); - var guid = Guids[^(i + 1)]; - var query = context.Foos.Where(e => e.Id == EF.Constant(guid)); + var query = i switch + { + 0 => context.Foos.Where(e => e.Id == new Guid("BB833808-1ADC-4FC2-ACB2-AA6EA31A7DBE")), + 1 => context.Foos.Where(e => e.Id == new Guid("BB833808-1ADC-4FC2-ACB2-AA6EA31A7DBD")), + 2 => context.Foos.Where(e => e.Id == new Guid("BB833808-1ADC-4FC2-ACB2-AA6EA31A7DBC")), + _ => throw new UnreachableException(), + }; _ = async ? await query.ToListAsync() : query.ToList(); @@ -303,14 +309,6 @@ private class Foo public Guid Id { get; set; } public int Token { get; set; } } - - private static readonly Guid[] Guids = - [ - new("BB833808-1ADC-4FC2-ACB2-AA6EA31A7DBF"), - new("BB833808-1ADC-4FC2-ACB2-AA6EA31A7DBE"), - new("BB833808-1ADC-4FC2-ACB2-AA6EA31A7DBD"), - new("BB833808-1ADC-4FC2-ACB2-AA6EA31A7DBC"), - ]; } [CollectionDefinition(nameof(MetricsDataCollection), DisableParallelization = true)]