未验证 提交 bba2dadf 编写于 作者: S Shay Rojansky 提交者: GitHub

Support DateOnly/TimeOnly (#1866)

Multitarget to net6.0

Closes #1781
Fixes #1865
上级 5b3d514a
......@@ -12,7 +12,6 @@
<Copyright>Copyright 2021 © The Npgsql Development Team</Copyright>
<Company>Npgsql</Company>
<VersionPrefix>6.0.0-preview5</VersionPrefix>
<TargetFramework>net5.0</TargetFramework>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageLicenseExpression>PostgreSQL</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/npgsql/efcore.pg</PackageProjectUrl>
......
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>Shay Rojansky</Authors>
<Description>NetTopologySuite PostGIS spatial support plugin for PostgreSQL/Npgsql Entity Framework Core provider.</Description>
<PackageTags>npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;spatial;postgis;nts</PackageTags>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework Condition="'$(DeveloperBuild)' == 'True'">net6.0</TargetFramework>
<AssemblyName>Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite</AssemblyName>
<RootNamespace>Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite</RootNamespace>
<Authors>Shay Rojansky</Authors>
<Description>NetTopologySuite PostGIS spatial support plugin for PostgreSQL/Npgsql Entity Framework Core provider.</Description>
<PackageTags>npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;spatial;postgis;nts</PackageTags>
</PropertyGroup>
<ItemGroup>
......
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>Shay Rojansky</Authors>
<Description>NodaTime support plugin for PostgreSQL/Npgsql Entity Framework Core provider.</Description>
<PackageTags>npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;nodatime</PackageTags>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework Condition="'$(DeveloperBuild)' == 'True'">net6.0</TargetFramework>
<AssemblyName>Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime</AssemblyName>
<RootNamespace>Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime</RootNamespace>
<Authors>Shay Rojansky</Authors>
<Description>NodaTime support plugin for PostgreSQL/Npgsql Entity Framework Core provider.</Description>
<PackageTags>npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;nodatime</PackageTags>
</PropertyGroup>
<ItemGroup>
......
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>Shay Rojansky;Austin Drenski;Yoh Deadfall;</Authors>
<Description>PostgreSQL/Npgsql provider for Entity Framework Core.</Description>
<PackageTags>npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql</PackageTags>
<TargetFrameworks>net5.0;net6.0</TargetFrameworks>
<TargetFrameworks Condition="'$(DeveloperBuild)' == 'True'">net6.0</TargetFrameworks>
<AssemblyName>Npgsql.EntityFrameworkCore.PostgreSQL</AssemblyName>
<RootNamespace>Npgsql.EntityFrameworkCore.PostgreSQL</RootNamespace>
<Authors>Shay Rojansky;Austin Drenski;Yoh Deadfall;</Authors>
<Description>PostgreSQL/Npgsql provider for Entity Framework Core.</Description>
<PackageTags>npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql</PackageTags>
</PropertyGroup>
<ItemGroup>
......
......@@ -30,7 +30,11 @@ public NpgsqlDateTimeMemberTranslator(NpgsqlSqlExpressionFactory sqlExpressionFa
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
var type = member.DeclaringType;
if (type != typeof(DateTime) && type != typeof(NpgsqlDateTime) && type != typeof(NpgsqlDate))
if (type != typeof(DateTime) && type != typeof(NpgsqlDateTime) && type != typeof(NpgsqlDate)
#if NET6_0_OR_GREATER
&& type != typeof(DateOnly) && type != typeof(TimeOnly)
#endif
)
return null;
return member.Name switch
......@@ -88,19 +92,6 @@ SqlFunctionExpression Now()
returnType);
}
/// <summary>
/// Constructs the DATE_PART expression.
/// </summary>
/// <param name="instance">The member expression.</param>
/// <param name="partName">The name of the DATE_PART to construct.</param>
/// <param name="floor">True if the result should be wrapped with FLOOR(...); otherwise, false.</param>
/// <returns>
/// The DATE_PART expression.
/// </returns>
/// <remarks>
/// DATE_PART returns doubles, which we floor and cast into ints
/// This also gets rid of sub-second components when retrieving seconds.
/// </remarks>
private SqlExpression GetDatePartExpression(
SqlExpression instance,
string partName,
......
......@@ -10,14 +10,8 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal
{
/// <summary>
/// Provides expression translation for <see cref="DateTime"/> addition methods.
/// </summary>
public class NpgsqlDateTimeMethodTranslator : IMethodCallTranslator
{
/// <summary>
/// The mapping of supported method translations.
/// </summary>
private static readonly Dictionary<MethodInfo, string> MethodInfoDatePartMapping = new()
{
{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddYears), new[] { typeof(int) })!, "years" },
......@@ -27,6 +21,7 @@ public class NpgsqlDateTimeMethodTranslator : IMethodCallTranslator
{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMinutes), new[] { typeof(double) })!, "mins" },
{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddSeconds), new[] { typeof(double) })!, "secs" },
//{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMilliseconds), new[] { typeof(double) })!, "milliseconds" },
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddYears), new[] { typeof(int) })!, "years" },
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMonths), new[] { typeof(int) })!, "months" },
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddDays), new[] { typeof(double) })!, "days" },
......@@ -34,15 +29,28 @@ public class NpgsqlDateTimeMethodTranslator : IMethodCallTranslator
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMinutes), new[] { typeof(double) })!, "mins" },
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddSeconds), new[] { typeof(double) })!, "secs" },
//{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), new[] { typeof(double) })!, "milliseconds" }
#if NET6_0_OR_GREATER
{ typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddYears), new[] { typeof(int) })!, "years" },
{ typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddMonths), new[] { typeof(int) })!, "months" },
{ typeof(DateOnly).GetRuntimeMethod(nameof(DateOnly.AddDays), new[] { typeof(int) })!, "days" },
{ typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.AddHours), new[] { typeof(int) })!, "hours" },
{ typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.AddMinutes), new[] { typeof(int) })!, "mins" },
#endif
};
#if NET6_0_OR_GREATER
private static readonly MethodInfo TimeOnlyIsBetweenMethod
= typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.IsBetween), new[] { typeof(TimeOnly), typeof(TimeOnly) })!;
private static readonly MethodInfo TimeOnlyAddTimeSpanMethod
= typeof(TimeOnly).GetRuntimeMethod(nameof(TimeOnly.Add), new[] { typeof(TimeSpan) })!;
#endif
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly RelationalTypeMapping _intervalMapping;
private readonly RelationalTypeMapping _textMapping;
/// <summary>
/// Initializes a new instance of the <see cref="Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal.NpgsqlDateTimeMethodTranslator"/> class.
/// </summary>
public NpgsqlDateTimeMethodTranslator(
IRelationalTypeMappingSource typeMappingSource,
ISqlExpressionFactory sqlExpressionFactory)
......@@ -59,12 +67,43 @@ public class NpgsqlDateTimeMethodTranslator : IMethodCallTranslator
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (!MethodInfoDatePartMapping.TryGetValue(method, out var datePart))
if (instance is null)
return null;
var interval = arguments[0];
if (TranslateDatePart(instance, method, arguments) is { } translated)
{
return translated;
}
#if NET6_0_OR_GREATER
if (method.DeclaringType == typeof(TimeOnly))
{
if (method == TimeOnlyIsBetweenMethod)
{
return _sqlExpressionFactory.And(
_sqlExpressionFactory.GreaterThanOrEqual(instance, arguments[0]),
_sqlExpressionFactory.LessThan(instance, arguments[1]));
}
if (method == TimeOnlyAddTimeSpanMethod)
{
return _sqlExpressionFactory.Add(instance, arguments[0]);
}
}
#endif
return null;
}
private SqlExpression? TranslateDatePart(
SqlExpression instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments)
{
if (!MethodInfoDatePartMapping.TryGetValue(method, out var datePart))
return null;
if (instance is null || interval is null)
if (arguments[0] is not { } interval)
return null;
// Note: ideally we'd simply generate a PostgreSQL interval expression, but the .NET mapping of that is TimeSpan,
......
......@@ -29,7 +29,7 @@ public class NpgsqlMemberTranslatorProvider : RelationalMemberTranslatorProvider
JsonPocoTranslator,
new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory),
new NpgsqlStringMemberTranslator(sqlExpressionFactory),
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory)
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory),
});
}
}
......
......@@ -26,6 +26,11 @@ public class NpgsqlSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExp
private static readonly ConstructorInfo DateTimeCtor2 =
typeof(DateTime).GetConstructor(new[] { typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int) })!;
#if NET6_0_OR_GREATER
private static readonly ConstructorInfo DateOnlyCtor =
typeof(DateOnly).GetConstructor(new[] { typeof(int), typeof(int), typeof(int) })!;
#endif
private static readonly MethodInfo Like2MethodInfo =
typeof(DbFunctionsExtensions).GetRuntimeMethod(
nameof(DbFunctionsExtensions.Like), new[] { typeof(DbFunctions), typeof(string), typeof(string) })!;
......@@ -432,6 +437,16 @@ protected override Expression VisitNew(NewExpression newExpression)
"make_timestamp", sqlArguments, nullable: true, TrueArrays[6], typeof(DateTime));
}
#if NET6_0_OR_GREATER
if (newExpression.Constructor == DateOnlyCtor)
{
return TryTranslateArguments(out var sqlArguments)
? _sqlExpressionFactory.Function(
"make_date", sqlArguments, nullable: true, TrueArrays[3], typeof(DateOnly))
: QueryCompilationContext.NotTranslatedExpression;
}
#endif
return QueryCompilationContext.NotTranslatedExpression;
bool TryTranslateArguments(out SqlExpression[] sqlArguments)
......
......@@ -294,16 +294,24 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(SqlBinaryExpression binary, Re
// DateTime + TimeSpan => DateTime
// DateTimeOffset + TimeSpan => DateTimeOffset
if (rightType == typeof(TimeSpan) && (
leftType == typeof(DateTime) ||
leftType == typeof(DateTimeOffset)) ||
rightType.FullName == "NodaTime.Period" && (
leftType.FullName == "NodaTime.LocalDateTime" ||
leftType.FullName == "NodaTime.LocalDate" ||
leftType.FullName == "NodaTime.LocalTime") ||
rightType.FullName == "NodaTime.Duration" && (
leftType.FullName == "NodaTime.Instant" ||
leftType.FullName == "NodaTime.ZonedDateTime"))
// TimeOnly + TimeSpan => TimeOnly
if (rightType == typeof(TimeSpan)
&& (
leftType == typeof(DateTime)
|| leftType == typeof(DateTimeOffset)
#if NET6_0_OR_GREATER
|| leftType == typeof(TimeOnly)
#endif
)
|| rightType.FullName == "NodaTime.Period"
&& (
leftType.FullName == "NodaTime.LocalDateTime"
|| leftType.FullName == "NodaTime.LocalDate"
|| leftType.FullName == "NodaTime.LocalTime")
|| rightType.FullName == "NodaTime.Duration"
&& (
leftType.FullName == "NodaTime.Instant"
|| leftType.FullName == "NodaTime.ZonedDateTime"))
{
var newLeft = ApplyTypeMapping(left, typeMapping);
var newRight = ApplyDefaultTypeMapping(right);
......@@ -314,15 +322,21 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(SqlBinaryExpression binary, Re
{
// DateTime - DateTime => TimeSpan
// DateTimeOffset - DateTimeOffset => TimeSpan
// DateOnly - DateOnly => TimeSpan
// TimeOnly - TimeOnly => TimeSpan
// Instant - Instant => Duration
// LocalDateTime - LocalDateTime => Period
if (leftType == typeof(DateTime) && rightType == typeof(DateTime) ||
leftType == typeof(DateTimeOffset) && rightType == typeof(DateTimeOffset) ||
leftType.FullName == "NodaTime.Instant" && rightType.FullName == "NodaTime.Instant" ||
leftType.FullName == "NodaTime.LocalDateTime" && rightType.FullName == "NodaTime.LocalDateTime" ||
leftType.FullName == "NodaTime.ZonedDateTime" && rightType.FullName == "NodaTime.ZonedDateTime" ||
leftType.FullName == "NodaTime.LocalDate" && rightType.FullName == "NodaTime.LocalDate" ||
leftType.FullName == "NodaTime.LocalTime" && rightType.FullName == "NodaTime.LocalTime")
if (leftType == typeof(DateTime) && rightType == typeof(DateTime)
|| leftType == typeof(DateTimeOffset) && rightType == typeof(DateTimeOffset)
#if NET6_0_OR_GREATER
|| leftType == typeof(DateOnly) && rightType == typeof(DateOnly)
|| leftType == typeof(TimeOnly) && rightType == typeof(TimeOnly)
#endif
|| leftType.FullName == "NodaTime.Instant" && rightType.FullName == "NodaTime.Instant"
|| leftType.FullName == "NodaTime.LocalDateTime" && rightType.FullName == "NodaTime.LocalDateTime"
|| leftType.FullName == "NodaTime.ZonedDateTime" && rightType.FullName == "NodaTime.ZonedDateTime"
|| leftType.FullName == "NodaTime.LocalDate" && rightType.FullName == "NodaTime.LocalDate"
|| leftType.FullName == "NodaTime.LocalTime" && rightType.FullName == "NodaTime.LocalTime")
{
var inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right);
......@@ -331,7 +345,7 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(SqlBinaryExpression binary, Re
ApplyTypeMapping(left, inferredTypeMapping),
ApplyTypeMapping(right, inferredTypeMapping),
binary.Type,
typeMapping ?? _typeMappingSource.FindMapping(binary.Type));
typeMapping ?? _typeMappingSource.FindMapping(binary.Type, "interval"));
}
}
}
......
......@@ -59,7 +59,7 @@ protected override string GenerateNonNullSqlLiteral(object value)
public class NpgsqlDateTypeMapping : NpgsqlTypeMapping
{
public NpgsqlDateTypeMapping() : base("date", typeof(DateTime), NpgsqlDbType.Date) {}
public NpgsqlDateTypeMapping(Type clrType) : base("date", clrType, NpgsqlDbType.Date) {}
protected NpgsqlDateTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Date) {}
......@@ -68,12 +68,19 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
=> new NpgsqlDateTypeMapping(parameters);
protected override string GenerateNonNullSqlLiteral(object value)
=> FormattableString.Invariant($"DATE '{(DateTime)value:yyyy-MM-dd}'");
=> value switch
{
DateTime d => FormattableString.Invariant($"DATE '{d:yyyy-MM-dd}'"),
#if NET6_0_OR_GREATER
DateOnly d => FormattableString.Invariant($"DATE '{d:yyyy-MM-dd}'"),
#endif
_ => throw new InvalidCastException($"Can't generate a date SQL literal for CLR type {value.GetType()}")
};
}
public class NpgsqlTimeTypeMapping : NpgsqlTypeMapping
{
public NpgsqlTimeTypeMapping() : base("time without time zone", typeof(TimeSpan), NpgsqlDbType.Time) {}
public NpgsqlTimeTypeMapping(Type clrType) : base("time without time zone", clrType, NpgsqlDbType.Time) {}
protected NpgsqlTimeTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Time) {}
......@@ -82,12 +89,18 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
=> new NpgsqlTimeTypeMapping(parameters);
protected override string GenerateNonNullSqlLiteral(object value)
{
var ts = (TimeSpan)value;
return ts.Ticks % 10000000 == 0
? FormattableString.Invariant($@"TIME '{(TimeSpan)value:hh\:mm\:ss}'")
: FormattableString.Invariant($@"TIME '{(TimeSpan)value:hh\:mm\:ss\.FFFFFF}'");
}
=> value switch
{
TimeSpan ts => ts.Ticks % 10000000 == 0
? FormattableString.Invariant($@"TIME '{value:hh\:mm\:ss}'")
: FormattableString.Invariant($@"TIME '{value:hh\:mm\:ss\.FFFFFF}'"),
#if NET6_0_OR_GREATER
TimeOnly t => t.Ticks % 10000000 == 0
? FormattableString.Invariant($@"TIME '{value:HH\:mm\:ss}'")
: FormattableString.Invariant($@"TIME '{value:HH\:mm\:ss\.FFFFFF}'"),
#endif
_ => throw new InvalidCastException($"Can't generate a time SQL literal for CLR type {value.GetType()}")
};
}
public class NpgsqlTimeTzTypeMapping : NpgsqlTypeMapping
......
......@@ -64,14 +64,19 @@ public class NpgsqlTypeMappingSource : RelationalTypeMappingSource
private readonly NpgsqlJsonTypeMapping _jsonElement = new("json", typeof(JsonElement));
// Date/Time types
private readonly NpgsqlDateTypeMapping _date = new();
private readonly NpgsqlDateTypeMapping _dateDateTime = new(typeof(DateTime));
private readonly NpgsqlTimestampTypeMapping _timestamp = new();
private readonly NpgsqlTimestampTzTypeMapping _timestamptz = new(typeof(DateTime));
private readonly NpgsqlTimestampTzTypeMapping _timestamptzDto = new(typeof(DateTimeOffset));
private readonly NpgsqlIntervalTypeMapping _interval = new();
private readonly NpgsqlTimeTypeMapping _time = new();
private readonly NpgsqlTimeTypeMapping _timeTimeSpan = new(typeof(TimeSpan));
private readonly NpgsqlTimeTzTypeMapping _timetz = new();
#if NET6_0_OR_GREATER
private readonly NpgsqlDateTypeMapping _dateDateOnly = new(typeof(DateOnly));
private readonly NpgsqlTimeTypeMapping _timeTimeOnly = new(typeof(TimeOnly));
#endif
// Network address types
private readonly NpgsqlMacaddrTypeMapping _macaddr = new();
private readonly NpgsqlMacaddr8TypeMapping _macaddr8 = new();
......@@ -181,14 +186,36 @@ public class NpgsqlTypeMappingSource : RelationalTypeMappingSource
{ "char", new[] { _char } },
{ "char(1)", new RelationalTypeMapping[] { _singleChar, _stringAsSingleChar } },
{ "character(1)", new RelationalTypeMapping[] { _singleChar, _stringAsSingleChar } },
{ "date", new[] { _date } },
{ "timestamp without time zone", new[] { _timestamp } },
{ "timestamp", new[] { _timestamp } },
{ "timestamp with time zone", new[] { _timestamptz, _timestamptzDto } },
{ "timestamptz", new[] { _timestamptz, _timestamptzDto } },
{ "interval", new[] { _interval } },
{ "time without time zone", new[] { _time } },
{ "time", new[] { _time } },
{ "date", new RelationalTypeMapping[]
#if NET6_0_OR_GREATER
{ _dateDateOnly, _dateDateTime }
#else
{ _dateDateTime }
#endif
},
{ "time without time zone", new RelationalTypeMapping[]
#if NET6_0_OR_GREATER
{ _timeTimeOnly, _timeTimeSpan }
#else
{ _timeTimeSpan }
#endif
},
{ "time", new RelationalTypeMapping[]
#if NET6_0_OR_GREATER
{ _timeTimeOnly, _timeTimeSpan }
#else
{ _timeTimeSpan }
#endif
},
{ "time with time zone", new[] { _timetz } },
{ "timetz", new[] { _timetz } },
{ "macaddr", new[] { _macaddr } },
......@@ -259,6 +286,11 @@ public class NpgsqlTypeMappingSource : RelationalTypeMappingSource
{ typeof(Dictionary<string, string>), _hstore },
{ typeof(NpgsqlTid), _tid },
#if NET6_0_OR_GREATER
{ typeof(DateOnly), _dateDateOnly },
{ typeof(TimeOnly), _timeTimeOnly },
#endif
{ typeof(NpgsqlPoint), _point },
{ typeof(NpgsqlBox), _box },
{ typeof(NpgsqlLine), _line },
......
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel;
using Xunit;
......@@ -200,6 +201,284 @@ public override async Task Where_TimeSpan_Milliseconds(bool async)
#endregion TimeSpan
#if DATEONLY_TIMEONLY_TESTS_IMPLEMENTED_UPSTREAM
#if NET6_0_OR_GREATER
#region DateOnly
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_DateOnly_ctor(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m =>
new DateOnly(EF.Property<DateOnly>(m, "Date").Year, EF.Property<DateOnly>(m, "Date").Month, 1) == new DateOnly(1996, 9, 11)));
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE make_date(date_part('year', m.""Date"")::INT, date_part('month', m.""Date"")::INT, 1) = DATE '1996-09-11'");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_DateOnly_Year(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Date.Year == 1990).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE date_part('year', m.""Date"")::INT = 1990");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_DateOnly_Month(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Date.Month == 11).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE date_part('month', m.""Date"")::INT = 11");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_DateOnly_Day(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Date.Day == 10).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE date_part('day', m.""Date"")::INT = 10");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_DateOnly_DayOfYear(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Date.DayOfYear == 314).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE date_part('doy', m.""Date"")::INT = 314");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_DateOnly_DayOfWeek(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Date.DayOfWeek == DayOfWeek.Saturday).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE floor(date_part('dow', m.""Date""))::INT = 6");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_DateOnly_AddYears(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Date.AddYears(3) == new DateOnly(1993, 11, 10)).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE (m.""Date"" + INTERVAL '3 years') = DATE '1993-11-10'");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_DateOnly_AddMonths(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Date.AddMonths(3) == new DateOnly(1991, 2, 10)).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE (m.""Date"" + INTERVAL '3 months') = DATE '1991-02-10'");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_DateOnly_AddDays(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Date.AddDays(3) == new DateOnly(1990, 11, 13)).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE (m.""Date"" + INTERVAL '3 days') = DATE '1990-11-13'");
}
#endregion DateOnly
#region TimeOnly
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_TimeOnly_Hour(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Time.Hour == 10).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE date_part('hour', m.""Time"")::INT = 10");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_TimeOnly_Minute(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Time.Minute == 15).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE date_part('minute', m.""Time"")::INT = 15");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_TimeOnly_Second(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Time.Second == 50).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE date_part('second', m.""Time"")::INT = 50");
}
// [ConditionalTheory]
// [MemberData(nameof(IsAsyncData))]
// public virtual Task Where_TimeOnly_Millisecond(bool async)
// {
// return Task.CompletedTask;
// // SQL translation not implemented, too annoying
// // await AssertQuery(
// // async,
// // ss => ss.Set<Mission>().Where(m => m.Time.Millisecond == 500).AsTracking(),
// // entryCount: 1);
// }
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_TimeOnly_AddHours(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Time.AddHours(3) == new TimeOnly(13, 15, 50, 500)).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE (m.""Time"" + INTERVAL '3 hours') = TIME '13:15:50.5'");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_TimeOnly_AddMinutes(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Time.AddMinutes(3) == new TimeOnly(10, 18, 50, 500)).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE (m.""Time"" + INTERVAL '3 mins') = TIME '10:18:50.5'");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_TimeOnly_Add_TimeSpan(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Time.Add(new TimeSpan(3, 0, 0)) == new TimeOnly(13, 15, 50, 500)).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE (m.""Time"" + INTERVAL '03:00:00') = TIME '13:15:50.5'");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_TimeOnly_IsBetween(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Time.IsBetween(new TimeOnly(10, 0, 0), new TimeOnly(11, 0, 0))).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE (m.""Time"" >= TIME '10:00:00') AND (m.""Time"" < TIME '11:00:00')");
}
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Where_TimeOnly_subtract_TimeOnly(bool async)
{
await AssertQuery(
async,
ss => ss.Set<Mission>().Where(m => m.Time - new TimeOnly(10, 0, 0) == new TimeSpan(0, 0, 15, 50, 500)).AsTracking(),
entryCount: 1);
AssertSql(
@"SELECT m.""Id"", m.""CodeName"", m.""Date"", m.""Duration"", m.""Rating"", m.""Time"", m.""Timeline""
FROM ""Missions"" AS m
WHERE (m.""Time"" - TIME '10:00:00') = INTERVAL '00:15:50.5'");
}
#endregion TimeOnly
#endif
#endif
private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
}
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
......@@ -24,9 +24,18 @@ public class NpgsqlTypeMappingTest
[Fact]
public void GenerateSqlLiteral_returns_date_literal()
=> Assert.Equal("DATE '2015-03-12'",
{
Assert.Equal(
"DATE '2015-03-12'",
GetMapping("date").GenerateSqlLiteral(new DateTime(2015, 3, 12)));
#if NET6_0_OR_GREATER
Assert.Equal(
"DATE '2015-03-12'",
GetMapping("date").GenerateSqlLiteral(new DateOnly(2015, 3, 12)));
#endif
}
[Fact]
public void GenerateSqlLiteral_returns_timestamp_literal()
{
......@@ -75,12 +84,23 @@ public void GenerateSqlLiteral_returns_timestamptz_datetimeoffset_literal()
public void GenerateSqlLiteral_returns_time_literal()
{
var mapping = GetMapping("time");
Assert.Equal("TIME '04:05:06.123456'",
mapping.GenerateSqlLiteral(new TimeSpan(0, 4, 5, 6, 123).Add(TimeSpan.FromTicks(4560))));
Assert.Equal("TIME '04:05:06.000123'",
mapping.GenerateSqlLiteral(new TimeSpan(0, 4, 5, 6).Add(TimeSpan.FromTicks(1230))));
Assert.Equal("TIME '04:05:06.123'", mapping.GenerateSqlLiteral(new TimeSpan(0, 4, 5, 6, 123)));
Assert.Equal("TIME '04:05:06'", mapping.GenerateSqlLiteral(new TimeSpan(4, 5, 6)));
#if NET6_0_OR_GREATER
Assert.Equal("TIME '04:05:06.123456'",
mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6, 123).Add(TimeSpan.FromTicks(4560))));
Assert.Equal("TIME '04:05:06.000123'",
mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6).Add(TimeSpan.FromTicks(1230))));
Assert.Equal("TIME '04:05:06.123'", mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6, 123)));
Assert.Equal("TIME '04:05:06'", mapping.GenerateSqlLiteral(new TimeOnly(4, 5, 6)));
Assert.Equal("TIME '13:05:06'", mapping.GenerateSqlLiteral(new TimeOnly(13, 5, 6)));
#endif
}
[Fact]
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册