diff --git a/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj b/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj index 38f7f4983e9d003dfce7609cdd4c4d19d924538e..5d4c0d4a4168e390fb1acf8513850c1e2e75ac00 100644 --- a/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj +++ b/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj @@ -23,7 +23,7 @@ - + True build diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalMultirangeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalMultirangeMapping.cs new file mode 100644 index 0000000000000000000000000000000000000000..06be7c6f00bcd476461a42a15345ff2a3c6fc3dc --- /dev/null +++ b/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalMultirangeMapping.cs @@ -0,0 +1,29 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; + +// ReSharper disable once CheckNamespace +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; + +public class DateIntervalMultirangeMapping : NpgsqlTypeMapping +{ + private readonly DateIntervalRangeMapping _dateIntervalRangeMapping; + + public DateIntervalMultirangeMapping(Type clrType, DateIntervalRangeMapping dateIntervalRangeMapping) + : base("datemultirange", clrType, NpgsqlDbType.DateMultirange) + => _dateIntervalRangeMapping = dateIntervalRangeMapping; + + protected DateIntervalMultirangeMapping(RelationalTypeMappingParameters parameters, DateIntervalRangeMapping dateIntervalRangeMapping) + : base(parameters, NpgsqlDbType.DateMultirange) + => _dateIntervalRangeMapping = dateIntervalRangeMapping; + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new DateIntervalMultirangeMapping(parameters, _dateIntervalRangeMapping); + + public override RelationalTypeMapping Clone(string storeType, int? size) + => new DateIntervalMultirangeMapping(Parameters.WithStoreTypeAndSize(storeType, size), _dateIntervalRangeMapping); + + public override CoreTypeMapping Clone(ValueConverter? converter) + => new DateIntervalMultirangeMapping(Parameters.WithComposedConverter(converter), _dateIntervalRangeMapping); + + protected override string GenerateNonNullSqlLiteral(object value) + => NpgsqlMultirangeTypeMapping.GenerateNonNullSqlLiteral(value, _dateIntervalRangeMapping, "datemultirange"); +} \ No newline at end of file diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs similarity index 67% rename from src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalMapping.cs rename to src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs index a6b1c29dd1f85735dbb96b4ebd4178ffe0b5ef66..6dc1ccc3016826d4d899ed9ada8e7ed92e90d6ee 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/DateIntervalRangeMapping.cs @@ -4,7 +4,7 @@ // ReSharper disable once CheckNamespace namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -public class DateIntervalMapping : NpgsqlTypeMapping +public class DateIntervalRangeMapping : NpgsqlTypeMapping { private static readonly ConstructorInfo _constructorWithDates = typeof(DateInterval).GetConstructor(new[] { typeof(LocalDate), typeof(LocalDate) })!; @@ -12,29 +12,32 @@ public class DateIntervalMapping : NpgsqlTypeMapping private static readonly ConstructorInfo _localDateConstructor = typeof(LocalDate).GetConstructor(new[] { typeof(int), typeof(int), typeof(int) })!; - public DateIntervalMapping() + public DateIntervalRangeMapping() : base("daterange", typeof(DateInterval), NpgsqlDbType.DateRange) { } - protected DateIntervalMapping(RelationalTypeMappingParameters parameters) + protected DateIntervalRangeMapping(RelationalTypeMappingParameters parameters) : base(parameters, NpgsqlDbType.DateRange) { } protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new DateIntervalMapping(parameters); + => new DateIntervalRangeMapping(parameters); public override RelationalTypeMapping Clone(string storeType, int? size) - => new DateIntervalMapping(Parameters.WithStoreTypeAndSize(storeType, size)); + => new DateIntervalRangeMapping(Parameters.WithStoreTypeAndSize(storeType, size)); public override CoreTypeMapping Clone(ValueConverter? converter) - => new DateIntervalMapping(Parameters.WithComposedConverter(converter)); + => new DateIntervalRangeMapping(Parameters.WithComposedConverter(converter)); protected override string GenerateNonNullSqlLiteral(object value) + => $"'{GenerateEmbeddedNonNullSqlLiteral(value)}'::daterange"; + + protected override string GenerateEmbeddedNonNullSqlLiteral(object value) { - var dateInverval = (DateInterval)value; - return $"'[{LocalDatePattern.Iso.Format(dateInverval.Start)},{LocalDatePattern.Iso.Format(dateInverval.End)}]'::daterange"; + var dateInterval = (DateInterval)value; + return $"[{LocalDatePattern.Iso.Format(dateInterval.Start)},{LocalDatePattern.Iso.Format(dateInterval.End)}]"; } public override Expression GenerateCodeLiteral(object value) diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/IntervalMultirangeMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/IntervalMultirangeMapping.cs new file mode 100644 index 0000000000000000000000000000000000000000..9f8f068e82f740c41355a0f5533028b72d3abbb7 --- /dev/null +++ b/src/EFCore.PG.NodaTime/Storage/Internal/IntervalMultirangeMapping.cs @@ -0,0 +1,29 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; + +// ReSharper disable once CheckNamespace +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; + +public class IntervalMultirangeMapping : NpgsqlTypeMapping +{ + private readonly IntervalRangeMapping _intervalRangeMapping; + + public IntervalMultirangeMapping(Type clrType, IntervalRangeMapping intervalRangeMapping) + : base("tstzmultirange", clrType, NpgsqlDbType.TimestampTzMultirange) + => _intervalRangeMapping = intervalRangeMapping; + + protected IntervalMultirangeMapping(RelationalTypeMappingParameters parameters, IntervalRangeMapping intervalRangeMapping) + : base(parameters, NpgsqlDbType.DateMultirange) + => _intervalRangeMapping = intervalRangeMapping; + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new IntervalMultirangeMapping(parameters, _intervalRangeMapping); + + public override RelationalTypeMapping Clone(string storeType, int? size) + => new IntervalMultirangeMapping(Parameters.WithStoreTypeAndSize(storeType, size), _intervalRangeMapping); + + public override CoreTypeMapping Clone(ValueConverter? converter) + => new IntervalMultirangeMapping(Parameters.WithComposedConverter(converter), _intervalRangeMapping); + + protected override string GenerateNonNullSqlLiteral(object value) + => NpgsqlMultirangeTypeMapping.GenerateNonNullSqlLiteral(value, _intervalRangeMapping, "tstzmultirange"); +} \ No newline at end of file diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/IntervalMapping.cs b/src/EFCore.PG.NodaTime/Storage/Internal/IntervalRangeMapping.cs similarity index 67% rename from src/EFCore.PG.NodaTime/Storage/Internal/IntervalMapping.cs rename to src/EFCore.PG.NodaTime/Storage/Internal/IntervalRangeMapping.cs index 8a6dd01aced25fdac600d70f3024f1eca1fdddaf..bef3ce9dea1b241b66d7bd0519f7ce4a4e5e9e87 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/IntervalMapping.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/IntervalRangeMapping.cs @@ -1,12 +1,13 @@ // ReSharper disable once CheckNamespace +using System.Text; using NodaTime.Text; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; // ReSharper disable once CheckNamespace namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -public class IntervalMapping : NpgsqlTypeMapping +public class IntervalRangeMapping : NpgsqlTypeMapping { private static readonly ConstructorInfo _constructor = typeof(Interval).GetConstructor(new[] { typeof(Instant), typeof(Instant) })!; @@ -14,37 +15,49 @@ public class IntervalMapping : NpgsqlTypeMapping private static readonly ConstructorInfo _constructorWithNulls = typeof(Interval).GetConstructor(new[] { typeof(Instant?), typeof(Instant?) })!; - public IntervalMapping() + public IntervalRangeMapping() : base("tstzrange", typeof(Interval), NpgsqlDbType.TimestampTzRange) { } - protected IntervalMapping(RelationalTypeMappingParameters parameters) + protected IntervalRangeMapping(RelationalTypeMappingParameters parameters) : base(parameters, NpgsqlDbType.TimestampTzRange) { } protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new IntervalMapping(parameters); + => new IntervalRangeMapping(parameters); public override RelationalTypeMapping Clone(string storeType, int? size) - => new IntervalMapping(Parameters.WithStoreTypeAndSize(storeType, size)); + => new IntervalRangeMapping(Parameters.WithStoreTypeAndSize(storeType, size)); public override CoreTypeMapping Clone(ValueConverter? converter) - => new IntervalMapping(Parameters.WithComposedConverter(converter)); + => new IntervalRangeMapping(Parameters.WithComposedConverter(converter)); protected override string GenerateNonNullSqlLiteral(object value) + => $"'{GenerateEmbeddedNonNullSqlLiteral(value)}'::tstzrange"; + + protected override string GenerateEmbeddedNonNullSqlLiteral(object value) { var interval = (Interval)value; - var start = interval.HasStart - ? InstantPattern.ExtendedIso.Format(interval.Start) - : ""; - var end = interval.HasEnd - ? InstantPattern.ExtendedIso.Format(interval.End) - : ""; + var stringBuilder = new StringBuilder("["); + + if (interval.HasStart) + { + stringBuilder.Append(InstantPattern.ExtendedIso.Format(interval.Start)); + } + + stringBuilder.Append(','); + + if (interval.HasEnd) + { + stringBuilder.Append(InstantPattern.ExtendedIso.Format(interval.End)); + } + + stringBuilder.Append(')'); - return $"'[{start},{end})'::tstzrange"; + return stringBuilder.ToString(); } public override Expression GenerateCodeLiteral(object value) diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs b/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs index 3316f64cc12a062408779e78eafb0eab2db00222..720a17972a760b47bafcbce0cac542c6616fc03d 100644 --- a/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs @@ -38,15 +38,34 @@ static NpgsqlNodaTimeTypeMappingSourcePlugin() private readonly PeriodIntervalMapping _periodInterval = new(); private readonly DurationIntervalMapping _durationInterval = new(); + // Built-in ranges private readonly NpgsqlRangeTypeMapping _timestampLocalDateTimeRange; private readonly NpgsqlRangeTypeMapping _legacyTimestampInstantRange; private readonly NpgsqlRangeTypeMapping _timestamptzInstantRange; private readonly NpgsqlRangeTypeMapping _timestamptzZonedDateTimeRange; private readonly NpgsqlRangeTypeMapping _timestamptzOffsetDateTimeRange; private readonly NpgsqlRangeTypeMapping _dateRange; - - private readonly DateIntervalMapping _dateInterval = new(); - private readonly IntervalMapping _interval = new(); + private readonly DateIntervalRangeMapping _dateIntervalRange = new(); + private readonly IntervalRangeMapping _intervalRange = new(); + + // Built-in multiranges + private readonly NpgsqlMultirangeTypeMapping _timestampLocalDateTimeMultirangeArray; + private readonly NpgsqlMultirangeTypeMapping _legacyTimestampInstantMultirangeArray; + private readonly NpgsqlMultirangeTypeMapping _timestamptzInstantMultirangeArray; + private readonly NpgsqlMultirangeTypeMapping _timestamptzZonedDateTimeMultirangeArray; + private readonly NpgsqlMultirangeTypeMapping _timestamptzOffsetDateTimeMultirangeArray; + private readonly NpgsqlMultirangeTypeMapping _dateRangeMultirangeArray; + private readonly DateIntervalMultirangeMapping _dateIntervalMultirangeArray; + private readonly IntervalMultirangeMapping _intervalMultirangeArray; + + private readonly NpgsqlMultirangeTypeMapping _timestampLocalDateTimeMultirangeList; + private readonly NpgsqlMultirangeTypeMapping _legacyTimestampInstantMultirangeList; + private readonly NpgsqlMultirangeTypeMapping _timestamptzInstantMultirangeList; + private readonly NpgsqlMultirangeTypeMapping _timestamptzZonedDateTimeMultirangeList; + private readonly NpgsqlMultirangeTypeMapping _timestamptzOffsetDateTimeMultirangeList; + private readonly NpgsqlMultirangeTypeMapping _dateRangeMultirangeList; + private readonly DateIntervalMultirangeMapping _dateIntervalMultirangeList; + private readonly IntervalMultirangeMapping _intervalMultirangeList; #endregion @@ -55,10 +74,10 @@ static NpgsqlNodaTimeTypeMappingSourcePlugin() /// public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationHelper) { - _legacyTimestampInstantRange - = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange), _legacyTimestampInstant, sqlGenerationHelper); _timestampLocalDateTimeRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange), _timestampLocalDateTime, sqlGenerationHelper); + _legacyTimestampInstantRange + = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange), _legacyTimestampInstant, sqlGenerationHelper); _timestamptzInstantRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange), _timestamptzInstant, sqlGenerationHelper); _timestamptzZonedDateTimeRange @@ -68,6 +87,36 @@ public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationH _dateRange = new NpgsqlRangeTypeMapping("daterange", typeof(NpgsqlRange), _date, sqlGenerationHelper); + _timestampLocalDateTimeMultirangeArray = new NpgsqlMultirangeTypeMapping( + "tsmultirange", typeof(NpgsqlRange[]), _timestampLocalDateTimeRange, sqlGenerationHelper); + _legacyTimestampInstantMultirangeArray = new NpgsqlMultirangeTypeMapping( + "tsmultirange", typeof(NpgsqlRange[]), _legacyTimestampInstantRange, sqlGenerationHelper); + _timestamptzInstantMultirangeArray = new NpgsqlMultirangeTypeMapping( + "tstzmultirange", typeof(NpgsqlRange[]), _timestamptzInstantRange, sqlGenerationHelper); + _timestamptzZonedDateTimeMultirangeArray = new NpgsqlMultirangeTypeMapping( + "tstzmultirange", typeof(NpgsqlRange[]), _timestamptzZonedDateTimeRange, sqlGenerationHelper); + _timestamptzOffsetDateTimeMultirangeArray = new NpgsqlMultirangeTypeMapping( + "tstzmultirange", typeof(NpgsqlRange[]), _timestamptzOffsetDateTimeRange, sqlGenerationHelper); + _dateRangeMultirangeArray = new NpgsqlMultirangeTypeMapping( + "datemultirange", typeof(NpgsqlRange[]), _dateRange, sqlGenerationHelper); + _dateIntervalMultirangeArray = new DateIntervalMultirangeMapping(typeof(DateInterval[]), _dateIntervalRange); + _intervalMultirangeArray = new IntervalMultirangeMapping(typeof(Interval[]), _intervalRange); + + _timestampLocalDateTimeMultirangeList = new NpgsqlMultirangeTypeMapping( + "tsmultirange", typeof(List>), _timestampLocalDateTimeRange, sqlGenerationHelper); + _legacyTimestampInstantMultirangeList = new NpgsqlMultirangeTypeMapping( + "tsmultirange", typeof(List>), _legacyTimestampInstantRange, sqlGenerationHelper); + _timestamptzInstantMultirangeList = new NpgsqlMultirangeTypeMapping( + "tstzmultirange", typeof(List>), _timestamptzInstantRange, sqlGenerationHelper); + _timestamptzZonedDateTimeMultirangeList = new NpgsqlMultirangeTypeMapping( + "tstzmultirange", typeof(List>), _timestamptzZonedDateTimeRange, sqlGenerationHelper); + _timestamptzOffsetDateTimeMultirangeList = new NpgsqlMultirangeTypeMapping( + "tstzmultirange", typeof(List>), _timestamptzOffsetDateTimeRange, sqlGenerationHelper); + _dateRangeMultirangeList = new NpgsqlMultirangeTypeMapping( + "datemultirange", typeof(List>), _dateRange, sqlGenerationHelper); + _dateIntervalMultirangeList = new DateIntervalMultirangeMapping(typeof(List), _dateIntervalRange); + _intervalMultirangeList = new IntervalMultirangeMapping(typeof(List), _intervalRange); + var storeTypeMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) { { @@ -88,8 +137,29 @@ public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationH ? new RelationalTypeMapping[] { _legacyTimestampInstantRange, _timestampLocalDateTimeRange } : new RelationalTypeMapping[] { _timestampLocalDateTimeRange, _legacyTimestampInstantRange } }, - { "tstzrange", new RelationalTypeMapping[] { _interval, _timestamptzInstantRange, _timestamptzZonedDateTimeRange, _timestamptzOffsetDateTimeRange } }, - { "daterange", new RelationalTypeMapping[] { _dateInterval, _dateRange } } + { "tstzrange", new RelationalTypeMapping[] { _intervalRange, _timestamptzInstantRange, _timestamptzZonedDateTimeRange, _timestamptzOffsetDateTimeRange } }, + { "daterange", new RelationalTypeMapping[] { _dateIntervalRange, _dateRange } }, + + { "tsmultirange", LegacyTimestampBehavior + ? new RelationalTypeMapping[] { _legacyTimestampInstantMultirangeArray, _legacyTimestampInstantMultirangeList, _timestampLocalDateTimeMultirangeArray, _timestampLocalDateTimeMultirangeList } + : new RelationalTypeMapping[] { _timestampLocalDateTimeMultirangeArray, _timestampLocalDateTimeMultirangeList, _legacyTimestampInstantMultirangeArray, _legacyTimestampInstantMultirangeList } + }, + { + "tstzmultirange", new RelationalTypeMapping[] + { + _intervalMultirangeArray, _intervalMultirangeList, + _timestamptzInstantMultirangeArray, _timestamptzInstantMultirangeList, + _timestamptzZonedDateTimeMultirangeArray, _timestamptzZonedDateTimeMultirangeList, + _timestamptzOffsetDateTimeMultirangeArray, _timestamptzOffsetDateTimeMultirangeList + } + }, + { + "datemultirange", new RelationalTypeMapping[] + { + _dateIntervalMultirangeArray, _dateIntervalMultirangeList, + _dateRangeMultirangeArray, _dateRangeMultirangeList + } + } }; // Set up aliases @@ -116,8 +186,24 @@ public NpgsqlNodaTimeTypeMappingSourcePlugin(ISqlGenerationHelper sqlGenerationH { typeof(NpgsqlRange), _timestamptzZonedDateTimeRange }, { typeof(NpgsqlRange), _timestamptzOffsetDateTimeRange }, { typeof(NpgsqlRange), _dateRange }, - { typeof(DateInterval), _dateInterval }, - { typeof(Interval), _interval } + { typeof(DateInterval), _dateIntervalRange }, + { typeof(Interval), _intervalRange }, + + { typeof(NpgsqlRange[]), LegacyTimestampBehavior ? _legacyTimestampInstantMultirangeArray : _timestamptzInstantMultirangeArray }, + { typeof(NpgsqlRange[]), _timestampLocalDateTimeMultirangeArray }, + { typeof(NpgsqlRange[]), _timestamptzZonedDateTimeMultirangeArray }, + { typeof(NpgsqlRange[]), _timestamptzOffsetDateTimeMultirangeArray }, + { typeof(NpgsqlRange[]), _dateRangeMultirangeArray }, + { typeof(DateInterval[]), _dateIntervalMultirangeArray }, + { typeof(Interval[]), _intervalMultirangeArray }, + + { typeof(List>), LegacyTimestampBehavior ? _legacyTimestampInstantMultirangeList : _timestamptzInstantMultirangeList }, + { typeof(List>), _timestampLocalDateTimeMultirangeList }, + { typeof(List>), _timestamptzZonedDateTimeMultirangeList }, + { typeof(List>), _timestamptzOffsetDateTimeMultirangeList }, + { typeof(List>), _dateRangeMultirangeList }, + { typeof(List), _dateIntervalMultirangeList }, + { typeof(List), _intervalMultirangeList } }; StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMultirangeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMultirangeTypeMapping.cs index f901f034159073f513fd621ad9212c3f4c5ee936..960a52d3f42162de7faa7e98488cb2c5de4deb9c 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMultirangeTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMultirangeTypeMapping.cs @@ -76,6 +76,9 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p => new NpgsqlMultirangeTypeMapping(parameters, NpgsqlDbType, RangeMapping, _sqlGenerationHelper); protected override string GenerateNonNullSqlLiteral(object value) + => GenerateNonNullSqlLiteral(value, RangeMapping, StoreType); + + public static string GenerateNonNullSqlLiteral(object value, RelationalTypeMapping rangeMapping, string multirangeStoreType) { var multirange = (IList)value; @@ -84,7 +87,7 @@ protected override string GenerateNonNullSqlLiteral(object value) for (var i = 0; i < multirange.Count; i++) { - sb.Append(RangeMapping.GenerateEmbeddedSqlLiteral(multirange[i])); + sb.Append(rangeMapping.GenerateEmbeddedSqlLiteral(multirange[i])); if (i < multirange.Count - 1) { sb.Append(", "); @@ -92,7 +95,7 @@ protected override string GenerateNonNullSqlLiteral(object value) } sb.Append("}'::"); - sb.Append(StoreType); + sb.Append(multirangeStoreType); return sb.ToString(); } diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs index 78eff112f8c26b7c67c179ab3ef644f8431de885..9cc3b03526a51db308888e11dd15dc4d7916a028 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs @@ -9,13 +9,14 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL; public class NpgsqlNodaTimeTypeMappingTest { - [Fact] - public void Timestamp_maps_to_LocalDateTime_by_default() - => Assert.Same(typeof(LocalDateTime), GetMapping("timestamp without time zone").ClrType); + #region Timestamp without time zone [Fact] - public void Timestamptz_maps_to_Instant_by_default() - => Assert.Same(typeof(Instant), GetMapping("timestamp with time zone").ClrType); + public void Timestamp_maps_to_LocalDateTime_by_default() + { + Assert.Equal("timestamp without time zone", GetMapping(typeof(LocalDateTime)).StoreType); + Assert.Same(typeof(LocalDateTime), GetMapping("timestamp without time zone").ClrType); + } // Mapping Instant to timestamp should only be possible in legacy mode. // However, when upgrading to 6.0 with existing migrations, model snapshots still contain old mappings (Instant mapped to timestamp), @@ -28,10 +29,6 @@ public void Instant_maps_to_timestamp_legacy() Assert.Equal("timestamp without time zone", mapping.StoreType); } - [Fact] - public void LocalDateTime_does_not_map_to_timestamptz() - => Assert.Null(GetMapping(typeof(LocalDateTime), "timestamp with time zone")); - [Fact] public void GenerateSqlLiteral_returns_LocalDateTime_literal() { @@ -64,6 +61,49 @@ public void GenerateSqlLiteral_returns_LocalDateTime_infinity_literal() Assert.Equal("TIMESTAMP 'infinity'", mapping.GenerateSqlLiteral(LocalDate.MaxIsoValue + LocalTime.MaxValue)); } + [Fact] + public void NpgsqlRange_of_LocalDateTime_is_properly_mapped() + { + Assert.Equal("tsrange", GetMapping(typeof(NpgsqlRange)).StoreType); + Assert.Same(typeof(NpgsqlRange), GetMapping("tsrange").ClrType); + } + + [Fact] + public void GenerateSqlLiteral_returns_tsrange_literal() + { + var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); + Assert.Equal("tsrange", mapping.StoreType); + Assert.Equal("timestamp without time zone", mapping.SubtypeMapping.StoreType); + + var value = new NpgsqlRange(new(2020, 1, 1, 12, 0, 0), new(2020, 1, 2, 12, 0, 0)); + Assert.Equal(@"'[""2020-01-01T12:00:00"",""2020-01-02T12:00:00""]'::tsrange", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void Array_of_NpgsqlRange_of_LocalDateTime_is_properly_mapped() + { + Assert.Equal("tsmultirange", GetMapping(typeof(NpgsqlRange[])).StoreType); + Assert.Same(typeof(NpgsqlRange[]), GetMapping("tsmultirange").ClrType); + } + + [Fact] + public void List_of_NpgsqlRange_of_LocalDateTime_is_properly_mapped() + { + Assert.Equal("tsmultirange", GetMapping(typeof(List>)).StoreType); + } + + #endregion Timestamp without time zone + + #region Timestamp with time zone + + [Fact] + public void Timestamptz_maps_to_Instant_by_default() + => Assert.Same(typeof(Instant), GetMapping("timestamp with time zone").ClrType); + + [Fact] + public void LocalDateTime_does_not_map_to_timestamptz() + => Assert.Null(GetMapping(typeof(LocalDateTime), "timestamp with time zone")); + [Fact] public void GenerateSqlLiteral_returns_timestamptz_Instant_literal() { @@ -136,6 +176,170 @@ public void GenerateCodeLiteral_returns_OffsetDate_time_literal() CodeLiteral(new OffsetDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33), Offset.FromSeconds(-1)))); } + [Fact] + public void Interval_is_properly_mapped() + { + Assert.Equal("tstzrange", GetMapping(typeof(Interval)).StoreType); + Assert.Same(typeof(Interval), GetMapping("tstzrange").ClrType); + } + + [Fact] + public void GenerateSqlLiteral_returns_tstzrange_Interval_literal() + { + var mapping = (IntervalRangeMapping)GetMapping("tstzrange"); + + var value = new Interval( + new LocalDateTime(2020, 1, 1, 12, 0, 0).InUtc().ToInstant(), + new LocalDateTime(2020, 1, 2, 12, 0, 0).InUtc().ToInstant()); + Assert.Equal(@"'[2020-01-01T12:00:00Z,2020-01-02T12:00:00Z)'::tstzrange", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void GenerateCodeLiteral_returns_tstzrange_Interval_literal() + { + Assert.Equal( + "new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(15778800000000000L), NodaTime.Instant.FromUnixTimeTicks(15782256000000000L))", + CodeLiteral(new Interval( + new LocalDateTime(2020, 01, 01, 12, 0, 0).InUtc().ToInstant(), + new LocalDateTime(2020, 01, 05, 12, 0, 0).InUtc().ToInstant()))); + + Assert.Equal( + "new NodaTime.Interval((NodaTime.Instant?)NodaTime.Instant.FromUnixTimeTicks(15778800000000000L), null)", + CodeLiteral(new Interval(new LocalDateTime(2020, 01, 01, 12, 0, 0).InUtc().ToInstant(), null))); + } + + [Fact] + public void Interval_array_is_properly_mapped() + { + Assert.Equal("tstzmultirange", GetMapping(typeof(Interval[])).StoreType); + Assert.Same(typeof(Interval[]), GetMapping("tstzmultirange").ClrType); + } + + [Fact] + public void Interval_list_is_properly_mapped() + => Assert.Equal("tstzmultirange", GetMapping(typeof(List)).StoreType); + + [Fact] + public void GenerateSqlLiteral_returns_Interval_array_literal() + { + var mapping = GetMapping(typeof(Interval[])); + + var interval = new Interval[] + { + new( + new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), + new( + new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), + }; + + Assert.Equal("'{[1998-04-12T13:26:38Z,1998-04-12T15:26:38Z), [1998-04-13T13:26:38Z,1998-04-13T15:26:38Z)}'::tstzmultirange", mapping.GenerateSqlLiteral(interval)); + } + + [Fact] + public void GenerateSqlLiteral_returns_Interval_list_literal() + { + var mapping = GetMapping(typeof(List)); + + var interval = new List + { + new( + new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), + new( + new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), + }; + + Assert.Equal("'{[1998-04-12T13:26:38Z,1998-04-12T15:26:38Z), [1998-04-13T13:26:38Z,1998-04-13T15:26:38Z)}'::tstzmultirange", mapping.GenerateSqlLiteral(interval)); + } + + [Fact] + public void GenerateCodeLiteral_returns_Interval_array_literal() + => Assert.Equal( + "new[] { new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(8923875980000000L), NodaTime.Instant.FromUnixTimeTicks(8923947980000000L)), new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(8924739980000000L), NodaTime.Instant.FromUnixTimeTicks(8924811980000000L)) }", + CodeLiteral(new Interval[] + { + new( + new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), + new( + new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), + })); + + [Fact] + public void GenerateCodeLiteral_returns_Interval_list_literal() + => Assert.Throws( + () => CodeLiteral(new List + { + new( + new LocalDateTime(1998, 4, 12, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 12, 15, 26, 38).InUtc().ToInstant()), + new( + new LocalDateTime(1998, 4, 13, 13, 26, 38).InUtc().ToInstant(), + new LocalDateTime(1998, 4, 13, 15, 26, 38).InUtc().ToInstant()), + })); + + [Fact] + public void GenerateSqlLiteral_returns_tstzrange_Instant_literal() + { + var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); + Assert.Equal("tstzrange", mapping.StoreType); + Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); + + var value = new NpgsqlRange( + new LocalDateTime(2020, 1, 1, 12, 0, 0).InUtc().ToInstant(), + new LocalDateTime(2020, 1, 2, 12, 0, 0).InUtc().ToInstant()); + Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void GenerateSqlLiteral_returns_tstzrange_ZonedDateTime_literal() + { + var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); + Assert.Equal("tstzrange", mapping.StoreType); + Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); + + var value = new NpgsqlRange( + new LocalDateTime(2020, 1, 1, 12, 0, 0).InUtc(), + new LocalDateTime(2020, 1, 2, 12, 0, 0).InUtc()); + Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void GenerateSqlLiteral_returns_tstzrange_OffsetDateTime_literal() + { + var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); + Assert.Equal("tstzrange", mapping.StoreType); + Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); + + var value = new NpgsqlRange( + new LocalDateTime(2020, 1, 1, 12, 0, 0).WithOffset(Offset.Zero), + new LocalDateTime(2020, 1, 2, 12, 0, 0).WithOffset(Offset.Zero)); + Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void Array_of_NpgsqlRange_of_Instant_is_properly_mapped() + => Assert.Equal("tstzmultirange", GetMapping(typeof(NpgsqlRange[])).StoreType); + + [Fact] + public void List_of_NpgsqlRange_of_Instant_is_properly_mapped() + => Assert.Equal("tstzmultirange", GetMapping(typeof(List>)).StoreType); + + #endregion Timestamp with time zone + + #region date/daterange/datemultirange + + [Fact] + public void LocalDate_is_properly_mapped() + { + Assert.Equal("date", GetMapping(typeof(LocalDate)).StoreType); + Assert.Same(typeof(LocalDate), GetMapping("date").ClrType); + } + [Fact] public void GenerateSqlLiteral_returns_LocalDate_literal() { @@ -160,6 +364,113 @@ public void GenerateCodeLiteral_returns_LocalDate_literal() Assert.Equal("new NodaTime.LocalDate(-2017, 4, 20)", CodeLiteral(new LocalDate(Era.BeforeCommon, 2018, 4, 20))); } + [Fact] + public void DateInterval_is_properly_mapped() + { + Assert.Equal("daterange", GetMapping(typeof(DateInterval)).StoreType); + Assert.Same(typeof(DateInterval), GetMapping("daterange").ClrType); + } + + [Fact] + public void GenerateSqlLiteral_returns_DateInterval_literal() + { + var mapping = GetMapping(typeof(DateInterval)); + Assert.Equal("daterange", mapping.StoreType); + + var interval = new DateInterval(new(2020, 01, 01), new(2020, 12, 25)); + Assert.Equal("'[2020-01-01,2020-12-25]'::daterange", mapping.GenerateSqlLiteral(interval)); + } + + [Fact] + public void GenerateCodeLiteral_returns_DateInterval_literal() + { + Assert.Equal( + "new NodaTime.DateInterval(new NodaTime.LocalDate(2020, 1, 1), new NodaTime.LocalDate(2020, 12, 25))", + CodeLiteral(new DateInterval(new(2020, 01, 01), new(2020, 12, 25)))); + } + + [Fact] + public void DateInterval_array_is_properly_mapped() + { + Assert.Equal("datemultirange", GetMapping(typeof(DateInterval[])).StoreType); + Assert.Same(typeof(DateInterval[]), GetMapping("datemultirange").ClrType); + } + + [Fact] + public void DateInterval_list_is_properly_mapped() + => Assert.Equal("datemultirange", GetMapping(typeof(List)).StoreType); + + [Fact] + public void GenerateSqlLiteral_returns_DateInterval_array_literal() + { + var mapping = GetMapping(typeof(DateInterval[])); + + var interval = new DateInterval[] + { + new(new(2002, 3, 4), new(2002, 3, 5)), + new(new(2002, 3, 8), new(2002, 3, 10)) + }; + + Assert.Equal("'{[2002-03-04,2002-03-05], [2002-03-08,2002-03-10]}'::datemultirange", mapping.GenerateSqlLiteral(interval)); + } + + [Fact] + public void GenerateSqlLiteral_returns_DateInterval_list_literal() + { + var mapping = GetMapping(typeof(List)); + + var interval = new List + { + new(new(2002, 3, 4), new(2002, 3, 5)), + new(new(2002, 3, 8), new(2002, 3, 10)) + }; + + Assert.Equal("'{[2002-03-04,2002-03-05], [2002-03-08,2002-03-10]}'::datemultirange", mapping.GenerateSqlLiteral(interval)); + } + + [Fact] + public void GenerateCodeLiteral_returns_DateInterval_array_literal() + => Assert.Equal( + "new[] { new NodaTime.DateInterval(new NodaTime.LocalDate(2002, 3, 4), new NodaTime.LocalDate(2002, 3, 5)), new NodaTime.DateInterval(new NodaTime.LocalDate(2002, 3, 8), new NodaTime.LocalDate(2002, 3, 10)) }", + CodeLiteral(new DateInterval[] + { + new(new(2002, 3, 4), new(2002, 3, 5)), + new(new(2002, 3, 8), new(2002, 3, 10)) + })); + + [Fact] + public void GenerateCodeLiteral_returns_DateInterval_list_literal() + => Assert.Throws( + () => CodeLiteral(new List + { + new(new(2002, 3, 4), new(2002, 3, 5)), + new(new(2002, 3, 8), new(2002, 3, 10)) + })); + + [Fact] + public void NpgsqlRange_of_LocalDate_is_properly_mapped() + => Assert.Equal("daterange", GetMapping(typeof(NpgsqlRange)).StoreType); + + [Fact] + public void GenerateSqlLiteral_returns_daterange_LocalDate_literal() + { + var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); + var value = new NpgsqlRange(new(2020, 1, 1), new(2020, 1, 2)); + Assert.Equal(@"'[2020-01-01,2020-01-02]'::daterange", mapping.GenerateSqlLiteral(value)); + } + + [Fact] + public void Array_of_NpgsqlRange_of_LocalDate_is_properly_mapped() + => Assert.Equal("datemultirange", GetMapping(typeof(NpgsqlRange[])).StoreType); + + [Fact] + public void List_of_NpgsqlRange_of_LocalDate_is_properly_mapped() + => Assert.Equal("datemultirange", GetMapping(typeof(List>)).StoreType); + + #endregion date/daterange/datemultirange + + #region time + [Fact] public void GenerateSqlLiteral_returns_LocalTime_literal() { @@ -179,6 +490,10 @@ public void GenerateCodeLiteral_returns_LocalTime_literal() Assert.Equal("NodaTime.LocalTime.FromHourMinuteSecondNanosecond(9, 30, 15, 1L)", CodeLiteral(LocalTime.FromHourMinuteSecondNanosecond(9, 30, 15, 1))); } + #endregion time + + #region timetz + [Fact] public void GenerateSqlLiteral_returns_OffsetTime_literal() { @@ -197,6 +512,10 @@ public void GenerateCodeLiteral_returns_OffsetTime_literal() => Assert.Equal("new NodaTime.OffsetTime(new NodaTime.LocalTime(10, 31, 33), NodaTime.Offset.FromHours(2))", CodeLiteral(new OffsetTime(new LocalTime(10, 31, 33), Offset.FromHours(2)))); + #endregion timetz + + #region interval + [Fact] public void GenerateSqlLiteral_returns_Period_literal() { @@ -242,109 +561,7 @@ public void GenerateCodeLiteral_returns_Duration_literal() Assert.Equal("NodaTime.Duration.Zero", CodeLiteral(Duration.Zero)); } - [Fact] - public void GenerateCodeLiteral_returns_DateInterval_literal() - { - Assert.Equal( - "new NodaTime.DateInterval(new NodaTime.LocalDate(2020, 1, 1), new NodaTime.LocalDate(2020, 12, 25))", - CodeLiteral(new DateInterval(new(2020, 01, 01), new(2020, 12, 25)))); - } - - [Fact] - public void GenerateSqlLiteral_returns_DateInterval_literal() - { - var mapping = GetMapping(typeof(DateInterval)); - - var interval = new DateInterval(new(2020, 01, 01), new(2020, 12, 25)); - Assert.Equal("'[2020-01-01,2020-12-25]'::daterange", mapping.GenerateSqlLiteral(interval)); - } - - [Fact] - public void GenerateSqlLiteral_returns_tsrange_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); - Assert.Equal("tsrange", mapping.StoreType); - Assert.Equal("timestamp without time zone", mapping.SubtypeMapping.StoreType); - - var value = new NpgsqlRange(new(2020, 1, 1, 12, 0, 0), new(2020, 1, 2, 12, 0, 0)); - Assert.Equal(@"'[""2020-01-01T12:00:00"",""2020-01-02T12:00:00""]'::tsrange", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void GenerateSqlLiteral_returns_tstzrange_Interval_literal() - { - var mapping = (IntervalMapping)GetMapping("tstzrange"); // default mapping - Assert.Same(typeof(Interval), mapping.ClrType); - - var value = new Interval( - new LocalDateTime(2020, 1, 1, 12, 0, 0).InUtc().ToInstant(), - new LocalDateTime(2020, 1, 2, 12, 0, 0).InUtc().ToInstant()); - Assert.Equal(@"'[2020-01-01T12:00:00Z,2020-01-02T12:00:00Z)'::tstzrange", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void GenerateCodeLiteral_returns_tstzrange_Interval_literal() - { - Assert.Equal( - "new NodaTime.Interval(NodaTime.Instant.FromUnixTimeTicks(15778800000000000L), NodaTime.Instant.FromUnixTimeTicks(15782256000000000L))", - CodeLiteral(new Interval( - new LocalDateTime(2020, 01, 01, 12, 0, 0).InUtc().ToInstant(), - new LocalDateTime(2020, 01, 05, 12, 0, 0).InUtc().ToInstant()))); - - Assert.Equal( - "new NodaTime.Interval((NodaTime.Instant?)NodaTime.Instant.FromUnixTimeTicks(15778800000000000L), null)", - CodeLiteral(new Interval(new LocalDateTime(2020, 01, 01, 12, 0, 0).InUtc().ToInstant(), null))); - } - - [Fact] - public void GenerateSqlLiteral_returns_tstzrange_Instant_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); - Assert.Equal("tstzrange", mapping.StoreType); - Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); - - var value = new NpgsqlRange( - new LocalDateTime(2020, 1, 1, 12, 0, 0).InUtc().ToInstant(), - new LocalDateTime(2020, 1, 2, 12, 0, 0).InUtc().ToInstant()); - Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void GenerateSqlLiteral_returns_tstzrange_ZonedDateTime_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); - Assert.Equal("tstzrange", mapping.StoreType); - Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); - - var value = new NpgsqlRange( - new LocalDateTime(2020, 1, 1, 12, 0, 0).InUtc(), - new LocalDateTime(2020, 1, 2, 12, 0, 0).InUtc()); - Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void GenerateSqlLiteral_returns_tstzrange_OffsetDateTime_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); - Assert.Equal("tstzrange", mapping.StoreType); - Assert.Equal("timestamp with time zone", mapping.SubtypeMapping.StoreType); - - var value = new NpgsqlRange( - new LocalDateTime(2020, 1, 1, 12, 0, 0).WithOffset(Offset.Zero), - new LocalDateTime(2020, 1, 2, 12, 0, 0).WithOffset(Offset.Zero)); - Assert.Equal(@"'[""2020-01-01T12:00:00Z"",""2020-01-02T12:00:00Z""]'::tstzrange", mapping.GenerateSqlLiteral(value)); - } - - [Fact] - public void GenerateSqlLiteral_returns_daterange_LocalDate_literal() - { - var mapping = (NpgsqlRangeTypeMapping)GetMapping(typeof(NpgsqlRange)); - Assert.Equal("daterange", mapping.StoreType); - Assert.Equal("date", mapping.SubtypeMapping.StoreType); - - var value = new NpgsqlRange(new(2020, 1, 1), new(2020, 1, 2)); - Assert.Equal(@"'[2020-01-01,2020-01-02]'::daterange", mapping.GenerateSqlLiteral(value)); - } + #endregion interval #region Support @@ -372,4 +589,4 @@ private static RelationalTypeMapping GetMapping(Type clrType, string storeType) private static string CodeLiteral(object value) => CsHelper.UnknownLiteral(value); #endregion Support -} \ No newline at end of file +}