Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add quotability to all SQL expression types #33210

Merged
merged 1 commit into from
Mar 6, 2024
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
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 @@ -430,10 +430,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
Loading