Skip to content

Commit 3837c36

Browse files
authored
Map datemultirange to NodaTime DateInterval[] (#4002)
Closes #3989
1 parent e280ae0 commit 3837c36

18 files changed

Lines changed: 412 additions & 66 deletions

Npgsql.sln.DotSettings

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
<s:Boolean x:Key="/Default/UserDictionary/Words/=bytea/@EntryIndexedValue">True</s:Boolean>
8585
<s:Boolean x:Key="/Default/UserDictionary/Words/=citext/@EntryIndexedValue">True</s:Boolean>
8686
<s:Boolean x:Key="/Default/UserDictionary/Words/=Conformant/@EntryIndexedValue">True</s:Boolean>
87+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Datemultirange/@EntryIndexedValue">True</s:Boolean>
8788
<s:Boolean x:Key="/Default/UserDictionary/Words/=daterange/@EntryIndexedValue">True</s:Boolean>
8889
<s:Boolean x:Key="/Default/UserDictionary/Words/=DDEX/@EntryIndexedValue">True</s:Boolean>
8990
<s:Boolean x:Key="/Default/UserDictionary/Words/=IANA/@EntryIndexedValue">True</s:Boolean>
@@ -93,13 +94,15 @@
9394
<s:Boolean x:Key="/Default/UserDictionary/Words/=ltxtquery/@EntryIndexedValue">True</s:Boolean>
9495
<s:Boolean x:Key="/Default/UserDictionary/Words/=macaddr/@EntryIndexedValue">True</s:Boolean>
9596
<s:Boolean x:Key="/Default/UserDictionary/Words/=MSDTC/@EntryIndexedValue">True</s:Boolean>
97+
<s:Boolean x:Key="/Default/UserDictionary/Words/=multidaterange/@EntryIndexedValue">True</s:Boolean>
9698
<s:Boolean x:Key="/Default/UserDictionary/Words/=multiquery/@EntryIndexedValue">True</s:Boolean>
9799
<s:Boolean x:Key="/Default/UserDictionary/Words/=multirange/@EntryIndexedValue">True</s:Boolean>
98100
<s:Boolean x:Key="/Default/UserDictionary/Words/=multiranges/@EntryIndexedValue">True</s:Boolean>
99101
<s:Boolean x:Key="/Default/UserDictionary/Words/=Noda/@EntryIndexedValue">True</s:Boolean>
100102
<s:Boolean x:Key="/Default/UserDictionary/Words/=NOEXPORT/@EntryIndexedValue">True</s:Boolean>
101103
<s:Boolean x:Key="/Default/UserDictionary/Words/=Npgsql/@EntryIndexedValue">True</s:Boolean>
102104
<s:Boolean x:Key="/Default/UserDictionary/Words/=Npgsql_0027s/@EntryIndexedValue">True</s:Boolean>
105+
<s:Boolean x:Key="/Default/UserDictionary/Words/=nummultirange/@EntryIndexedValue">True</s:Boolean>
103106
<s:Boolean x:Key="/Default/UserDictionary/Words/=numrange/@EntryIndexedValue">True</s:Boolean>
104107
<s:Boolean x:Key="/Default/UserDictionary/Words/=oidvector/@EntryIndexedValue">True</s:Boolean>
105108
<s:Boolean x:Key="/Default/UserDictionary/Words/=pgoutput/@EntryIndexedValue">True</s:Boolean>
@@ -117,8 +120,10 @@
117120
<s:Boolean x:Key="/Default/UserDictionary/Words/=subtransaction/@EntryIndexedValue">True</s:Boolean>
118121
<s:Boolean x:Key="/Default/UserDictionary/Words/=timestamptz/@EntryIndexedValue">True</s:Boolean>
119122
<s:Boolean x:Key="/Default/UserDictionary/Words/=timetz/@EntryIndexedValue">True</s:Boolean>
123+
<s:Boolean x:Key="/Default/UserDictionary/Words/=tsmultirange/@EntryIndexedValue">True</s:Boolean>
120124
<s:Boolean x:Key="/Default/UserDictionary/Words/=tsquery/@EntryIndexedValue">True</s:Boolean>
121125
<s:Boolean x:Key="/Default/UserDictionary/Words/=tsrange/@EntryIndexedValue">True</s:Boolean>
126+
<s:Boolean x:Key="/Default/UserDictionary/Words/=tstzmultirange/@EntryIndexedValue">True</s:Boolean>
122127
<s:Boolean x:Key="/Default/UserDictionary/Words/=tstzrange/@EntryIndexedValue">True</s:Boolean>
123128
<s:Boolean x:Key="/Default/UserDictionary/Words/=tsvector/@EntryIndexedValue">True</s:Boolean>
124129
<s:Boolean x:Key="/Default/UserDictionary/Words/=typname/@EntryIndexedValue">True</s:Boolean>

src/Npgsql.Json.NET/Internal/JsonHandler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Newtonsoft.Json;
66
using Npgsql.BackendMessages;
77
using Npgsql.Internal;
8+
using Npgsql.Internal.TypeHandling;
89
using Npgsql.PostgresTypes;
910

1011
namespace Npgsql.Json.NET.Internal

src/Npgsql.Json.NET/Internal/JsonbHandler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Newtonsoft.Json;
66
using Npgsql.BackendMessages;
77
using Npgsql.Internal;
8+
using Npgsql.Internal.TypeHandling;
89
using Npgsql.PostgresTypes;
910

1011
namespace Npgsql.Json.NET.Internal
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+

2+
using System;
3+
4+
using System.Collections.Generic;
5+
6+
using System.Threading;
7+
8+
using System.Threading.Tasks;
9+
10+
using Npgsql.Internal;
11+
12+
using NodaTime;
13+
14+
using Npgsql.BackendMessages;
15+
16+
using Npgsql.Internal.TypeHandlers;
17+
18+
using Npgsql.Internal.TypeHandling;
19+
20+
using Npgsql.PostgresTypes;
21+
22+
using NpgsqlTypes;
23+
24+
25+
#nullable enable
26+
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
27+
#pragma warning disable RS0016 // Add public types and members to the declared API
28+
#pragma warning disable 618 // Member is obsolete
29+
30+
namespace Npgsql.NodaTime.Internal
31+
{
32+
partial class DateMultirangeHandler
33+
{
34+
public override int ValidateObjectAndGetLength(object value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
35+
=> value switch
36+
{
37+
38+
DateInterval[] converted => ((INpgsqlTypeHandler<DateInterval[]>)this).ValidateAndGetLength(converted, ref lengthCache, parameter),
39+
40+
List<DateInterval> converted => ((INpgsqlTypeHandler<List<DateInterval>>)this).ValidateAndGetLength(converted, ref lengthCache, parameter),
41+
42+
NpgsqlRange<LocalDate>[] converted => ((INpgsqlTypeHandler<NpgsqlRange<LocalDate>[]>)this).ValidateAndGetLength(converted, ref lengthCache, parameter),
43+
44+
List<NpgsqlRange<LocalDate>> converted => ((INpgsqlTypeHandler<List<NpgsqlRange<LocalDate>>>)this).ValidateAndGetLength(converted, ref lengthCache, parameter),
45+
46+
47+
_ => throw new InvalidCastException($"Can't write CLR type {value.GetType()} with handler type DateMultirangeHandler")
48+
};
49+
50+
public override Task WriteObjectWithLength(object? value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken = default)
51+
=> value switch
52+
{
53+
54+
DateInterval[] converted => WriteWithLength(converted, buf, lengthCache, parameter, async, cancellationToken),
55+
56+
List<DateInterval> converted => WriteWithLength(converted, buf, lengthCache, parameter, async, cancellationToken),
57+
58+
NpgsqlRange<LocalDate>[] converted => WriteWithLength(converted, buf, lengthCache, parameter, async, cancellationToken),
59+
60+
List<NpgsqlRange<LocalDate>> converted => WriteWithLength(converted, buf, lengthCache, parameter, async, cancellationToken),
61+
62+
63+
DBNull => WriteWithLength(DBNull.Value, buf, lengthCache, parameter, async, cancellationToken),
64+
null => WriteWithLength(DBNull.Value, buf, lengthCache, parameter, async, cancellationToken),
65+
_ => throw new InvalidCastException($"Can't write CLR type {value.GetType()} with handler type DateMultirangeHandler")
66+
};
67+
}
68+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using NodaTime;
6+
using Npgsql.BackendMessages;
7+
using Npgsql.Internal;
8+
using Npgsql.Internal.TypeHandlers;
9+
using Npgsql.Internal.TypeHandling;
10+
using Npgsql.PostgresTypes;
11+
using NpgsqlTypes;
12+
13+
namespace Npgsql.NodaTime.Internal
14+
{
15+
public partial class DateMultirangeHandler : MultirangeHandler<LocalDate>,
16+
INpgsqlTypeHandler<DateInterval[]>, INpgsqlTypeHandler<List<DateInterval>>
17+
{
18+
public DateMultirangeHandler(PostgresMultirangeType multirangePostgresType, NpgsqlTypeHandler subtypeHandler)
19+
: base(multirangePostgresType, new DateRangeHandler(multirangePostgresType.Subrange, subtypeHandler))
20+
{
21+
}
22+
23+
public override Type GetFieldType(FieldDescription? fieldDescription = null) => typeof(DateInterval[]);
24+
public override Type GetProviderSpecificFieldType(FieldDescription? fieldDescription = null) => typeof(DateInterval[]);
25+
26+
public override async ValueTask<object> ReadAsObject(NpgsqlReadBuffer buf, int len, bool async,
27+
FieldDescription? fieldDescription = null)
28+
=> (await Read<DateInterval[]>(buf, len, async, fieldDescription))!;
29+
30+
async ValueTask<DateInterval[]> INpgsqlTypeHandler<DateInterval[]>.Read(
31+
NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
32+
{
33+
await buf.Ensure(4, async);
34+
var numRanges = buf.ReadInt32();
35+
var multirange = new DateInterval[numRanges];
36+
37+
for (var i = 0; i < multirange.Length; i++)
38+
{
39+
await buf.Ensure(4, async);
40+
var rangeLen = buf.ReadInt32();
41+
var range = await RangeHandler.Read(buf, rangeLen, async, fieldDescription);
42+
multirange[i] = new(range.LowerBound, range.UpperBound - Period.FromDays(1));
43+
}
44+
45+
return multirange;
46+
}
47+
48+
async ValueTask<List<DateInterval>> INpgsqlTypeHandler<List<DateInterval>>.Read(
49+
NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
50+
{
51+
await buf.Ensure(4, async);
52+
var numRanges = buf.ReadInt32();
53+
var multirange = new List<DateInterval>(numRanges);
54+
55+
for (var i = 0; i < numRanges; i++)
56+
{
57+
await buf.Ensure(4, async);
58+
var rangeLen = buf.ReadInt32();
59+
var range = await RangeHandler.Read(buf, rangeLen, async, fieldDescription);
60+
multirange.Add(new(range.LowerBound, range.UpperBound - Period.FromDays(1)));
61+
}
62+
63+
return multirange;
64+
}
65+
66+
public int ValidateAndGetLength(DateInterval[] value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
67+
=> ValidateAndGetLengthCore(value, ref lengthCache);
68+
69+
public int ValidateAndGetLength(List<DateInterval> value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
70+
=> ValidateAndGetLengthCore(value, ref lengthCache);
71+
72+
int ValidateAndGetLengthCore(IList<DateInterval> value, ref NpgsqlLengthCache? lengthCache)
73+
{
74+
lengthCache ??= new NpgsqlLengthCache(1);
75+
if (lengthCache.IsPopulated)
76+
return lengthCache.Get();
77+
78+
var sum = 4 + 4 * value.Count;
79+
for (var i = 0; i < value.Count; i++)
80+
{
81+
var interval = value[i];
82+
sum += RangeHandler.ValidateAndGetLength(
83+
new NpgsqlRange<LocalDate>(interval.Start, interval.End),
84+
ref lengthCache,
85+
parameter: null);
86+
}
87+
88+
return lengthCache.Set(sum);
89+
}
90+
91+
public async Task Write(
92+
DateInterval[] value,
93+
NpgsqlWriteBuffer buf,
94+
NpgsqlLengthCache? lengthCache,
95+
NpgsqlParameter? parameter,
96+
bool async,
97+
CancellationToken cancellationToken = default)
98+
{
99+
if (buf.WriteSpaceLeft < 4)
100+
await buf.Flush(async, cancellationToken);
101+
102+
buf.WriteInt32(value.Length);
103+
104+
for (var i = 0; i < value.Length; i++)
105+
{
106+
var interval = value[i];
107+
await RangeHandler.WriteWithLength(
108+
new NpgsqlRange<LocalDate>(interval.Start, interval.End), buf, lengthCache, parameter: null, async, cancellationToken);
109+
}
110+
}
111+
112+
public async Task Write(
113+
List<DateInterval> value,
114+
NpgsqlWriteBuffer buf,
115+
NpgsqlLengthCache? lengthCache,
116+
NpgsqlParameter? parameter,
117+
bool async,
118+
CancellationToken cancellationToken = default)
119+
{
120+
if (buf.WriteSpaceLeft < 4)
121+
await buf.Flush(async, cancellationToken);
122+
123+
buf.WriteInt32(value.Count);
124+
125+
for (var i = 0; i < value.Count; i++)
126+
{
127+
var interval = value[i];
128+
await RangeHandler.WriteWithLength(
129+
new NpgsqlRange<LocalDate>(interval.Start, interval.End), buf, lengthCache, parameter: null, async, cancellationToken);
130+
}
131+
}
132+
}
133+
}

src/Npgsql.NodaTime/Internal/DateRangeHandler.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,19 @@ public DateRangeHandler(PostgresType rangePostgresType, NpgsqlTypeHandler subtyp
1818
{
1919
}
2020

21-
async ValueTask<DateInterval> INpgsqlTypeHandler<DateInterval>.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
22-
{
23-
var range = await Read(buf, len, async, fieldDescription);
24-
return new(range.LowerBound, range.UpperBound - Period.FromDays(1));
25-
}
26-
2721
public override Type GetFieldType(FieldDescription? fieldDescription = null) => typeof(DateInterval);
2822
public override Type GetProviderSpecificFieldType(FieldDescription? fieldDescription = null) => typeof(DateInterval);
2923

3024
public override async ValueTask<object> ReadAsObject(NpgsqlReadBuffer buf, int len, bool async,
3125
FieldDescription? fieldDescription = null)
3226
=> (await Read<DateInterval>(buf, len, async, fieldDescription))!;
3327

28+
async ValueTask<DateInterval> INpgsqlTypeHandler<DateInterval>.Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
29+
{
30+
var range = await Read(buf, len, async, fieldDescription);
31+
return new(range.LowerBound, range.UpperBound - Period.FromDays(1));
32+
}
33+
3434
public int ValidateAndGetLength(DateInterval value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
3535
=> ValidateAndGetLength(new NpgsqlRange<LocalDate>(value.Start, value.End), ref lengthCache, parameter);
3636

src/Npgsql.NodaTime/Internal/NodaTimeTypeHandlerResolver.cs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Data;
34
using NodaTime;
45
using Npgsql.Internal;
@@ -21,6 +22,7 @@ public class NodaTimeTypeHandlerResolver : TypeHandlerResolver
2122
readonly TimeTzHandler _timeTzHandler;
2223
readonly IntervalHandler _intervalHandler;
2324
readonly DateRangeHandler _dateRangeHandler;
25+
DateMultirangeHandler? _dateMultirangeHandler;
2426

2527
internal NodaTimeTypeHandlerResolver(NpgsqlConnector connector)
2628
{
@@ -49,6 +51,8 @@ internal NodaTimeTypeHandlerResolver(NpgsqlConnector connector)
4951
"time with time zone" => _timeTzHandler,
5052
"interval" => _intervalHandler,
5153
"daterange" => _dateRangeHandler,
54+
"datemultirange"
55+
=> _dateMultirangeHandler ??= new DateMultirangeHandler((PostgresMultirangeType)PgType("datemultirange"), _dateHandler),
5256

5357
_ => null
5458
};
@@ -82,10 +86,11 @@ internal NodaTimeTypeHandlerResolver(NpgsqlConnector connector)
8286
return _intervalHandler;
8387
if (typeof(T) == typeof(Duration))
8488
return _intervalHandler;
89+
90+
// Note that DateInterval is a reference type, so not included in this method
8591
if (typeof(T) == typeof(NpgsqlRange<LocalDate>))
8692
return _dateRangeHandler;
8793

88-
8994
return null;
9095
}
9196

@@ -108,6 +113,13 @@ internal NodaTimeTypeHandlerResolver(NpgsqlConnector connector)
108113
return "interval";
109114
if (type == typeof(DateInterval) || type == typeof(NpgsqlRange<LocalDate>))
110115
return "daterange";
116+
if (type == typeof(DateInterval[]) ||
117+
type == typeof(List<DateInterval>) ||
118+
type == typeof(NpgsqlRange<LocalDate>[]) ||
119+
type == typeof(List<NpgsqlRange<LocalDate>>))
120+
{
121+
return "datemultirange";
122+
}
111123

112124
return null;
113125
}
@@ -118,17 +130,19 @@ internal NodaTimeTypeHandlerResolver(NpgsqlConnector connector)
118130
internal static TypeMappingInfo? DoGetMappingByDataTypeName(string dataTypeName)
119131
=> dataTypeName switch
120132
{
121-
"timestamp" or "timestamp without time zone" => new(NpgsqlDbType.Timestamp, DbType.DateTime, "timestamp without time zone"),
122-
"timestamptz" or "timestamp with time zone" => new(NpgsqlDbType.TimestampTz, DbType.DateTime, "timestamp with time zone"),
123-
"date" => new(NpgsqlDbType.Date, DbType.Date, "date"),
124-
"time without time zone" => new(NpgsqlDbType.Time, DbType.Time, "time without time zone"),
125-
"time with time zone" => new(NpgsqlDbType.TimeTz, DbType.Object, "time with time zone"),
126-
"interval" => new(NpgsqlDbType.Interval, DbType.Object, "interval"),
127-
"daterange" => new(NpgsqlDbType.DateRange, DbType.Object, "daterange"),
133+
"timestamp" or "timestamp without time zone" => new(NpgsqlDbType.Timestamp, DbType.DateTime, "timestamp without time zone"),
134+
"timestamptz" or "timestamp with time zone" => new(NpgsqlDbType.TimestampTz, DbType.DateTime, "timestamp with time zone"),
135+
"date" => new(NpgsqlDbType.Date, DbType.Date, "date"),
136+
"time without time zone" => new(NpgsqlDbType.Time, DbType.Time, "time without time zone"),
137+
"time with time zone" => new(NpgsqlDbType.TimeTz, DbType.Object, "time with time zone"),
138+
"interval" => new(NpgsqlDbType.Interval, DbType.Object, "interval"),
139+
"daterange" => new(NpgsqlDbType.DateRange, DbType.Object, "daterange"),
140+
"datemultirange" => new(NpgsqlDbType.DateMultirange, DbType.Object, "datemultirange"),
128141

129142
_ => null
130143
};
131144

145+
132146
PostgresType PgType(string pgTypeName) => _databaseInfo.GetPostgresTypeByName(pgTypeName);
133147
}
134148
}

src/Npgsql/Internal/TypeHandlers/CompositeHandlers/CompositeMemberHandler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Reflection;
33
using System.Threading;
44
using System.Threading.Tasks;
5+
using Npgsql.Internal.TypeHandling;
56
using Npgsql.PostgresTypes;
67

78
namespace Npgsql.Internal.TypeHandlers.CompositeHandlers

0 commit comments

Comments
 (0)