• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Giorgi / DuckDB.NET / 42177187567

06 Nov 2026 06:50AM UTC coverage: 93.637% (+0.3%) from 93.368%
42177187567

push

github

Giorgi
Build a wide DECIMAL that fits a decimal from its digits and scale

A DECIMAL of more than 18 digits, stored as a HUGEINT, was divided on every
read. A value that fits the 96 bits of a decimal, with a scale of at most 28,
is now built from its digits and the scale like the narrower sizes, so it
keeps the scale of its column: 1.5000 from a DECIMAL(38,4), where it was 1.5.
GetDecimal hands a HUGEINT to GetWideDecimal so its own frame stays as it
was, and the division moves to DivideWideDecimal for the rest.

Linux VM, .NET 10, against the build before: DECIMAL(38,4) reads 0.20,
DECIMAL(18,4) 0.95 (unchanged code), HUGEINT as long 1.01.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F9b3CGWkCMzmhoxge1QiZ8

2773 of 3009 branches covered (92.16%)

Branch coverage included in aggregate %.

4 of 4 new or added lines in 1 file covered. (100.0%)

16 existing lines in 8 files now uncovered.

4335 of 4582 relevant lines covered (94.61%)

1056194.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.64
/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs
1
namespace DuckDB.NET.Data.DataChunk.Reader;
2

3
internal sealed class DateTimeVectorDataReader : VectorDataReaderBase
4
{
5
    // DuckDB stores a DATE as days and a timestamp as seconds, milliseconds, microseconds or nanoseconds, both
6
    // counted from 1970-01-01. A DateTime counts ticks of 100 nanoseconds from 0001-01-01 and a DateOnly counts days
7
    // from there. So a read as one of the .NET types is a multiplication and an addition, and nothing is asked of
8
    // DuckDB. A read as DuckDBDateOnly or DuckDBTimestamp needs the year, month and day, and has DuckDB split the value.
9
    // A TIMETZ packs microseconds from midnight and the offset into 64 bits. As DuckDBTimeTz DuckDB splits the value;
10
    // as DateTimeOffset it is read by arithmetic. TIME and TIME_NS have a reader of their own, TimeVectorDataReader.
11
    // TemporalConversionTests compares the two ways, so a change in DuckDB shows there.
12
    private const int UnixEpochDayNumber = DateTimeExtensions.UnixEpochDayNumber;
13
    private const int MaxDayNumber = 3_652_058;                     // DateOnly.MaxValue.DayNumber
14
    private const long UnixEpochTicks = DateTimeExtensions.UnixEpochTicks;
15
    private const long MaxTicks = 3_155_378_975_999_999_999;        // DateTime.MaxValue.Ticks
16

17
    // The layout of a TIMETZ is described where the constants are.
18
    private const int TimeTzOffsetBits = DateTimeExtensions.TimeTzOffsetBits;
19
    private const ulong TimeTzOffsetMask = DateTimeExtensions.TimeTzOffsetMask;
20
    private const int TimeTzMaximumOffsetSeconds = DateTimeExtensions.TimeTzMaximumOffsetSeconds;
21

22
    private const long MinimumDuckDBMicroseconds = -9_223_372_022_400_000_000;  // TIMESTAMP '290309-12-22 (BC) 00:00:00'
23

24
    // For a timestamp column: the ticks in one unit of the column, and the range of a DateTime in that unit.
25
    // Zero for TIMESTAMP_NS, whose unit is smaller than a tick and whose whole range a DateTime holds.
26
    private readonly long ticksPerUnit;
27
    private readonly long minimumUnits;
28
    private readonly long maximumUnits;
29

30
    internal unsafe DateTimeVectorDataReader(void* dataPointer, ulong* validityMaskPointer, DuckDBType columnType, string columnName) : base(dataPointer, validityMaskPointer, columnType, columnName)
7,374 ✔
31
    {
32
        // A DATE or TIMETZ column gets the values of a microsecond column and never uses them.
33
        ticksPerUnit = columnType switch
7,374 ✔
34
        {
7,374 ✔
35
            DuckDBType.TimestampS => TimeSpan.TicksPerSecond,
234 ✔
36
            DuckDBType.TimestampMs => TimeSpan.TicksPerMillisecond,
234 ✔
37
            DuckDBType.TimestampNs => 0,
285 ✔
38
            _ => TimeSpan.TicksPerMicrosecond
6,621 ✔
39
        };
7,374 ✔
40

41
        if (ticksPerUnit > 0)
7,374 ✔
42
        {
43
            minimumUnits = -UnixEpochTicks / ticksPerUnit;
7,089 ✔
44
            maximumUnits = (MaxTicks - UnixEpochTicks) / ticksPerUnit;
7,089 ✔
45
        }
46
    }
7,374 ✔
47

48
    internal override DateTime ReadDateTime(ulong offset) => GetValidValue<DateTime>(offset);
23,845,354 ✔
49
    internal override DateTimeOffset ReadDateTimeOffset(ulong offset) => GetValidValue<DateTimeOffset>(offset);
1,244,552 ✔
50
    internal override DateOnly ReadDateOnly(ulong offset) => GetValidValue<DateOnly>(offset);
11,114,729 ✔
51
    internal override TimeOnly ReadTimeOnly(ulong offset) => GetValidValue<TimeOnly>(offset);
3 ✔
52

53
    protected override T GetValidValue<T>(ulong offset)
54
    {
55
        if (DuckDBType == DuckDBType.Date)
48,108,572 ✔
56
        {
57
            if (typeof(T) == typeof(DateTime))
45,199,443 ✔
58
            {
59
                return (T)(object)new DateTime(GetDayNumber(offset, typeof(DateTime)) * TimeSpan.TicksPerDay);
22,802,062 ✔
60
            }
61

62
            if (typeof(T) == typeof(DateOnly))
22,397,381 ✔
63
            {
64
                return (T)(object)DateOnly.FromDayNumber(GetDayNumber(offset, typeof(DateOnly)));
11,114,729 ✔
65
            }
66

67
            return (T)(object)DuckDBDateOnly.FromDuckDBDate(GetFieldData<DuckDBDate>(offset));
11,282,652 ✔
68
        }
69

70
        if (DuckDBType == DuckDBType.TimeTz)
2,909,129 ✔
71
        {
72
            if (typeof(T) == typeof(DateTimeOffset))
316,016 ✔
73
            {
74
                return (T)(object)GetTimeTzAsDateTimeOffset(offset);
127,043 ✔
75
            }
76

77
            return (T)(object)GetTimeTz(offset);
188,973 ✔
78
        }
79

80
        return DuckDBType switch
2,593,113 !
81
        {
2,593,113 ✔
82
            DuckDBType.Timestamp or DuckDBType.TimestampS or
2,593,113 ✔
83
            DuckDBType.TimestampTz or DuckDBType.TimestampMs or
2,593,113 ✔
84
            DuckDBType.TimestampNs => ReadTimestamp<T>(offset),
2,593,113 ✔
UNCOV
85
            _ => base.GetValidValue<T>(offset)
×
86
        };
2,593,113 ✔
87
    }
88

89
    private T ReadTimestamp<T>(ulong offset)
90
    {
91
        if (typeof(T) == typeof(DateTime))
2,593,113 ✔
92
        {
93
            return (T)(object)new DateTime(GetTicks(offset, typeof(DateTime)));
1,043,292 ✔
94
        }
95

96
        if (typeof(T) == typeof(DateTimeOffset))
1,549,821 ✔
97
        {
98
            return (T)(object)new DateTimeOffset(new DateTime(GetTicks(offset, typeof(DateTimeOffset))), TimeSpan.Zero);
1,117,509 ✔
99
        }
100

101
        return (T)(object)GetTimestamp(offset);
432,312 ✔
102
    }
103

104
    internal override object GetValueCore(ulong offset, Type targetType)
105
    {
106
        return DuckDBType switch
11,452,926 !
107
        {
11,452,926 ✔
108
            DuckDBType.Date => GetDate(offset, targetType),
10,956,387 ✔
109
            DuckDBType.TimeTz => GetDateTimeOffset(offset, targetType),
294 ✔
110
            DuckDBType.Timestamp or DuckDBType.TimestampS or
11,452,926 ✔
111
            DuckDBType.TimestampTz or DuckDBType.TimestampMs or
11,452,926 ✔
112
            DuckDBType.TimestampNs => GetDateTime(offset, targetType),
496,245 ✔
UNCOV
113
            _ => base.GetValueCore(offset, targetType)
×
114
        };
11,452,926 ✔
115
    }
116

117
    // The days from 0001-01-01 to the date, for a date that a DateTime and a DateOnly hold.
118
    private int GetDayNumber(ulong offset, Type targetType)
119
    {
120
        var date = GetFieldData<DuckDBDate>(offset);
44,873,172 ✔
121
        var dayNumber = date.Days + UnixEpochDayNumber;
44,873,172 ✔
122

123
        // One comparison for both ends of the range. The two infinity markers are outside it, like any other
124
        // date that is too early or too late (a sum that wraps around is negative, and so outside as well).
125
        if ((uint)dayNumber > MaxDayNumber)
44,873,172 ✔
126
        {
127
            throw DateOutOfRange(date, targetType);
93 ✔
128
        }
129

130
        return dayNumber;
44,873,079 ✔
131
    }
132

133
    // The ticks from 0001-01-01 to the timestamp, for a timestamp that a DateTime holds.
134
    private long GetTicks(ulong offset, Type targetType)
135
    {
136
        // The count in the unit of the column. The field is called Micros for every unit.
137
        var timestamp = GetFieldData<DuckDBTimestampStruct>(offset);
2,657,022 ✔
138
        var units = timestamp.Micros;
2,657,022 ✔
139

140
        if (ticksPerUnit == 0)
2,657,022 ✔
141
        {
142
            if (timestamp.IsInfinity)
148,500 ✔
143
            {
144
                throw TimestampOutOfRange(timestamp, targetType);
57 ✔
145
            }
146

147
            // What is below a tick is dropped toward minus infinity, so that a moment before 1970 keeps its date and
148
            // time of day: a nanosecond before 1970 is 1969-12-31 23:59:59.9999999, not 1970-01-01. DuckDB 2.0 casts
149
            // a TIMESTAMP_NS to a TIMESTAMP the same way; 1.5 dropped the digits toward zero.
150
            var ticks = units / DateTimeExtensions.NanosecondsPerTick;
148,443 ✔
151

152
            if (units % DateTimeExtensions.NanosecondsPerTick < 0)
148,443 ✔
153
            {
154
                ticks--;
11,001 ✔
155
            }
156

157
            return UnixEpochTicks + ticks;
148,443 ✔
158
        }
159

160
        // One comparison for both ends of the range, as for a date. Without it the multiplication below overflows
161
        // for a value far outside the range, and gives a wrong date.
162
        if ((ulong)(units - minimumUnits) > (ulong)(maximumUnits - minimumUnits))
2,508,522 ✔
163
        {
164
            throw TimestampOutOfRange(timestamp, targetType);
324 ✔
165
        }
166

167
        return UnixEpochTicks + units * ticksPerUnit;
2,508,198 ✔
168
    }
169

170
    // DuckDB splits the value into year, month, day and time. What a TIMESTAMP_NS has below a microsecond is lost.
171
    private DuckDBTimestamp GetTimestamp(ulong offset)
172
    {
173
        var timestampStruct = GetFieldData<DuckDBTimestampStruct>(offset);
432,336 ✔
174

175
        if (timestampStruct.IsInfinity)
432,336 ✔
176
        {
177
            return DuckDBTimestamp.FromDuckDBTimestampStruct(timestampStruct);
108 ✔
178
        }
179

180
        return timestampStruct.ToDuckDBTimestamp(DuckDBType).result;
432,228 ✔
181
    }
182

183
    // A DateTimeOffset needs a date, and a TIMETZ has none. The time is put on 0001-01-02, the second day a DateTime
184
    // holds, as Npgsql does: on the first day, a time earlier than its own positive offset would be an instant before
185
    // year 1, which a DateTimeOffset refuses. DuckDB allows an offset up to 15:59:59; a DateTimeOffset refuses one
186
    // beyond 14 hours or with seconds, and such a value is read as DuckDBTimeTz.
187
    private DateTimeOffset GetTimeTzAsDateTimeOffset(ulong offset)
188
    {
189
        var bits = GetFieldData<DuckDBTimeTzStruct>(offset).Bits;
127,331 ✔
190

191
        var ticks = (long)(bits >> TimeTzOffsetBits) * TimeSpan.TicksPerMicrosecond;
127,331 ✔
192
        var offsetSeconds = TimeTzMaximumOffsetSeconds - (int)(bits & TimeTzOffsetMask);
127,331 ✔
193

194
        return new DateTimeOffset(new DateTime(TimeSpan.TicksPerDay + ticks), new TimeSpan(offsetSeconds * TimeSpan.TicksPerSecond));
127,331 ✔
195
    }
196

197
    private DuckDBTimeTz GetTimeTz(ulong offset)
198
    {
199
        var data = GetFieldData<DuckDBTimeTzStruct>(offset);
188,979 ✔
200

201
        return NativeMethods.DateTimeHelpers.DuckDBFromTimeTz(data);
188,979 ✔
202
    }
203

204
    private object GetDate(ulong offset, Type targetType)
205
    {
206
        if (targetType == typeof(DateTime))
10,956,387 !
207
        {
UNCOV
208
            return new DateTime(GetDayNumber(offset, targetType) * TimeSpan.TicksPerDay);
×
209
        }
210

211
        if (targetType == typeof(DateOnly))
10,956,387 ✔
212
        {
213
            return DateOnly.FromDayNumber(GetDayNumber(offset, targetType));
10,956,381 ✔
214
        }
215

216
        return DuckDBDateOnly.FromDuckDBDate(GetFieldData<DuckDBDate>(offset));
6 ✔
217
    }
218

219
    private object GetDateTime(ulong offset, Type targetType)
220
    {
221
        if (targetType == typeof(DateTime))
496,245 ✔
222
        {
223
            return new DateTime(GetTicks(offset, targetType));
496,221 ✔
224
        }
225

226
        if (targetType == typeof(DateTimeOffset))
24 !
227
        {
UNCOV
228
            return new DateTimeOffset(new DateTime(GetTicks(offset, targetType)), TimeSpan.Zero);
×
229
        }
230

231
        return GetTimestamp(offset);
24 ✔
232
    }
233

234
    private object GetDateTimeOffset(ulong offset, Type targetType)
235
    {
236
        if (targetType == typeof(DateTimeOffset))
294 ✔
237
        {
238
            return GetTimeTzAsDateTimeOffset(offset);
288 ✔
239
        }
240

241
        return GetTimeTz(offset);
6 ✔
242
    }
243

244
    // The methods below build the exceptions, so that no message is put together in the methods that read.
245
    private Exception DateOutOfRange(DuckDBDate date, Type targetType)
246
    {
247
        if (date.IsInfinity)
93 ✔
248
        {
249
            return new InvalidOperationException(
57 ✔
250
                "Cannot convert infinite date value to DateTime or DateOnly. " +
57 ✔
251
                "Use DuckDBDateOnly to read this value and check IsInfinity, IsPositiveInfinity, or IsNegativeInfinity before converting to .NET types.");
57 ✔
252
        }
253

254
        return OutOfRange(targetType, "Use GetFieldValue<DuckDBDateOnly>() to read it.");
36 ✔
255
    }
256

257
    private Exception TimestampOutOfRange(DuckDBTimestampStruct timestamp, Type targetType)
258
    {
259
        if (timestamp.IsInfinity)
381 ✔
260
        {
261
            return new InvalidOperationException(
285 ✔
262
                "Cannot convert infinite timestamp value to DateTime or DateTimeOffset. " +
285 ✔
263
                "Use DuckDBTimestamp to read this value and check IsInfinity, IsPositiveInfinity, or IsNegativeInfinity before converting to .NET types.");
285 ✔
264
        }
265

266
        // DuckDB cannot split a timestamp below its own minimum either, so that one is not read as DuckDBTimestamp.
267
        // The scaling is for a column in seconds or milliseconds. No test reaches it: SQL does not produce such a
268
        // value in those units.
269
        var microseconds = (Int128)timestamp.Micros * ticksPerUnit / TimeSpan.TicksPerMicrosecond;
96 ✔
270

271
        return OutOfRange(targetType, microseconds < MinimumDuckDBMicroseconds ? "" : "Use GetFieldValue<DuckDBTimestamp>() to read it.");
96 ✔
272
    }
273

274
    private ArgumentOutOfRangeException OutOfRange(Type targetType, string advice)
275
    {
276
        // The reader of an argument of a user-defined function has no column name.
277
        var value = ColumnName.Length == 0 ? "Value" : $"Value in column {ColumnName}";
132 ✔
278

279
        return new ArgumentOutOfRangeException($"{value} is outside the range of System.{targetType.Name}. {advice}".TrimEnd(), (Exception?)null);
132 ✔
280
    }
281
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc