Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public SqlServerMemberTranslatorProvider([NotNull] RelationalMemberTranslatorPro
AddTranslators(
new IMemberTranslator[]
{
new SqlServerTimeSpanMemberTranslator(sqlExpressionFactory),
new SqlServerDateTimeMemberTranslator(sqlExpressionFactory),
new SqlServerStringMemberTranslator(sqlExpressionFactory)
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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 JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal
{
public class SqlServerTimeSpanMemberTranslator : IMemberTranslator
{
private static readonly Dictionary<string, string> _datePartMapping
= new Dictionary<string, string>
{
{ nameof(TimeSpan.Hours), "hour" },
{ nameof(TimeSpan.Minutes), "minute" },
{ nameof(TimeSpan.Seconds), "second" },
{ nameof(TimeSpan.Milliseconds), "millisecond" }
};

private readonly ISqlExpressionFactory _sqlExpressionFactory;

public SqlServerTimeSpanMemberTranslator([NotNull] ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}

public virtual SqlExpression Translate(SqlExpression instance, MemberInfo member, Type returnType)
{
Check.NotNull(member, nameof(member));
Check.NotNull(returnType, nameof(returnType));

var declaringType = member.DeclaringType;

if (declaringType == typeof(TimeSpan))
{
var memberName = member.Name;

if (_datePartMapping.TryGetValue(memberName, out var datePart))
{
return _sqlExpressionFactory.Function(
"DATEPART",
new[] { _sqlExpressionFactory.Fragment(datePart), instance },
returnType);
}

switch (memberName)
{
case nameof(TimeSpan.TotalMilliseconds):
return GetTotalTimeExpression(instance, 1000 * 1000);
case nameof(TimeSpan.TotalSeconds):
return GetTotalTimeExpression(instance, 1000 * 1000 * 1000);
case nameof(TimeSpan.TotalMinutes):
return GetTotalTimeExpression(instance, 60L * 1000 * 1000 * 1000);
case nameof(TimeSpan.TotalHours):
return GetTotalTimeExpression(instance, 60L * 60 * 1000 * 1000 * 1000);
case nameof(TimeSpan.TotalDays):
return GetTotalTimeExpression(instance, 24L * 60 * 60 * 1000 * 1000 * 1000);
}
}

return null;
}

private SqlExpression GetTotalTimeExpression(SqlExpression timeSpan, long nanosFactor)
{
var totalNanoSeconds = GetNanosecondsExpression(timeSpan);

return _sqlExpressionFactory.Divide(_sqlExpressionFactory.Convert(totalNanoSeconds, typeof(double)), _sqlExpressionFactory.Fragment(nanosFactor.ToString()));
}

private SqlExpression GetNanosecondsExpression(SqlExpression timeSpan)
{
var hoursExpression = _sqlExpressionFactory.Function(
"DATEPART",
new[] { _sqlExpressionFactory.Fragment("hour"), timeSpan },
typeof(int));
var hoursInNanos =
_sqlExpressionFactory.Multiply(hoursExpression, _sqlExpressionFactory.Fragment((60 * 60 * 1000000000L).ToString()));

var minutesExpression = _sqlExpressionFactory.Function(
"DATEPART",
new[] { _sqlExpressionFactory.Fragment("minute"), timeSpan },
typeof(int));
var minutesInNanos = _sqlExpressionFactory.Multiply(
minutesExpression, _sqlExpressionFactory.Fragment((60 * 1000000000L).ToString()));

var secondsExpression = _sqlExpressionFactory.Function(
"DATEPART",
new[] { _sqlExpressionFactory.Fragment("second"), timeSpan },
typeof(int));
var secondsInNanos = _sqlExpressionFactory.Multiply(
secondsExpression, _sqlExpressionFactory.Fragment((1000000000).ToString()));

var nanosecondsExpression = _sqlExpressionFactory.Function(
"DATEPART",
new[] { _sqlExpressionFactory.Fragment("nanosecond"), timeSpan },
typeof(int));

return _sqlExpressionFactory.Add(
hoursInNanos, _sqlExpressionFactory.Add(minutesInNanos, _sqlExpressionFactory.Add(secondsInNanos, nanosecondsExpression)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ public SqliteMemberTranslatorProvider([NotNull] RelationalMemberTranslatorProvid
AddTranslators(
new IMemberTranslator[]
{
new SqliteDateTimeMemberTranslator(sqlExpressionFactory), new SqliteStringLengthTranslator(sqlExpressionFactory)
new SqliteTimeSpanMemberTranslator(sqlExpressionFactory),
new SqliteDateTimeMemberTranslator(sqlExpressionFactory),
new SqliteStringLengthTranslator(sqlExpressionFactory)
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// 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 JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal
{
public class SqliteTimeSpanMemberTranslator : IMemberTranslator
{
private static readonly Dictionary<string, string> _datePartMapping
= new Dictionary<string, string>
{
{ nameof(TimeSpan.Hours), "%H" },
{ nameof(TimeSpan.Minutes), "%M" },
{ nameof(TimeSpan.Seconds), "%S" }
};

private readonly ISqlExpressionFactory _sqlExpressionFactory;

public SqliteTimeSpanMemberTranslator([NotNull] ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}

public virtual SqlExpression Translate(SqlExpression instance, MemberInfo member, Type returnType)
{
Check.NotNull(member, nameof(member));
Check.NotNull(returnType, nameof(returnType));

var declaringType = member.DeclaringType;

if (declaringType == typeof(TimeSpan))
{
var memberName = member.Name;

if (_datePartMapping.TryGetValue(memberName, out var datePart))
{
return _sqlExpressionFactory.Convert(
SqliteExpression.Strftime(
_sqlExpressionFactory,
typeof(string),
datePart,
instance),
returnType);
}

switch (memberName)
{
case nameof(TimeSpan.TotalMilliseconds):
return GetTotalTimeExpression(instance, 1.0 / 1000);
case nameof(TimeSpan.TotalSeconds):
return GetTotalTimeExpression(instance, 1);
case nameof(TimeSpan.TotalMinutes):
return GetTotalTimeExpression(instance, 60);
case nameof(TimeSpan.TotalHours):
return GetTotalTimeExpression(instance, 60 * 60);
case nameof(TimeSpan.TotalDays):
return GetTotalTimeExpression(instance, 24 * 60 * 60);
}
}

return null;
}

private SqlExpression GetTotalTimeExpression(SqlExpression timeSpan, double secondsFactor)
{
var totalNanoSeconds = GetSecondsExpression(timeSpan);

return _sqlExpressionFactory.Divide(_sqlExpressionFactory.Convert(totalNanoSeconds, typeof(double)), _sqlExpressionFactory.Fragment(secondsFactor.ToString()));
}

private SqlExpression GetSecondsExpression(SqlExpression timeSpan)
{
var hoursExpression =_sqlExpressionFactory.Convert(
SqliteExpression.Strftime(
_sqlExpressionFactory,
typeof(string),
"%H",
timeSpan),
typeof(double));
var hoursInSeconds =
_sqlExpressionFactory.Multiply(hoursExpression, _sqlExpressionFactory.Fragment((60 * 60).ToString()));

var minutesExpression =_sqlExpressionFactory.Convert(
SqliteExpression.Strftime(
_sqlExpressionFactory,
typeof(string),
"%M",
timeSpan),
typeof(double));
var minutesInSeconds = _sqlExpressionFactory.Multiply(
minutesExpression, _sqlExpressionFactory.Fragment(60.ToString()));

var secondsExpression = _sqlExpressionFactory.Convert(
SqliteExpression.Strftime(
_sqlExpressionFactory,
typeof(string),
"%S",
timeSpan),
typeof(double));


return _sqlExpressionFactory.Add(
hoursInSeconds, _sqlExpressionFactory.Add(minutesInSeconds, secondsExpression));
}
}
}
7 changes: 6 additions & 1 deletion test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace Microsoft.EntityFrameworkCore
public abstract class BuiltInDataTypesTestBase<TFixture> : IClassFixture<TFixture>
where TFixture : BuiltInDataTypesTestBase<TFixture>.BuiltInDataTypesFixtureBase, new()
{

protected BuiltInDataTypesTestBase(TFixture fixture) => Fixture = fixture;

protected TFixture Fixture { get; }
Expand Down Expand Up @@ -1978,7 +1979,7 @@ public virtual void Can_read_back_mapped_enum_from_collection_first_or_default()
var query = from animal in context.Set<Animal>()
select new { animal.Id, animal.IdentificationMethods.FirstOrDefault().Method };

var result = query.SingleOrDefault();
var result = query.FirstOrDefault();
Assert.Equal(IdentificationMethod.EarTag, result.Method);
}

Expand Down Expand Up @@ -3022,13 +3023,15 @@ public EnumS8? EnumS8

protected class Animal
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public ICollection<AnimalIdentification> IdentificationMethods { get; set; }
public AnimalDetails Details { get; set; }
}

protected class AnimalDetails
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public int? AnimalId { get; set; }

Expand All @@ -3038,9 +3041,11 @@ protected class AnimalDetails

protected class AnimalIdentification
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public int AnimalId { get; set; }
public IdentificationMethod Method { get; set; }
public TimeSpan TimeSpanField { get; set; }
}

protected enum IdentificationMethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace Microsoft.EntityFrameworkCore
public class BuiltInDataTypesSqlServerTest : BuiltInDataTypesTestBase<BuiltInDataTypesSqlServerTest.BuiltInDataTypesSqlServerFixture>
{
private static readonly string _eol = Environment.NewLine;
private const double Epsilon = 0.000_000_000_001;

public BuiltInDataTypesSqlServerTest(BuiltInDataTypesSqlServerFixture fixture, ITestOutputHelper testOutputHelper)
: base(fixture)
Expand Down Expand Up @@ -2481,6 +2482,7 @@ public virtual void Columns_have_expected_data_types()
AnimalIdentification.AnimalId ---> [int] [Precision = 10 Scale = 0]
AnimalIdentification.Id ---> [int] [Precision = 10 Scale = 0]
AnimalIdentification.Method ---> [int] [Precision = 10 Scale = 0]
AnimalIdentification.TimeSpanField ---> [time] [Precision = 7]
BinaryForeignKeyDataType.BinaryKeyDataTypeId ---> [nullable varbinary] [MaxLength = 900]
BinaryForeignKeyDataType.Id ---> [int] [Precision = 10 Scale = 0]
BinaryKeyDataType.Ex ---> [nullable nvarchar] [MaxLength = -1]
Expand Down Expand Up @@ -2914,6 +2916,38 @@ public void Can_get_column_types_from_built_model()
}
}
}

[ConditionalFact]
public virtual void Can_sum_nested_timespan_members()
{
using var context = CreateContext();

var animalEntry = context.Set<Animal>().Add(new Animal { Id = 50 });
context.SaveChanges();
var numHours = 40;

for (var i = 0; i < numHours; i++)
{
context.Set<AnimalIdentification>().Add(new AnimalIdentification { Id = 100 + i, AnimalId = animalEntry.Entity.Id, TimeSpanField = TimeSpan.FromHours(1) });
}
context.SaveChanges();

var milliseconds = context.Set<Animal>().Where(a => a.Id == animalEntry.Entity.Id)
.Select(a => a.IdentificationMethods.Sum(i => i.TimeSpanField.TotalMilliseconds)).Single();
var seconds = context.Set<Animal>().Where(a => a.Id == animalEntry.Entity.Id).Select(a => a.IdentificationMethods.Sum(i => i.TimeSpanField.TotalSeconds)).Single();
var minutes = context.Set<Animal>().Where(a => a.Id == animalEntry.Entity.Id)
.Select(a => a.IdentificationMethods.Sum(i => i.TimeSpanField.TotalMinutes)).Single();
var hours = context.Set<Animal>().Where(a => a.Id == animalEntry.Entity.Id)
.Select(a => a.IdentificationMethods.Sum(i => i.TimeSpanField.TotalHours)).Single();
var days = context.Set<Animal>().Where(a => a.Id == animalEntry.Entity.Id)
.Select(a => a.IdentificationMethods.Sum(i => i.TimeSpanField.TotalDays)).Single();

Assert.Equal(numHours * 60 * 60 * 1000, milliseconds);
Assert.Equal(numHours * 60 * 60, seconds);
Assert.Equal(numHours * 60, minutes);
Assert.Equal(numHours, hours);
Assert.True(numHours / 24.0 + Epsilon > days && numHours / 24.0 - Epsilon < days);
}

public static string QueryForColumnTypes(DbContext context, params string[] tablesToIgnore)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public virtual void Columns_have_expected_data_types()
AnimalIdentification.AnimalId ---> [int] [Precision = 10 Scale = 0]
AnimalIdentification.Id ---> [int] [Precision = 10 Scale = 0]
AnimalIdentification.Method ---> [nvarchar] [MaxLength = 6]
AnimalIdentification.TimeSpanField ---> [time] [Precision = 7]
BinaryForeignKeyDataType.BinaryKeyDataTypeId ---> [nullable nvarchar] [MaxLength = 450]
BinaryForeignKeyDataType.Id ---> [int] [Precision = 10 Scale = 0]
BinaryKeyDataType.Ex ---> [nullable nvarchar] [MaxLength = -1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public virtual void Columns_have_expected_data_types()
AnimalIdentification.AnimalId ---> [int] [Precision = 10 Scale = 0]
AnimalIdentification.Id ---> [int] [Precision = 10 Scale = 0]
AnimalIdentification.Method ---> [int] [Precision = 10 Scale = 0]
AnimalIdentification.TimeSpanField ---> [time] [Precision = 7]
BinaryForeignKeyDataType.BinaryKeyDataTypeId ---> [nullable varbinary] [MaxLength = 900]
BinaryForeignKeyDataType.Id ---> [int] [Precision = 10 Scale = 0]
BinaryKeyDataType.Ex ---> [nullable nvarchar] [MaxLength = -1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public virtual void Columns_have_expected_data_types()
AnimalIdentification.AnimalId ---> [varbinary] [MaxLength = 4]
AnimalIdentification.Id ---> [varbinary] [MaxLength = 4]
AnimalIdentification.Method ---> [varbinary] [MaxLength = 4]
AnimalIdentification.TimeSpanField ---> [varbinary] [MaxLength = 8]
BinaryForeignKeyDataType.BinaryKeyDataTypeId ---> [nullable varbinary] [MaxLength = 900]
BinaryForeignKeyDataType.Id ---> [varbinary] [MaxLength = 4]
BinaryKeyDataType.Ex ---> [nullable varbinary] [MaxLength = -1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public virtual void Columns_have_expected_data_types()
AnimalIdentification.AnimalId ---> [nvarchar] [MaxLength = 64]
AnimalIdentification.Id ---> [nvarchar] [MaxLength = 64]
AnimalIdentification.Method ---> [nvarchar] [MaxLength = -1]
AnimalIdentification.TimeSpanField ---> [nvarchar] [MaxLength = 48]
BinaryForeignKeyDataType.BinaryKeyDataTypeId ---> [nullable nvarchar] [MaxLength = 450]
BinaryForeignKeyDataType.Id ---> [nvarchar] [MaxLength = 64]
BinaryKeyDataType.Ex ---> [nullable nvarchar] [MaxLength = -1]
Expand Down
Loading