Skip to content

Commit

Permalink
Add quotability to all SQL expression types (#33210)
Browse files Browse the repository at this point in the history
Closes #33008
  • Loading branch information
roji committed Mar 6, 2024
1 parent 13f7edd commit fbfd468
Show file tree
Hide file tree
Showing 67 changed files with 976 additions and 78 deletions.
1 change: 1 addition & 0 deletions src/EFCore.Design/EFCore.Design.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<DevelopmentDependency>true</DevelopmentDependency>
<ImplicitUsings>true</ImplicitUsings>
<NoWarn>EF1003</NoWarn> <!-- Precompiled query is experimental -->
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions src/EFCore.Relational/EFCore.Relational.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<RootNamespace>Microsoft.EntityFrameworkCore</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ImplicitUsings>true</ImplicitUsings>
<NoWarn>$(NoWarn);EF1003</NoWarn> <!-- Precomiled query is experimental -->
</PropertyGroup>

<ItemGroup>
Expand Down
7 changes: 7 additions & 0 deletions src/EFCore.Relational/Metadata/IRelationalModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ IEnumerable<ISequence> Sequences
/// <returns>The table with a given name or <see langword="null" /> if no table with the given name is defined.</returns>
ITable? FindTable(string name, string? schema);

/// <summary>
/// Gets the default table with the given name. Returns <see langword="null" /> if no table with the given name is defined.
/// </summary>
/// <param name="name">The name of the table.</param>
/// <returns>The default table with a given name or <see langword="null" /> if no table with the given name is defined.</returns>
TableBase? FindDefaultTable(string name);

/// <summary>
/// Gets the view with the given name. Returns <see langword="null" /> if no view with the given name is defined.
/// </summary>
Expand Down
25 changes: 10 additions & 15 deletions src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,33 +91,28 @@ public override bool IsReadOnly

/// <inheritdoc />
public virtual ITable? FindTable(string name, string? schema)
=> Tables.TryGetValue((name, schema), out var table)
? table
: null;
=> Tables.GetValueOrDefault((name, schema));

// TODO: Confirm that this makes sense
/// <inheritdoc />
public virtual TableBase? FindDefaultTable(string name)
=> DefaultTables.GetValueOrDefault(name);

/// <inheritdoc />
public virtual IView? FindView(string name, string? schema)
=> Views.TryGetValue((name, schema), out var view)
? view
: null;
=> Views.GetValueOrDefault((name, schema));

/// <inheritdoc />
public virtual ISqlQuery? FindQuery(string name)
=> Queries.TryGetValue(name, out var query)
? query
: null;
=> Queries.GetValueOrDefault(name);

/// <inheritdoc />
public virtual IStoreFunction? FindFunction(string name, string? schema, IReadOnlyList<string> parameters)
=> Functions.TryGetValue((name, schema, parameters), out var function)
? function
: null;
=> Functions.GetValueOrDefault((name, schema, parameters));

/// <inheritdoc />
public virtual IStoreStoredProcedure? FindStoredProcedure(string name, string? schema)
=> StoredProcedures.TryGetValue((name, schema), out var storedProcedure)
? storedProcedure
: null;
=> StoredProcedures.GetValueOrDefault((name, schema));

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
20 changes: 20 additions & 0 deletions src/EFCore.Relational/Query/IRelationalQuotableExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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;

/// <summary>
/// Represents an expression that is quotable, that is, capable of returning an expression that, when evaluated, would construct an
/// expression identical to this one. Used to generate code for precompiled queries, which reconstructs this expression.
/// </summary>
[Experimental("EF1003")]
public interface IRelationalQuotableExpression
{
/// <summary>
/// Quotes the expression; that is, returns an expression that, when evaluated, would construct an expression identical to this
/// one. Used to generate code for precompiled queries, which reconstructs this expression.
/// </summary>
Expression Quote();
}
2 changes: 1 addition & 1 deletion src/EFCore.Relational/Query/ISqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ SqlFunctionExpression NiladicFunction(
/// <param name="value">A value.</param>
/// <param name="typeMapping">The <see cref="RelationalTypeMapping" /> associated with the expression.</param>
/// <returns>An expression representing a constant in a SQL tree.</returns>
SqlConstantExpression Constant(object? value, RelationalTypeMapping? typeMapping = null);
SqlConstantExpression Constant(object value, RelationalTypeMapping? typeMapping = null);

/// <summary>
/// Creates a new <see cref="SqlConstantExpression" /> which represents a constant in a SQL tree.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ object ProcessConstantValue(object? existingConstantValue)
}

return _sqlExpressionFactory.Constant(
existingConstantValue, _typeMappingSource.GetMappingForValue(existingConstantValue));
existingConstantValue,
existingConstantValue?.GetType() ?? typeof(object),
_typeMappingSource.GetMappingForValue(existingConstantValue));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public GetValueOrDefaultTranslator(ISqlExpressionFactory sqlExpressionFactory)
return _sqlExpressionFactory.Coalesce(
instance,
arguments.Count == 0
? new SqlConstantExpression(method.ReturnType.GetDefaultValueConstant(), null)
? new SqlConstantExpression(method.ReturnType.GetDefaultValue(), method.ReturnType, typeMapping: null)
: arguments[0],
instance.TypeMapping);
}
Expand Down
4 changes: 4 additions & 0 deletions src/EFCore.Relational/Query/Internal/TpcTablesExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ protected override TpcTablesExpression WithAnnotations(IReadOnlyDictionary<strin
public override TpcTablesExpression WithAlias(string newAlias)
=> new(newAlias, EntityType, SelectExpressions, DiscriminatorColumn, DiscriminatorValues, Annotations);

/// <inheritdoc />
public override Expression Quote()
=> throw new UnreachableException("TpcTablesExpression is a temporary tree representation and should never be quoted");

/// <inheritdoc />
public override TableExpressionBase Clone(string? alias, ExpressionVisitor cloningExpressionVisitor)
{
Expand Down
19 changes: 18 additions & 1 deletion src/EFCore.Relational/Query/PathSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ namespace Microsoft.EntityFrameworkCore.Query;
/// not used in application code.
/// </para>
/// </summary>
public readonly struct PathSegment
public readonly struct PathSegment : IRelationalQuotableExpression
{
private static ConstructorInfo? _pathSegmentPropertyConstructor, _pathSegmentArrayIndexConstructor;

/// <summary>
/// Creates a new <see cref="PathSegment" /> struct representing JSON property access.
/// </summary>
Expand Down Expand Up @@ -46,6 +48,21 @@ public PathSegment(SqlExpression arrayIndex)
/// </summary>
public SqlExpression? ArrayIndex { get; }

/// <inheritdoc />
public Expression Quote()
=> this switch
{
{ PropertyName: string propertyName }
=> Expression.New(
_pathSegmentPropertyConstructor ??= typeof(PathSegment).GetConstructor([typeof(string)])!,
Expression.Constant(propertyName)),
{ ArrayIndex: SqlExpression arrayIndex }
=> Expression.New(
_pathSegmentArrayIndexConstructor ??= typeof(PathSegment).GetConstructor([typeof(SqlExpression)])!,
arrayIndex.Quote()),
_ => throw new UnreachableException()
};

/// <inheritdoc />
public override string ToString()
=> PropertyName
Expand Down
150 changes: 150 additions & 0 deletions src/EFCore.Relational/Query/RelationalExpressionQuotingUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using static System.Linq.Expressions.Expression;

namespace Microsoft.EntityFrameworkCore.Query;

/// <summary>
/// Utilities used for implementing <see cref="IRelationalQuotableExpression" />.
/// </summary>
[Experimental("EF1003")]
public static class RelationalExpressionQuotingUtilities
{
private static readonly ParameterExpression RelationalModelParameter
= Parameter(typeof(RelationalModel), "relationalModel");
private static readonly ParameterExpression RelationalTypeMappingSourceParameter
= Parameter(typeof(RelationalTypeMappingSource), "relationalTypeMappingSource");

private static readonly MethodInfo RelationalModelFindTableMethod
= typeof(RelationalModel).GetMethod(nameof(RelationalModel.FindTable), [typeof(string), typeof(string)])!;

private static readonly MethodInfo RelationalModelFindDefaultTableMethod
= typeof(RelationalModel).GetMethod(nameof(RelationalModel.FindDefaultTable), [typeof(string)])!;

private static readonly MethodInfo RelationalModelFindViewMethod
= typeof(RelationalModel).GetMethod(nameof(RelationalModel.FindView), [typeof(string), typeof(string)])!;

private static readonly MethodInfo RelationalModelFindQueryMethod
= typeof(RelationalModel).GetMethod(nameof(RelationalModel.FindQuery), [typeof(string)])!;

private static readonly MethodInfo RelationalModelFindFunctionMethod
= typeof(RelationalModel).GetMethod(
nameof(RelationalModel.FindFunction), [typeof(string), typeof(string), typeof(IReadOnlyList<string>)])!;

private static ConstructorInfo? _annotationConstructor;
private static ConstructorInfo? _dictionaryConstructor;
private static MethodInfo? _dictionaryAddMethod;
private static MethodInfo? _hashSetAddMethod;

private static readonly MethodInfo RelationalTypeMappingSourceFindMappingMethod
= typeof(RelationalTypeMappingSource)
.GetMethod(
nameof(RelationalTypeMappingSource.FindMapping),
[
typeof(Type), typeof(string), typeof(bool), typeof(bool), typeof(int), typeof(bool), typeof(bool), typeof(int),
typeof(int)
])!;

/// <summary>
/// If <paramref name="expression" /> is <see langword="null" />, returns a <see cref="ConstantExpression" /> with a
/// <see langword="null" /> value. Otherwise, calls <see cref="IRelationalQuotableExpression.Quote" /> and returns the result.
/// </summary>
public static Expression VisitOrNull<T>(T? expression) where T : IRelationalQuotableExpression
=> expression is null ? Constant(null, typeof(T)) : expression.Quote();

/// <summary>
/// Quotes a relational type mapping.
/// </summary>
public static Expression QuoteTypeMapping(RelationalTypeMapping? typeMapping)
=> typeMapping is null
? Constant(null, typeof(RelationalTypeMapping))
: Call(
RelationalTypeMappingSourceParameter,
RelationalTypeMappingSourceFindMappingMethod,
Constant(typeMapping.ClrType, typeof(Type)),
Constant(typeMapping.StoreType, typeof(string)),
Constant(false), // TODO: keyOrIndex not accessible
Constant(typeMapping.IsUnicode, typeof(bool?)),
Constant(typeMapping.Size, typeof(int?)),
Constant(false, typeof(bool?)), // TODO: rowversion not accessible
Constant(typeMapping.IsFixedLength, typeof(bool?)),
Constant(typeMapping.Precision, typeof(int?)),
Constant(typeMapping.Scale, typeof(int?)));

/// <summary>
/// Quotes an <see cref="ITableBase" />.
/// </summary>
public static Expression QuoteTableBase(ITableBase tableBase)
=> tableBase switch
{
ITable table
=> Call(
RelationalModelParameter,
RelationalModelFindTableMethod,
Constant(table.Name, typeof(string)),
Constant(table.Schema, typeof(string))),

TableBase table
=> Call(
RelationalModelParameter,
RelationalModelFindDefaultTableMethod,
Constant(table.Name, typeof(string))),

IView view
=> Call(
RelationalModelParameter,
RelationalModelFindViewMethod,
Constant(view.Name, typeof(string)),
Constant(view.Schema, typeof(string))),

ISqlQuery query
=> Call(
RelationalModelParameter,
RelationalModelFindQueryMethod,
Constant(query.Name, typeof(string))),

IStoreFunction function
=> Call(
RelationalModelParameter,
RelationalModelFindFunctionMethod,
Constant(function.Name, typeof(string)),
Constant(function.Schema, typeof(string)),
NewArrayInit(typeof(string), function.Parameters.Select(p => Constant(p.StoreType)))),

IStoreStoredProcedure => throw new UnreachableException(),

_ => throw new UnreachableException()
};

/// <summary>
/// Quotes a set of string tags.
/// </summary>
public static Expression QuoteTags(ISet<string> tags)
=> ListInit(
New(typeof(HashSet<string>)),
tags.Select(
t => ElementInit(
_hashSetAddMethod ??= typeof(HashSet<string>).GetMethod(nameof(HashSet<string>.Add))!,
Constant(t))));

/// <summary>
/// Quotes the annotations on a <see cref="TableExpressionBase" />.
/// </summary>
public static Expression QuoteAnnotations(IReadOnlyDictionary<string, IAnnotation>? annotations)
=> annotations is null or { Count: 0 }
? Constant(null, typeof(IReadOnlyDictionary<string, IAnnotation>))
: ListInit(
New(_dictionaryConstructor ??= typeof(IDictionary<string, IAnnotation>).GetConstructor([])!),
annotations.Select(
a => ElementInit(
_dictionaryAddMethod ??= typeof(Dictionary<string, IAnnotation>).GetMethod("Add")!,
Constant(a.Key),
New(
_annotationConstructor ??= typeof(Annotation).GetConstructor([typeof(string), typeof(object)])!,
Constant(a.Key),
Constant(a.Value)))));
}
Original file line number Diff line number Diff line change
Expand Up @@ -431,10 +431,13 @@ private void AddEntitySelectConditions(SelectExpression selectExpression, IEntit
var concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToList();
var predicate = concreteEntityTypes.Count == 1
? (SqlExpression)_sqlExpressionFactory.Equal(
discriminatorColumn, _sqlExpressionFactory.Constant(concreteEntityTypes[0].GetDiscriminatorValue()))
discriminatorColumn,
_sqlExpressionFactory.Constant(concreteEntityTypes[0].GetDiscriminatorValue(), discriminatorColumn.Type))
: _sqlExpressionFactory.In(
discriminatorColumn,
concreteEntityTypes.Select(et => _sqlExpressionFactory.Constant(et.GetDiscriminatorValue())).ToArray());
concreteEntityTypes
.Select(et => _sqlExpressionFactory.Constant(et.GetDiscriminatorValue(), discriminatorColumn.Type))
.ToArray());

selectExpression.ApplyPredicate(predicate);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2482,7 +2482,7 @@ protected virtual ValuesExpression ApplyTypeMappingsOnValuesExpression(ValuesExp
newRowValues[i] = new RowValueExpression(newValues);
}

return new ValuesExpression(valuesExpression.Alias, newRowValues, newColumnNames, valuesExpression.GetAnnotations());
return new ValuesExpression(valuesExpression.Alias, newRowValues, newColumnNames);
}
}
}
Loading

0 comments on commit fbfd468

Please sign in to comment.