Skip to content

Commit ead124a

Browse files
authored
Revert instructions for date/time default values (#439)
1 parent bb6c816 commit ead124a

File tree

4 files changed

+151
-10
lines changed

4 files changed

+151
-10
lines changed

conceptual/Npgsql/release-notes/10.0.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ With .NET 6 being out of support since November 2024, Npgsql 10.0 also drops sup
7272

7373
The PostgreSQL `date` and `time` types are now read as .NET [`DateOnly`](https://learn.microsoft.com/dotnet/api/system.dateonly) and [`TimeOnly`](https://learn.microsoft.com/dotnet/api/system.timeonly), instead of [`DateTime`](https://learn.microsoft.com/dotnet/api/system.datetime) and [`TimeSpan`](https://learn.microsoft.com/dotnet/api/system.timespan) by default, respectively. This affects non-generic read methods which return `object`, such as <xref:Npgsql.NpgsqlCommand.ExecuteScalarAsync*> and <xref:Npgsql.NpgsqlDataReader.GetValue*?displayProperty=nameWithType>; you can still read `DateTime` and `TimeSpan` via the generic <xref:Npgsql.NpgsqlDataReader.GetFieldValue%2A>.
7474

75+
If your code relies on Npgsql returning `DateTime`/`TimeSpan` by default, and you are unable to either call `GetFieldValue()` or change your code to handle `DateOnly`/`TimeOnly` instead, then you can revert Npgsql's behavior to the pre-10.0 behavior. To do this, drop the [LegacyDateAndTimeResolverFactory.cs](/static/LegacyDateAndTimeResolverFactory.cs) into your project, and register it with your `NpgsqlDataSourceBuilder` as follows:
76+
77+
```c#
78+
NpgsqlDataSourceBuilder build = ...;
79+
builder.AddTypeInfoResolverFactory(new LegacyDateAndTimeResolverFactory());
80+
```
81+
82+
Alternatively, if you're not yet using `NpgsqlDataSource`, register it with the global type mapper:
83+
84+
```c#
85+
NpgsqlConnection.GlobalTypeMapper.AddTypeInfoResolverFactory(new LegacyDateAndTimeResolverFactory());
86+
```
87+
7588
### `cidr` is now mapped to `IPNetwork`
7689

7790
With .NET 6 no longer supported by Npgsql, the PostgreSQL `cidr` type is now mapped to `IPNetwork` by default instead of `NpgsqlCidr`. In addition, `NpgsqlCidr` is now obsolete and will be removed in the future.

conceptual/Npgsql/types/datetime.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,22 @@ Note: in versions prior to 6.0, the connection string parameter `Convert Infinit
4848

4949
## Detailed Behavior: Reading values from the database
5050

51-
PostgreSQL type | Default .NET type | Non-default .NET types
52-
--------------------------- | -------------------------- | ----------------------
53-
timestamp with time zone | DateTime (Utc<sup>1</sup>) | DateTimeOffset (Offset=0)<sup>2</sup>
54-
timestamp without time zone | DateTime (Unspecified) |
55-
date | DateTime | DateOnly (6.0+)
56-
time without time zone | TimeSpan | TimeOnly (6.0+)
57-
time with time zone | DateTimeOffset |
58-
interval | TimeSpan (<sup>3</sup>) | <xref:NpgsqlTypes.NpgsqlInterval>
51+
PostgreSQL type | Default .NET type | Non-default .NET types
52+
--------------------------- | ---------------------------- | ----------------------
53+
timestamp with time zone | DateTime (Utc<sup>1</sup>) | DateTimeOffset (Offset=0)<sup>2</sup>
54+
timestamp without time zone | DateTime (Unspecified) |
55+
date | DateOnly (10.0+)<sup>3</sup> | DateTime
56+
time without time zone | TimeOnly (10.0+)<sup>3</sup> | TimeSpan
57+
time with time zone | DateTimeOffset |
58+
interval | TimeSpan (<sup>4</sup>) | <xref:NpgsqlTypes.NpgsqlInterval>
5959

6060
<sup>1</sup> In versions prior to 6.0 (or when `Npgsql.EnableLegacyTimestampBehavior` is enabled), reading a `timestamp with time zone` returns a Local DateTime instead of Utc. [See the breaking change note for more info](../release-notes/6.0.md#major-changes-to-timestamp-mapping).
6161

6262
<sup>2</sup> In versions prior to 6.0 (or when `Npgsql.EnableLegacyTimestampBehavior` is enabled), reading a `timestamp with time zone` as a DateTimeOffset returns a local offset based on the timezone of the server where Npgsql is running.
6363

64-
<sup>3</sup> PostgreSQL intervals with month or year components cannot be read as TimeSpan. Consider using NodaTime's [Period](https://nodatime.org/3.0.x/api/NodaTime.Period.html) type, or <xref:NpgsqlTypes.NpgsqlInterval>.
64+
<sup>3</sup> In versions prior to 10.0, reading `date` and `time` returned .NET `DateTime` and `TimeSpan` by default (although reading `DateOnly` and `TimeOnly` was possible). To switch back to the old behavior, see the [10.0 breaking change note](../release-notes/10.0.md#date-and-time-are-now-mapped-to-dateonly-and-timeonly).
65+
66+
<sup>4</sup> PostgreSQL intervals with month or year components cannot be read as TimeSpan. Consider using NodaTime's [Period](https://nodatime.org/3.0.x/api/NodaTime.Period.html) type, or <xref:NpgsqlTypes.NpgsqlInterval>.
6567

6668
## Detailed Behavior: Sending values to the database
6769

docfx.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"resource":
7474
[
7575
{
76-
"files": [ "img/**", "styles/**", "CNAME" ]
76+
"files": [ "img/**", "styles/**", "static/**", "CNAME" ]
7777
},
7878
{
7979
"files": [ "**" ],
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using System;
2+
using Npgsql.Internal;
3+
using Npgsql.Internal.Postgres;
4+
using NpgsqlTypes;
5+
6+
sealed class LegacyDateAndTimeResolverFactory : PgTypeInfoResolverFactory
7+
{
8+
public override IPgTypeInfoResolver CreateResolver() => new Resolver();
9+
public override IPgTypeInfoResolver CreateArrayResolver() => new ArrayResolver();
10+
public override IPgTypeInfoResolver CreateRangeResolver() => new RangeResolver();
11+
public override IPgTypeInfoResolver CreateRangeArrayResolver() => new RangeArrayResolver();
12+
public override IPgTypeInfoResolver CreateMultirangeResolver() => new MultirangeResolver();
13+
public override IPgTypeInfoResolver CreateMultirangeArrayResolver() => new MultirangeArrayResolver();
14+
15+
const string Date = "pg_catalog.date";
16+
const string Time = "pg_catalog.time";
17+
const string DateRange = "pg_catalog.daterange";
18+
const string DateMultirange = "pg_catalog.datemultirange";
19+
20+
class Resolver : IPgTypeInfoResolver
21+
{
22+
TypeInfoMappingCollection? _mappings;
23+
protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new());
24+
25+
public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
26+
=> type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null;
27+
28+
static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)
29+
{
30+
mappings.AddStructType<DateTime>(Date,
31+
static (options, mapping, _) => options.GetTypeInfo(typeof(DateTime), new DataTypeName(mapping.DataTypeName))!,
32+
matchRequirement: MatchRequirement.DataTypeName);
33+
34+
mappings.AddStructType<TimeSpan>(Time,
35+
static (options, mapping, _) => options.GetTypeInfo(typeof(TimeSpan), new DataTypeName(mapping.DataTypeName))!,
36+
isDefault: true);
37+
38+
return mappings;
39+
}
40+
}
41+
42+
sealed class ArrayResolver : Resolver, IPgTypeInfoResolver
43+
{
44+
TypeInfoMappingCollection? _mappings;
45+
new TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(base.Mappings));
46+
47+
public new PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
48+
=> type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null;
49+
50+
static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)
51+
{
52+
mappings.AddStructArrayType<DateTime>(Date);
53+
mappings.AddStructArrayType<TimeSpan>(Time);
54+
55+
return mappings;
56+
}
57+
}
58+
59+
class RangeResolver : IPgTypeInfoResolver
60+
{
61+
TypeInfoMappingCollection? _mappings;
62+
protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new());
63+
64+
public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
65+
=> type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null;
66+
67+
static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)
68+
{
69+
mappings.AddStructType<NpgsqlRange<DateTime>>(DateRange,
70+
static (options, mapping, _) => options.GetTypeInfo(typeof(NpgsqlRange<DateTime>), new DataTypeName(mapping.DataTypeName))!,
71+
matchRequirement: MatchRequirement.DataTypeName);
72+
73+
return mappings;
74+
}
75+
}
76+
77+
sealed class RangeArrayResolver : RangeResolver, IPgTypeInfoResolver
78+
{
79+
TypeInfoMappingCollection? _mappings;
80+
new TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(base.Mappings));
81+
82+
public new PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
83+
=> type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null;
84+
85+
static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)
86+
{
87+
mappings.AddStructArrayType<NpgsqlRange<DateTime>>(DateRange);
88+
89+
return mappings;
90+
}
91+
}
92+
93+
class MultirangeResolver : IPgTypeInfoResolver
94+
{
95+
TypeInfoMappingCollection? _mappings;
96+
protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new());
97+
98+
public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
99+
=> type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null;
100+
101+
static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)
102+
{
103+
mappings.AddType<NpgsqlRange<DateTime>[]>(DateMultirange,
104+
static (options, mapping, _) => options.GetTypeInfo(typeof(NpgsqlRange<DateTime>[]), new DataTypeName(mapping.DataTypeName))!,
105+
matchRequirement: MatchRequirement.DataTypeName);
106+
107+
return mappings;
108+
}
109+
}
110+
111+
sealed class MultirangeArrayResolver : MultirangeResolver, IPgTypeInfoResolver
112+
{
113+
TypeInfoMappingCollection? _mappings;
114+
new TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(base.Mappings));
115+
116+
public new PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
117+
=> type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null;
118+
119+
static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)
120+
{
121+
mappings.AddArrayType<NpgsqlRange<DateTime>[]>(DateMultirange);
122+
123+
return mappings;
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)