Skip to content

Commit

Permalink
Add AtTimeZoneExpression and some relevant translations
Browse files Browse the repository at this point in the history
  • Loading branch information
roji committed Dec 14, 2021
1 parent 0e1e95b commit c74a696
Show file tree
Hide file tree
Showing 12 changed files with 326 additions and 3 deletions.
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
39 changes: 37 additions & 2 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ namespace Microsoft.EntityFrameworkCore.Query;
/// </summary>
public class QuerySqlGenerator : SqlExpressionVisitor
{

private static readonly Dictionary<ExpressionType, string> OperatorMap = new()
{
{ ExpressionType.Equal, " = " },
Expand Down Expand Up @@ -820,6 +819,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 @@ -839,7 +874,7 @@ protected virtual bool RequiresParentheses(SqlExpression outerExpression, SqlExp
{
switch (innerExpression)
{
case LikeExpression:
case AtTimeZoneExpression or LikeExpression:
return true;

case SqlUnaryExpression sqlUnaryExpression:
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 @@ -25,6 +25,9 @@ protected override Expression VisitExtension(Expression extensionExpression)
return shapedQueryExpression.Update(
Visit(shapedQueryExpression.QueryExpression), shapedQueryExpression.ShaperExpression);

case AtTimeZoneExpression atTimeZoneExpression:
return VisitAtTimeZone(atTimeZoneExpression);

case CaseExpression caseExpression:
return VisitCase(caseExpression);

Expand Down Expand Up @@ -116,6 +119,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 @@ -278,7 +289,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
91 changes: 91 additions & 0 deletions src/EFCore.Relational/Query/SqlExpressions/AtTimeZoneExpression.cs
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 left operand.
/// </summary>
public virtual SqlExpression Operand { get; }

/// <summary>
/// The right operand.
/// </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 @@ -348,6 +348,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 @@ -404,6 +406,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
18 changes: 18 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1438,4 +1438,22 @@ 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"/>.
/// </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="dateTime">The value to convert to <c>datetimeoffset</c>.</param>
/// <param name="timeZone">The time zone to convert to.</param>
/// <returns>The <c>datetimeoffset</c> resulting from the conversion.</returns>
public static DateTimeOffset AtTimeZone(
this DbFunctions _,
DateTime dateTime,
string timeZone)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(AtTimeZone)));
}
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,12 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator
{ typeof(DateTimeOffset).GetRequiredRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), typeof(double)), "millisecond" }
};

private static readonly MethodInfo _atTimeZoneMethodInfo = typeof(SqlServerDbFunctionsExtensions)
.GetRequiredRuntimeMethod(nameof(SqlServerDbFunctionsExtensions.AtTimeZone), typeof(DbFunctions), typeof(DateTime), typeof(string));

private static readonly MethodInfo _convertTimeBySystemTimeZoneIdMethodInfo = typeof(TimeZoneInfo)
.GetRequiredRuntimeMethod(nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), typeof(DateTimeOffset), typeof(string));

private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly IRelationalTypeMappingSource _typeMappingSource;

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

if (method == _convertTimeBySystemTimeZoneIdMethodInfo)
{
var (operand, timeZone) = (arguments[0], arguments[1]);

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

operand = _sqlExpressionFactory.ApplyDefaultTypeMapping(operand);
timeZone = _sqlExpressionFactory.ApplyDefaultTypeMapping(timeZone);

return new AtTimeZoneExpression(operand, timeZone, typeof(DateTimeOffset), operand.TypeMapping);
}

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

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

return new AtTimeZoneExpression(
_sqlExpressionFactory.ApplyDefaultTypeMapping(operand),
_sqlExpressionFactory.ApplyDefaultTypeMapping(timeZone),
typeof(DateTimeOffset),
_typeMappingSource.FindMapping(typeof(DateTimeOffset)));
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8269,6 +8269,15 @@ public virtual Task Enum_matching_take_value_gets_different_type_mapping(bool as
.Select(g => g.Rank & value));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Where_TimeZoneInfo_ConvertTimeBySystemTimeZoneId_DateTimeOffset(bool async)
=> AssertQuery(
async,
ss => ss.Set<Mission>().Where(
m => TimeZoneInfo.ConvertTimeBySystemTimeZoneId(m.Timeline, "UTC")
== new DateTimeOffset(2, 3, 1, 13, 0, 0, TimeSpan.Zero)));

protected GearsOfWarContext CreateContext()
=> Fixture.CreateContext();

Expand Down
Loading

0 comments on commit c74a696

Please sign in to comment.