• 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

93.02
/DuckDB.NET.Data/DataChunk/Reader/VectorDataReaderBase.cs
1
using System.Diagnostics.CodeAnalysis;
2
using System.IO;
3
using System.Runtime.CompilerServices;
4

5
namespace DuckDB.NET.Data.DataChunk.Reader;
6

7
internal class VectorDataReaderBase : IDisposable, IDuckDBDataReader
8
{
9
    private unsafe ulong* validityMaskPointer;
10

11
    public Type ClrType => field ??= GetColumnType();
13,119,245 ✔
12

13
    public Type ProviderSpecificClrType => field ??= GetColumnProviderSpecificType();
312 ✔
14

15

16
    public string ColumnName { get; }
238,880 ✔
17
    public DuckDBType DuckDBType { get; }
80,879,096 ✔
18
    private protected unsafe void* DataPointer { get; private set; }
96,377,589 ✔
19

20
    // The .NET type whose memory layout is exactly what the vector holds, or null when every read needs a
21
    // conversion. A reader sets it once, and a read of that type then skips GetValidValue, which is a generic
22
    // virtual method and so is looked up at run time on every call.
23
    private protected Type? StorageType { get; init; }
76,973,493 ✔
24

25
    internal unsafe VectorDataReaderBase(void* dataPointer, ulong* validityMaskPointer, DuckDBType columnType, string columnName)
57,161 ✔
26
    {
27
        DataPointer = dataPointer;
57,161 ✔
28
        this.validityMaskPointer = validityMaskPointer;
57,161 ✔
29

30
        DuckDBType = columnType;
57,161 ✔
31
        ColumnName = columnName;
57,161 ✔
32
    }
57,161 ✔
33

34
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
35
    public unsafe bool IsValid(ulong offset)
36
    {
37
        if (validityMaskPointer == default)
116,377,276 ✔
38
        {
39
            return true;
101,354,758 ✔
40
        }
41

42
        return ValidityMask.IsValid(validityMaskPointer, offset);
15,022,518 ✔
43
    }
44

45
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
46
    public T GetValue<T>(ulong offset) => GetValue<T>(offset, strict: false);
333,492 ✔
47

48
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
49
    internal T GetValueStrict<T>(ulong offset) => GetValue<T>(offset, strict: true);
54,384,730 ✔
50

51
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
52
    internal T GetValue<T>(ulong offset, bool strict)
53
    {
54
        // When T is Nullable<TUnderlying> (e.g. int?), we can't call GetValidValue<int>() directly
55
        // because we only have T=int? at compile time. NullableHandler uses a pre-compiled expression
56
        // tree that calls GetValidValue<int>() and converts to int?, avoiding boxing through the
57
        // non-generic GetValue(offset, Type) path.
58
        if (NullableHandler<T>.IsNullableValueType)
54,718,222 ✔
59
        {
60
            return NullableHandler<T>.Read(this, offset);
326,142 ✔
61
        }
62

63
        if (IsValid(offset))
54,392,080 ✔
64
        {
65
            return ReadValidValue<T>(offset);
54,391,888 ✔
66
        }
67

68
        if (strict || !NullableHandler<T>.IsReferenceType)
192 ✔
69
        {
70
            throw new InvalidCastException($"Column '{ColumnName}' value is null");
189 ✔
71
        }
72
        return default!;
3 ✔
73
    }
74

75
    /// <summary>
76
    /// Called when the value at specified <param name="offset">offset</param> is valid (isn't null)
77
    /// </summary>
78
    /// <typeparam name="T">Type of the return value</typeparam>
79
    /// <param name="offset">Position to read the data from</param>
80
    /// <returns>Data at the specified offset</returns>
81
    protected virtual T GetValidValue<T>(ulong offset) => (T)GetValue(offset, typeof(T));
5,238,787 ✔
82

83
    // One method for each type that DbDataReader has a getter for, and for DateTimeOffset, DateOnly and TimeOnly,
84
    // which are read with GetFieldValue. GetValidValue is a generic virtual method, which
85
    // is looked up at run time on every call. These are plain virtual methods, which are not, and ReadValidValue
86
    // calls them for their types. A reader overrides the ones it produces, and an override calls GetValidValue of
87
    // its own class, so a value is converted by the same code whichever way it is reached.
88
    internal virtual bool ReadBoolean(ulong offset) => GetValidValue<bool>(offset);
6 ✔
89
    internal virtual byte ReadByte(ulong offset) => GetValidValue<byte>(offset);
303 ✔
90
    internal virtual short ReadInt16(ulong offset) => GetValidValue<short>(offset);
300 ✔
91
    internal virtual int ReadInt32(ulong offset) => GetValidValue<int>(offset);
339 ✔
92
    internal virtual long ReadInt64(ulong offset) => GetValidValue<long>(offset);
306 ✔
93
    internal virtual float ReadFloat(ulong offset) => GetValidValue<float>(offset);
2,550 ✔
94
    internal virtual double ReadDouble(ulong offset) => GetValidValue<double>(offset);
2,559 ✔
95
    internal virtual decimal ReadDecimal(ulong offset) => GetValidValue<decimal>(offset);
1,296 ✔
96
    internal virtual DateTime ReadDateTime(ulong offset) => GetValidValue<DateTime>(offset);
6 ✔
97
    internal virtual DateTimeOffset ReadDateTimeOffset(ulong offset) => GetValidValue<DateTimeOffset>(offset);
×
98
    internal virtual DateOnly ReadDateOnly(ulong offset) => GetValidValue<DateOnly>(offset);
×
99
    internal virtual TimeOnly ReadTimeOnly(ulong offset) => GetValidValue<TimeOnly>(offset);
×
100
    internal virtual Guid ReadGuid(ulong offset) => GetValidValue<Guid>(offset);
9 ✔
101
    internal virtual string ReadString(ulong offset) => GetValidValue<string>(offset);
9,241 ✔
102
    internal virtual Stream ReadStream(ulong offset) => GetValidValue<Stream>(offset);
3 ✔
103

104
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
105
    internal unsafe T ReadValidValue<T>(ulong offset)
106
    {
107
        // typeof(T).IsValueType is a constant for the JIT, so a reference type does not keep this test.
108
        if (typeof(T).IsValueType && typeof(T) == StorageType)
84,487,747 ✔
109
        {
110
            return Unsafe.Read<T>((byte*)DataPointer + offset * (ulong)Unsafe.SizeOf<T>());
18,526,204 ✔
111
        }
112

113
        // A test of T against a type is a constant for the JIT too, so at most one of these lines is left.
114
        if (typeof(T) == typeof(bool))     return (T)(object)ReadBoolean(offset);
65,961,549 ✔
115
        if (typeof(T) == typeof(byte))     return (T)(object)ReadByte(offset);
65,973,832 ✔
116
        if (typeof(T) == typeof(short))    return (T)(object)ReadInt16(offset);
65,961,531 ✔
117
        if (typeof(T) == typeof(int))      return (T)(object)ReadInt32(offset);
65,983,670 ✔
118
        if (typeof(T) == typeof(long))     return (T)(object)ReadInt64(offset);
66,142,621 ✔
119
        if (typeof(T) == typeof(float))    return (T)(object)ReadFloat(offset);
65,643,392 ✔
120
        if (typeof(T) == typeof(double))   return (T)(object)ReadDouble(offset);
65,756,309 ✔
121
        if (typeof(T) == typeof(decimal))  return (T)(object)ReadDecimal(offset);
67,135,159 ✔
122
        if (typeof(T) == typeof(DateTime)) return (T)(object)ReadDateTime(offset);
88,504,435 ✔
123
        if (typeof(T) == typeof(DateTimeOffset)) return (T)(object)ReadDateTimeOffset(offset);
40,503,043 ✔
124
        if (typeof(T) == typeof(DateOnly)) return (T)(object)ReadDateOnly(offset);
49,128,668 ✔
125
        if (typeof(T) == typeof(TimeOnly)) return (T)(object)ReadTimeOnly(offset);
27,554,337 ✔
126
        if (typeof(T) == typeof(Guid))     return (T)(object)ReadGuid(offset);
27,625,686 ✔
127
        if (typeof(T) == typeof(string))   return (T)(object)ReadString(offset);
27,034,009 ✔
128
        if (typeof(T) == typeof(Stream))   return (T)(object)ReadStream(offset);
22,790,008 ✔
129
        if (typeof(T) == typeof(object))   return (T)GetValueCore(offset, ClrType);
22,593,280 ✔
130

131
        return GetValidValue<T>(offset);
22,590,508 ✔
132
    }
133

134
    public object GetValue(ulong offset)
135
    {
136
        if (!IsValid(offset)) return null!;
12,665,449 !
137
        return GetValueCore(offset, ClrType);
12,665,449 ✔
138
    }
139

140
    // The value as targetType, which is known only at run time: a list item, a map key or value, a struct field read
141
    // into a property without dynamic code. object means the column's own .NET type, as GetFieldValue<object> and
142
    // GetValue(ordinal) give it, for every reader; ReadValidValue<object> does the same on the typed path.
143
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
144
    internal object GetValue(ulong offset, Type targetType) => GetValueCore(offset, targetType == typeof(object) ? ClrType : targetType);
5,349,565 ✔
145

146
    // What each reader converts. Called with any type but object.
147
    internal virtual object GetValueCore(ulong offset, Type targetType)
148
    {
149
        return DuckDBType switch
3 !
150
        {
3 ✔
151
            DuckDBType.Invalid => throw new DuckDBException($"Invalid type for column {ColumnName}"),
×
152
            _ => throw new ArgumentException($"Unrecognised type {DuckDBType} ({(int)DuckDBType}) for column {ColumnName}")
3 ✔
153
        };
3 ✔
154
    }
155

156
    internal object GetProviderSpecificValue(ulong offset) => GetValueCore(offset, ProviderSpecificClrType);
117 ✔
157

158
    protected virtual Type GetColumnType()
159
    {
160
        return DuckDBType switch
20,966 !
161
        {
20,966 ✔
162
            DuckDBType.Invalid => throw new DuckDBException($"Invalid type for column {ColumnName}"),
×
163
            DuckDBType.Boolean => typeof(bool),
267 ✔
164
            DuckDBType.TinyInt => typeof(sbyte),
159 ✔
165
            DuckDBType.SmallInt => typeof(short),
186 ✔
166
            DuckDBType.Integer => typeof(int),
1,908 ✔
167
            DuckDBType.BigInt => typeof(long),
624 ✔
168
            DuckDBType.UnsignedTinyInt => typeof(byte),
129 ✔
169
            DuckDBType.UnsignedSmallInt => typeof(ushort),
141 ✔
170
            DuckDBType.UnsignedInteger => typeof(uint),
156 ✔
171
            DuckDBType.UnsignedBigInt => typeof(ulong),
210 ✔
172
            DuckDBType.Float => typeof(float),
219 ✔
173
            DuckDBType.Double => typeof(double),
273 ✔
174
            DuckDBType.Timestamp => typeof(DateTime),
129 ✔
175
            DuckDBType.Interval => typeof(TimeSpan),
48 ✔
176
            DuckDBType.Date => typeof(DateOnly),
117 ✔
177
            DuckDBType.Time => typeof(TimeOnly),
135 ✔
178
            DuckDBType.TimeNs => typeof(TimeOnly),
15 ✔
179
            DuckDBType.TimeTz => typeof(DateTimeOffset),
72 ✔
180
            // The exact types of the two columns (BigInteger before 2.0). VARINT has no bound and stays BigInteger.
20,966 ✔
181
            DuckDBType.HugeInt => typeof(Int128),
261 ✔
182
            DuckDBType.UnsignedHugeInt => typeof(UInt128),
189 ✔
183
            DuckDBType.Varchar => typeof(string),
1,062 ✔
184
            DuckDBType.Decimal => typeof(decimal),
843 ✔
185
            DuckDBType.TimestampS => typeof(DateTime),
48 ✔
186
            DuckDBType.TimestampMs => typeof(DateTime),
48 ✔
187
            DuckDBType.TimestampNs => typeof(DateTime),
93 ✔
188
            // A copy (Stream before 2.0). The stream points into the chunk, so a stream from GetValue or
20,966 ✔
189
            // ExecuteScalar read freed memory once the reader moved on. GetStream and GetFieldValue<Stream> still give one.
20,966 ✔
190
            DuckDBType.Blob => typeof(byte[]),
33 ✔
191
            DuckDBType.Enum => typeof(string),
192 ✔
192
            DuckDBType.Uuid => typeof(Guid),
48 ✔
193
            DuckDBType.Struct => typeof(Dictionary<string, object>),
54 ✔
194
            DuckDBType.Bit => typeof(string),
21 ✔
195
            DuckDBType.TimestampTz => typeof(DateTime),
48 ✔
196
            DuckDBType.VarInt => typeof(BigInteger),
13,223 ✔
197
            DuckDBType.SqlNull => typeof(object),
9 ✔
198
            _ => throw new ArgumentException($"Unrecognised type {DuckDBType} ({(int)DuckDBType}) for column {ColumnName}")
6 ✔
199
        };
20,966 ✔
200
    }
201

202
    protected virtual Type GetColumnProviderSpecificType()
203
    {
204
        return DuckDBType switch
195 !
205
        {
195 ✔
UNCOV
206
            DuckDBType.Invalid => throw new DuckDBException($"Invalid type for column {ColumnName}"),
×
207
            DuckDBType.Boolean => typeof(bool),
3 ✔
208
            DuckDBType.TinyInt => typeof(sbyte),
3 ✔
209
            DuckDBType.SmallInt => typeof(short),
3 ✔
210
            DuckDBType.Integer => typeof(int),
9 ✔
211
            DuckDBType.BigInt => typeof(long),
3 ✔
212
            DuckDBType.UnsignedTinyInt => typeof(byte),
3 ✔
213
            DuckDBType.UnsignedSmallInt => typeof(ushort),
3 ✔
214
            DuckDBType.UnsignedInteger => typeof(uint),
3 ✔
215
            DuckDBType.UnsignedBigInt => typeof(ulong),
3 ✔
216
            DuckDBType.Float => typeof(float),
3 ✔
217
            DuckDBType.Double => typeof(double),
6 ✔
218
            DuckDBType.Timestamp => typeof(DuckDBTimestamp),
9 ✔
219
            DuckDBType.Interval => typeof(DuckDBInterval),
3 ✔
220
            DuckDBType.Date => typeof(DuckDBDateOnly),
6 ✔
221
            DuckDBType.Time => typeof(DuckDBTimeOnly),
3 ✔
222
            DuckDBType.TimeNs => typeof(DuckDBTimeOnly),
6 ✔
223
            DuckDBType.TimeTz => typeof(DuckDBTimeTz),
3 ✔
224
            DuckDBType.HugeInt => typeof(DuckDBHugeInt),
18 ✔
225
            DuckDBType.UnsignedHugeInt => typeof(DuckDBUHugeInt),
15 ✔
226
            DuckDBType.Varchar => typeof(string),
9 ✔
227
            DuckDBType.Decimal => typeof(DuckDBDecimal),
39 ✔
228
            DuckDBType.TimestampS => typeof(DuckDBTimestamp),
3 ✔
229
            DuckDBType.TimestampMs => typeof(DuckDBTimestamp),
3 ✔
230
            DuckDBType.TimestampNs => typeof(DuckDBTimestamp),
3 ✔
231
            DuckDBType.Blob => typeof(byte[]),
6 ✔
232
            DuckDBType.Enum => typeof(string),
9 ✔
233
            DuckDBType.Uuid => typeof(Guid),
3 ✔
234
            DuckDBType.Struct => typeof(Dictionary<string, object>),
3 ✔
235
            DuckDBType.Bit => typeof(string),
3 ✔
236
            DuckDBType.TimestampTz => typeof(DuckDBTimestamp),
6 ✔
237
            DuckDBType.VarInt => typeof(BigInteger),
3 ✔
UNCOV
238
            DuckDBType.SqlNull => typeof(object),
×
UNCOV
239
            _ => throw new ArgumentException($"Unrecognised type {DuckDBType} ({(int)DuckDBType}) for column {ColumnName}")
×
240
        };
195 ✔
241
    }
242

243
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
244
    protected unsafe T GetFieldData<T>(ulong offset) where T : unmanaged => *((T*)DataPointer + offset);
71,292,715 ✔
245

246
    // Kept out of the callers: building the message inline puts its buffers in the stack frame of a method
247
    // that almost never throws, and that frame is cleared on every call.
248
    [MethodImpl(MethodImplOptions.NoInlining)]
249
    private protected InvalidCastException CannotCast(Type from, Type to) => CannotCast(from.Name, to);
25,929 ✔
250

251
    [MethodImpl(MethodImplOptions.NoInlining)]
252
    private protected InvalidCastException CannotCast(string from, Type to) => new($"Cannot cast from {from} to {to.Name} in column {ColumnName}");
126,633 ✔
253

254
    // A FLOAT, DOUBLE or DECIMAL with a fraction, read as an integer type.
255
    [MethodImpl(MethodImplOptions.NoInlining)]
256
    private protected InvalidCastException CannotCastFraction(Type to) => new($"Cannot cast a value with a non-zero fractional part to {to.Name} in column {ColumnName}");
10,341 ✔
257

258
    /// <summary>
259
    /// Updates the data and validity pointers for a new chunk without recreating the reader.
260
    /// Composite readers (Struct, List, Map, Decimal) override this to also reset nested readers.
261
    /// </summary>
262
    internal virtual unsafe void Reset(IntPtr vector)
263
    {
264
        DataPointer = NativeMethods.Vectors.DuckDBVectorGetData(vector);
16,755 ✔
265
        validityMaskPointer = NativeMethods.Vectors.DuckDBVectorGetValidity(vector);
16,755 ✔
266
    }
16,755 ✔
267

268
    public virtual void Dispose()
269
    {
270
    }
40,136 ✔
271

272
    private sealed class NullableReader<TValue> where TValue : struct
273
    {
274
        public TValue? Read(VectorDataReaderBase reader, ulong offset)
275
        {
276
            return reader.IsValid(offset) ? reader.ReadValidValue<TValue>(offset) : null;
326,142 ✔
277
        }
278
    }
279

280
    private static class NullableHandler<T>
281
    {
282
        private static readonly Type? underlyingType;
283

284
        static NullableHandler()
285
        {
286
            var type = typeof(T);
1,305 ✔
287

288
            var allowsNullValue = type.AllowsNullValue(out IsNullableValueType, out underlyingType);
1,305 ✔
289

290
            Read = IsNullableValueType ? Compile() : null!;
1,305 ✔
291
            IsReferenceType = allowsNullValue && !IsNullableValueType;
1,305 ✔
292
        }
1,305 ✔
293

294
        public static readonly bool IsNullableValueType;
295
        public static readonly bool IsReferenceType;
296
        public static readonly Func<VectorDataReaderBase, ulong, T> Read;
297

298
        // For T = int?, a delegate for the Read method of a NullableReader<int>. It is an ordinary method, so the
299
        // JIT compiles it in tiers and with profile data like any other; a delegate compiled from an expression
300
        // tree is compiled once. An instance method, because a delegate for a static method is slower to call.
301
        //
302
        // The reader is found in two ways, because of applications that are trimmed or compiled ahead of time:
303
        // - The common types, and the structs of this library, are named below. That is code a compiler can see,
304
        //   so an application compiled ahead of time has a NullableReader for each of them.
305
        // - Any other type, such as an enum of the caller, is found through reflection. The attribute keeps the
306
        //   trimmer from removing the constructor and the Read method, which nothing calls by name.
307
        // - An application compiled ahead of time has no code for such a type and cannot make any. There the
308
        //   value is read as an object and unboxed, which needs no code for the type. It is slower.
309
        [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods, typeof(NullableReader<>))]
310
        private static Func<VectorDataReaderBase, ulong, T> Compile()
311
        {
312
            if (underlyingType is null) return null!;
108 !
313

314
            var known = Known<bool>() ?? Known<byte>() ?? Known<sbyte>() ?? Known<short>() ?? Known<ushort>()
108 ✔
315
                        ?? Known<int>() ?? Known<uint>() ?? Known<long>() ?? Known<ulong>() ?? Known<Int128>() ?? Known<UInt128>()
108 ✔
316
                        ?? Known<float>() ?? Known<double>() ?? Known<decimal>() ?? Known<BigInteger>()
108 ✔
317
                        ?? Known<DateTime>() ?? Known<DateOnly>() ?? Known<TimeOnly>() ?? Known<TimeSpan>() ?? Known<DateTimeOffset>()
108 ✔
318
                        ?? Known<Guid>()
108 ✔
319
                        ?? Known<DuckDBDateOnly>() ?? Known<DuckDBTimeOnly>() ?? Known<DuckDBTimeTz>() ?? Known<DuckDBTimestamp>() ?? Known<DuckDBInterval>()
108 ✔
320
                        ?? Known<DuckDBHugeInt>() ?? Known<DuckDBUHugeInt>() ?? Known<DuckDBDecimal>();
108 ✔
321

322
            if (known != null)
108 ✔
323
            {
324
                return known;
87 ✔
325
            }
326

327
            if (RuntimeFeature.IsDynamicCodeSupported)
21 !
328
            {
329
                var reader = Activator.CreateInstance(typeof(NullableReader<>).MakeGenericType(underlyingType))!;
21 ✔
330

331
                return (Func<VectorDataReaderBase, ulong, T>)Delegate.CreateDelegate(typeof(Func<VectorDataReaderBase, ulong, T>), reader, nameof(NullableReader<int>.Read));
21 ✔
332
            }
333

334
            var valueType = underlyingType;
×
335

UNCOV
336
            return (reader, offset) => reader.IsValid(offset) ? (T)reader.GetValue(offset, valueType) : default!;
×
337
        }
338

339
        private static Func<VectorDataReaderBase, ulong, T>? Known<TValue>() where TValue : struct
340
        {
341
            if (typeof(T) != typeof(TValue?))
1,914 ✔
342
            {
343
                return null;
1,827 ✔
344
            }
345

346
            return (Func<VectorDataReaderBase, ulong, T>)(object)new Func<VectorDataReaderBase, ulong, TValue?>(new NullableReader<TValue>().Read);
87 ✔
347
        }
348
    }
349
}
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