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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions src/EFCore.Cosmos/Query/Internal/Expressions/InExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -154,17 +160,9 @@ public virtual InExpression Update(
SqlExpression item,
IReadOnlyList<SqlExpression>? 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);
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ public override Expression Process(Expression query)

return result;
}

/// <inheritdoc />
protected override bool IsEfConstantSupported
=> true;
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,9 @@
<data name="OneOfThreeValuesMustBeSet" xml:space="preserve">
<value>Exactly one of '{param1}', '{param2}' or '{param3}' must be set.</value>
</data>
<data name="OneOfTwoValuesMustBeSet" xml:space="preserve">
<value>Exactly one of '{param1}' or '{param2}' must be set.</value>
</data>
<data name="OnlyConstantsSupportedInInlineCollectionQueryRoots" xml:space="preserve">
<value>Only constants are supported inside inline collection query roots.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface IRelationalParameterBasedSqlProcessorFactory
/// <summary>
/// Creates a new <see cref="RelationalParameterBasedSqlProcessor" />.
/// </summary>
/// <param name="useRelationalNulls">A bool value indicating if relational nulls should be used.</param>
/// <param name="parameters">Parameters for <see cref="RelationalParameterBasedSqlProcessor" />.</param>
/// <returns>A relational parameter based sql processor.</returns>
RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls);
RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ public RelationalCommandCache(
IQuerySqlGeneratorFactory querySqlGeneratorFactory,
IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory,
Expression queryExpression,
bool useRelationalNulls)
bool useRelationalNulls,
HashSet<string> parametersToConstantize)
{
_memoryCache = memoryCache;
_querySqlGeneratorFactory = querySqlGeneratorFactory;
_queryExpression = queryExpression;
_relationalParameterBasedSqlProcessor = relationalParameterBasedSqlProcessorFactory.Create(useRelationalNulls);
_relationalParameterBasedSqlProcessor = relationalParameterBasedSqlProcessorFactory.Create(new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls, parametersToConstantize));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public virtual RelationalParameterBasedSqlProcessor Create(bool useRelationalNulls)
=> new(Dependencies, useRelationalNulls);
public virtual RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters)
=> new(Dependencies, parameters);
}
4 changes: 4 additions & 0 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,10 @@ protected override Expression VisitValues(ValuesExpression valuesExpression)
/// <param name="valuesExpression">The <see cref="ValuesExpression" /> for which to generate SQL.</param>
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ public class RelationalParameterBasedSqlProcessor
/// Creates a new instance of the <see cref="RelationalParameterBasedSqlProcessor" /> class.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this class.</param>
/// <param name="useRelationalNulls">A bool value indicating if relational nulls should be used.</param>
/// <param name="parameters">Parameter object containing parameters for this class.</param>
public RelationalParameterBasedSqlProcessor(
RelationalParameterBasedSqlProcessorDependencies dependencies,
bool useRelationalNulls)
RelationalParameterBasedSqlProcessorParameters parameters)
{
Dependencies = dependencies;
UseRelationalNulls = useRelationalNulls;
Parameters = parameters;
}

/// <summary>
Expand All @@ -36,9 +36,9 @@ public RelationalParameterBasedSqlProcessor(
protected virtual RelationalParameterBasedSqlProcessorDependencies Dependencies { get; }

/// <summary>
/// A bool value indicating if relational nulls should be used.
/// Parameter object containing parameters for this class.
/// </summary>
protected virtual bool UseRelationalNulls { get; }
protected virtual RelationalParameterBasedSqlProcessorParameters Parameters { get; }

/// <summary>
/// Optimizes the query expression for given parameter values.
Expand Down Expand Up @@ -74,7 +74,7 @@ protected virtual Expression ProcessSqlNullability(
Expression queryExpression,
IReadOnlyDictionary<string, object?> parametersValues,
out bool canCache)
=> new SqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process(queryExpression, parametersValues, out canCache);
=> new SqlNullabilityProcessor(Dependencies, Parameters).Process(queryExpression, parametersValues, out canCache);

/// <summary>
/// Expands the parameters to <see cref="FromSqlExpression" /> inside the query expression for given parameter values.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Parameters for <see cref="RelationalParameterBasedSqlProcessor" />.
/// </summary>
public sealed record RelationalParameterBasedSqlProcessorParameters
{
/// <summary>
/// A value indicating if relational nulls should be used.
/// </summary>
public bool UseRelationalNulls { get; init; }

/// <summary>
/// A collection of parameter names to constantize.
/// </summary>
public HashSet<string> ParametersToConstantize { get; init; }

/// <summary>
/// Creates a new instance of <see cref="RelationalParameterBasedSqlProcessorParameters" />.
/// </summary>
/// <param name="useRelationalNulls">A value indicating if relational nulls should be used.</param>
/// <param name="parametersToConstantize">A collection of parameter names to constantize.</param>
[EntityFrameworkInternal]
public RelationalParameterBasedSqlProcessorParameters(bool useRelationalNulls, HashSet<string> parametersToConstantize)
{
UseRelationalNulls = useRelationalNulls;
ParametersToConstantize = parametersToConstantize;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ public override Expression NormalizeQueryableMethod(Expression expression)
/// <inheritdoc />
protected override Expression ProcessQueryRoots(Expression expression)
=> new RelationalQueryRootProcessor(Dependencies, RelationalDependencies, QueryCompilationContext).Visit(expression);

/// <inheritdoc />
protected override bool IsEfConstantSupported
=> true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -378,7 +396,6 @@ JsonScalarExpression jsonScalar
for (var i = 0; i < sqlExpressions.Length; i++)
{
var sqlExpression = sqlExpressions[i];

rowExpressions[i] =
new RowValueExpression(
new[]
Expand All @@ -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);
}

/// <inheritdoc />
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
Expand All @@ -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);
}

/// <summary>
/// This visitor has been obsoleted; Extend RelationalTypeMappingPostprocessor instead, and invoke it from
/// <see cref="RelationalQueryTranslationPostprocessor.ProcessTypeMappings" />.
Expand Down
Loading