Skip to content

Commit

Permalink
Implement DateOnly/TimeOnly support for Sqlite (#24989)
Browse files Browse the repository at this point in the history
Closes #24506
  • Loading branch information
roji authored Jun 15, 2021
1 parent b5915fb commit f6b109b
Show file tree
Hide file tree
Showing 24 changed files with 1,308 additions and 16 deletions.
57 changes: 57 additions & 0 deletions src/EFCore.Relational/Storage/DateOnlyTypeMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Data;

namespace Microsoft.EntityFrameworkCore.Storage
{
/// <summary>
/// <para>
/// Represents the mapping between a .NET <see cref="DateOnly" /> type and a database type.
/// </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 DateOnlyTypeMapping : RelationalTypeMapping
{
private const string DateOnlyFormatConst = @"{0:yyyy-MM-dd}";

/// <summary>
/// Initializes a new instance of the <see cref="DateOnlyTypeMapping" /> class.
/// </summary>
/// <param name="storeType"> The name of the database type. </param>
/// <param name="dbType"> The <see cref="DbType" /> to be used. </param>
public DateOnlyTypeMapping(
string storeType,
DbType? dbType = null)
: base(storeType, typeof(DateOnly), dbType)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DateOnlyTypeMapping" /> class.
/// </summary>
/// <param name="parameters"> Parameter object for <see cref="RelationalTypeMapping" />. </param>
protected DateOnlyTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}

/// <summary>
/// Creates a copy of this mapping.
/// </summary>
/// <param name="parameters"> The parameters for this mapping. </param>
/// <returns> The newly created mapping. </returns>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new DateOnlyTypeMapping(parameters);

/// <summary>
/// Gets the string format to be used to generate SQL literals of this type.
/// </summary>
protected override string SqlLiteralFormatString
=> "DATE '" + DateOnlyFormatConst + "'";
}
}
61 changes: 61 additions & 0 deletions src/EFCore.Relational/Storage/TimeOnlyTypeMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Globalization;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Storage
{
/// <summary>
/// <para>
/// Represents the mapping between a .NET <see cref="TimeOnly" /> type and a database type.
/// </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 TimeOnlyTypeMapping : RelationalTypeMapping
{
/// <summary>
/// Initializes a new instance of the <see cref="TimeOnlyTypeMapping" /> class.
/// </summary>
/// <param name="storeType"> The name of the database type. </param>
/// <param name="dbType"> The <see cref="DbType" /> to be used. </param>
public TimeOnlyTypeMapping(
string storeType,
DbType? dbType = null)
: base(storeType, typeof(TimeOnly), dbType)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TimeOnlyTypeMapping" /> class.
/// </summary>
/// <param name="parameters"> Parameter object for <see cref="RelationalTypeMapping" />. </param>
protected TimeOnlyTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}

/// <summary>
/// Creates a copy of this mapping.
/// </summary>
/// <param name="parameters"> The parameters for this mapping. </param>
/// <returns> The newly created mapping. </returns>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new TimeOnlyTypeMapping(parameters);

/// <inheritdoc />
protected override string GenerateNonNullSqlLiteral(object value)
{
var timeOnly = (TimeOnly)value;

return timeOnly.Ticks % TimeSpan.TicksPerSecond == 0
? FormattableString.Invariant($@"TIME '{value:HH\:mm\:ss}'")
: FormattableString.Invariant($@"TIME '{value:HH\:mm\:ss\.FFFFFFF}'");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Sqlite.Query.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 SqliteDateOnlyMemberTranslator : IMemberTranslator
{
private static readonly Dictionary<string, string> _datePartMapping
= new()
{
{ nameof(DateOnly.Year), "%Y" },
{ nameof(DateOnly.Month), "%m" },
{ nameof(DateOnly.DayOfYear), "%j" },
{ nameof(DateOnly.Day), "%d" },
{ nameof(DateOnly.DayOfWeek), "%w" }
};

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 SqliteDateOnlyMemberTranslator(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)
{
Check.NotNull(member, nameof(member));
Check.NotNull(returnType, nameof(returnType));
Check.NotNull(logger, nameof(logger));

return member.DeclaringType == typeof(DateOnly) && _datePartMapping.TryGetValue(member.Name, out var datePart)
? _sqlExpressionFactory.Convert(
SqliteExpression.Strftime(
_sqlExpressionFactory,
typeof(string),
datePart,
instance!),
returnType)
: null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ private static readonly MethodInfo _addTicks
{ typeof(DateTime).GetRequiredRuntimeMethod(nameof(DateTime.AddDays), new[] { typeof(double) }), " days" },
{ typeof(DateTime).GetRequiredRuntimeMethod(nameof(DateTime.AddHours), new[] { typeof(double) }), " hours" },
{ typeof(DateTime).GetRequiredRuntimeMethod(nameof(DateTime.AddMinutes), new[] { typeof(double) }), " minutes" },
{ typeof(DateTime).GetRequiredRuntimeMethod(nameof(DateTime.AddSeconds), new[] { typeof(double) }), " seconds" }
{ typeof(DateTime).GetRequiredRuntimeMethod(nameof(DateTime.AddSeconds), new[] { typeof(double) }), " seconds" },

{ typeof(DateOnly).GetRequiredRuntimeMethod(nameof(DateOnly.AddYears), new[] { typeof(int) }), " years" },
{ typeof(DateOnly).GetRequiredRuntimeMethod(nameof(DateOnly.AddMonths), new[] { typeof(int) }), " months" },
{ typeof(DateOnly).GetRequiredRuntimeMethod(nameof(DateOnly.AddDays), new[] { typeof(int) }), " days" },
};

private readonly ISqlExpressionFactory _sqlExpressionFactory;
Expand Down Expand Up @@ -64,6 +68,18 @@ public SqliteDateTimeAddTranslator(ISqlExpressionFactory sqlExpressionFactory)
Check.NotNull(arguments, nameof(arguments));
Check.NotNull(logger, nameof(logger));

return method.DeclaringType == typeof(DateTime)
? TranslateDateTime(instance, method, arguments)
: method.DeclaringType == typeof(DateOnly)
? TranslateDateOnly(instance, method, arguments)
: null;
}

private SqlExpression? TranslateDateTime(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments)
{
SqlExpression? modifier = null;
if (_addMilliseconds.Equals(method))
{
Expand Down Expand Up @@ -122,5 +138,29 @@ public SqliteDateTimeAddTranslator(ISqlExpressionFactory sqlExpressionFactory)

return null;
}

private SqlExpression? TranslateDateOnly(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments)
{
if (instance is not null && _methodInfoToUnitSuffix.TryGetValue(method, out var unitSuffix))
{
return _sqlExpressionFactory.Function(
"date",
new[]
{
instance,
_sqlExpressionFactory.Add(
_sqlExpressionFactory.Convert(arguments[0], typeof(string)),
_sqlExpressionFactory.Constant(unitSuffix))
},
argumentsPropagateNullability: new[] { true, true },
nullable: true,
returnType: method.ReturnType);
}

return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public SqliteMemberTranslatorProvider(RelationalMemberTranslatorProviderDependen
AddTranslators(
new IMemberTranslator[]
{
new SqliteDateTimeMemberTranslator(sqlExpressionFactory), new SqliteStringLengthTranslator(sqlExpressionFactory)
new SqliteDateTimeMemberTranslator(sqlExpressionFactory),
new SqliteStringLengthTranslator(sqlExpressionFactory),
new SqliteDateOnlyMemberTranslator(sqlExpressionFactory)
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Data;
using Microsoft.EntityFrameworkCore.Storage;

namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.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 SqliteDateOnlyTypeMapping : DateOnlyTypeMapping
{
private const string DateOnlyFormatConst = @"'{0:yyyy\-MM\-dd}'";

/// <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 SqliteDateOnlyTypeMapping(
string storeType,
DbType? dbType = null)
: base(storeType, dbType)
{
}

/// <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 SqliteDateOnlyTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}

/// <summary>
/// Creates a copy of this mapping.
/// </summary>
/// <param name="parameters"> The parameters for this mapping. </param>
/// <returns> The newly created mapping. </returns>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new SqliteDateOnlyTypeMapping(parameters);

/// <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 string SqlLiteralFormatString
=> DateOnlyFormatConst;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Data;
using Microsoft.EntityFrameworkCore.Storage;

namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.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 SqliteTimeOnlyTypeMapping : TimeOnlyTypeMapping
{
/// <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 SqliteTimeOnlyTypeMapping(
string storeType,
DbType? dbType = null)
: base(storeType, dbType)
{
}

/// <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 SqliteTimeOnlyTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}

/// <summary>
/// Creates a copy of this mapping.
/// </summary>
/// <param name="parameters"> The parameters for this mapping. </param>
/// <returns> The newly created mapping. </returns>
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new SqliteTimeOnlyTypeMapping(parameters);

/// <inheritdoc />
protected override string GenerateNonNullSqlLiteral(object value)
{
var timeOnly = (TimeOnly)value;

return timeOnly.Ticks % TimeSpan.TicksPerSecond == 0
? FormattableString.Invariant($@"'{value:HH\:mm\:ss}'")
: FormattableString.Invariant($@"'{value:HH\:mm\:ss\.fffffff}'");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ private static readonly HashSet<string> _spatialiteTypes
{ typeof(DateTime), new SqliteDateTimeTypeMapping(TextTypeName) },
{ typeof(DateTimeOffset), new SqliteDateTimeOffsetTypeMapping(TextTypeName) },
{ typeof(TimeSpan), new TimeSpanTypeMapping(TextTypeName) },
{ typeof(DateOnly), new SqliteDateOnlyTypeMapping(TextTypeName) },
{ typeof(TimeOnly), new SqliteTimeOnlyTypeMapping(TextTypeName) },
{ typeof(decimal), new SqliteDecimalTypeMapping(TextTypeName) },
{ typeof(double), _real },
{ typeof(float), new FloatTypeMapping(RealTypeName) },
Expand Down
Loading

0 comments on commit f6b109b

Please sign in to comment.