Skip to content

Commit 868f93a

Browse files
committed
Refactor DateTime/DuckDBTimestamp conversion
1 parent e8f9244 commit 868f93a

File tree

6 files changed

+84
-90
lines changed

6 files changed

+84
-90
lines changed

DuckDB.NET.Data/Extensions/DateTimeExtensions.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using DuckDB.NET.Native;
23

34
namespace DuckDB.NET.Data.Extensions;
45

@@ -16,4 +17,53 @@ public static int Nanoseconds(this DateTime self)
1617
return (int)(self.Ticks % TimeSpan.TicksPerMillisecond % TicksPerMicrosecond) * NanosecondsPerTick;
1718
#endif
1819
}
20+
21+
public static DuckDBTimestampStruct ToTimestampStruct(this DateTime value, DuckDBType duckDBType)
22+
{
23+
var timestamp = NativeMethods.DateTimeHelpers.DuckDBToTimestamp(DuckDBTimestamp.FromDateTime(value));
24+
25+
if (duckDBType == DuckDBType.TimestampNs)
26+
{
27+
timestamp.Micros *= 1000;
28+
29+
timestamp.Micros += value.Nanoseconds();
30+
}
31+
32+
if (duckDBType == DuckDBType.TimestampMs)
33+
{
34+
timestamp.Micros /= 1000;
35+
}
36+
37+
if (duckDBType == DuckDBType.TimestampS)
38+
{
39+
timestamp.Micros /= 1000000;
40+
}
41+
42+
return timestamp;
43+
}
44+
45+
public static (DuckDBTimestamp result, int additionalTicks) ToDuckDBTimestamp(this DuckDBTimestampStruct timestamp, DuckDBType duckDBType)
46+
{
47+
var additionalTicks = 0;
48+
49+
if (duckDBType == DuckDBType.TimestampNs)
50+
{
51+
additionalTicks = (int)(timestamp.Micros % 1000 / 100);
52+
timestamp.Micros /= 1000;
53+
}
54+
55+
if (duckDBType == DuckDBType.TimestampMs)
56+
{
57+
timestamp.Micros *= 1000;
58+
}
59+
60+
if (duckDBType == DuckDBType.TimestampS)
61+
{
62+
timestamp.Micros *= 1000000;
63+
}
64+
65+
var result = NativeMethods.DateTimeHelpers.DuckDBFromTimestamp(timestamp);
66+
67+
return (result, additionalTicks);
68+
}
1969
}

DuckDB.NET.Data/Internal/ClrToDuckDBConverter.cs

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log
3939
(DuckDBType.Varchar, string value) => StringToDuckDBValue(value),
4040
(DuckDBType.Uuid, Guid value) => GuidToDuckDBValue(value),
4141

42-
(DuckDBType.Timestamp, DateTime value) => DateTimeToTimestamp(DuckDBType.Timestamp, value),
43-
(DuckDBType.TimestampS, DateTime value) => DateTimeToTimestamp(DuckDBType.TimestampS, value, divisor: 1000000),
44-
(DuckDBType.TimestampMs, DateTime value) => DateTimeToTimestamp(DuckDBType.TimestampMs, value, divisor: 1000),
45-
(DuckDBType.TimestampNs, DateTime value) => DateTimeToTimestamp(DuckDBType.TimestampNs, value, factor: 1000, extra: value.Nanoseconds()),
42+
(DuckDBType.Timestamp, DateTime value) => NativeMethods.Value.DuckDBCreateTimestamp(value.ToTimestampStruct(duckDBType)),
43+
(DuckDBType.TimestampS, DateTime value) => NativeMethods.Value.DuckDBCreateTimestampS(value.ToTimestampStruct(duckDBType)),
44+
(DuckDBType.TimestampMs, DateTime value) => NativeMethods.Value.DuckDBCreateTimestampMs(value.ToTimestampStruct(duckDBType)),
45+
(DuckDBType.TimestampNs, DateTime value) => NativeMethods.Value.DuckDBCreateTimestampNs(value.ToTimestampStruct(duckDBType)),
4646
(DuckDBType.Interval, TimeSpan value) => NativeMethods.Value.DuckDBCreateInterval(value),
4747
(DuckDBType.Date, DateTime value) => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate((DuckDBDateOnly)value)),
4848
(DuckDBType.Date, DuckDBDateOnly value) => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate(value)),
@@ -71,21 +71,6 @@ T ConvertTo<T>()
7171
throw new ArgumentOutOfRangeException($"Cannot bind parameter type {item.GetType().FullName} to column of type {duckDBType}");
7272
}
7373
}
74-
75-
DuckDBValue DateTimeToTimestamp(DuckDBType type, DateTime value, int factor = 1, int divisor = 1, int extra = 0)
76-
{
77-
var timestamp = NativeMethods.DateTimeHelpers.DuckDBToTimestamp(DuckDBTimestamp.FromDateTime(value));
78-
79-
timestamp.Micros = timestamp.Micros * factor / divisor;
80-
81-
return type switch
82-
{
83-
DuckDBType.Timestamp => NativeMethods.Value.DuckDBCreateTimestamp(timestamp),
84-
DuckDBType.TimestampS => NativeMethods.Value.DuckDBCreateTimestampS(timestamp),
85-
DuckDBType.TimestampMs => NativeMethods.Value.DuckDBCreateTimestampMs(timestamp),
86-
DuckDBType.TimestampNs => NativeMethods.Value.DuckDBCreateTimestampNs(timestamp),
87-
};
88-
}
8974
}
9075

9176
private static DuckDBValue CreateCollectionValue(DuckDBLogicalType logicalType, ICollection collection, bool isList)

DuckDB.NET.Data/Internal/Reader/DateTimeVectorDataReader.cs

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
using System;
2-
using DuckDB.NET.Data.Extensions;
1+
using DuckDB.NET.Data.Extensions;
32
using DuckDB.NET.Native;
3+
using System;
44

55
namespace DuckDB.NET.Data.Internal.Reader;
66

@@ -81,18 +81,16 @@ protected override T GetValidValue<T>(ulong offset, Type targetType)
8181

8282
return DuckDBType switch
8383
{
84-
DuckDBType.Timestamp => ReadTimestamp<T>(offset, targetType),
85-
DuckDBType.TimestampTz => ReadTimestamp<T>(offset, targetType),
86-
DuckDBType.TimestampS => ReadTimestamp<T>(offset, targetType, 1000000),
87-
DuckDBType.TimestampMs => ReadTimestamp<T>(offset, targetType, 1000),
88-
DuckDBType.TimestampNs => ReadTimestamp<T>(offset, targetType, 1, 1000, true),
84+
DuckDBType.Timestamp or DuckDBType.TimestampS or
85+
DuckDBType.TimestampTz or DuckDBType.TimestampMs or
86+
DuckDBType.TimestampNs => ReadTimestamp<T>(offset, targetType),
8987
_ => base.GetValidValue<T>(offset, targetType)
9088
};
9189
}
9290

93-
private T ReadTimestamp<T>(ulong offset, Type targetType, int factor = 1, int divisor = 1, bool keepNanoseconds = false)
91+
private T ReadTimestamp<T>(ulong offset, Type targetType)
9492
{
95-
var (additionalTicks, timestamp) = ReadTimestamp(offset, factor, divisor, keepNanoseconds);
93+
var (timestamp, additionalTicks) = GetFieldData<DuckDBTimestampStruct>(offset).ToDuckDBTimestamp(DuckDBType);
9694

9795
if (targetType == DateTimeType || targetType == DateTimeNullableType)
9896
{
@@ -110,11 +108,9 @@ internal override object GetValue(ulong offset, Type targetType)
110108
DuckDBType.Date => GetDate(offset, targetType),
111109
DuckDBType.Time => GetTime(offset, targetType),
112110
DuckDBType.TimeTz => GetDateTimeOffset(offset, targetType),
113-
DuckDBType.Timestamp => GetDateTime(offset, targetType),
114-
DuckDBType.TimestampTz => GetDateTime(offset, targetType),
115-
DuckDBType.TimestampS => GetDateTime(offset, targetType, 1000000),
116-
DuckDBType.TimestampMs => GetDateTime(offset, targetType, 1000),
117-
DuckDBType.TimestampNs => GetDateTime(offset, targetType, 1, 1000, true),
111+
DuckDBType.Timestamp or DuckDBType.TimestampS or
112+
DuckDBType.TimestampTz or DuckDBType.TimestampMs or
113+
DuckDBType.TimestampNs => GetDateTime(offset, targetType),
118114
_ => base.GetValue(offset, targetType)
119115
};
120116
}
@@ -172,9 +168,9 @@ private object GetTime(ulong offset, Type targetType)
172168
return timeOnly;
173169
}
174170

175-
private object GetDateTime(ulong offset, Type targetType, int factor = 1, int divisor = 1, bool keepNanoseconds = false)
171+
private object GetDateTime(ulong offset, Type targetType)
176172
{
177-
var (additionalTicks, timestamp) = ReadTimestamp(offset, factor, divisor, keepNanoseconds);
173+
var (timestamp, additionalTicks) = GetFieldData<DuckDBTimestampStruct>(offset).ToDuckDBTimestamp(DuckDBType);
178174

179175
if (targetType == typeof(DateTime))
180176
{
@@ -197,20 +193,4 @@ private object GetDateTimeOffset(ulong offset, Type targetType)
197193

198194
return timeTz;
199195
}
200-
201-
private (int additionalTicks, DuckDBTimestamp timestamp) ReadTimestamp(ulong offset, int factor, int divisor, bool keepNanoseconds)
202-
{
203-
var data = GetFieldData<DuckDBTimestampStruct>(offset);
204-
var additionalTicks = 0;
205-
206-
if (keepNanoseconds)
207-
{
208-
additionalTicks = (int)(data.Micros % 1000 / 100);
209-
}
210-
211-
data.Micros = (data.Micros * factor / divisor);
212-
213-
var timestamp = NativeMethods.DateTimeHelpers.DuckDBFromTimestamp(data);
214-
return (additionalTicks, timestamp);
215-
}
216196
}

DuckDB.NET.Data/Internal/Writer/DateTimeVectorDataWriter.cs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,7 @@ internal override bool AppendDateTime(DateTime value, ulong rowIndex)
1313
return AppendValueInternal(NativeMethods.DateTimeHelpers.DuckDBToDate((DuckDBDateOnly)value.Date), rowIndex);
1414
}
1515

16-
var timestamp = NativeMethods.DateTimeHelpers.DuckDBToTimestamp(DuckDBTimestamp.FromDateTime(value));
17-
18-
if (ColumnType == DuckDBType.TimestampNs)
19-
{
20-
timestamp.Micros *= 1000;
21-
22-
timestamp.Micros += value.Nanoseconds();
23-
}
24-
25-
if (ColumnType == DuckDBType.TimestampMs)
26-
{
27-
timestamp.Micros /= 1000;
28-
}
29-
30-
if (ColumnType == DuckDBType.TimestampS)
31-
{
32-
timestamp.Micros /= 1000000;
33-
}
16+
var timestamp = value.ToTimestampStruct(ColumnType);
3417

3518
return AppendValueInternal(timestamp, rowIndex);
3619
}

DuckDB.NET.Test/Parameters/TimestampTests.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using DuckDB.NET.Native;
33
using FluentAssertions;
44
using System;
5+
using FluentAssertions.Extensions;
56
using Xunit;
67

78
namespace DuckDB.NET.Test.Parameters;
@@ -70,15 +71,15 @@ public void BindTest(int year, int mon, int day, int hour, int minute, int secon
7071
}
7172

7273
[Theory]
73-
[InlineData(1992, 09, 20, 12, 15, 17, 350_000)]
74-
[InlineData(2022, 05, 04, 12, 17, 15, 450_000)]
75-
[InlineData(2022, 04, 05, 18, 15, 17, 125_000)]
76-
[InlineData(1992, 09, 20, 12, 15, 17, 350_300)]
77-
[InlineData(2022, 05, 04, 12, 17, 15, 450_500)]
78-
[InlineData(2022, 04, 05, 18, 15, 17, 125_700)]
79-
public void InsertAndQueryTest(int year, int mon, int day, byte hour, byte minute, byte second, int microsecond)
74+
[InlineData(1992, 09, 20, 12, 15, 17, 350_000, 0)]
75+
[InlineData(2022, 05, 04, 12, 17, 15, 450_000, 0)]
76+
[InlineData(2022, 04, 05, 18, 15, 17, 125_000, 0)]
77+
[InlineData(1992, 09, 20, 12, 15, 17, 350_300, 0)]
78+
[InlineData(2022, 05, 04, 12, 17, 15, 450_543, 200)]
79+
[InlineData(2022, 04, 05, 18, 15, 17, 125_700, 0)]
80+
public void InsertAndQueryTest(int year, int mon, int day, byte hour, byte minute, byte second, int microsecond, int nanosecond)
8081
{
81-
var expectedValue = new DateTime(year, mon, day, hour, minute, second).AddTicks(microsecond * 10);
82+
var expectedValue = new DateTime(year, mon, day, hour, minute, second).AddTicks(microsecond * 10).AddNanoseconds(nanosecond);
8283

8384
TestTimestampInsert("TIMESTAMP", DuckDBType.Timestamp, expectedValue);
8485

@@ -93,6 +94,7 @@ private void TestTimestampInsert(string timestampType, DuckDBType duckDBType, Da
9394
{
9495
expectedValue = duckDBType switch
9596
{
97+
DuckDBType.Timestamp => Trim(expectedValue, TimeSpan.TicksPerMicrosecond),
9698
DuckDBType.TimestampS => Trim(expectedValue, TimeSpan.TicksPerSecond),
9799
DuckDBType.TimestampMs => Trim(expectedValue, TimeSpan.TicksPerMillisecond),
98100
DuckDBType.TimestampNs => Trim(expectedValue, TimeSpan.FromTicks(100).Ticks),

DuckDB.NET.Test/TableFunctionTests.cs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public void RegisterTableFunctionWithTwoParameterTwoColumns()
5555
writers[1].WriteValue(pair.Value, rowIndex);
5656
});
5757

58-
var data = Connection.Query<(int, string)>($"SELECT * FROM demo2(30::SmallInt, 'DuckDB');").ToList();
58+
var data = Connection.Query<(int, string)>("SELECT * FROM demo2(30::SmallInt, 'DuckDB');").ToList();
5959

6060
data.Select(tuple => tuple.Item1).Should().BeEquivalentTo(Enumerable.Range(30, count));
6161
data.Select(tuple => tuple.Item2).Should().BeEquivalentTo(Enumerable.Range(30, count).Select(i => $"DuckDB{i}"));
@@ -84,7 +84,7 @@ public void RegisterTableFunctionWithThreeParameters()
8484
writers[0].WriteValue((DateTime)item, rowIndex);
8585
});
8686

87-
var data = Connection.Query<DateTime>($"SELECT * FROM demo3('2024-11-06'::TIMESTAMP, 10, 2.5 );").ToList();
87+
var data = Connection.Query<DateTime>("SELECT * FROM demo3('2024-11-06'::TIMESTAMP, 10, 2.5 );").ToList();
8888

8989
var dateTimes = Enumerable.Range(0, count).Select(i => startDate.AddDays(i).AddMinutes(minutesParam).AddSeconds(secondsParam));
9090
data.Should().BeEquivalentTo(dateTimes);
@@ -145,7 +145,7 @@ public void RegisterTableFunctionWithEmptyResult()
145145
writers[0].WriteValue((int)item, rowIndex);
146146
});
147147

148-
var data = Connection.Query<int>($"SELECT * FROM demo5(1::TINYINT, 2::USMALLINT, 3::UINTEGER, 4::UBIGINT, 5.6);").ToList();
148+
var data = Connection.Query<int>("SELECT * FROM demo5(1::TINYINT, 2::USMALLINT, 3::UINTEGER, 4::UBIGINT, 5.6);").ToList();
149149

150150
data.Should().BeEquivalentTo(Enumerable.Empty<int>());
151151
}
@@ -177,25 +177,19 @@ public void RegisterTableFunctionWithBigInteger()
177177
[Fact]
178178
public void RegisterTableFunctionWithErrors()
179179
{
180-
Connection.RegisterTableFunction<string>("bind_err", parameters =>
181-
{
182-
throw new Exception("bind_err_msg");
183-
}, (item, writer, rowIndex) =>
180+
Connection.RegisterTableFunction<string>("bind_err", _ => throw new Exception("bind_err_msg"), (_, _, _) =>
184181
{
185182
});
186183

187184
Connection.Invoking(con=>con.Query<int>("SELECT * FROM bind_err('')")). Should().Throw<DuckDBException>().WithMessage("*bind_err_msg*");
188185

189-
Connection.RegisterTableFunction<string>("map_err", parameters =>
186+
Connection.RegisterTableFunction<string>("map_err", _ =>
190187
{
191188
return new TableFunction(
192189
new[] { new ColumnInfo("t1", typeof(string)) },
193190
new[] { "a" }
194191
);
195-
}, (item, writer, rowIndex) =>
196-
{
197-
throw new NotSupportedException("map_err_msg");
198-
});
192+
}, (_, _, _) => throw new NotSupportedException("map_err_msg"));
199193

200194
Connection.Invoking(con => con.Query<int>("SELECT * FROM map_err('')")).Should().Throw<DuckDBException>().WithMessage("*map_err_msg*");
201195
}
@@ -216,7 +210,7 @@ public void RegisterTableFunctionWithNullParameter()
216210
writers[0].WriteValue((int)item, rowIndex);
217211
});
218212

219-
var data = Connection.Query<int>($"SELECT * FROM nullParam(NULL::INTEGER);").ToList();
213+
var data = Connection.Query<int>("SELECT * FROM nullParam(NULL::INTEGER);").ToList();
220214

221215
data.Should().BeEquivalentTo(Enumerable.Empty<int>());
222216
}

0 commit comments

Comments
 (0)