@@ -29,9 +29,7 @@ public class NpgsqlDateTimeMethodTranslator : IMethodCallTranslator
29
29
{ typeof ( DateTimeOffset ) . GetRuntimeMethod ( nameof ( DateTimeOffset . AddSeconds ) , new [ ] { typeof ( double ) } ) ! , "secs" } ,
30
30
//{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), new[] { typeof(double) })!, "milliseconds" }
31
31
32
- { typeof ( DateOnly ) . GetRuntimeMethod ( nameof ( DateOnly . AddYears ) , new [ ] { typeof ( int ) } ) ! , "years" } ,
33
- { typeof ( DateOnly ) . GetRuntimeMethod ( nameof ( DateOnly . AddMonths ) , new [ ] { typeof ( int ) } ) ! , "months" } ,
34
- { typeof ( DateOnly ) . GetRuntimeMethod ( nameof ( DateOnly . AddDays ) , new [ ] { typeof ( int ) } ) ! , "days" } ,
32
+ // DateOnly.AddDays, AddMonths and AddYears have a specialized translation, see below
35
33
{ typeof ( TimeOnly ) . GetRuntimeMethod ( nameof ( TimeOnly . AddHours ) , new [ ] { typeof ( int ) } ) ! , "hours" } ,
36
34
{ typeof ( TimeOnly ) . GetRuntimeMethod ( nameof ( TimeOnly . AddMinutes ) , new [ ] { typeof ( int ) } ) ! , "mins" } ,
37
35
} ;
@@ -60,6 +58,15 @@ private static readonly MethodInfo DateOnly_Distance
60
58
= typeof ( NpgsqlDbFunctionsExtensions ) . GetRuntimeMethod (
61
59
nameof ( NpgsqlDbFunctionsExtensions . Distance ) , new [ ] { typeof ( DbFunctions ) , typeof ( DateOnly ) , typeof ( DateOnly ) } ) ! ;
62
60
61
+ private static readonly MethodInfo DateOnly_AddDays
62
+ = typeof ( DateOnly ) . GetRuntimeMethod ( nameof ( DateOnly . AddDays ) , new [ ] { typeof ( int ) } ) ! ;
63
+
64
+ private static readonly MethodInfo DateOnly_AddMonths
65
+ = typeof ( DateOnly ) . GetRuntimeMethod ( nameof ( DateOnly . AddMonths ) , new [ ] { typeof ( int ) } ) ! ;
66
+
67
+ private static readonly MethodInfo DateOnly_AddYears
68
+ = typeof ( DateOnly ) . GetRuntimeMethod ( nameof ( DateOnly . AddYears ) , new [ ] { typeof ( int ) } ) ! ;
69
+
63
70
private static readonly MethodInfo TimeOnly_FromDateTime
64
71
= typeof ( TimeOnly ) . GetRuntimeMethod ( nameof ( TimeOnly . FromDateTime ) , new [ ] { typeof ( DateTime ) } ) ! ;
65
72
@@ -118,60 +125,21 @@ public NpgsqlDateTimeMethodTranslator(
118
125
MethodInfo method ,
119
126
IReadOnlyList < SqlExpression > arguments ,
120
127
IDiagnosticsLogger < DbLoggerCategory . Query > logger )
121
- => TranslateDatePart ( instance , method , arguments )
122
- ?? TranslateDateTime ( instance , method , arguments )
123
- ?? TranslateDateOnly ( instance , method , arguments )
124
- ?? TranslateTimeOnly ( instance , method , arguments )
125
- ?? TranslateTimeZoneInfo ( method , arguments ) ;
128
+ => TranslateDateTime ( instance , method , arguments )
129
+ ?? TranslateDateOnly ( instance , method , arguments )
130
+ ?? TranslateTimeOnly ( instance , method , arguments )
131
+ ?? TranslateTimeZoneInfo ( method , arguments )
132
+ ?? TranslateDatePart ( instance , method , arguments ) ;
126
133
127
134
private SqlExpression ? TranslateDatePart (
128
135
SqlExpression ? instance ,
129
136
MethodInfo method ,
130
137
IReadOnlyList < SqlExpression > arguments )
131
- {
132
- if ( instance is null || ! MethodInfoDatePartMapping . TryGetValue ( method , out var datePart ) )
133
- {
134
- return null ;
135
- }
136
-
137
- if ( arguments [ 0 ] is not { } interval )
138
- {
139
- return null ;
140
- }
141
-
142
- // Note: ideally we'd simply generate a PostgreSQL interval expression, but the .NET mapping of that is TimeSpan,
143
- // which does not work for months, years, etc. So we generate special fragments instead.
144
- if ( interval is SqlConstantExpression constantExpression )
145
- {
146
- // We generate constant intervals as INTERVAL '1 days'
147
- if ( constantExpression . Type == typeof ( double )
148
- && ( ( double ) constantExpression . Value ! >= int . MaxValue || ( double ) constantExpression . Value <= int . MinValue ) )
149
- {
150
- return null ;
151
- }
152
-
153
- interval = _sqlExpressionFactory . Fragment ( FormattableString . Invariant ( $ "INTERVAL '{ constantExpression . Value } { datePart } '") ) ;
154
- }
155
- else
156
- {
157
- // For non-constants, we can't parameterize INTERVAL '1 days'. Instead, we use CAST($1 || ' days' AS interval).
158
- // Note that a make_interval() function also exists, but accepts only int (for all fields except for
159
- // seconds), so we don't use it.
160
- // Note: we instantiate SqlBinaryExpression manually rather than via sqlExpressionFactory because
161
- // of the non-standard Add expression (concatenate int with text)
162
- interval = _sqlExpressionFactory . Convert (
163
- new SqlBinaryExpression (
164
- ExpressionType . Add ,
165
- _sqlExpressionFactory . Convert ( interval , typeof ( string ) , _textMapping ) ,
166
- _sqlExpressionFactory . Constant ( ' ' + datePart , _textMapping ) ,
167
- typeof ( string ) ,
168
- _textMapping ) ,
169
- typeof ( TimeSpan ) ,
170
- _intervalMapping ) ;
171
- }
172
-
173
- return _sqlExpressionFactory . Add ( instance , interval , instance . TypeMapping ) ;
174
- }
138
+ => instance is not null
139
+ && MethodInfoDatePartMapping . TryGetValue ( method , out var datePart )
140
+ && CreateIntervalExpression ( arguments [ 0 ] , datePart ) is SqlExpression interval
141
+ ? _sqlExpressionFactory . Add ( instance , interval , instance . TypeMapping )
142
+ : null ;
175
143
176
144
private SqlExpression ? TranslateDateTime (
177
145
SqlExpression ? instance ,
@@ -270,6 +238,28 @@ public NpgsqlDateTimeMethodTranslator(
270
238
typeof ( DateTime ) ,
271
239
_timestampMapping ) ;
272
240
}
241
+
242
+ // In PG, date + int = date (int interpreted as days)
243
+ if ( method == DateOnly_AddDays )
244
+ {
245
+ return _sqlExpressionFactory . Add ( instance , arguments [ 0 ] ) ;
246
+ }
247
+
248
+ // For months and years, date + interval yields a timestamp (since interval could have a time component), so we need to cast
249
+ // the results back to date
250
+ if ( method == DateOnly_AddMonths
251
+ && CreateIntervalExpression ( arguments [ 0 ] , "months" ) is SqlExpression interval1 )
252
+ {
253
+ return _sqlExpressionFactory . Convert (
254
+ _sqlExpressionFactory . Add ( instance , interval1 , instance . TypeMapping ) , typeof ( DateOnly ) ) ;
255
+ }
256
+
257
+ if ( method == DateOnly_AddYears
258
+ && CreateIntervalExpression ( arguments [ 0 ] , "years" ) is SqlExpression interval2 )
259
+ {
260
+ return _sqlExpressionFactory . Convert (
261
+ _sqlExpressionFactory . Add ( instance , interval2 , instance . TypeMapping ) , typeof ( DateOnly ) ) ;
262
+ }
273
263
}
274
264
275
265
return null ;
@@ -360,4 +350,36 @@ public NpgsqlDateTimeMethodTranslator(
360
350
361
351
return null ;
362
352
}
353
+
354
+ private SqlExpression ? CreateIntervalExpression ( SqlExpression intervalNum , string datePart )
355
+ {
356
+ // Note: ideally we'd simply generate a PostgreSQL interval expression, but the .NET mapping of that is TimeSpan,
357
+ // which does not work for months, years, etc. So we generate special fragments instead.
358
+ if ( intervalNum is SqlConstantExpression constantExpression )
359
+ {
360
+ // We generate constant intervals as INTERVAL '1 days'
361
+ if ( constantExpression . Type == typeof ( double )
362
+ && ( ( double ) constantExpression . Value ! >= int . MaxValue || ( double ) constantExpression . Value <= int . MinValue ) )
363
+ {
364
+ return null ;
365
+ }
366
+
367
+ return _sqlExpressionFactory . Fragment ( FormattableString . Invariant ( $ "INTERVAL '{ constantExpression . Value } { datePart } '") ) ;
368
+ }
369
+
370
+ // For non-constants, we can't parameterize INTERVAL '1 days'. Instead, we use CAST($1 || ' days' AS interval).
371
+ // Note that a make_interval() function also exists, but accepts only int (for all fields except for
372
+ // seconds), so we don't use it.
373
+ // Note: we instantiate SqlBinaryExpression manually rather than via sqlExpressionFactory because
374
+ // of the non-standard Add expression (concatenate int with text)
375
+ return _sqlExpressionFactory . Convert (
376
+ new SqlBinaryExpression (
377
+ ExpressionType . Add ,
378
+ _sqlExpressionFactory . Convert ( intervalNum , typeof ( string ) , _textMapping ) ,
379
+ _sqlExpressionFactory . Constant ( ' ' + datePart , _textMapping ) ,
380
+ typeof ( string ) ,
381
+ _textMapping ) ,
382
+ typeof ( TimeSpan ) ,
383
+ _intervalMapping ) ;
384
+ }
363
385
}
0 commit comments