Initial support for DateOnly/TimeOnly
parent
ba913012ce
commit
c1d263aedc
@ -0,0 +1,63 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
|
||||
|
||||
namespace EntityFrameworkCore.Jet.Query.ExpressionTranslators.Internal;
|
||||
|
||||
/// <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>
|
||||
public class JetDateOnlyMemberTranslator : IMemberTranslator
|
||||
{
|
||||
private static readonly Dictionary<string, string> DatePartMapping
|
||||
= new()
|
||||
{
|
||||
{ nameof(DateTime.Year), "yyyy" },
|
||||
{ nameof(DateTime.Month), "m" },
|
||||
{ nameof(DateTime.DayOfYear), "y" },
|
||||
{ nameof(DateTime.Day), "d" }
|
||||
};
|
||||
|
||||
private readonly ISqlExpressionFactory _sqlExpressionFactory;
|
||||
|
||||
/// <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>
|
||||
public JetDateOnlyMemberTranslator(ISqlExpressionFactory sqlExpressionFactory)
|
||||
{
|
||||
_sqlExpressionFactory = sqlExpressionFactory;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public virtual SqlExpression? Translate(
|
||||
SqlExpression? instance,
|
||||
MemberInfo member,
|
||||
Type returnType,
|
||||
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
|
||||
=> member.DeclaringType == typeof(DateOnly) && DatePartMapping.TryGetValue(member.Name, out var datePart)
|
||||
? _sqlExpressionFactory.Function(
|
||||
"DATEPART",
|
||||
new[] { _sqlExpressionFactory.Constant(datePart), instance! },
|
||||
nullable: true,
|
||||
argumentsPropagateNullability: new[] { false, true },
|
||||
returnType)
|
||||
: null;
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
|
||||
|
||||
namespace EntityFrameworkCore.Jet.Query.ExpressionTranslators.Internal;
|
||||
|
||||
/// <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>
|
||||
public class JetDateOnlyMethodTranslator : IMethodCallTranslator
|
||||
{
|
||||
private readonly Dictionary<MethodInfo, string> _methodInfoDatePartMapping = new()
|
||||
{
|
||||
{ typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddYears), new[] { typeof(int) })!, "yyyy" },
|
||||
{ typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddMonths), new[] { typeof(int) })!, "m" },
|
||||
{ typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddDays), new[] { typeof(int) })!, "d" }
|
||||
};
|
||||
|
||||
private readonly ISqlExpressionFactory _sqlExpressionFactory;
|
||||
|
||||
/// <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>
|
||||
public JetDateOnlyMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
|
||||
{
|
||||
_sqlExpressionFactory = sqlExpressionFactory;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public virtual SqlExpression? Translate(
|
||||
SqlExpression? instance,
|
||||
MethodInfo method,
|
||||
IReadOnlyList<SqlExpression> arguments,
|
||||
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
|
||||
{
|
||||
if (_methodInfoDatePartMapping.TryGetValue(method, out var datePart)
|
||||
&& instance != null)
|
||||
{
|
||||
instance = _sqlExpressionFactory.ApplyDefaultTypeMapping(instance);
|
||||
|
||||
return _sqlExpressionFactory.Function(
|
||||
"DATEADD",
|
||||
new[] { _sqlExpressionFactory.Constant(datePart), _sqlExpressionFactory.Convert(arguments[0], typeof(int)), instance },
|
||||
nullable: true,
|
||||
argumentsPropagateNullability: new[] { false, true, true },
|
||||
instance.Type,
|
||||
instance.TypeMapping);
|
||||
}
|
||||
|
||||
if (method.DeclaringType == typeof(DateOnly)
|
||||
&& method.Name == nameof(DateOnly.FromDateTime)
|
||||
&& arguments.Count == 1)
|
||||
{
|
||||
return _sqlExpressionFactory.Convert(arguments[0], typeof(DateOnly));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
|
||||
|
||||
namespace EntityFrameworkCore.Jet.Query.ExpressionTranslators.Internal;
|
||||
|
||||
/// <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>
|
||||
public class JetTimeOnlyMemberTranslator : IMemberTranslator
|
||||
{
|
||||
private static readonly Dictionary<string, string> DatePartMappings = new()
|
||||
{
|
||||
{ nameof(TimeOnly.Hour), "h" },
|
||||
{ nameof(TimeOnly.Minute), "n" },
|
||||
{ nameof(TimeOnly.Second), "s" },
|
||||
{ nameof(TimeOnly.Millisecond), "millisecond" }
|
||||
};
|
||||
|
||||
private readonly ISqlExpressionFactory _sqlExpressionFactory;
|
||||
|
||||
/// <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>
|
||||
public JetTimeOnlyMemberTranslator(ISqlExpressionFactory sqlExpressionFactory)
|
||||
{
|
||||
_sqlExpressionFactory = sqlExpressionFactory;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public virtual SqlExpression? Translate(
|
||||
SqlExpression? instance,
|
||||
MemberInfo member,
|
||||
Type returnType,
|
||||
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
|
||||
{
|
||||
if (member.DeclaringType == typeof(TimeOnly) && DatePartMappings.TryGetValue(member.Name, out var value))
|
||||
{
|
||||
return _sqlExpressionFactory.Function(
|
||||
"DATEPART", new[] { _sqlExpressionFactory.Constant(value), instance! },
|
||||
nullable: true,
|
||||
argumentsPropagateNullability: new[] { false, true },
|
||||
returnType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
|
||||
using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions;
|
||||
|
||||
namespace EntityFrameworkCore.Jet.Query.ExpressionTranslators.Internal;
|
||||
|
||||
/// <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>
|
||||
public class JetTimeOnlyMethodTranslator : IMethodCallTranslator
|
||||
{
|
||||
private static readonly MethodInfo AddHoursMethod = typeof(TimeOnly).GetRuntimeMethod(
|
||||
nameof(TimeOnly.AddHours), new[] { typeof(double) })!;
|
||||
private static readonly MethodInfo AddMinutesMethod = typeof(TimeOnly).GetRuntimeMethod(
|
||||
nameof(TimeOnly.AddMinutes), new[] { typeof(double) })!;
|
||||
private static readonly MethodInfo IsBetweenMethod = typeof(TimeOnly).GetRuntimeMethod(
|
||||
nameof(TimeOnly.IsBetween), new[] { typeof(TimeOnly), typeof(TimeOnly) })!;
|
||||
|
||||
private readonly ISqlExpressionFactory _sqlExpressionFactory;
|
||||
|
||||
/// <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>
|
||||
public JetTimeOnlyMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
|
||||
{
|
||||
_sqlExpressionFactory = sqlExpressionFactory;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public virtual SqlExpression? Translate(
|
||||
SqlExpression? instance,
|
||||
MethodInfo method,
|
||||
IReadOnlyList<SqlExpression> arguments,
|
||||
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
|
||||
{
|
||||
if (method.DeclaringType != typeof(TimeOnly) || instance is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (method == AddHoursMethod || method == AddMinutesMethod)
|
||||
{
|
||||
var datePart = method == AddHoursMethod ? "h" : "n";
|
||||
|
||||
// Some Add methods accept a double, and SQL Server DateAdd does not accept number argument outside of int range
|
||||
if (arguments[0] is SqlConstantExpression { Value: double and (<= int.MinValue or >= int.MaxValue) })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
instance = _sqlExpressionFactory.ApplyDefaultTypeMapping(instance);
|
||||
|
||||
var dadd = _sqlExpressionFactory.Function(
|
||||
"DATEADD",
|
||||
new[] { _sqlExpressionFactory.Constant(datePart), _sqlExpressionFactory.Convert(arguments[0], typeof(int)), instance },
|
||||
nullable: true,
|
||||
argumentsPropagateNullability: new[] { false, true, true },
|
||||
instance.Type,
|
||||
instance.TypeMapping);
|
||||
|
||||
return _sqlExpressionFactory.Function("TIMEVALUE", new[] { dadd }, true,
|
||||
argumentsPropagateNullability: new[] { true }, instance.Type, instance.TypeMapping);
|
||||
}
|
||||
|
||||
// Translate TimeOnly.IsBetween to a >= b AND a < c.
|
||||
// Since a is evaluated multiple times, only translate for simple constructs (i.e. avoid duplicating complex subqueries).
|
||||
if (method == IsBetweenMethod
|
||||
&& instance is ColumnExpression or SqlConstantExpression or SqlParameterExpression)
|
||||
{
|
||||
var typeMapping = ExpressionExtensions.InferTypeMapping(instance, arguments[0], arguments[1]);
|
||||
instance = _sqlExpressionFactory.ApplyTypeMapping(instance, typeMapping);
|
||||
|
||||
return _sqlExpressionFactory.And(
|
||||
_sqlExpressionFactory.GreaterThanOrEqual(
|
||||
instance,
|
||||
_sqlExpressionFactory.ApplyTypeMapping(arguments[0], typeMapping)),
|
||||
_sqlExpressionFactory.LessThan(
|
||||
instance,
|
||||
_sqlExpressionFactory.ApplyTypeMapping(arguments[1], typeMapping)));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using EntityFrameworkCore.Jet.Data;
|
||||
using EntityFrameworkCore.Jet.Infrastructure.Internal;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace EntityFrameworkCore.Jet.Storage.Internal
|
||||
{
|
||||
public class JetDateOnlyTypeMapping : DateOnlyTypeMapping
|
||||
{
|
||||
private readonly IJetOptions _options;
|
||||
|
||||
public JetDateOnlyTypeMapping(
|
||||
[NotNull] string storeType,
|
||||
[NotNull] IJetOptions options,
|
||||
DbType? dbType = null)
|
||||
: base(storeType, dbType ?? System.Data.DbType.DateTime)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
protected JetDateOnlyTypeMapping(RelationalTypeMappingParameters parameters, IJetOptions options)
|
||||
: base(parameters)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
|
||||
=> new JetDateOnlyTypeMapping(parameters, _options);
|
||||
|
||||
protected override void ConfigureParameter(DbParameter parameter)
|
||||
{
|
||||
base.ConfigureParameter(parameter);
|
||||
if (parameter.Value != null)
|
||||
{
|
||||
((DateOnly)parameter.Value).Deconstruct(out int year, out int month, out int day);
|
||||
parameter.Value = new DateTime(year, month, day);
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GenerateNonNullSqlLiteral(object value)
|
||||
=> GenerateNonNullSqlLiteral(value, false);
|
||||
|
||||
public virtual string GenerateNonNullSqlLiteral(object value, bool defaultClauseCompatible)
|
||||
{
|
||||
var dateTime = ConvertToDateTimeCompatibleValue(value);
|
||||
|
||||
dateTime = CheckDateTimeValue(dateTime);
|
||||
|
||||
var literal = new StringBuilder();
|
||||
|
||||
literal.Append(
|
||||
defaultClauseCompatible
|
||||
? "'"
|
||||
: "#");
|
||||
|
||||
literal.AppendFormat(CultureInfo.InvariantCulture, "{0:yyyy-MM-dd}", dateTime);
|
||||
literal.Append(
|
||||
defaultClauseCompatible
|
||||
? "'"
|
||||
: "#");
|
||||
|
||||
return literal.ToString();
|
||||
}
|
||||
|
||||
protected virtual DateTime ConvertToDateTimeCompatibleValue(object value)
|
||||
{
|
||||
((DateOnly)value).Deconstruct(out int year, out int month, out int day);
|
||||
return new DateTime(year, month, day);
|
||||
}
|
||||
|
||||
private static DateTime CheckDateTimeValue(DateTime dateTime)
|
||||
{
|
||||
if (dateTime < JetConfiguration.TimeSpanOffset)
|
||||
{
|
||||
if (dateTime != default)
|
||||
{
|
||||
throw new InvalidOperationException($"The {nameof(DateTime)} value '{dateTime}' is smaller than the minimum supported value of '{JetConfiguration.TimeSpanOffset}'.");
|
||||
}
|
||||
|
||||
dateTime = JetConfiguration.TimeSpanOffset;
|
||||
}
|
||||
|
||||
return dateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using EntityFrameworkCore.Jet.Data;
|
||||
using EntityFrameworkCore.Jet.Infrastructure.Internal;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace EntityFrameworkCore.Jet.Storage.Internal
|
||||
{
|
||||
public class JetTimeOnlyTypeMapping : TimeOnlyTypeMapping
|
||||
{
|
||||
[NotNull] private readonly IJetOptions _options;
|
||||
|
||||
public JetTimeOnlyTypeMapping(
|
||||
[NotNull] string storeType,
|
||||
[NotNull] IJetOptions options)
|
||||
: base(storeType, System.Data.DbType.DateTime)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
protected JetTimeOnlyTypeMapping(RelationalTypeMappingParameters parameters, IJetOptions options)
|
||||
: base(parameters)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
protected override void ConfigureParameter(DbParameter parameter)
|
||||
{
|
||||
base.ConfigureParameter(parameter);
|
||||
if (parameter.Value != null)
|
||||
{
|
||||
((TimeOnly)parameter.Value).Deconstruct(out int hour, out int min, out int sec);
|
||||
parameter.Value = JetConfiguration.TimeSpanOffset.Add(new TimeSpan(hour, min, sec));
|
||||
}
|
||||
}
|
||||
|
||||
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
|
||||
=> new JetTimeOnlyTypeMapping(parameters, _options);
|
||||
|
||||
protected override string GenerateNonNullSqlLiteral(object value)
|
||||
{
|
||||
//Format time without any milliseconds
|
||||
if (!_options.EnableMillisecondsSupport)
|
||||
{
|
||||
return FormattableString.Invariant($@"TIMEVALUE('{value:HH\:mm\:ss}')");
|
||||
}
|
||||
else
|
||||
{
|
||||
//TODO: Treat as double
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue