Skip to content

Commit 3e8244f

Browse files
Add raw integer value support for extended-range date/time types (#569)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: KirillKurdyukov <[email protected]>
1 parent 86fcdf7 commit 3e8244f

File tree

5 files changed

+181
-7
lines changed

5 files changed

+181
-7
lines changed

src/Ydb.Sdk/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
- Feat ADO.NET: Added raw integer / long value support for extended-range `DateTime` types.
12
- Feat ADO.NET: Support `YdbStruct` support.
23

34
## v0.25.2

src/Ydb.Sdk/src/Ado/Internal/YdbPrimitiveTypeInfo.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ internal static readonly YdbPrimitiveTypeInfo
2525
Date = new(Type.Types.PrimitiveTypeId.Date, TryPackDate),
2626
Date32 = new(Type.Types.PrimitiveTypeId.Date32, TryPackDate32),
2727
Datetime = new(Type.Types.PrimitiveTypeId.Datetime, TryPack<DateTime>(PackDatetime)),
28-
Datetime64 = new(Type.Types.PrimitiveTypeId.Datetime64, TryPack<DateTime>(PackDatetime64)),
28+
Datetime64 = new(Type.Types.PrimitiveTypeId.Datetime64, TryPackDatetime64),
2929
Timestamp = new(Type.Types.PrimitiveTypeId.Timestamp, TryPack<DateTime>(PackTimestamp)),
30-
Timestamp64 = new(Type.Types.PrimitiveTypeId.Timestamp64, TryPack<DateTime>(PackTimestamp64)),
30+
Timestamp64 = new(Type.Types.PrimitiveTypeId.Timestamp64, TryPackTimestamp64),
3131
Interval = new(Type.Types.PrimitiveTypeId.Interval, TryPack<TimeSpan>(PackInterval)),
32-
Interval64 = new(Type.Types.PrimitiveTypeId.Interval64, TryPack<TimeSpan>(PackInterval64));
32+
Interval64 = new(Type.Types.PrimitiveTypeId.Interval64, TryPackInterval64);
3333

3434
private YdbPrimitiveTypeInfo(Type.Types.PrimitiveTypeId primitiveTypeId, Func<object, Ydb.Value?> pack)
3535
{
@@ -154,6 +154,31 @@ private YdbPrimitiveTypeInfo(Type.Types.PrimitiveTypeId primitiveTypeId, Func<ob
154154
{
155155
DateTime dateTimeValue => PackDate32(dateTimeValue),
156156
DateOnly dateOnlyValue => PackDate32(dateOnlyValue.ToDateTime(TimeOnly.MinValue)),
157+
int intValue => new Ydb.Value { Int32Value = intValue },
158+
_ => null
159+
};
160+
161+
private static Ydb.Value? TryPackDatetime64(object value) => value switch
162+
{
163+
DateTime dateTimeValue => PackDatetime64(dateTimeValue),
164+
long longValue => new Ydb.Value { Int64Value = longValue },
165+
int intValue => new Ydb.Value { Int64Value = intValue },
166+
_ => null
167+
};
168+
169+
private static Ydb.Value? TryPackTimestamp64(object value) => value switch
170+
{
171+
DateTime dateTimeValue => PackTimestamp64(dateTimeValue),
172+
long longValue => new Ydb.Value { Int64Value = longValue },
173+
int intValue => new Ydb.Value { Int64Value = intValue },
174+
_ => null
175+
};
176+
177+
private static Ydb.Value? TryPackInterval64(object value) => value switch
178+
{
179+
TimeSpan timeSpanValue => PackInterval64(timeSpanValue),
180+
long longValue => new Ydb.Value { Int64Value = longValue },
181+
int intValue => new Ydb.Value { Int64Value = intValue },
157182
_ => null
158183
};
159184
}

src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ internal static DateTime UnpackDate(this Ydb.Value value) =>
152152
{ Int32Value = (int)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerDay) };
153153

154154
internal static DateTime UnpackDate32(this Ydb.Value value) =>
155-
UnixEpoch.AddTicks(value.Int32Value * TimeSpan.TicksPerDay);
155+
UnixEpoch.AddTicks(checked(value.Int32Value * TimeSpan.TicksPerDay));
156156

157157
internal static Ydb.Value PackDatetime(DateTime value) => new()
158158
{ Uint32Value = checked((uint)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerSecond)) };
@@ -164,7 +164,7 @@ internal static DateTime UnpackDatetime(this Ydb.Value value) =>
164164
{ Int64Value = value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerSecond };
165165

166166
internal static DateTime UnpackDatetime64(this Ydb.Value value) =>
167-
UnixEpoch.AddTicks(value.Int64Value * TimeSpan.TicksPerSecond);
167+
UnixEpoch.AddTicks(checked(value.Int64Value * TimeSpan.TicksPerSecond));
168168

169169
internal static Ydb.Value PackTimestamp(DateTime value) => new()
170170
{ Uint64Value = checked((ulong)(value.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpanUtils.TicksPerMicrosecond) };
@@ -176,7 +176,7 @@ internal static DateTime UnpackTimestamp(this Ydb.Value value) =>
176176
{ Int64Value = (value.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpanUtils.TicksPerMicrosecond };
177177

178178
internal static DateTime UnpackTimestamp64(this Ydb.Value value) =>
179-
UnixEpoch.AddTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond);
179+
UnixEpoch.AddTicks(checked(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond));
180180

181181
internal static Ydb.Value PackInterval(TimeSpan value) => new()
182182
{ Int64Value = value.Ticks / TimeSpanUtils.TicksPerMicrosecond };
@@ -188,5 +188,5 @@ internal static TimeSpan UnpackInterval(this Ydb.Value value) =>
188188
{ Int64Value = value.Ticks / TimeSpanUtils.TicksPerMicrosecond };
189189

190190
internal static TimeSpan UnpackInterval64(this Ydb.Value value) =>
191-
TimeSpan.FromTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond);
191+
TimeSpan.FromTicks(checked(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond));
192192
}

src/Ydb.Sdk/src/Ado/YdbDataReader.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,15 @@ public ushort GetUint16(int ordinal)
458458
/// </summary>
459459
/// <param name="ordinal">The zero-based column ordinal.</param>
460460
/// <returns>The value of the specified column.</returns>
461+
/// <remarks>
462+
/// <para>
463+
/// For <b>Date32</b> type, this method returns the raw storage value as a signed 32-bit integer
464+
/// representing the number of days since Unix epoch (1970-01-01).
465+
/// </para>
466+
/// <para>
467+
/// This allows reading dates outside the DateTime supported range without conversion errors.
468+
/// </para>
469+
/// </remarks>
461470
public override int GetInt32(int ordinal)
462471
{
463472
var type = UnwrapColumnType(ordinal);
@@ -469,6 +478,7 @@ public override int GetInt32(int ordinal)
469478
Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].UnpackInt8(),
470479
Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].UnpackUint16(),
471480
Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].UnpackUint8(),
481+
Type.Types.PrimitiveTypeId.Date32 => CurrentRow[ordinal].Int32Value,
472482
_ => throw InvalidCastException<int>(ordinal)
473483
};
474484
}
@@ -496,6 +506,25 @@ public uint GetUint32(int ordinal)
496506
/// </summary>
497507
/// <param name="ordinal">The zero-based column ordinal.</param>
498508
/// <returns>The value of the specified column.</returns>
509+
/// <remarks>
510+
/// <para>
511+
/// For extended range date/time types, this method returns the raw storage value:
512+
/// </para>
513+
/// <list type="bullet">
514+
/// <item><description>
515+
/// <b>Datetime64</b>: Returns the number of seconds since Unix epoch (1970-01-01 00:00:00 UTC).
516+
/// </description></item>
517+
/// <item><description>
518+
/// <b>Timestamp64</b>: Returns the number of microseconds since Unix epoch (1970-01-01 00:00:00 UTC).
519+
/// </description></item>
520+
/// <item><description>
521+
/// <b>Interval64</b>: Returns the number of microseconds in the time interval.
522+
/// </description></item>
523+
/// </list>
524+
/// <para>
525+
/// This allows reading values outside the DateTime/TimeSpan supported range without conversion errors.
526+
/// </para>
527+
/// </remarks>
499528
public override long GetInt64(int ordinal)
500529
{
501530
var type = UnwrapColumnType(ordinal);
@@ -509,6 +538,9 @@ public override long GetInt64(int ordinal)
509538
Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].UnpackUint32(),
510539
Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].UnpackUint16(),
511540
Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].UnpackUint8(),
541+
Type.Types.PrimitiveTypeId.Datetime64 => CurrentRow[ordinal].Int64Value,
542+
Type.Types.PrimitiveTypeId.Timestamp64 => CurrentRow[ordinal].Int64Value,
543+
Type.Types.PrimitiveTypeId.Interval64 => CurrentRow[ordinal].Int64Value,
512544
_ => throw InvalidCastException<long>(ordinal)
513545
};
514546
}

src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,122 @@ public Task ExecuteReaderAsync_WhenEmptyList_ReturnEmptyResultSet() => RunTestWi
280280
Assert.False(await reader.ReadAsync());
281281
});
282282

283+
[Fact]
284+
public Task Write_ReadRowValue_When_Date32_Datetime64_Timestamp64_Interval64_WithGetInt32_GetInt64() =>
285+
RunTestWithTemporaryTable("""
286+
CREATE TABLE `{0}` (
287+
Id Serial,
288+
Date32Column Date32,
289+
Datetime64Column Datetime64,
290+
Timestamp64Column Timestamp64,
291+
Interval64Column Interval64,
292+
PRIMARY KEY (Id)
293+
)
294+
""", $"ReadRowValue_ExtendedDateTypes_{Guid.NewGuid()}",
295+
async (ydbConnection, tableName) =>
296+
{
297+
const int minDate32 = -53375809;
298+
const long minDatetime64 = -4611669897600;
299+
const long minTimestamp64 = -4611669897600000000;
300+
const int maxDate32 = 53375807;
301+
const long maxDatetime64 = 4611669811199;
302+
const long maxTimestamp64 = 4611669811199999999;
303+
const long maxInterval64 = maxTimestamp64 - minTimestamp64;
304+
305+
await new YdbCommand($"""
306+
INSERT INTO `{tableName}`
307+
(Date32Column, Datetime64Column, Timestamp64Column, Interval64Column)
308+
VALUES
309+
(@Date32Min, @Datetime64Min, @Timestamp64Min, @Interval64Min),
310+
(@Date32Max, @Datetime64Max, @Timestamp64Max, @Interval64Max);
311+
""",
312+
ydbConnection)
313+
{
314+
Parameters =
315+
{
316+
new YdbParameter("@Date32Min", YdbDbType.Date32, minDate32),
317+
new YdbParameter("@Datetime64Min", YdbDbType.Datetime64, minDatetime64),
318+
new YdbParameter("@Timestamp64Min", YdbDbType.Timestamp64, minTimestamp64),
319+
new YdbParameter("@Interval64Min", YdbDbType.Interval64, -maxInterval64),
320+
new YdbParameter("@Date32Max", YdbDbType.Date32, maxDate32),
321+
new YdbParameter("@Datetime64Max", YdbDbType.Datetime64, maxDatetime64),
322+
new YdbParameter("@Timestamp64Max", YdbDbType.Timestamp64, maxTimestamp64),
323+
new YdbParameter("@Interval64Max", YdbDbType.Interval64, maxInterval64)
324+
}
325+
}.ExecuteNonQueryAsync();
326+
327+
var ydbDataReader = await new YdbCommand(
328+
$"""
329+
SELECT Date32Column, Datetime64Column, Timestamp64Column, Interval64Column
330+
FROM `{tableName}` ORDER BY Id
331+
""", ydbConnection).ExecuteReaderAsync();
332+
333+
await ydbDataReader.ReadAsync();
334+
Assert.Equal(minDate32, ydbDataReader.GetInt32(0));
335+
Assert.Throws<OverflowException>(() => ydbDataReader.GetDateTime(0));
336+
Assert.Equal(minDatetime64, ydbDataReader.GetInt64(1));
337+
Assert.Throws<OverflowException>(() => ydbDataReader.GetDateTime(1));
338+
Assert.Equal(minTimestamp64, ydbDataReader.GetInt64(2));
339+
Assert.Throws<OverflowException>(() => ydbDataReader.GetDateTime(2));
340+
Assert.Equal(-maxInterval64, ydbDataReader.GetInt64(3));
341+
Assert.Throws<OverflowException>(() => ydbDataReader.GetInterval(3));
342+
343+
await ydbDataReader.ReadAsync();
344+
Assert.Equal(maxDate32, ydbDataReader.GetInt32(0));
345+
Assert.Throws<OverflowException>(() => ydbDataReader.GetDateTime(0));
346+
Assert.Equal(maxDatetime64, ydbDataReader.GetInt64(1));
347+
Assert.Throws<OverflowException>(() => ydbDataReader.GetDateTime(1));
348+
Assert.Equal(maxTimestamp64, ydbDataReader.GetInt64(2));
349+
Assert.Throws<OverflowException>(() => ydbDataReader.GetDateTime(2));
350+
Assert.Equal(maxInterval64, ydbDataReader.GetInt64(3));
351+
Assert.Throws<OverflowException>(() => ydbDataReader.GetInterval(3));
352+
353+
Assert.False(await ydbDataReader.ReadAsync());
354+
355+
Assert.Equal(2ul, await new YdbCommand(
356+
$"""
357+
SELECT COUNT(*) FROM `{tableName}` WHERE
358+
Date32Column IN @Date32List AND
359+
Datetime64Column IN @Datetime64List AND
360+
Timestamp64Column IN @Timestamp64List AND
361+
Interval64Column IN @Interval64List;
362+
""", ydbConnection)
363+
{
364+
Parameters =
365+
{
366+
new YdbParameter("@Date32List", YdbDbType.List | YdbDbType.Date32,
367+
new[] { minDate32, maxDate32 }),
368+
new YdbParameter("@Datetime64List", YdbDbType.List | YdbDbType.Datetime64,
369+
new[] { minDatetime64, maxDatetime64 }),
370+
new YdbParameter("@Timestamp64List", YdbDbType.List | YdbDbType.Timestamp64,
371+
new[] { minTimestamp64, maxTimestamp64 }),
372+
new YdbParameter("@Interval64List", YdbDbType.List | YdbDbType.Interval64,
373+
new[] { -maxInterval64, maxInterval64 })
374+
}
375+
}.ExecuteScalarAsync());
376+
}
377+
);
378+
379+
[Fact]
380+
public async Task OutsideOfDateTime_ThrowsArgumentOutOfRangeException()
381+
{
382+
await using var ydbConnection = await CreateOpenConnectionAsync();
383+
var ydbDataReader = await new YdbCommand("""
384+
SELECT
385+
CAST(-1000000 AS Date32),
386+
CAST(-100000000000 AS Datetime64),
387+
CAST(-100000000000000000 AS Timestamp64)
388+
""", ydbConnection).ExecuteReaderAsync();
389+
390+
await ydbDataReader.ReadAsync();
391+
Assert.Equal(-1000000, ydbDataReader.GetInt32(0));
392+
Assert.Throws<ArgumentOutOfRangeException>(() => ydbDataReader.GetDateTime(0));
393+
Assert.Equal(-100000000000, ydbDataReader.GetInt64(1));
394+
Assert.Throws<ArgumentOutOfRangeException>(() => ydbDataReader.GetDateTime(1));
395+
Assert.Equal(-100000000000000000, ydbDataReader.GetInt64(2));
396+
Assert.Throws<ArgumentOutOfRangeException>(() => ydbDataReader.GetDateTime(2));
397+
}
398+
283399
public static readonly TheoryData<DbType, object, bool> DbTypeTestCases = new()
284400
{
285401
{ DbType.Boolean, true, false },

0 commit comments

Comments
 (0)