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 AtTimeZoneExpression and some relevant translations #26972

Merged
merged 1 commit into from
May 24, 2022
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 All.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ The .NET Foundation licenses this file to you under the MIT license.
<s:Boolean x:Key="/Default/UserDictionary/Words/=composability/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=composable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Constructible/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=datetimeoffset/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=doesnt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=efcore/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=evaluatable/@EntryIndexedValue">True</s:Boolean>
Expand Down
38 changes: 37 additions & 1 deletion src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,42 @@ protected override Expression VisitIn(InExpression inExpression)
return inExpression;
}

/// <inheritdoc />
protected override Expression VisitAtTimeZone(AtTimeZoneExpression atTimeZoneExpression)
{
var requiresBrackets = RequiresParentheses(atTimeZoneExpression, atTimeZoneExpression.Operand);

if (requiresBrackets)
{
_relationalCommandBuilder.Append("(");
}

Visit(atTimeZoneExpression.Operand);

if (requiresBrackets)
{
_relationalCommandBuilder.Append(")");
}

_relationalCommandBuilder.Append(" AT TIME ZONE ");

requiresBrackets = RequiresParentheses(atTimeZoneExpression, atTimeZoneExpression.TimeZone);

if (requiresBrackets)
{
_relationalCommandBuilder.Append("(");
}

Visit(atTimeZoneExpression.TimeZone);

if (requiresBrackets)
{
_relationalCommandBuilder.Append(")");
}

return atTimeZoneExpression;
}

/// <summary>
/// Gets a SQL operator for a SQL binary operation.
/// </summary>
Expand All @@ -914,7 +950,7 @@ protected virtual bool RequiresParentheses(SqlExpression outerExpression, SqlExp
{
switch (innerExpression)
{
case LikeExpression:
case AtTimeZoneExpression or LikeExpression:
return true;

case SqlUnaryExpression sqlUnaryExpression:
Expand Down
4 changes: 4 additions & 0 deletions src/EFCore.Relational/Query/SqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)

return sqlExpression switch
{
AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping),
CaseExpression e => ApplyTypeMappingOnCase(e, typeMapping),
CollateExpression e => ApplyTypeMappingOnCollate(e, typeMapping),
DistinctExpression e => ApplyTypeMappingOnDistinct(e, typeMapping),
Expand All @@ -69,6 +70,9 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
};
}

private SqlExpression ApplyTypeMappingOnAtTimeZone(AtTimeZoneExpression atTimeZoneExpression, RelationalTypeMapping? typeMapping)
=> new AtTimeZoneExpression(atTimeZoneExpression.Operand, atTimeZoneExpression.TimeZone, atTimeZoneExpression.Type, typeMapping);

private SqlExpression ApplyTypeMappingOnLike(LikeExpression likeExpression)
{
var inferredTypeMapping = (likeExpression.EscapeChar == null
Expand Down
13 changes: 12 additions & 1 deletion src/EFCore.Relational/Query/SqlExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ protected override Expression VisitExtension(Expression extensionExpression)
case ShapedQueryExpression shapedQueryExpression:
return shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression));

case AtTimeZoneExpression atTimeZoneExpression:
return VisitAtTimeZone(atTimeZoneExpression);

case CaseExpression caseExpression:
return VisitCase(caseExpression);

Expand Down Expand Up @@ -115,6 +118,14 @@ protected override Expression VisitExtension(Expression extensionExpression)
return base.VisitExtension(extensionExpression);
}


/// <summary>
/// Visits the children of the sql "at time zone" expression.
/// </summary>
/// <param name="atTimeZoneExpression">The expression to visit.</param>
/// <returns>The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.</returns>
protected abstract Expression VisitAtTimeZone(AtTimeZoneExpression atTimeZoneExpression);

/// <summary>
/// Visits the children of the case expression.
/// </summary>
Expand Down Expand Up @@ -277,7 +288,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
protected abstract Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression);

/// <summary>
/// Visits the children of the sql fragent expression.
/// Visits the children of the sql fragment expression.
/// </summary>
/// <param name="sqlFragmentExpression">The expression to visit.</param>
/// <returns>The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.</returns>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// 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.SqlExpressions;

/// <summary>
/// <para>
/// An expression that represents an AT TIME ZONE operation in a SQL tree.
/// </para>
/// <para>
/// This type is typically used by database providers (and other extensions). It is generally
/// not used in application code.
/// </para>
/// </summary>
public class AtTimeZoneExpression : SqlExpression
{
/// <summary>
/// Creates a new instance of the <see cref="AtTimeZoneExpression" /> class.
/// </summary>
/// <param name="operand">The operand on which to perform the time zone conversion.</param>
/// <param name="timeZone">The time zone to convert to.</param>
/// <param name="type">The <see cref="Type" /> of the expression.</param>
/// <param name="typeMapping">The <see cref="RelationalTypeMapping" /> associated with the expression.</param>
public AtTimeZoneExpression(
SqlExpression operand,
SqlExpression timeZone,
Type type,
RelationalTypeMapping? typeMapping)
: base(type, typeMapping)
{
Operand = operand;
TimeZone = timeZone;
}

/// <summary>
/// The input operand on which to apply the time zone.
/// </summary>
public virtual SqlExpression Operand { get; }

/// <summary>
/// The time zone to be applied.
/// </summary>
public virtual SqlExpression TimeZone { get; }

/// <inheritdoc />
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var operand = (SqlExpression)visitor.Visit(Operand);
var timeZone = (SqlExpression)visitor.Visit(TimeZone);

return Update(operand, timeZone);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="operand">The <see cref="Operand" /> property of the result.</param>
/// <param name="timeZone">The <see cref="TimeZone" /> property of the result.</param>
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
public virtual AtTimeZoneExpression Update(SqlExpression operand, SqlExpression timeZone)
=> operand != Operand || timeZone != TimeZone
? new AtTimeZoneExpression(operand, timeZone, Type, TypeMapping)
: this;

/// <inheritdoc />
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Visit(Operand);

expressionPrinter.Append(" AT TIME ZONE ");

expressionPrinter.Visit(TimeZone);
}

/// <inheritdoc />
public override bool Equals(object? obj)
=> obj != null
&& (ReferenceEquals(this, obj)
|| obj is AtTimeZoneExpression atTimeZoneExpression
&& Equals(atTimeZoneExpression));

private bool Equals(AtTimeZoneExpression atTimeZoneExpression)
=> base.Equals(atTimeZoneExpression)
&& Operand.Equals(atTimeZoneExpression.Operand)
&& TimeZone.Equals(atTimeZoneExpression.TimeZone);

/// <inheritdoc />
public override int GetHashCode()
=> HashCode.Combine(base.GetHashCode(), Operand, TimeZone);
}
21 changes: 21 additions & 0 deletions src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,8 @@ protected virtual SelectExpression Visit(SelectExpression selectExpression)
var nullValueColumnsCount = _nullValueColumns.Count;
var result = sqlExpression switch
{
AtTimeZoneExpression sqlAtTimeZoneExpression
=> VisitAtTimeZone(sqlAtTimeZoneExpression, allowOptimizedExpansion, out nullable),
CaseExpression caseExpression
=> VisitCase(caseExpression, allowOptimizedExpansion, out nullable),
CollateExpression collateExpression
Expand Down Expand Up @@ -413,6 +415,25 @@ protected virtual SqlExpression VisitCustomSqlExpression(
=> throw new InvalidOperationException(
RelationalStrings.UnhandledExpressionInVisitor(sqlExpression, sqlExpression.GetType(), nameof(SqlNullabilityProcessor)));

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected virtual SqlExpression VisitAtTimeZone(
AtTimeZoneExpression atTimeZoneExpression,
bool allowOptimizedExpansion,
out bool nullable)
{
var operand = Visit(atTimeZoneExpression.Operand, out var operandNullable);
var timeZone = Visit(atTimeZoneExpression.TimeZone, out var timeZoneNullable);

nullable = operandNullable || timeZoneNullable;

return atTimeZoneExpression.Update(operand, timeZone);
}

/// <summary>
/// Visits a <see cref="CaseExpression" /> and computes its nullability.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1438,4 +1438,50 @@ public static bool IsNumeric(
this DbFunctions _,
string expression)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(IsNumeric)));

/// <summary>
/// Converts <paramref name="dateTime" /> to the corresponding <c>datetimeoffset</c> in the target <paramref name="timeZone" />.
/// Corresponds to the SQL Server's <c>AT TIME ZONE</c> construct.
/// </summary>
/// <remarks>
/// <para>
/// Note that the <see cref="DateTime.Kind" /> of <paramref name="dateTime" /> is not taken into account when performing the
/// conversion; the offset for the provided time zone is simply applied as-is.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
/// for more information and examples.
/// </para>
/// </remarks>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="dateTime">The value to convert to <c>datetimeoffset</c>.</param>
/// <param name="timeZone">A valid SQL Server time zone ID.</param>
/// <returns>The <c>datetimeoffset</c> resulting from the conversion.</returns>
roji marked this conversation as resolved.
Show resolved Hide resolved
/// <seealso href="https://docs.microsoft.com/sql/t-sql/queries/at-time-zone-transact-sql">SQL Server documentation for <c>AT TIME ZONE</c>.</seealso>
public static DateTimeOffset AtTimeZone(
this DbFunctions _,
DateTime dateTime,
string timeZone)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(AtTimeZone)));

/// <summary>
/// Converts <paramref name="dateTimeOffset" /> to the time zone specified by <paramref name="timeZone"/>.
/// Corresponds to the SQL Server's <c>AT TIME ZONE</c> construct.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
/// for more information and examples.
/// </remarks>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="dateTimeOffset">The value on which to perform the time zone conversion.</param>
/// <param name="timeZone">A valid SQL Server time zone ID.</param>
/// <returns>The <c>datetimeoffset</c> resulting from the conversion.</returns>
/// <seealso href="https://docs.microsoft.com/sql/t-sql/queries/at-time-zone-transact-sql">SQL Server documentation for <c>AT TIME ZONE</c>.</seealso>
public static DateTimeOffset AtTimeZone(
this DbFunctions _,
DateTimeOffset dateTimeOffset,
string timeZone)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(AtTimeZone)));
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec
.TryAdd<IMemberTranslatorProvider, SqlServerMemberTranslatorProvider>()
.TryAdd<IQuerySqlGeneratorFactory, SqlServerQuerySqlGeneratorFactory>()
.TryAdd<IRelationalSqlTranslatingExpressionVisitorFactory, SqlServerSqlTranslatingExpressionVisitorFactory>()
.TryAdd<ISqlExpressionFactory, SqlServerSqlExpressionFactory>()
.TryAdd<IRelationalParameterBasedSqlProcessorFactory, SqlServerParameterBasedSqlProcessorFactory>()
.TryAdd<INavigationExpansionExtensibilityHelper, SqlServerNavigationExpansionExtensibilityHelper>()
.TryAdd<IQueryableMethodTranslatingExpressionVisitorFactory, SqlServerQueryableMethodTranslatingExpressionVisitorFactory>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,23 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
: selectExpression;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override Expression VisitAtTimeZone(AtTimeZoneExpression atTimeZoneExpression)
{
var parentSearchCondition = _isSearchCondition;
_isSearchCondition = false;
var operand = (SqlExpression)Visit(atTimeZoneExpression.Operand);
var timeZone = (SqlExpression)Visit(atTimeZoneExpression.TimeZone);
_isSearchCondition = parentSearchCondition;

return atTimeZoneExpression.Update(operand, timeZone);
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), new[] { typeof(double) })!, "millisecond" }
};

private static readonly MethodInfo AtTimeZoneDateTimeOffsetMethodInfo = typeof(SqlServerDbFunctionsExtensions)
.GetRuntimeMethod(
nameof(SqlServerDbFunctionsExtensions.AtTimeZone), new[] { typeof(DbFunctions), typeof(DateTimeOffset), typeof(string) })!;

private static readonly MethodInfo AtTimeZoneDateTimeMethodInfo = typeof(SqlServerDbFunctionsExtensions)
.GetRuntimeMethod(
nameof(SqlServerDbFunctionsExtensions.AtTimeZone), new[] { typeof(DbFunctions), typeof(DateTime), typeof(string) })!;

private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly IRelationalTypeMappingSource _typeMappingSource;

Expand Down Expand Up @@ -89,6 +97,48 @@ public SqlServerDateTimeMethodTranslator(
instance.TypeMapping);
}

if (method == AtTimeZoneDateTimeOffsetMethodInfo || method == AtTimeZoneDateTimeMethodInfo)
{
var (operand, timeZone) = (arguments[1], arguments[2]);

RelationalTypeMapping? resultTypeMapping = null;

// The AT TIME ZONE construct bubbles up the precision of its operand, so when invoked over datetime2(2) it returns a
// datetimeoffset(2). So if the operand has a type mapping, bubble it up accordingly, otherwise allow the result type mapping
// to be inferred.
if (operand.TypeMapping is { } operandTypeMapping)
{
switch (operandTypeMapping.StoreTypeNameBase)
{
case "datetimeoffset":
resultTypeMapping = operandTypeMapping;
break;
case "datetime" or "datetime2" or "smalldatetime":
resultTypeMapping = _typeMappingSource.FindMapping(
typeof(DateTimeOffset), "datetimeoffset", precision: operandTypeMapping.Precision);
break;
default:
Check.DebugAssert(
false,
$"Unknown operand type mapping '{operandTypeMapping.StoreTypeNameBase}' when translating EF.Functions.AtTimeZone");
break;
}
}

if (operand is SqlConstantExpression)
{
// Our constant representation for datetime/datetimeoffset is an untyped string literal, which the AT TIME ZONE expression
// does not accept. Type it explicitly.
operand = _sqlExpressionFactory.Convert(operand, operand.Type);
}

return new AtTimeZoneExpression(
operand,
_sqlExpressionFactory.ApplyTypeMapping(timeZone, _typeMappingSource.FindMapping("varchar")),
typeof(DateTimeOffset),
resultTypeMapping);
}

return null;
}
}
Loading