From eba151a60830e0072bb291c2badcc80d93a2ebea Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 15 Nov 2023 13:05:47 +0100 Subject: [PATCH] Proper JSON serialization support for NodaTime and BCL date/time types Leftovers from #2922 --- .../Storage/Internal/DateMapping.cs | 35 +- .../Internal/DurationIntervalMapping.cs | 26 +- .../Storage/Internal/PeriodIntervalMapping.cs | 15 +- .../Storage/Internal/TimeMapping.cs | 15 +- .../Storage/Internal/TimeTzMapping.cs | 22 +- .../Internal/TimestampLocalDateTimeMapping.cs | 43 ++- .../Internal/TimestampTzInstantMapping.cs | 39 ++- .../TimestampTzOffsetDateTimeMapping.cs | 23 +- .../TimestampTzZonedDateTimeMapping.cs | 22 +- .../NpgsqlDateTimeMemberTranslator.cs | 4 +- ...apping.cs => NpgsqlDateOnlyTypeMapping.cs} | 83 +++-- .../Mapping/NpgsqlDateTimeDateTypeMapping.cs | 107 ++++++ .../Mapping/NpgsqlIntervalTypeMapping.cs | 77 ++++- .../Mapping/NpgsqlTimestampTypeMapping.cs | 36 +- .../Mapping/NpgsqlTimestampTzTypeMapping.cs | 150 +++++--- .../Internal/NpgsqlTypeMappingSource.cs | 4 +- .../NonSharedModelBulkUpdatesNpgsqlTest.cs | 2 +- .../NorthwindBulkUpdatesNpgsqlTest.cs | 2 +- .../JsonTypesNpgsqlTest.cs | 325 +++++++++++++++++- .../Migrations/MigrationsNpgsqlTest.cs | 2 +- .../Query/GearsOfWarQueryNpgsqlTest.cs | 2 +- .../Query/JsonQueryAdHocNpgsqlTest.cs | 76 +++- .../Query/JsonQueryNpgsqlTest.cs | 8 +- .../NorthwindDbFunctionsQueryNpgsqlTest.cs | 2 +- .../NorthwindMiscellaneousQueryNpgsqlTest.cs | 2 +- .../PrimitiveCollectionsQueryNpgsqlTest.cs | 4 +- .../Query/TPCGearsOfWarQueryNpgsqlTest.cs | 2 +- .../Query/TPTGearsOfWarQueryNpgsqlTest.cs | 2 +- .../Query/TimestampQueryTest.cs | 34 +- .../Update/JsonUpdateNpgsqlTest.cs | 6 +- .../NodaTimeQueryNpgsqlTest.cs | 2 +- .../NpgsqlNodaTimeTypeMappingTest.cs | 173 ++++++++++ .../Storage/LegacyNpgsqlTypeMappingTest.cs | 8 +- .../Storage/NpgsqlTypeMappingTest.cs | 28 +- 34 files changed, 1165 insertions(+), 216 deletions(-) rename src/EFCore.PG/Storage/Internal/Mapping/{NpgsqlDateTypeMapping.cs => NpgsqlDateOnlyTypeMapping.cs} (65%) create mode 100644 src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTimeDateTypeMapping.cs diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs index 211338e00..fe03975f2 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/DateMapping.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util; @@ -23,7 +25,7 @@ public class DateMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public DateMapping() - : base("date", typeof(LocalDate), NpgsqlDbType.Date) + : base("date", typeof(LocalDate), NpgsqlDbType.Date, JsonLocalDateReaderWriter.Instance) { } @@ -72,9 +74,10 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - { - var date = (LocalDate)value; + => FormatLocalDate((LocalDate)value); + private static string FormatLocalDate(LocalDate date) + { if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) { if (date == LocalDate.MinIsoValue) @@ -102,4 +105,30 @@ public override Expression GenerateCodeLiteral(object value) var date = (LocalDate)value; return ConstantNew(Constructor, date.Year, date.Month, date.Day); } + + private sealed class JsonLocalDateReaderWriter : JsonValueReaderWriter + { + public static JsonLocalDateReaderWriter Instance { get; } = new(); + + public override LocalDate FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + { + var s = manager.CurrentReader.GetString()!; + + if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return LocalDate.MinIsoValue; + case "infinity": + return LocalDate.MaxIsoValue; + } + } + + return LocalDatePattern.Iso.Parse(s).GetValueOrThrow(); + } + + public override void ToJsonTyped(Utf8JsonWriter writer, LocalDate value) + => writer.WriteStringValue(FormatLocalDate(value)); + } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs index 4919a0f84..3bf4ea683 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/DurationIntervalMapping.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace @@ -32,7 +34,7 @@ public class DurationIntervalMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public DurationIntervalMapping() - : base("interval", typeof(Duration), NpgsqlDbType.Interval) + : base("interval", typeof(Duration), NpgsqlDbType.Interval, JsonDurationReaderWriter.Instance) { } @@ -72,7 +74,16 @@ public override RelationalTypeMapping WithStoreTypeAndSize(string storeType, int /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => $"INTERVAL '{NpgsqlIntervalTypeMapping.FormatTimeSpanAsInterval(((Duration)value).ToTimeSpan())}'"; + => $"INTERVAL '{GenerateEmbeddedNonNullSqlLiteral(value)}'"; + + /// + /// 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. + /// + protected override string GenerateEmbeddedNonNullSqlLiteral(object value) + => NpgsqlIntervalTypeMapping.FormatTimeSpanAsInterval(((Duration)value).ToTimeSpan()); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -115,4 +126,15 @@ public override Expression GenerateCodeLiteral(object value) void Compose(Expression toAdd) => e = e is null ? toAdd : Expression.Add(e, toAdd); } + + private sealed class JsonDurationReaderWriter : JsonValueReaderWriter + { + public static JsonDurationReaderWriter Instance { get; } = new(); + + public override Duration FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + => Duration.FromTimeSpan(NpgsqlIntervalTypeMapping.ParseIntervalAsTimeSpan(manager.CurrentReader.GetString()!)); + + public override void ToJsonTyped(Utf8JsonWriter writer, Duration value) + => writer.WriteStringValue(NpgsqlIntervalTypeMapping.FormatTimeSpanAsInterval(value.ToTimeSpan())); + } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs index 4cc6bcc9f..f5e47c9c8 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/PeriodIntervalMapping.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; @@ -35,7 +37,7 @@ public class PeriodIntervalMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public PeriodIntervalMapping() - : base("interval", typeof(Period), NpgsqlDbType.Interval) + : base("interval", typeof(Period), NpgsqlDbType.Interval, JsonPeriodReaderWriter.Instance) { } @@ -150,4 +152,15 @@ public override Expression GenerateCodeLiteral(object value) void Compose(Expression toAdd) => e = e is null ? toAdd : Expression.Add(e, toAdd); } + + private sealed class JsonPeriodReaderWriter : JsonValueReaderWriter + { + public static JsonPeriodReaderWriter Instance { get; } = new(); + + public override Period FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + => PeriodPattern.NormalizingIso.Parse(manager.CurrentReader.GetString()!).GetValueOrThrow(); + + public override void ToJsonTyped(Utf8JsonWriter writer, Period value) + => writer.WriteStringValue(PeriodPattern.NormalizingIso.Format(value)); + } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs index c61622d83..b3e1452c7 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimeMapping.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util; @@ -31,7 +33,7 @@ public class TimeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimeMapping() - : base("time", typeof(LocalTime), NpgsqlDbType.Time) + : base("time", typeof(LocalTime), NpgsqlDbType.Time, JsonLocalTimeReaderWriter.Instance) { } @@ -106,4 +108,15 @@ public override Expression GenerateCodeLiteral(object value) ? ConstantNew(ConstructorWithSeconds, time.Hour, time.Minute, time.Second) : ConstantNew(ConstructorWithMinutes, time.Hour, time.Minute); } + + private sealed class JsonLocalTimeReaderWriter : JsonValueReaderWriter + { + public static JsonLocalTimeReaderWriter Instance { get; } = new(); + + public override LocalTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + => LocalTimePattern.ExtendedIso.Parse(manager.CurrentReader.GetString()!).GetValueOrThrow(); + + public override void ToJsonTyped(Utf8JsonWriter writer, LocalTime value) + => writer.WriteStringValue(LocalTimePattern.ExtendedIso.Format(value)); + } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs index 1c3482112..c8cd44af2 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimeTzMapping.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util; @@ -43,7 +45,7 @@ public class TimeTzMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimeTzMapping() - : base("time with time zone", typeof(OffsetTime), NpgsqlDbType.TimeTz) + : base("time with time zone", typeof(OffsetTime), NpgsqlDbType.TimeTz, JsonOffsetTimeReaderWriter.Instance) { } @@ -92,7 +94,7 @@ protected override string ProcessStoreType(RelationalTypeMappingParameters param /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => $"TIMETZ '{GenerateLiteralCore(value)}'"; + => $"TIMETZ '{Pattern.Format((OffsetTime)value)}'"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -101,10 +103,7 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{GenerateLiteralCore(value)}"""; - - private string GenerateLiteralCore(object value) - => Pattern.Format((OffsetTime)value); + => $@"""{Pattern.Format((OffsetTime)value)}"""; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -140,4 +139,15 @@ public override Expression GenerateCodeLiteral(object value) ? ConstantCall(OffsetFromHoursMethod, offsetSeconds / 3600) : ConstantCall(OffsetFromSeconds, offsetSeconds)); } + + private sealed class JsonOffsetTimeReaderWriter : JsonValueReaderWriter + { + public static JsonOffsetTimeReaderWriter Instance { get; } = new(); + + public override OffsetTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + => Pattern.Parse(manager.CurrentReader.GetString()!).GetValueOrThrow(); + + public override void ToJsonTyped(Utf8JsonWriter writer, OffsetTime value) + => writer.WriteStringValue(Pattern.Format(value)); + } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs index 469031715..3f6d743ea 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampLocalDateTimeMapping.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util; @@ -29,7 +31,7 @@ public class TimestampLocalDateTimeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimestampLocalDateTimeMapping() - : base("timestamp without time zone", typeof(LocalDateTime), NpgsqlDbType.Timestamp) + : base("timestamp without time zone", typeof(LocalDateTime), NpgsqlDbType.Timestamp, JsonLocalDateTimeReaderWriter.Instance) { } @@ -78,7 +80,7 @@ protected override string ProcessStoreType(RelationalTypeMappingParameters param /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => $"TIMESTAMP '{GenerateLiteralCore(value)}'"; + => $"TIMESTAMP '{Format((LocalDateTime)value)}'"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -87,21 +89,18 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{GenerateLiteralCore(value)}"""; + => $@"""{Format((LocalDateTime)value)}"""; - private string GenerateLiteralCore(object value) + private static string Format(LocalDateTime localDateTime) { - var localDateTime = (LocalDateTime)value; - - // TODO: Switch to use LocalDateTime.MinMaxValue when available (#4061) if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) { - if (localDateTime == LocalDate.MinIsoValue + LocalTime.MinValue) + if (localDateTime == LocalDateTime.MinIsoValue) { return "-infinity"; } - if (localDateTime == LocalDate.MaxIsoValue + LocalTime.MaxValue) + if (localDateTime == LocalDateTime.MaxIsoValue) { return "infinity"; } @@ -133,4 +132,30 @@ internal static Expression GenerateCodeLiteral(LocalDateTime dateTime) ? newExpr : Expression.Call(newExpr, PlusNanosecondsMethod, Expression.Constant((long)dateTime.NanosecondOfSecond)); } + + private sealed class JsonLocalDateTimeReaderWriter : JsonValueReaderWriter + { + public static JsonLocalDateTimeReaderWriter Instance { get; } = new(); + + public override LocalDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + { + var s = manager.CurrentReader.GetString()!; + + if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return LocalDateTime.MinIsoValue; + case "infinity": + return LocalDateTime.MaxIsoValue; + } + } + + return LocalDateTimePattern.ExtendedIso.Parse(s).GetValueOrThrow(); + } + + public override void ToJsonTyped(Utf8JsonWriter writer, LocalDateTime value) + => writer.WriteStringValue(Format(value)); + } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs index 4b0d0e235..1ed2ec735 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzInstantMapping.cs @@ -1,3 +1,6 @@ +using System.Globalization; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; @@ -19,7 +22,7 @@ public class TimestampTzInstantMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimestampTzInstantMapping() - : base("timestamp with time zone", typeof(Instant), NpgsqlDbType.TimestampTz) + : base("timestamp with time zone", typeof(Instant), NpgsqlDbType.TimestampTz, JsonInstantReaderWriter.Instance) { } @@ -68,7 +71,7 @@ protected override string ProcessStoreType(RelationalTypeMappingParameters param /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => $"TIMESTAMPTZ '{GenerateLiteralCore(value)}'"; + => $"TIMESTAMPTZ '{Format((Instant)value)}'"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -77,12 +80,10 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{GenerateLiteralCore(value)}"""; + => $@"""{Format((Instant)value)}"""; - private string GenerateLiteralCore(object value) + private static string Format(Instant instant) { - var instant = (Instant)value; - if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) { if (instant == Instant.MinValue) @@ -113,4 +114,30 @@ public override Expression GenerateCodeLiteral(object value) private static readonly MethodInfo _fromUnixTimeTicks = typeof(Instant).GetRuntimeMethod(nameof(Instant.FromUnixTimeTicks), new[] { typeof(long) })!; + + private sealed class JsonInstantReaderWriter : JsonValueReaderWriter + { + public static JsonInstantReaderWriter Instance { get; } = new(); + + public override Instant FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + { + var s = manager.CurrentReader.GetString()!; + + if (!NpgsqlNodaTimeTypeMappingSourcePlugin.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return Instant.MinValue; + case "infinity": + return Instant.MaxValue; + } + } + + return InstantPattern.ExtendedIso.Parse(s).GetValueOrThrow(); + } + + public override void ToJsonTyped(Utf8JsonWriter writer, Instant value) + => writer.WriteStringValue(Format(value)); + } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs index 8d81b36af..a6207767e 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzOffsetDateTimeMapping.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using static Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.Utilties.Util; @@ -29,7 +31,7 @@ public class TimestampTzOffsetDateTimeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimestampTzOffsetDateTimeMapping() - : base("timestamp with time zone", typeof(OffsetDateTime), NpgsqlDbType.TimestampTz) + : base("timestamp with time zone", typeof(OffsetDateTime), NpgsqlDbType.TimestampTz, JsonOffsetDateTimeReaderWriter.Instance) { } @@ -78,7 +80,7 @@ protected override string ProcessStoreType(RelationalTypeMappingParameters param /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => $"TIMESTAMPTZ '{GenerateLiteralCore(value)}'"; + => $"TIMESTAMPTZ '{Format((OffsetDateTime)value)}'"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -87,10 +89,10 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{GenerateLiteralCore(value)}"""; + => $@"""{Format((OffsetDateTime)value)}"""; - private string GenerateLiteralCore(object value) - => OffsetDateTimePattern.ExtendedIso.Format((OffsetDateTime)value); + private static string Format(OffsetDateTime offsetDateTime) + => OffsetDateTimePattern.ExtendedIso.Format(offsetDateTime); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -110,4 +112,15 @@ public override Expression GenerateCodeLiteral(object value) ? ConstantCall(OffsetFromHoursMethod, offsetSeconds / 3600) : ConstantCall(OffsetFromSecondsMethod, offsetSeconds)); } + + private sealed class JsonOffsetDateTimeReaderWriter : JsonValueReaderWriter + { + public static JsonOffsetDateTimeReaderWriter Instance { get; } = new(); + + public override OffsetDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + => OffsetDateTimePattern.ExtendedIso.Parse(manager.CurrentReader.GetString()!).GetValueOrThrow(); + + public override void ToJsonTyped(Utf8JsonWriter writer, OffsetDateTime value) + => writer.WriteStringValue(Format(value)); + } } diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs index cdb6ea741..c20f129ac 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/TimestampTzZonedDateTimeMapping.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Text; using NodaTime.TimeZones; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; @@ -25,7 +27,7 @@ public class TimestampTzZonedDateTimeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public TimestampTzZonedDateTimeMapping() - : base("timestamp with time zone", typeof(ZonedDateTime), NpgsqlDbType.TimestampTz) + : base("timestamp with time zone", typeof(ZonedDateTime), NpgsqlDbType.TimestampTz, JsonZonedDateTimeReaderWriter.Instance) { } @@ -74,7 +76,7 @@ protected override string ProcessStoreType(RelationalTypeMappingParameters param /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => $"TIMESTAMPTZ '{GenerateLiteralCore(value)}'"; + => $"TIMESTAMPTZ '{Pattern.Format((ZonedDateTime)value)}'"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -83,10 +85,7 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => $@"""{GenerateLiteralCore(value)}"""; - - private string GenerateLiteralCore(object value) - => Pattern.Format((ZonedDateTime)value); + => $@"""{Pattern.Format((ZonedDateTime)value)}"""; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -119,4 +118,15 @@ public override Expression GenerateCodeLiteral(object value) typeof(TzdbDateTimeZoneSource).GetRuntimeMethod( nameof(TzdbDateTimeZoneSource.ForId), new[] { typeof(string) })!; + + private sealed class JsonZonedDateTimeReaderWriter : JsonValueReaderWriter + { + public static JsonZonedDateTimeReaderWriter Instance { get; } = new(); + + public override ZonedDateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + => Pattern.Parse(manager.CurrentReader.GetString()!).GetValueOrThrow(); + + public override void ToJsonTyped(Utf8JsonWriter writer, ZonedDateTime value) + => writer.WriteStringValue(Pattern.Format(value)); + } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs index efb7ec008..a709728ee 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs @@ -86,7 +86,7 @@ public NpgsqlDateTimeMemberTranslator(IRelationalTypeMappingSource typeMappingSo instance.TypeMapping); // If DateTime.Date is invoked on a PostgreSQL date (or DateOnly, which can only be mapped to datE), simply no-op. - case { TypeMapping: NpgsqlDateTypeMapping }: + case { TypeMapping: NpgsqlDateTimeDateTypeMapping }: case { Type: var type } when type == typeof(DateOnly): return instance; @@ -236,7 +236,7 @@ private bool TryConvertAwayFromTimestampTz(SqlExpression timestamp, [NotNullWhen switch (timestamp) { // We're already dealing with a non-timestamptz mapping, no conversion needed. - case { TypeMapping: NpgsqlTimestampTypeMapping or NpgsqlDateTypeMapping or NpgsqlTimeTypeMapping }: + case { TypeMapping: NpgsqlTimestampTypeMapping or NpgsqlDateTimeDateTypeMapping or NpgsqlTimeTypeMapping }: case { Type: var type } when type == typeof(DateOnly) || type == typeof(TimeOnly): result = timestamp; return true; diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateOnlyTypeMapping.cs similarity index 65% rename from src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTypeMapping.cs rename to src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateOnlyTypeMapping.cs index fec95d0f5..4d2e79e46 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateOnlyTypeMapping.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; @@ -9,7 +10,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; /// 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. /// -public class NpgsqlDateTypeMapping : NpgsqlTypeMapping +public class NpgsqlDateOnlyTypeMapping : NpgsqlTypeMapping { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -17,16 +18,8 @@ public class NpgsqlDateTypeMapping : NpgsqlTypeMapping /// 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. /// - public NpgsqlDateTypeMapping(Type clrType) - : base( - "date", - clrType, - NpgsqlDbType.Date, - jsonValueReaderWriter: clrType == typeof(DateTime) - ? JsonDateTimeReaderWriter.Instance - : clrType == typeof(DateOnly) - ? JsonDateOnlyReaderWriter.Instance - : throw new ArgumentException("clrType must be DateTime or DateOnly", nameof(clrType))) + public NpgsqlDateOnlyTypeMapping() + : base("date", typeof(DateOnly), NpgsqlDbType.Date, NpgsqlJsonDateOnlyReaderWriter.Instance) { } @@ -36,7 +29,7 @@ public NpgsqlDateTypeMapping(Type clrType) /// 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. /// - protected NpgsqlDateTypeMapping(RelationalTypeMappingParameters parameters) + protected NpgsqlDateOnlyTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters, NpgsqlDbType.Date) { } @@ -48,7 +41,7 @@ protected NpgsqlDateTypeMapping(RelationalTypeMappingParameters parameters) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlDateTypeMapping(parameters); + => new NpgsqlDateOnlyTypeMapping(parameters); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -66,43 +59,49 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) + => Format((DateOnly)value); + + private static string Format(DateOnly date) { - switch (value) + if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) { - case DateTime dateTime: - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - if (dateTime == DateTime.MinValue) - { - return "-infinity"; - } + if (date == DateOnly.MinValue) + { + return "-infinity"; + } - if (dateTime == DateTime.MaxValue) - { - return "infinity"; - } - } + if (date == DateOnly.MaxValue) + { + return "infinity"; + } + } - return dateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + return date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + } - case DateOnly dateOnly: - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) - { - if (dateOnly == DateOnly.MinValue) - { - return "-infinity"; - } + private sealed class NpgsqlJsonDateOnlyReaderWriter : JsonValueReaderWriter + { + public static NpgsqlJsonDateOnlyReaderWriter Instance { get; } = new(); - if (dateOnly == DateOnly.MaxValue) - { - return "infinity"; - } - } + public override DateOnly FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + { + var s = manager.CurrentReader.GetString()!; - return dateOnly.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return DateOnly.MinValue; + case "infinity": + return DateOnly.MaxValue; + } + } - default: - throw new InvalidCastException($"Can't generate a date SQL literal for CLR type {value.GetType()}"); + return DateOnly.Parse(s, CultureInfo.InvariantCulture); } + + public override void ToJsonTyped(Utf8JsonWriter writer, DateOnly value) + => writer.WriteStringValue(Format(value)); } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTimeDateTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTimeDateTypeMapping.cs new file mode 100644 index 000000000..0e5e52522 --- /dev/null +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlDateTimeDateTypeMapping.cs @@ -0,0 +1,107 @@ +using System.Globalization; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; + +/// +/// 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. +/// +public class NpgsqlDateTimeDateTypeMapping : NpgsqlTypeMapping +{ + /// + /// 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. + /// + public NpgsqlDateTimeDateTypeMapping() + : base("date", typeof(DateTime), NpgsqlDbType.Date, NpgsqlJsonDateTimeReaderWriter.Instance) + { + } + + /// + /// 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. + /// + protected NpgsqlDateTimeDateTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters, NpgsqlDbType.Date) + { + } + + /// + /// 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. + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new NpgsqlDateTimeDateTypeMapping(parameters); + + /// + /// 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. + /// + protected override string GenerateNonNullSqlLiteral(object value) + => $"DATE '{GenerateEmbeddedNonNullSqlLiteral(value)}'"; + + /// + /// 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. + /// + protected override string GenerateEmbeddedNonNullSqlLiteral(object value) + => Format((DateTime)value); + + private static string Format(DateTime date) + { + if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) + { + if (date == DateTime.MinValue) + { + return "-infinity"; + } + + if (date == DateTime.MaxValue) + { + return "infinity"; + } + } + + return date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + } + + private sealed class NpgsqlJsonDateTimeReaderWriter : JsonValueReaderWriter + { + public static NpgsqlJsonDateTimeReaderWriter Instance { get; } = new(); + + public override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + { + var s = manager.CurrentReader.GetString()!; + + if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return DateTime.MinValue; + case "infinity": + return DateTime.MaxValue; + } + } + + return DateTime.Parse(s, CultureInfo.InvariantCulture); + } + + public override void ToJsonTyped(Utf8JsonWriter writer, DateTime value) + => writer.WriteStringValue(Format(value)); + } +} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlIntervalTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlIntervalTypeMapping.cs index 9a4df2510..5d43c577e 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlIntervalTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlIntervalTypeMapping.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; @@ -20,7 +21,7 @@ public class NpgsqlIntervalTypeMapping : NpgsqlTypeMapping public NpgsqlIntervalTypeMapping() : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(TimeSpan), jsonValueReaderWriter: JsonTimeSpanReaderWriter.Instance), + new CoreTypeMappingParameters(typeof(TimeSpan), jsonValueReaderWriter: NpgsqlJsonTimeSpanReaderWriter.Instance), "interval")) { } @@ -82,4 +83,78 @@ public static string FormatTimeSpanAsInterval(TimeSpan ts) => ts.ToString( $@"{(ts < TimeSpan.Zero ? "\\-" : "")}{(ts.Days == 0 ? "" : "d\\ ")}hh\:mm\:ss{(ts.Ticks % 10000000 == 0 ? "" : "\\.FFFFFF")}", CultureInfo.InvariantCulture); + + /// + /// 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. + /// + public static TimeSpan ParseIntervalAsTimeSpan(ReadOnlySpan s) + { + // We store the a PG-compatible interval representation in JSON so it can be queried out, but unfortunately this isn't + // compatible with TimeSpan, so we have to parse manually. + + // Note + var isNegated = false; + if (s[0] == '-') + { + isNegated = true; + s = s[1..]; + } + + int i; + for (i = 0; i < s.Length; i++) + { + if (!char.IsDigit(s[i])) + { + break; + } + } + + if (i == s.Length) + { + throw new FormatException(); + } + + // Space is the separator between the days and the hours:minutes:seconds components + var days = 0; + if (s[i] == ' ') + { + days = int.Parse(s[..i]); + s = s[i..]; + } + + var timeSpan = TimeSpan.Parse(s, CultureInfo.InvariantCulture); + + // We've already extracted the days, so there shouldn't be a days component in the rest (indicates an incompatible/malformed string) + if (timeSpan.Days != 0) + { + throw new FormatException(); + } + + if (days > 0) + { + timeSpan = new TimeSpan( + days, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, timeSpan.Milliseconds, timeSpan.Microseconds); + } + + if (isNegated) + { + timeSpan = -timeSpan; + } + + return timeSpan; + } + + private sealed class NpgsqlJsonTimeSpanReaderWriter : JsonValueReaderWriter + { + public static NpgsqlJsonTimeSpanReaderWriter Instance { get; } = new(); + + public override TimeSpan FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + => ParseIntervalAsTimeSpan(manager.CurrentReader.GetString()!); + + public override void ToJsonTyped(Utf8JsonWriter writer, TimeSpan value) + => writer.WriteStringValue(FormatTimeSpanAsInterval(value)); + } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTypeMapping.cs index 897f34bba..bff9b6bce 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTypeMapping.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; @@ -18,7 +19,7 @@ public class NpgsqlTimestampTypeMapping : NpgsqlTypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public NpgsqlTimestampTypeMapping() - : base("timestamp without time zone", typeof(DateTime), NpgsqlDbType.Timestamp, JsonDateTimeReaderWriter.Instance) + : base("timestamp without time zone", typeof(DateTime), NpgsqlDbType.Timestamp, NpgsqlJsonTimestampReaderWriter.Instance) { } @@ -70,9 +71,10 @@ protected override string GenerateEmbeddedNonNullSqlLiteral(object value) => $@"""{GenerateLiteralCore(value)}"""; private string GenerateLiteralCore(object value) - { - var dateTime = (DateTime)value; + => FormatDateTime((DateTime)value); + private static string FormatDateTime(DateTime dateTime) + { if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) { if (dateTime == DateTime.MinValue) @@ -87,7 +89,33 @@ private string GenerateLiteralCore(object value) } return NpgsqlTypeMappingSource.LegacyTimestampBehavior || dateTime.Kind != DateTimeKind.Utc - ? dateTime.ToString("yyyy-MM-dd HH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) + ? dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) : throw new ArgumentException("'timestamp without time zone' literal cannot be generated for a UTC DateTime"); } + + private sealed class NpgsqlJsonTimestampReaderWriter : JsonValueReaderWriter + { + public static NpgsqlJsonTimestampReaderWriter Instance { get; } = new(); + + public override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + { + var s = manager.CurrentReader.GetString()!; + + if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) + { + switch (s) + { + case "-infinity": + return DateTime.MinValue; + case "infinity": + return DateTime.MaxValue; + } + } + + return DateTime.Parse(s, CultureInfo.InvariantCulture); + } + + public override void ToJsonTyped(Utf8JsonWriter writer, DateTime value) + => writer.WriteStringValue(FormatDateTime(value)); + } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTzTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTzTypeMapping.cs index 2ecbbfaa1..5194f95a3 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTzTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlTimestampTzTypeMapping.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.Json; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; @@ -23,9 +24,9 @@ public NpgsqlTimestampTzTypeMapping(Type clrType) clrType, NpgsqlDbType.TimestampTz, clrType == typeof(DateTime) - ? JsonDateTimeReaderWriter.Instance + ? NpgsqlJsonTimestampTzDateTimeReaderWriter.Instance : clrType == typeof(DateTimeOffset) - ? JsonDateTimeOffsetReaderWriter.Instance + ? NpgsqlJsonTimestampTzDateTimeOffsetReaderWriter.Instance : throw new ArgumentException("clrType must be DateTime or DateTimeOffset", nameof(clrType))) { } @@ -66,7 +67,7 @@ protected override string ProcessStoreType(RelationalTypeMappingParameters param /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => $"TIMESTAMPTZ '{GenerateLiteralCore(value)}'"; + => $"TIMESTAMPTZ '{Format(value)}'"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -75,62 +76,119 @@ protected override string GenerateNonNullSqlLiteral(object value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateEmbeddedNonNullSqlLiteral(object value) - => @$"""{GenerateLiteralCore(value)}"""; + => @$"""{Format(value)}"""; - private string GenerateLiteralCore(object value) + private static string Format(object value) + => value switch + { + DateTime dateTime => Format(dateTime), + DateTimeOffset dateTimeOffset => Format(dateTimeOffset), + _ => throw new InvalidCastException( + $"Attempted to generate timestamptz literal for type {value.GetType()}, only DateTime and DateTimeOffset are supported") + }; + + private static string Format(DateTime dateTime) + { + if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) + { + if (dateTime == DateTime.MinValue) + { + return "-infinity"; + } + + if (dateTime == DateTime.MaxValue) + { + return "infinity"; + } + } + + return dateTime.Kind switch + { + DateTimeKind.Utc => dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) + 'Z', + + DateTimeKind.Unspecified => NpgsqlTypeMappingSource.LegacyTimestampBehavior || dateTime == default + ? dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) + 'Z' + : throw new ArgumentException( + $"'timestamp with time zone' literal cannot be generated for {dateTime.Kind} DateTime: a UTC DateTime is required"), + + DateTimeKind.Local => NpgsqlTypeMappingSource.LegacyTimestampBehavior + ? dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFzzz", CultureInfo.InvariantCulture) + : throw new ArgumentException( + $"'timestamp with time zone' literal cannot be generated for {dateTime.Kind} DateTime: a UTC DateTime is required"), + + _ => throw new UnreachableException() + }; + } + + private static string Format(DateTimeOffset dateTimeOffset) { - switch (value) + if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) { - case DateTime dateTime: - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) + if (dateTimeOffset == DateTimeOffset.MinValue) + { + return "-infinity"; + } + + if (dateTimeOffset == DateTimeOffset.MaxValue) + { + return "infinity"; + } + } + + return dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFzzz", CultureInfo.InvariantCulture); + } + + private sealed class NpgsqlJsonTimestampTzDateTimeReaderWriter : JsonValueReaderWriter + { + public static NpgsqlJsonTimestampTzDateTimeReaderWriter Instance { get; } = new(); + + public override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + { + var s = manager.CurrentReader.GetString()!; + + if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) + { + switch (s) { - if (dateTime == DateTime.MinValue) - { - return "-infinity"; - } - - if (dateTime == DateTime.MaxValue) - { - return "infinity"; - } + case "-infinity": + return DateTime.MinValue; + case "infinity": + return DateTime.MaxValue; } + } - return dateTime.Kind switch - { - DateTimeKind.Utc => dateTime.ToString("yyyy-MM-dd HH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) + 'Z', + // Our JSON string representation ends with Z (UTC), but DateTime.Parse returns a Local timestamp even in that case. Convert + // it in order to return a DateTime with Kind UTC. + return DateTime.Parse(s, CultureInfo.InvariantCulture).ToUniversalTime(); + } - DateTimeKind.Unspecified => NpgsqlTypeMappingSource.LegacyTimestampBehavior || dateTime == default - ? dateTime.ToString("yyyy-MM-dd HH:mm:ss.FFFFFF", CultureInfo.InvariantCulture) + 'Z' - : throw new ArgumentException( - $"'timestamp with time zone' literal cannot be generated for {dateTime.Kind} DateTime: a UTC DateTime is required"), + public override void ToJsonTyped(Utf8JsonWriter writer, DateTime value) + => writer.WriteStringValue(Format(value)); + } - DateTimeKind.Local => NpgsqlTypeMappingSource.LegacyTimestampBehavior - ? dateTime.ToString("yyyy-MM-dd HH:mm:ss.FFFFFFzzz", CultureInfo.InvariantCulture) - : throw new ArgumentException( - $"'timestamp with time zone' literal cannot be generated for {dateTime.Kind} DateTime: a UTC DateTime is required"), + private sealed class NpgsqlJsonTimestampTzDateTimeOffsetReaderWriter : JsonValueReaderWriter + { + public static NpgsqlJsonTimestampTzDateTimeOffsetReaderWriter Instance { get; } = new(); - _ => throw new UnreachableException() - }; + public override DateTimeOffset FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) + { + var s = manager.CurrentReader.GetString()!; - case DateTimeOffset dateTimeOffset: - if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) + if (!NpgsqlTypeMappingSource.DisableDateTimeInfinityConversions) + { + switch (s) { - if (dateTimeOffset == DateTimeOffset.MinValue) - { - return "-infinity"; - } - - if (dateTimeOffset == DateTimeOffset.MaxValue) - { - return "infinity"; - } + case "-infinity": + return DateTimeOffset.MinValue; + case "infinity": + return DateTimeOffset.MaxValue; } + } - return dateTimeOffset.ToString("yyyy-MM-dd HH:mm:ss.FFFFFFzzz", CultureInfo.InvariantCulture); - - default: - throw new InvalidCastException( - $"Attempted to generate timestamptz literal for type {value.GetType()}, only DateTime and DateTimeOffset are supported"); + return DateTimeOffset.Parse(s, CultureInfo.InvariantCulture); } + + public override void ToJsonTyped(Utf8JsonWriter writer, DateTimeOffset value) + => writer.WriteStringValue(Format(value)); } } diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index d1b460ad1..576860cbc 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -98,7 +98,7 @@ static NpgsqlTypeMappingSource() private readonly NpgsqlJsonTypeMapping _jsonElement = new("json", typeof(JsonElement)); // Date/Time types - private readonly NpgsqlDateTypeMapping _dateDateTime = new(typeof(DateTime)); + private readonly NpgsqlDateTimeDateTypeMapping _dateDateTime = new(); private readonly NpgsqlTimestampTypeMapping _timestamp = new(); private readonly NpgsqlTimestampTzTypeMapping _timestamptz = new(typeof(DateTime)); private readonly NpgsqlTimestampTzTypeMapping _timestamptzDto = new(typeof(DateTimeOffset)); @@ -106,7 +106,7 @@ static NpgsqlTypeMappingSource() private readonly NpgsqlTimeTypeMapping _timeTimeSpan = new(typeof(TimeSpan)); private readonly NpgsqlTimeTzTypeMapping _timetz = new(); - private readonly NpgsqlDateTypeMapping _dateDateOnly = new(typeof(DateOnly)); + private readonly NpgsqlDateOnlyTypeMapping _dateDateOnly = new(); private readonly NpgsqlTimeTypeMapping _timeTimeOnly = new(typeof(TimeOnly)); // Network address types diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs index 7fa2b3178..943d6c8f9 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs @@ -117,7 +117,7 @@ await AssertUpdate( AssertSql( """ UPDATE "Blogs" AS b -SET "CreationTimestamp" = TIMESTAMPTZ '2020-01-01 00:00:00Z' +SET "CreationTimestamp" = TIMESTAMPTZ '2020-01-01T00:00:00Z' """); } diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs index 3295da4ed..098e57b2e 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesNpgsqlTest.cs @@ -1443,7 +1443,7 @@ await AssertUpdate( SET "Quantity" = 1::smallint FROM "Products" AS p, "Orders" AS o0 -WHERE o."OrderID" = o0."OrderID" AND o."ProductID" = p."ProductID" AND p."Discontinued" AND o0."OrderDate" > TIMESTAMP '1990-01-01 00:00:00' +WHERE o."OrderID" = o0."OrderID" AND o."ProductID" = p."ProductID" AND p."Discontinued" AND o0."OrderDate" > TIMESTAMP '1990-01-01T00:00:00' """); } diff --git a/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs index 068e2dc91..2e1279923 100644 --- a/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/JsonTypesNpgsqlTest.cs @@ -1,6 +1,8 @@ #nullable enable using System.Collections; + +using System.Globalization; using System.Numerics; namespace Npgsql.EntityFrameworkCore.PostgreSQL; @@ -60,6 +62,320 @@ public override void Can_read_write_collection_of_nullable_ulong_enum_JSON_value """{"Prop":[0,null,-1,0,1,8]}""", mappedCollection: true); + #region TimeSpan + + public override void Can_read_write_TimeSpan_JSON_values(string value, string json) + { + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_TimeSpan_JSON_values_sqlite instead. + } + + [ConditionalTheory] + [InlineData("-10675199.02:48:05.477580", """{"Prop":"-10675199 02:48:05.47758"}""")] + [InlineData("10675199.02:48:05.477580", """{"Prop":"10675199 02:48:05.47758"}""")] + [InlineData("00:00:00", """{"Prop":"00:00:00"}""")] + [InlineData("12:23:23.801885", """{"Prop":"12:23:23.801885"}""")] + public virtual void Can_read_write_TimeSpan_JSON_values_npgsql(string value, string json) + => Can_read_and_write_JSON_value( + nameof(TimeSpanType.TimeSpan), + TimeSpan.Parse(value, CultureInfo.InvariantCulture), + json); + + public override void Can_read_write_nullable_TimeSpan_JSON_values(string? value, string json) + { + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_TimeSpan_JSON_values_sqlite instead. + } + + [ConditionalTheory] + [InlineData("-10675199.02:48:05.47758", """{"Prop":"-10675199 02:48:05.47758"}""")] + [InlineData("10675199.02:48:05.47758", """{"Prop":"10675199 02:48:05.47758"}""")] + [InlineData("00:00:00", """{"Prop":"00:00:00"}""")] + [InlineData("12:23:23.801885", """{"Prop":"12:23:23.801885"}""")] + [InlineData(null, """{"Prop":null}""")] + public virtual void Can_read_write_nullable_TimeSpan_JSON_values_npgsql(string? value, string json) + => Can_read_and_write_JSON_value( + nameof(NullableTimeSpanType.TimeSpan), + value == null ? default(TimeSpan?) : TimeSpan.Parse(value), json); + + [ConditionalFact] + public override void Can_read_write_collection_of_TimeSpan_JSON_values() + { + Can_read_and_write_JSON_value>( + nameof(TimeSpanCollectionType.TimeSpan), + new List + { + // We cannot roundtrip MinValue and MaxValue since these contain sub-microsecond components, which PG does not support. + // TimeSpan.MinValue, + new(1, 2, 3, 4), + new(0, 2, 3, 4, 5, 678), + // TimeSpan.MaxValue + }, + """{"Prop":["1 02:03:04","02:03:04.005678"]}""", + mappedCollection: true); + } + + [ConditionalFact] + public override void Can_read_write_collection_of_nullable_TimeSpan_JSON_values() + => Can_read_and_write_JSON_value>( + nameof(NullableTimeSpanCollectionType.TimeSpan), + new List + { + // We cannot roundtrip MinValue and MaxValue since these contain sub-microsecond components, which PG does not support. + // TimeSpan.MinValue, + new(1, 2, 3, 4), + new(0, 2, 3, 4, 5, 678), + // TimeSpan.MaxValue + null + }, + """{"Prop":["1 02:03:04","02:03:04.005678",null]}""", + mappedCollection: true); + + #endregion TimeSpan + + #region DateOnly + + public override void Can_read_write_DateOnly_JSON_values(string value, string json) + { + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_TimeSpan_JSON_values_sqlite instead. + } + + [ConditionalTheory] + [InlineData("1/1/0001", """{"Prop":"-infinity"}""")] + [InlineData("12/31/9999", """{"Prop":"infinity"}""")] + [InlineData("5/29/2023", """{"Prop":"2023-05-29"}""")] + public virtual void Can_read_write_DateOnly_JSON_values_npgsql(string value, string json) + => Can_read_and_write_JSON_value( + nameof(DateOnlyType.DateOnly), + DateOnly.Parse(value, CultureInfo.InvariantCulture), json); + + public override void Can_read_write_nullable_DateOnly_JSON_values(string? value, string json) + { + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_DateOnly_JSON_values_sqlite instead. + } + + [ConditionalTheory] + [InlineData("1/1/0001", """{"Prop":"-infinity"}""")] + [InlineData("12/31/9999", """{"Prop":"infinity"}""")] + [InlineData("5/29/2023", """{"Prop":"2023-05-29"}""")] + [InlineData(null, """{"Prop":null}""")] + public virtual void Can_read_write_nullable_DateOnly_JSON_values_npgsql(string? value, string json) + => Can_read_and_write_JSON_value( + nameof(NullableDateOnlyType.DateOnly), + value == null ? default(DateOnly?) : DateOnly.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalFact] + public virtual void Can_read_write_DateOnly_JSON_values_infinity() + { + Can_read_and_write_JSON_value(nameof(DateOnlyType.DateOnly), DateOnly.MinValue, """{"Prop":"-infinity"}"""); + Can_read_and_write_JSON_value(nameof(DateOnlyType.DateOnly), DateOnly.MaxValue, """{"Prop":"infinity"}"""); + } + + [ConditionalFact] + public override void Can_read_write_collection_of_DateOnly_JSON_values() + => Can_read_and_write_JSON_value>( + nameof(DateOnlyCollectionType.DateOnly), + new List + { + DateOnly.MinValue, + new(2023, 5, 29), + DateOnly.MaxValue + }, + """{"Prop":["-infinity","2023-05-29","infinity"]}""", + mappedCollection: true); + + [ConditionalFact] + public override void Can_read_write_collection_of_nullable_DateOnly_JSON_values() + => Can_read_and_write_JSON_value>( + nameof(NullableDateOnlyCollectionType.DateOnly), + new List + { + DateOnly.MinValue, + new(2023, 5, 29), + DateOnly.MaxValue, + null + }, + """{"Prop":["-infinity","2023-05-29","infinity",null]}""", + mappedCollection: true); + + #endregion DateOnly + + #region DateTime + + public override void Can_read_write_DateTime_JSON_values(string value, string json) + { + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_TimeSpan_JSON_values_sqlite instead. + } + + [ConditionalTheory] + [InlineData("2023-05-29T10:52:47.206435Z", """{"Prop":"2023-05-29T10:52:47.206435Z"}""")] + public virtual void Can_read_write_DateTime_JSON_values_npgsql(string value, string json) + => Can_read_and_write_JSON_value( + nameof(DateTimeType.DateTime), + DateTime.Parse(value, CultureInfo.InvariantCulture).ToUniversalTime(), json); + + [ConditionalFact] + public virtual void Can_read_write_DateTime_JSON_values_npgsql_infinity() + => Can_read_and_write_JSON_value( + nameof(DateTimeType.DateTime), + DateTime.MaxValue, """{"Prop":"infinity"}"""); + + [ConditionalFact] + public virtual void Can_read_write_DateTime_JSON_values_npgsql_negative_infinity() + => Can_read_and_write_JSON_value( + nameof(DateTimeType.DateTime), + DateTime.MinValue, """{"Prop":"-infinity"}"""); + + public override void Can_read_write_nullable_DateTime_JSON_values(string? value, string json) + { + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_TimeSpan_JSON_values_sqlite instead. + } + + [ConditionalTheory] + [InlineData("0001-01-01T00:00:00.0000000", """{"Prop":"-infinity"}""")] + [InlineData("9999-12-31T23:59:59.9999999", """{"Prop":"infinity"}""")] + [InlineData("2023-05-29T10:52:47.206435", """{"Prop":"2023-05-29T10:52:47.206435Z"}""")] + [InlineData(null, """{"Prop":null}""")] + public virtual void Can_read_write_nullable_DateTime_JSON_values_npgsql(string? value, string json) + => Can_read_and_write_JSON_value( + nameof(NullableDateTimeType.DateTime), + value == null + ? default(DateTime?) + : DateTime.SpecifyKind(DateTime.Parse(value, CultureInfo.InvariantCulture), DateTimeKind.Utc), json); + + [ConditionalFact] + public override void Can_read_write_collection_of_DateTime_JSON_values() + => Can_read_and_write_JSON_value>( + nameof(DateTimeCollectionType.DateTime), + new List + { + DateTime.MinValue, + new(2023, 5, 29, 10, 52, 47, DateTimeKind.Utc), + DateTime.MaxValue + }, + """{"Prop":["-infinity","2023-05-29T10:52:47Z","infinity"]}""", + mappedCollection: true); + + [ConditionalFact] + public override void Can_read_write_collection_of_nullable_DateTime_JSON_values() + => Can_read_and_write_JSON_value>( + nameof(NullableDateTimeCollectionType.DateTime), + new List + { + DateTime.MinValue, + null, + new(2023, 5, 29, 10, 52, 47, DateTimeKind.Utc), + DateTime.MaxValue + }, + """{"Prop":["-infinity",null,"2023-05-29T10:52:47Z","infinity"]}""", + mappedCollection: true); + + [ConditionalFact] + public virtual void Can_read_write_DateTime_timestamptz_JSON_values_infinity() + { + Can_read_and_write_JSON_value(nameof(DateTimeType.DateTime), DateTime.MinValue, """{"Prop":"-infinity"}"""); + Can_read_and_write_JSON_value(nameof(DateTimeType.DateTime), DateTime.MaxValue, """{"Prop":"infinity"}"""); + } + + [ConditionalFact] + public virtual void Can_read_write_DateTime_timestamp_JSON_values_infinity() + { + Can_read_and_write_JSON_property_value( + b => b.HasColumnType("timestamp without time zone"), + nameof(DateTimeType.DateTime), + DateTime.MinValue, + """{"Prop":"-infinity"}"""); + + Can_read_and_write_JSON_property_value( + b => b.HasColumnType("timestamp without time zone"), + nameof(DateTimeType.DateTime), + DateTime.MaxValue, + """{"Prop":"infinity"}"""); + } + + #endregion DateTime + + #region DateTimeOffset + + public override void Can_read_write_DateTimeOffset_JSON_values(string value, string json) + { + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_DateTimeOffset_JSON_values_sqlite instead. + } + + [ConditionalTheory] + [InlineData("0001-01-01T00:00:00.000000-01:00", """{"Prop":"0001-01-01T00:00:00-01:00"}""")] + [InlineData("9999-12-31T23:59:59.999999+02:00", """{"Prop":"9999-12-31T23:59:59.999999\u002B02:00"}""")] + [InlineData("0001-01-01T00:00:00.000000-03:00", """{"Prop":"0001-01-01T00:00:00-03:00"}""")] + [InlineData("2023-05-29T11:11:15.567285+04:00", """{"Prop":"2023-05-29T11:11:15.567285\u002B04:00"}""")] + public virtual void Can_read_write_DateTimeOffset_JSON_values_npgsql(string value, string json) + => Can_read_and_write_JSON_value( + nameof(DateTimeOffsetType.DateTimeOffset), + DateTimeOffset.Parse(value, CultureInfo.InvariantCulture), json); + + public override void Can_read_write_nullable_DateTimeOffset_JSON_values(string? value, string json) + { + // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need + // to override. See Can_read_write_DateTimeOffset_JSON_values_sqlite instead. + } + + [ConditionalTheory] + [InlineData("0001-01-01T00:00:00.000000-01:00", """{"Prop":"0001-01-01T00:00:00-01:00"}""")] + [InlineData("9999-12-31T23:59:59.999999+02:00", """{"Prop":"9999-12-31T23:59:59.999999\u002B02:00"}""")] + [InlineData("0001-01-01T00:00:00.000000-03:00", """{"Prop":"0001-01-01T00:00:00-03:00"}""")] + [InlineData("2023-05-29T11:11:15.567285+04:00", """{"Prop":"2023-05-29T11:11:15.567285\u002B04:00"}""")] + [InlineData(null, """{"Prop":null}""")] + public virtual void Can_read_write_nullable_DateTimeOffset_JSON_values_npgsql(string? value, string json) + => Can_read_and_write_JSON_value( + nameof(NullableDateTimeOffsetType.DateTimeOffset), + value == null ? default(DateTimeOffset?) : DateTimeOffset.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalFact] + public override void Can_read_write_collection_of_DateTimeOffset_JSON_values() + => Can_read_and_write_JSON_value>( + nameof(DateTimeOffsetCollectionType.DateTimeOffset), + new List + { + DateTimeOffset.MinValue, + new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(-2, 0, 0)), + new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(0, 0, 0)), + new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(2, 0, 0)), + DateTimeOffset.MaxValue + }, + """{"Prop":["-infinity","2023-05-29T10:52:47-02:00","2023-05-29T10:52:47\u002B00:00","2023-05-29T10:52:47\u002B02:00","infinity"]}""", + mappedCollection: true); + + [ConditionalFact] + public override void Can_read_write_collection_of_nullable_DateTimeOffset_JSON_values() + => Can_read_and_write_JSON_value>( + nameof(NullableDateTimeOffsetCollectionType.DateTimeOffset), + new List + { + DateTimeOffset.MinValue, + new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(-2, 0, 0)), + new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(0, 0, 0)), + null, + new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(2, 0, 0)), + DateTimeOffset.MaxValue + }, + """{"Prop":["-infinity","2023-05-29T10:52:47-02:00","2023-05-29T10:52:47\u002B00:00",null,"2023-05-29T10:52:47\u002B02:00","infinity"]}""", + mappedCollection: true); + + #endregion DateTimeOffset + + [ConditionalTheory] + [InlineData("12:34:56.123456+05:00", """{"Prop":"12:34:56.123456\u002B5"}""")] + public virtual void Can_read_write_timetz_JSON_values(string value, string json) + => Can_read_and_write_JSON_property_value( + b => b.HasColumnType("timetz"), + nameof(DateTimeOffsetType.DateTimeOffset), + DateTimeOffset.Parse(value), + json); + [ConditionalTheory] [InlineData(Mood.Happy, """{"Prop":"Happy"}""")] [InlineData(Mood.Sad, """{"Prop":"Sad"}""")] @@ -150,15 +466,6 @@ protected class LogSequenceNumberType public NpgsqlLogSequenceNumber LogSequenceNumber { get; set; } } - [ConditionalTheory] - [InlineData("12:34:56.123456+05:00", """{"Prop":"12:34:56.123456\u002B5"}""")] - public virtual void Can_read_write_timetz_JSON_values(string value, string json) - => Can_read_and_write_JSON_property_value( - b => b.HasColumnType("timetz"), - nameof(DateTimeOffsetType.DateTimeOffset), - DateTimeOffset.Parse(value), - json); - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => base.OnConfiguring(optionsBuilder.UseNpgsql(b => b.UseNetTopologySuite())); diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index 35a6d336c..0ffca7ec7 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -615,7 +615,7 @@ await Test( }); AssertSql( - @"ALTER TABLE ""People"" ADD ""Birthday"" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '2015-04-12 17:05:00Z';"); + @"ALTER TABLE ""People"" ADD ""Birthday"" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '2015-04-12T17:05:00Z';"); } [Fact] diff --git a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs index 5e295f6af..a68e38eb0 100644 --- a/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/GearsOfWarQueryNpgsqlTest.cs @@ -135,7 +135,7 @@ await AssertQuery( """ SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m -WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC') > TIMESTAMP '0001-01-01 00:00:00' +WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC') > TIMESTAMP '0001-01-01T00:00:00' """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonQueryAdHocNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonQueryAdHocNpgsqlTest.cs index bccb2f1a2..5a9004d92 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonQueryAdHocNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonQueryAdHocNpgsqlTest.cs @@ -17,16 +17,15 @@ public override Task Junk_in_json_basic_tracking(bool async) public override Task Junk_in_json_basic_no_tracking(bool async) => base.Junk_in_json_basic_no_tracking(async); - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual async Task Json_predicate_on_bytea(bool async) { - var contextFactory = await InitializeAsync( + var contextFactory = await InitializeAsync( seed: context => { context.Entities.AddRange( - new ByteaContainerEntity { JsonEntity = new ByteaJsonEntity { Bytea = new byte[] { 1, 2, 3 } } }, - new ByteaContainerEntity { JsonEntity = new ByteaJsonEntity { Bytea = new byte[] { 1, 2, 4 } } }); + new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Bytea = new byte[] { 1, 2, 3 } } }, + new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Bytea = new byte[] { 1, 2, 4 } } }); context.SaveChanges(); }); @@ -39,31 +38,72 @@ public virtual async Task Json_predicate_on_bytea(bool async) : query.Single(); Assert.Equal(2, result.Id); + + AssertSql( + """ +SELECT e."Id", e."JsonEntity" +FROM "Entities" AS e +WHERE (decode(e."JsonEntity" ->> 'Bytea', 'base64')) = BYTEA E'\\x010204' +LIMIT 2 +"""); } } - protected class ByteaDbContext : DbContext + [ConditionalTheory, MemberData(nameof(IsAsyncData))] + public virtual async Task Json_predicate_on_interval(bool async) { - public ByteaDbContext(DbContextOptions options) + var contextFactory = await InitializeAsync( + seed: context => + { + context.Entities.AddRange( + new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Interval = new TimeSpan(1, 2, 3, 4, 123, 456) } }, + new TypesContainerEntity { JsonEntity = new TypesJsonEntity { Interval = new TimeSpan(2, 2, 3, 4, 123, 456) } }); + context.SaveChanges(); + }); + + using (var context = contextFactory.CreateContext()) + { + var query = context.Entities.Where(x => x.JsonEntity.Interval == new TimeSpan(2, 2, 3, 4, 123, 456)); + + var result = async + ? await query.SingleAsync() + : query.Single(); + + Assert.Equal(2, result.Id); + + AssertSql( + """ +SELECT e."Id", e."JsonEntity" +FROM "Entities" AS e +WHERE (CAST(e."JsonEntity" ->> 'Interval' AS interval)) = INTERVAL '2 02:03:04.123456' +LIMIT 2 +"""); + } + } + + protected class TypesDbContext : DbContext + { + public TypesDbContext(DbContextOptions options) : base(options) { } - public DbSet Entities { get; set; } + public DbSet Entities { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity().OwnsOne(b => b.JsonEntity).ToJson(); + => modelBuilder.Entity().OwnsOne(b => b.JsonEntity).ToJson(); } - public class ByteaContainerEntity + public class TypesContainerEntity { public int Id { get; set; } - public ByteaJsonEntity JsonEntity { get; set; } + public TypesJsonEntity JsonEntity { get; set; } } - public class ByteaJsonEntity + public class TypesJsonEntity { public byte[] Bytea { get; set; } + public TimeSpan Interval { get; set; } } protected override void Seed29219(MyContext29219 ctx) @@ -228,8 +268,7 @@ protected override void SeedNotICollection(MyContextNotICollection ctx) #region EnumLegacyValues - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual async Task Read_enum_property_with_legacy_values(bool async) { var contextFactory = await InitializeAsync( @@ -255,8 +294,7 @@ public virtual async Task Read_enum_property_with_legacy_values(bool async) } } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual async Task Read_json_entity_with_enum_properties_with_legacy_values(bool async) { var contextFactory = await InitializeAsync( @@ -294,8 +332,7 @@ public virtual async Task Read_json_entity_with_enum_properties_with_legacy_valu l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ULongEnumLegacyValues)))); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual async Task Read_json_entity_collection_with_enum_properties_with_legacy_values(bool async) { var contextFactory = await InitializeAsync( @@ -426,4 +463,7 @@ private enum ULongEnumLegacyValues : ulong } #endregion + + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs index 157847087..081b7c140 100644 --- a/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/JsonQueryNpgsqlTest.cs @@ -1332,7 +1332,7 @@ FROM ROWS FROM (jsonb_to_recordset(o."OwnedCollectionBranch") AS ( "OwnedCollectionLeaf" jsonb, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o0 - WHERE o0."Date" <> TIMESTAMP '2000-01-01 00:00:00' + WHERE o0."Date" <> TIMESTAMP '2000-01-01T00:00:00' ) AS t ON TRUE ) AS t0 ON TRUE ORDER BY j."Id" NULLS FIRST, t0.ordinality NULLS FIRST @@ -1540,7 +1540,7 @@ FROM ROWS FROM (jsonb_to_recordset(o1."OwnedCollectionBranch") AS ( "OwnedCollectionLeaf" jsonb, "OwnedReferenceLeaf" jsonb )) WITH ORDINALITY AS o2 - WHERE o2."Date" <> TIMESTAMP '2000-01-01 00:00:00' + WHERE o2."Date" <> TIMESTAMP '2000-01-01T00:00:00' ) AS t2 ON TRUE ) AS t1 ON TRUE LEFT JOIN "JsonEntitiesBasicForCollection" AS j0 ON j."Id" = j0."ParentId" @@ -2487,7 +2487,7 @@ public override async Task Json_predicate_on_datetime(bool async) """ SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j -WHERE (CAST(j."Reference" ->> 'TestDateTime' AS timestamp without time zone)) <> TIMESTAMP '2000-01-03 00:00:00' OR (CAST(j."Reference" ->> 'TestDateTime' AS timestamp without time zone)) IS NULL +WHERE (CAST(j."Reference" ->> 'TestDateTime' AS timestamp without time zone)) <> TIMESTAMP '2000-01-03T00:00:00' OR (CAST(j."Reference" ->> 'TestDateTime' AS timestamp without time zone)) IS NULL """); } @@ -2499,7 +2499,7 @@ public override async Task Json_predicate_on_datetimeoffset(bool async) """ SELECT j."Id", j."TestDateTimeCollection", j."TestDecimalCollection", j."TestDefaultStringCollection", j."TestEnumWithIntConverterCollection", j."TestGuidCollection", j."TestInt32Collection", j."TestInt64Collection", j."TestMaxLengthStringCollection", j."TestNullableEnumWithConverterThatHandlesNullsCollection", j."TestSignedByteCollection", j."TestSingleCollection", j."TestTimeSpanCollection", j."TestUnsignedInt32Collection", j."Collection", j."Reference" FROM "JsonEntitiesAllTypes" AS j -WHERE (CAST(j."Reference" ->> 'TestDateTimeOffset' AS timestamp with time zone)) <> TIMESTAMPTZ '2000-01-04 00:00:00+03:02' OR (CAST(j."Reference" ->> 'TestDateTimeOffset' AS timestamp with time zone)) IS NULL +WHERE (CAST(j."Reference" ->> 'TestDateTimeOffset' AS timestamp with time zone)) <> TIMESTAMPTZ '2000-01-04T00:00:00+03:02' OR (CAST(j."Reference" ->> 'TestDateTimeOffset' AS timestamp with time zone)) IS NULL """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs index f3525bfe4..57e6c9f71 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindDbFunctionsQueryNpgsqlTest.cs @@ -158,7 +158,7 @@ public void Distance_with_timestamp() """ SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" FROM "Orders" AS o -ORDER BY o."OrderDate" <-> TIMESTAMP '1997-06-28 00:00:00' NULLS FIRST +ORDER BY o."OrderDate" <-> TIMESTAMP '1997-06-28T00:00:00' NULLS FIRST LIMIT 1 """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs index 6f4a02eb2..6fec39611 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/NorthwindMiscellaneousQueryNpgsqlTest.cs @@ -75,7 +75,7 @@ await AssertQuery( """ SELECT o."OrderID", o."CustomerID", o."EmployeeID", o."OrderDate" FROM "Orders" AS o -WHERE o."OrderDate" - INTERVAL '1 00:00:00' = TIMESTAMP '1997-10-08 00:00:00' +WHERE o."OrderDate" - INTERVAL '1 00:00:00' = TIMESTAMP '1997-10-08T00:00:00' """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs index da5ebade1..ef84acab7 100644 --- a/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/PrimitiveCollectionsQueryNpgsqlTest.cs @@ -475,7 +475,7 @@ public override async Task Column_collection_index_datetime(bool async) """ SELECT p."Id", p."Bool", p."Bools", p."DateTime", p."DateTimes", p."Enum", p."Enums", p."Int", p."Ints", p."NullableInt", p."NullableInts", p."NullableString", p."NullableStrings", p."String", p."Strings" FROM "PrimitiveCollectionsEntity" AS p -WHERE p."DateTimes"[2] = TIMESTAMPTZ '2020-01-10 12:30:00Z' +WHERE p."DateTimes"[2] = TIMESTAMPTZ '2020-01-10T12:30:00Z' """); } @@ -1166,7 +1166,7 @@ WHERE date_part('day', d.value AT TIME ZONE 'UTC')::int <> 1 OR d.value AT TIME LEFT JOIN LATERAL ( SELECT d0.value, d0.ordinality FROM unnest(p."DateTimes") WITH ORDINALITY AS d0(value) - WHERE d0.value > TIMESTAMPTZ '2000-01-01 00:00:00Z' + WHERE d0.value > TIMESTAMPTZ '2000-01-01T00:00:00Z' ) AS t0 ON TRUE ORDER BY p."Id" NULLS FIRST, i.ordinality NULLS FIRST, i0.value DESC NULLS LAST, i0.ordinality NULLS FIRST, t.ordinality NULLS FIRST """); diff --git a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs index ee8e8b44d..5f047dd73 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPCGearsOfWarQueryNpgsqlTest.cs @@ -84,7 +84,7 @@ await AssertQuery( """ SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m -WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC') > TIMESTAMP '0001-01-01 00:00:00' +WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC') > TIMESTAMP '0001-01-01T00:00:00' """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs index 1b4ec8dec..9247f53db 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TPTGearsOfWarQueryNpgsqlTest.cs @@ -88,7 +88,7 @@ await AssertQuery( """ SELECT m."Id", m."CodeName", m."Date", m."Duration", m."Rating", m."Time", m."Timeline" FROM "Missions" AS m -WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC') > TIMESTAMP '0001-01-01 00:00:00' +WHERE date_trunc('day', m."Timeline" AT TIME ZONE 'UTC') > TIMESTAMP '0001-01-01T00:00:00' """); } diff --git a/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs index 159a7cfb0..28ab41a48 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs @@ -100,7 +100,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE e."TimestampDateTime" = TIMESTAMP '1998-04-12 15:26:38' +WHERE e."TimestampDateTime" = TIMESTAMP '1998-04-12T15:26:38' """); } @@ -181,7 +181,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE e."TimestamptzDateTime" = TIMESTAMPTZ '1998-04-12 13:26:38Z' +WHERE e."TimestamptzDateTime" = TIMESTAMPTZ '1998-04-12T13:26:38Z' """); } @@ -362,7 +362,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE date_trunc('day', e."TimestamptzDateTime", 'UTC') = TIMESTAMPTZ '1998-04-12 00:00:00Z' +WHERE date_trunc('day', e."TimestamptzDateTime", 'UTC') = TIMESTAMPTZ '1998-04-12T00:00:00Z' """); } @@ -378,7 +378,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE date_trunc('day', e."TimestampDateTime") = TIMESTAMP '1998-04-12 00:00:00' +WHERE date_trunc('day', e."TimestampDateTime") = TIMESTAMP '1998-04-12T00:00:00' """); } @@ -400,7 +400,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE e."TimestampDateTimeOffset" AT TIME ZONE 'UTC' = TIMESTAMP '1998-04-12 13:26:38' +WHERE e."TimestampDateTimeOffset" AT TIME ZONE 'UTC' = TIMESTAMP '1998-04-12T13:26:38' """); } @@ -419,7 +419,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE e."TimestampDateTimeOffset" = TIMESTAMPTZ '1998-04-12 13:26:38Z' +WHERE e."TimestampDateTimeOffset" = TIMESTAMPTZ '1998-04-12T13:26:38Z' """); } @@ -442,7 +442,7 @@ public async Task DateTimeOffset_LocalDateTime() """ SELECT count(*)::int FROM "Entities" AS e -WHERE e."TimestampDateTimeOffset"::timestamp = TIMESTAMP '1998-04-12 15:26:38' +WHERE e."TimestampDateTimeOffset"::timestamp = TIMESTAMP '1998-04-12T15:26:38' """); } @@ -489,7 +489,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE make_date(date_part('year', e."TimestampDateTime")::int, date_part('month', e."TimestampDateTime")::int, 1) = TIMESTAMP '1998-04-12 00:00:00' +WHERE make_date(date_part('year', e."TimestampDateTime")::int, date_part('month', e."TimestampDateTime")::int, 1) = TIMESTAMP '1998-04-12T00:00:00' """); } @@ -508,7 +508,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE make_timestamp(date_part('year', e."TimestampDateTime")::int, date_part('month', e."TimestampDateTime")::int, 1, 0, 0, 0::double precision) = TIMESTAMP '1998-04-12 00:00:00' +WHERE make_timestamp(date_part('year', e."TimestampDateTime")::int, date_part('month', e."TimestampDateTime")::int, 1, 0, 0, 0::double precision) = TIMESTAMP '1998-04-12T00:00:00' """); } @@ -528,7 +528,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE make_timestamp(date_part('year', e."TimestampDateTime")::int, date_part('month', e."TimestampDateTime")::int, 1, 0, 0, 0::double precision) = TIMESTAMP '1996-09-11 00:00:00' +WHERE make_timestamp(date_part('year', e."TimestampDateTime")::int, date_part('month', e."TimestampDateTime")::int, 1, 0, 0, 0::double precision) = TIMESTAMP '1996-09-11T00:00:00' """); } @@ -547,7 +547,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE make_timestamptz(date_part('year', e."TimestamptzDateTime" AT TIME ZONE 'UTC')::int, date_part('month', e."TimestamptzDateTime" AT TIME ZONE 'UTC')::int, 1, 0, 0, 0::double precision, 'UTC') = TIMESTAMPTZ '1998-04-01 00:00:00Z' +WHERE make_timestamptz(date_part('year', e."TimestamptzDateTime" AT TIME ZONE 'UTC')::int, date_part('month', e."TimestamptzDateTime" AT TIME ZONE 'UTC')::int, 1, 0, 0, 0::double precision, 'UTC') = TIMESTAMPTZ '1998-04-01T00:00:00Z' """); } @@ -569,7 +569,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE e."TimestampDateTime" AT TIME ZONE 'UTC' = TIMESTAMPTZ '1998-04-12 15:26:38Z' +WHERE e."TimestampDateTime" AT TIME ZONE 'UTC' = TIMESTAMPTZ '1998-04-12T15:26:38Z' """); } @@ -587,7 +587,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE e."TimestamptzDateTime" AT TIME ZONE 'UTC' = TIMESTAMP '1998-04-12 13:26:38' +WHERE e."TimestamptzDateTime" AT TIME ZONE 'UTC' = TIMESTAMP '1998-04-12T13:26:38' """); } @@ -605,7 +605,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE e."TimestamptzDateTime" AT TIME ZONE 'UTC' = TIMESTAMP '1998-04-12 13:26:38' +WHERE e."TimestamptzDateTime" AT TIME ZONE 'UTC' = TIMESTAMP '1998-04-12T13:26:38' """); } @@ -640,7 +640,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE e."TimestamptzDateTime" AT TIME ZONE 'Europe/Berlin' = TIMESTAMP '1998-04-12 15:26:38' +WHERE e."TimestamptzDateTime" AT TIME ZONE 'Europe/Berlin' = TIMESTAMP '1998-04-12T15:26:38' """); } @@ -672,7 +672,7 @@ public virtual async Task Where_ConvertTimeToUtc_on_DateTime_timestamp_column() """ SELECT count(*)::int FROM "Entities" AS e -WHERE e."TimestampDateTime"::timestamptz = TIMESTAMPTZ '1998-04-12 13:26:38Z' +WHERE e."TimestampDateTime"::timestamptz = TIMESTAMPTZ '1998-04-12T13:26:38Z' """); } @@ -740,7 +740,7 @@ await AssertQuery( """ SELECT e."Id", e."TimestampDateTime", e."TimestampDateTimeArray", e."TimestampDateTimeOffset", e."TimestampDateTimeOffsetArray", e."TimestampDateTimeRange", e."TimestamptzDateTime", e."TimestamptzDateTimeArray", e."TimestamptzDateTimeRange" FROM "Entities" AS e -WHERE CAST(e."TimestamptzDateTime" AT TIME ZONE 'UTC' AS date) + TIME '15:26:38' = TIMESTAMP '1998-04-12 15:26:38' +WHERE CAST(e."TimestamptzDateTime" AT TIME ZONE 'UTC' AS date) + TIME '15:26:38' = TIMESTAMP '1998-04-12T15:26:38' """); } diff --git a/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs index 96949e20a..7a555425c 100644 --- a/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Update/JsonUpdateNpgsqlTest.cs @@ -1096,8 +1096,8 @@ public override async Task Edit_two_properties_on_same_entity_updates_the_entire AssertSql( """ -@p0='{"TestBoolean":false,"TestBooleanCollection":[true,false],"TestByte":25,"TestByteCollection":null,"TestCharacter":"h","TestCharacterCollection":["A","B","\u0022"],"TestDateOnly":"2323-04-03","TestDateOnlyCollection":["3234-01-23","4331-01-21"],"TestDateTime":"2100-11-11T12:34:56","TestDateTimeCollection":["2000-01-01T12:34:56","3000-01-01T12:34:56"],"TestDateTimeOffset":"2200-11-11T12:34:56-05:00","TestDateTimeOffsetCollection":["2000-01-01T12:34:56-08:00"],"TestDecimal":-123450.01,"TestDecimalCollection":[-1234567890.01],"TestDefaultString":"MyDefaultStringInCollection1","TestDefaultStringCollection":["S1","\u0022S2\u0022","S3"],"TestDouble":-1.2345,"TestDoubleCollection":[-1.23456789,1.23456789,0],"TestEnum":0,"TestEnumCollection":[0,2,-7],"TestEnumWithIntConverter":1,"TestEnumWithIntConverterCollection":[0,2,-7],"TestGuid":"00000000-0000-0000-0000-000000000000","TestGuidCollection":["12345678-1234-4321-7777-987654321000"],"TestInt16":-12,"TestInt16Collection":[-32768,0,32767],"TestInt32":32,"TestInt32Collection":[-2147483648,0,2147483647],"TestInt64":64,"TestInt64Collection":[-9223372036854775808,0,9223372036854775807],"TestMaxLengthString":"Baz","TestMaxLengthStringCollection":["S1","S2","S3"],"TestNullableEnum":0,"TestNullableEnumCollection":[0,null,2,-7],"TestNullableEnumWithConverterThatHandlesNulls":"Two","TestNullableEnumWithConverterThatHandlesNullsCollection":[0,null,-7],"TestNullableEnumWithIntConverter":2,"TestNullableEnumWithIntConverterCollection":[0,null,2,-7],"TestNullableInt32":90,"TestNullableInt32Collection":[null,-2147483648,0,null,2147483647,null],"TestSignedByte":-18,"TestSignedByteCollection":[-128,0,127],"TestSingle":-1.4,"TestSingleCollection":[-1.234,0,-1.234],"TestTimeOnly":"05:07:08.0000000","TestTimeOnlyCollection":["13:42:23.0000000","07:17:25.0000000"],"TestTimeSpan":"6:05:04.003","TestTimeSpanCollection":["10:09:08.007","-9:50:51.993"],"TestUnsignedInt16":12,"TestUnsignedInt16Collection":[0,0,65535],"TestUnsignedInt32":12345,"TestUnsignedInt32Collection":[0,0,4294967295],"TestUnsignedInt64":1234567867,"TestUnsignedInt64Collection":[0,0,18446744073709551615]}' (Nullable = false) (DbType = Object) -@p1='{"TestBoolean":true,"TestBooleanCollection":[true,false],"TestByte":255,"TestByteCollection":null,"TestCharacter":"a","TestCharacterCollection":["A","B","\u0022"],"TestDateOnly":"2023-10-10","TestDateOnlyCollection":["1234-01-23","4321-01-21"],"TestDateTime":"2000-01-01T12:34:56","TestDateTimeCollection":["2000-01-01T12:34:56","3000-01-01T12:34:56"],"TestDateTimeOffset":"2000-01-01T12:34:56-08:00","TestDateTimeOffsetCollection":["2000-01-01T12:34:56-08:00"],"TestDecimal":-1234567890.01,"TestDecimalCollection":[-1234567890.01],"TestDefaultString":"MyDefaultStringInReference1","TestDefaultStringCollection":["S1","\u0022S2\u0022","S3"],"TestDouble":-1.23456789,"TestDoubleCollection":[-1.23456789,1.23456789,0],"TestEnum":0,"TestEnumCollection":[0,2,-7],"TestEnumWithIntConverter":1,"TestEnumWithIntConverterCollection":[0,2,-7],"TestGuid":"12345678-1234-4321-7777-987654321000","TestGuidCollection":["12345678-1234-4321-7777-987654321000"],"TestInt16":-1234,"TestInt16Collection":[-32768,0,32767],"TestInt32":32,"TestInt32Collection":[-2147483648,0,2147483647],"TestInt64":64,"TestInt64Collection":[-9223372036854775808,0,9223372036854775807],"TestMaxLengthString":"Foo","TestMaxLengthStringCollection":["S1","S2","S3"],"TestNullableEnum":0,"TestNullableEnumCollection":[0,null,2,-7],"TestNullableEnumWithConverterThatHandlesNulls":"Three","TestNullableEnumWithConverterThatHandlesNullsCollection":[0,null,-7],"TestNullableEnumWithIntConverter":1,"TestNullableEnumWithIntConverterCollection":[0,null,2,-7],"TestNullableInt32":78,"TestNullableInt32Collection":[null,-2147483648,0,null,2147483647,null],"TestSignedByte":-128,"TestSignedByteCollection":[-128,0,127],"TestSingle":-1.234,"TestSingleCollection":[-1.234,0,-1.234],"TestTimeOnly":"11:12:13.0000000","TestTimeOnlyCollection":["11:42:23.0000000","07:17:27.0000000"],"TestTimeSpan":"10:09:08.007","TestTimeSpanCollection":["10:09:08.007","-9:50:51.993"],"TestUnsignedInt16":1234,"TestUnsignedInt16Collection":[0,0,65535],"TestUnsignedInt32":1234565789,"TestUnsignedInt32Collection":[0,0,4294967295],"TestUnsignedInt64":1234567890123456789,"TestUnsignedInt64Collection":[0,0,18446744073709551615]}' (Nullable = false) (DbType = Object) +@p0='{"TestBoolean":false,"TestBooleanCollection":[true,false],"TestByte":25,"TestByteCollection":null,"TestCharacter":"h","TestCharacterCollection":["A","B","\u0022"],"TestDateOnly":"2323-04-03","TestDateOnlyCollection":["3234-01-23","4331-01-21"],"TestDateTime":"2100-11-11T12:34:56","TestDateTimeCollection":["2000-01-01T12:34:56","3000-01-01T12:34:56"],"TestDateTimeOffset":"2200-11-11T12:34:56-05:00","TestDateTimeOffsetCollection":["2000-01-01T12:34:56-08:00"],"TestDecimal":-123450.01,"TestDecimalCollection":[-1234567890.01],"TestDefaultString":"MyDefaultStringInCollection1","TestDefaultStringCollection":["S1","\u0022S2\u0022","S3"],"TestDouble":-1.2345,"TestDoubleCollection":[-1.23456789,1.23456789,0],"TestEnum":0,"TestEnumCollection":[0,2,-7],"TestEnumWithIntConverter":1,"TestEnumWithIntConverterCollection":[0,2,-7],"TestGuid":"00000000-0000-0000-0000-000000000000","TestGuidCollection":["12345678-1234-4321-7777-987654321000"],"TestInt16":-12,"TestInt16Collection":[-32768,0,32767],"TestInt32":32,"TestInt32Collection":[-2147483648,0,2147483647],"TestInt64":64,"TestInt64Collection":[-9223372036854775808,0,9223372036854775807],"TestMaxLengthString":"Baz","TestMaxLengthStringCollection":["S1","S2","S3"],"TestNullableEnum":0,"TestNullableEnumCollection":[0,null,2,-7],"TestNullableEnumWithConverterThatHandlesNulls":"Two","TestNullableEnumWithConverterThatHandlesNullsCollection":[0,null,-7],"TestNullableEnumWithIntConverter":2,"TestNullableEnumWithIntConverterCollection":[0,null,2,-7],"TestNullableInt32":90,"TestNullableInt32Collection":[null,-2147483648,0,null,2147483647,null],"TestSignedByte":-18,"TestSignedByteCollection":[-128,0,127],"TestSingle":-1.4,"TestSingleCollection":[-1.234,0,-1.234],"TestTimeOnly":"05:07:08.0000000","TestTimeOnlyCollection":["13:42:23.0000000","07:17:25.0000000"],"TestTimeSpan":"06:05:04.003","TestTimeSpanCollection":["10:09:08.007","-09:50:51.993"],"TestUnsignedInt16":12,"TestUnsignedInt16Collection":[0,0,65535],"TestUnsignedInt32":12345,"TestUnsignedInt32Collection":[0,0,4294967295],"TestUnsignedInt64":1234567867,"TestUnsignedInt64Collection":[0,0,18446744073709551615]}' (Nullable = false) (DbType = Object) +@p1='{"TestBoolean":true,"TestBooleanCollection":[true,false],"TestByte":255,"TestByteCollection":null,"TestCharacter":"a","TestCharacterCollection":["A","B","\u0022"],"TestDateOnly":"2023-10-10","TestDateOnlyCollection":["1234-01-23","4321-01-21"],"TestDateTime":"2000-01-01T12:34:56","TestDateTimeCollection":["2000-01-01T12:34:56","3000-01-01T12:34:56"],"TestDateTimeOffset":"2000-01-01T12:34:56-08:00","TestDateTimeOffsetCollection":["2000-01-01T12:34:56-08:00"],"TestDecimal":-1234567890.01,"TestDecimalCollection":[-1234567890.01],"TestDefaultString":"MyDefaultStringInReference1","TestDefaultStringCollection":["S1","\u0022S2\u0022","S3"],"TestDouble":-1.23456789,"TestDoubleCollection":[-1.23456789,1.23456789,0],"TestEnum":0,"TestEnumCollection":[0,2,-7],"TestEnumWithIntConverter":1,"TestEnumWithIntConverterCollection":[0,2,-7],"TestGuid":"12345678-1234-4321-7777-987654321000","TestGuidCollection":["12345678-1234-4321-7777-987654321000"],"TestInt16":-1234,"TestInt16Collection":[-32768,0,32767],"TestInt32":32,"TestInt32Collection":[-2147483648,0,2147483647],"TestInt64":64,"TestInt64Collection":[-9223372036854775808,0,9223372036854775807],"TestMaxLengthString":"Foo","TestMaxLengthStringCollection":["S1","S2","S3"],"TestNullableEnum":0,"TestNullableEnumCollection":[0,null,2,-7],"TestNullableEnumWithConverterThatHandlesNulls":"Three","TestNullableEnumWithConverterThatHandlesNullsCollection":[0,null,-7],"TestNullableEnumWithIntConverter":1,"TestNullableEnumWithIntConverterCollection":[0,null,2,-7],"TestNullableInt32":78,"TestNullableInt32Collection":[null,-2147483648,0,null,2147483647,null],"TestSignedByte":-128,"TestSignedByteCollection":[-128,0,127],"TestSingle":-1.234,"TestSingleCollection":[-1.234,0,-1.234],"TestTimeOnly":"11:12:13.0000000","TestTimeOnlyCollection":["11:42:23.0000000","07:17:27.0000000"],"TestTimeSpan":"10:09:08.007","TestTimeSpanCollection":["10:09:08.007","-09:50:51.993"],"TestUnsignedInt16":1234,"TestUnsignedInt16Collection":[0,0,65535],"TestUnsignedInt32":1234565789,"TestUnsignedInt32Collection":[0,0,4294967295],"TestUnsignedInt64":1234567890123456789,"TestUnsignedInt64Collection":[0,0,18446744073709551615]}' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0}', @p0), "Reference" = @p1 @@ -1600,7 +1600,7 @@ public override async Task Edit_single_property_collection_of_timespan() AssertSql( """ @p0='["10:09:08.007","10:01:01.007"]' (Nullable = false) (DbType = Object) -@p1='["10:01:01.007","-9:50:51.993"]' (Nullable = false) (DbType = Object) +@p1='["10:01:01.007","-09:50:51.993"]' (Nullable = false) (DbType = Object) @p2='1' UPDATE "JsonEntitiesAllTypes" SET "Collection" = jsonb_set("Collection", '{0,TestTimeSpanCollection}', @p0), "Reference" = jsonb_set("Reference", '{TestTimeSpanCollection}', @p1) diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs index 9c72b15cf..6886d312c 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs @@ -1573,7 +1573,7 @@ await AssertQuery( """ SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime" FROM "NodaTimeTypes" AS n -WHERE n."Instant"::timestamptz = TIMESTAMPTZ '2018-04-20 10:31:33.666Z' +WHERE n."Instant"::timestamptz = TIMESTAMPTZ '2018-04-20T10:31:33.666Z' """); } diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs index 99687b978..ddf7e078d 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs @@ -1,6 +1,8 @@ +using System.Text; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Storage.Json; using NodaTime.Calendars; +using NodaTime.Text; using NodaTime.TimeZones; using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; @@ -68,6 +70,25 @@ public void GenerateSqlLiteral_returns_LocalDateTime_infinity_literal() Assert.Equal("TIMESTAMP 'infinity'", mapping.GenerateSqlLiteral(LocalDate.MaxIsoValue + LocalTime.MaxValue)); } + [ConditionalTheory] + [InlineData("0001-01-01T00:00:00")] + [InlineData("9999-12-31T23:59:59.9999999")] + [InlineData("2023-05-29T10:52:47.2064353")] + public void LocalDateTime_json(string dateString) + { + var readerWriter = GetMapping(typeof(LocalDateTime)).JsonValueReaderWriter!; + + var date = LocalDateTimePattern.ExtendedIso.Parse(dateString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(date)[1..^1]; + Assert.Equal(dateString, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{dateString}\"")), null); + readerManager.MoveNext(); + var actualDate = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(date, actualDate); + } + [Fact] public void NpgsqlRange_of_LocalDateTime_is_properly_mapped() { @@ -186,6 +207,43 @@ public void GenerateCodeLiteral_returns_OffsetDate_time_literal() CodeLiteral(new OffsetDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33), Offset.FromSeconds(-1)))); } + [ConditionalTheory] + [InlineData("0001-01-01T00:00:00Z")] + [InlineData("2023-05-29T10:52:47.2064353Z")] + [InlineData("-0005-05-05T05:55:55.555Z")] + public void Instant_json(string instantString) + { + var readerWriter = GetMapping(typeof(Instant)).JsonValueReaderWriter!; + + var date = InstantPattern.ExtendedIso.Parse(instantString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(date)[1..^1]; + Assert.Equal(instantString, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{instantString}\"")), null); + readerManager.MoveNext(); + var actualInstant = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(date, actualInstant); + } + + [ConditionalFact] + public void Instant_json_infinity() + { + var readerWriter = GetMapping(typeof(Instant)).JsonValueReaderWriter!; + + Assert.Equal("infinity", readerWriter.ToJsonString(Instant.MaxValue)[1..^1]); + Assert.Equal("-infinity", readerWriter.ToJsonString(Instant.MinValue)[1..^1]); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"infinity\""u8.ToArray()), null); + readerManager.MoveNext(); + Assert.Equal(Instant.MaxValue, readerWriter.FromJson(ref readerManager, existingObject: null)); + + readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"-infinity\""u8.ToArray()), null); + readerManager.MoveNext(); + Assert.Equal(Instant.MinValue, readerWriter.FromJson(ref readerManager, existingObject: null)); + } + [Fact] public void Interval_is_properly_mapped() { @@ -382,6 +440,43 @@ public void GenerateCodeLiteral_returns_LocalDate_literal() Assert.Equal("new NodaTime.LocalDate(-2017, 4, 20)", CodeLiteral(new LocalDate(Era.BeforeCommon, 2018, 4, 20))); } + [ConditionalTheory] + [InlineData("0001-01-01")] + [InlineData("2023-05-29")] + [InlineData("-0005-05-05")] + public void LocalDate_json(string dateString) + { + var readerWriter = GetMapping(typeof(LocalDate)).JsonValueReaderWriter!; + + var date = LocalDatePattern.Iso.Parse(dateString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(date)[1..^1]; + Assert.Equal(dateString, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{dateString}\"")), null); + readerManager.MoveNext(); + var actualDate = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(date, actualDate); + } + + [ConditionalFact] + public void LocalDate_json_infinity() + { + var readerWriter = GetMapping(typeof(LocalDate)).JsonValueReaderWriter!; + + Assert.Equal("infinity", readerWriter.ToJsonString(LocalDate.MaxIsoValue)[1..^1]); + Assert.Equal("-infinity", readerWriter.ToJsonString(LocalDate.MinIsoValue)[1..^1]); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"infinity\""u8.ToArray()), null); + readerManager.MoveNext(); + Assert.Equal(LocalDate.MaxIsoValue, readerWriter.FromJson(ref readerManager, existingObject: null)); + + readerManager = new Utf8JsonReaderManager(new JsonReaderData("\"-infinity\""u8.ToArray()), null); + readerManager.MoveNext(); + Assert.Equal(LocalDate.MinIsoValue, readerWriter.FromJson(ref readerManager, existingObject: null)); + } + [Fact] public void DateInterval_is_properly_mapped() { @@ -521,6 +616,25 @@ public void LocalTime_array_is_properly_mapped() public void LocalTime_list_is_properly_mapped() => Assert.Equal("time[]", GetMapping(typeof(List)).StoreType); + [ConditionalTheory] + [InlineData("00:00:00.0000000", "00:00:00")] + [InlineData("23:59:59.9999999", "23:59:59.9999999")] + [InlineData("11:05:12.3456789", "11:05:12.3456789")] + public void LocalTime_json(string timeString, string json) + { + var readerWriter = GetMapping(typeof(LocalTime)).JsonValueReaderWriter!; + + var time = LocalTimePattern.ExtendedIso.Parse(timeString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(time)[1..^1]; + Assert.Equal(json, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{json}\"")), null); + readerManager.MoveNext(); + var actualTime = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(time, actualTime); + } + #endregion time #region timetz @@ -547,6 +661,25 @@ public void GenerateCodeLiteral_returns_OffsetTime_literal() "new NodaTime.OffsetTime(new NodaTime.LocalTime(10, 31, 33), NodaTime.Offset.FromHours(2))", CodeLiteral(new OffsetTime(new LocalTime(10, 31, 33), Offset.FromHours(2)))); + [ConditionalTheory] + [InlineData("00:00:00.0000000Z", "00:00:00Z")] + [InlineData("23:59:59.999999Z", "23:59:59.999999Z")] + [InlineData("11:05:12-02", "11:05:12-02")] + public void OffsetTime_json(string timeString, string json) + { + var readerWriter = GetMapping(typeof(OffsetTime)).JsonValueReaderWriter!; + + var timeOffset = OffsetTimePattern.ExtendedIso.Parse(timeString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(timeOffset)[1..^1]; + Assert.Equal(json, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{json}\"")), null); + readerManager.MoveNext(); + var actualTimeOffset = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(timeOffset, actualTimeOffset); + } + #endregion timetz #region interval @@ -630,6 +763,46 @@ public void GenerateCodeLiteral_returns_Duration_literal() Assert.Equal("NodaTime.Duration.Zero", CodeLiteral(Duration.Zero)); } + [ConditionalTheory] + [InlineData("-10675199:02:48:05.477580", "-10675199 02:48:05.47758")] + [InlineData("10675199:02:48:05.477580", "10675199 02:48:05.47758")] + [InlineData("00:00:00", "00:00:00")] + [InlineData("12:23:23.801885", "12:23:23.801885")] + public void Duration_json(string durationString, string json) + { + var readerWriter = GetMapping(typeof(Duration)).JsonValueReaderWriter!; + + var duration = durationString.Count(c => c == ':') == 3 // there's a Days component + ? DurationPattern.Roundtrip.Parse(durationString).GetValueOrThrow() + : DurationPattern.JsonRoundtrip.Parse(durationString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(duration)[1..^1]; + Assert.Equal(json, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{json}\"")), null); + readerManager.MoveNext(); + var actualDuration = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(duration, actualDuration); + } + + [ConditionalTheory] + [InlineData("P2018Y4M20DT4H3M2S")] + public void Period_json(string intervalString) + { + var readerWriter = GetMapping(typeof(Period)).JsonValueReaderWriter!; + + var period = PeriodPattern.NormalizingIso.Parse(intervalString).GetValueOrThrow(); + var actualJson = readerWriter.ToJsonString(period)[1..^1]; + Assert.Equal(intervalString, actualJson); + + // TODO: The following should just do ToJsonString(), but see https://github.com/dotnet/efcore/issues/32269 + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes($"\"{intervalString}\"")), null); + readerManager.MoveNext(); + var actualPeriod = readerWriter.FromJson(ref readerManager, existingObject: null); + Assert.Equal(period, actualPeriod); + } + + #endregion interval #region DateTimeZone diff --git a/test/EFCore.PG.Tests/Storage/LegacyNpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/LegacyNpgsqlTypeMappingTest.cs index d47f2b6b7..4c880d6ba 100644 --- a/test/EFCore.PG.Tests/Storage/LegacyNpgsqlTypeMappingTest.cs +++ b/test/EFCore.PG.Tests/Storage/LegacyNpgsqlTypeMappingTest.cs @@ -26,20 +26,20 @@ public void GenerateSqlLiteral_returns_timestamptz_datetime_literal() { var mapping = GetMapping("timestamptz"); Assert.Equal( - "TIMESTAMPTZ '1997-12-17 07:37:16Z'", + "TIMESTAMPTZ '1997-12-17T07:37:16Z'", mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Utc))); Assert.Equal( - "TIMESTAMPTZ '1997-12-17 07:37:16Z'", + "TIMESTAMPTZ '1997-12-17T07:37:16Z'", mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Unspecified))); var offset = TimeZoneInfo.Local.BaseUtcOffset; var offsetStr = (offset < TimeSpan.Zero ? '-' : '+') + offset.ToString(@"hh\:mm"); Assert.StartsWith( - $"TIMESTAMPTZ '1997-12-17 07:37:16{offsetStr}", + $"TIMESTAMPTZ '1997-12-17T07:37:16{offsetStr}", mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Local))); Assert.Equal( - "TIMESTAMPTZ '1997-12-17 07:37:16.345678Z'", + "TIMESTAMPTZ '1997-12-17T07:37:16.345678Z'", mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, 345, DateTimeKind.Utc).AddTicks(6780))); } diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs index f21f409df..dc972ffe9 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs @@ -60,7 +60,7 @@ public void GenerateSqlLiteral_returns_date_literal() { Assert.Equal( "DATE '2015-03-12'", - GetMapping("date").GenerateSqlLiteral(new DateTime(2015, 3, 12))); + GetMapping(typeof(DateTime), "date").GenerateSqlLiteral(new DateTime(2015, 3, 12))); Assert.Equal( "DATE '2015-03-12'", @@ -72,11 +72,11 @@ public void GenerateSqlLiteral_returns_date_infinity_literals() { Assert.Equal( "DATE '-infinity'", - GetMapping("date").GenerateSqlLiteral(DateTime.MinValue)); + GetMapping(typeof(DateTime), "date").GenerateSqlLiteral(DateTime.MinValue)); Assert.Equal( "DATE 'infinity'", - GetMapping("date").GenerateSqlLiteral(DateTime.MaxValue)); + GetMapping(typeof(DateTime), "date").GenerateSqlLiteral(DateTime.MaxValue)); Assert.Equal( "DATE '-infinity'", @@ -92,13 +92,13 @@ public void GenerateSqlLiteral_returns_timestamp_literal() { var mapping = GetMapping("timestamp without time zone"); Assert.Equal( - "TIMESTAMP '1997-12-17 07:37:16'", + "TIMESTAMP '1997-12-17T07:37:16'", mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Local))); Assert.Equal( - "TIMESTAMP '1997-12-17 07:37:16'", + "TIMESTAMP '1997-12-17T07:37:16'", mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Unspecified))); Assert.Equal( - "TIMESTAMP '1997-12-17 07:37:16.345'", + "TIMESTAMP '1997-12-17T07:37:16.345'", mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, 345))); } @@ -146,10 +146,10 @@ public void GenerateSqlLiteral_returns_timestamptz_datetime_literal() { var mapping = GetMapping("timestamptz"); Assert.Equal( - "TIMESTAMPTZ '1997-12-17 07:37:16Z'", + "TIMESTAMPTZ '1997-12-17T07:37:16Z'", mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, DateTimeKind.Utc))); Assert.Equal( - "TIMESTAMPTZ '1997-12-17 07:37:16.345678Z'", + "TIMESTAMPTZ '1997-12-17T07:37:16.345678Z'", mapping.GenerateSqlLiteral(new DateTime(1997, 12, 17, 7, 37, 16, 345, DateTimeKind.Utc).AddTicks(6780))); } @@ -166,11 +166,11 @@ public void GenerateSqlLiteral_returns_timestamptz_infinity_literals() Assert.Equal( "TIMESTAMPTZ '-infinity'", - GetMapping("timestamptz").GenerateSqlLiteral(DateTimeOffset.MinValue)); + GetMapping(typeof(DateTimeOffset), "timestamptz").GenerateSqlLiteral(DateTimeOffset.MinValue)); Assert.Equal( "TIMESTAMPTZ 'infinity'", - GetMapping("timestamptz").GenerateSqlLiteral(DateTimeOffset.MaxValue)); + GetMapping(typeof(DateTimeOffset), "timestamptz").GenerateSqlLiteral(DateTimeOffset.MaxValue)); } [Fact] @@ -188,10 +188,10 @@ public void GenerateSqlLiteral_returns_timestamptz_datetimeoffset_literal() { var mapping = GetMapping("timestamptz"); Assert.Equal( - "TIMESTAMPTZ '1997-12-17 07:37:16+02:00'", + "TIMESTAMPTZ '1997-12-17T07:37:16+02:00'", mapping.GenerateSqlLiteral(new DateTimeOffset(1997, 12, 17, 7, 37, 16, TimeSpan.FromHours(2)))); Assert.Equal( - "TIMESTAMPTZ '1997-12-17 07:37:16.345+02:00'", + "TIMESTAMPTZ '1997-12-17T07:37:16.345+02:00'", mapping.GenerateSqlLiteral(new DateTimeOffset(1997, 12, 17, 7, 37, 16, 345, TimeSpan.FromHours(2)))); } @@ -715,7 +715,7 @@ public void GenerateSqlLiteral_returns_tsrange_literal() Assert.Equal("timestamp without time zone", mapping.SubtypeMapping.StoreType); var value = new NpgsqlRange(new DateTime(2020, 1, 1, 12, 0, 0), new DateTime(2020, 1, 2, 12, 0, 0)); - Assert.Equal(@"'[""2020-01-01 12:00:00"",""2020-01-02 12:00:00""]'::tsrange", mapping.GenerateSqlLiteral(value)); + Assert.Equal(@"'[""2020-01-01T12:00:00"",""2020-01-02T12:00:00""]'::tsrange", mapping.GenerateSqlLiteral(value)); } [Fact] @@ -726,7 +726,7 @@ public void GenerateSqlLiteral_returns_tstzrange_literal() var value = new NpgsqlRange( new DateTime(2020, 1, 1, 12, 0, 0, DateTimeKind.Utc), new DateTime(2020, 1, 2, 12, 0, 0, DateTimeKind.Utc)); - Assert.Equal(@"'[""2020-01-01 12:00:00Z"",""2020-01-02 12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); + Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); } [Fact]