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

Giorgi / DuckDB.NET / 42028758342

05 Nov 2026 11:59AM UTC coverage: 93.133% (+0.4%) from 92.771%
42028758342

push

github

Giorgi
Load Int128 and UInt128 from a HUGEINT and UHUGEINT vector without a conversion

StorageType of the two columns is Int128 and UInt128, their default types,
where it was the DuckDB structs: the vector holds the 16 bytes of an Int128 on a
little-endian machine. ReadValidValue<Int128> then compares the type and loads
the value (vmovups, unaligned), for GetFieldValue, nullable reads, list items,
map keys and values, struct fields and function inputs. A read of the DuckDB
structs, which lose the fast path, is taken from the vector in GetValidValue, so
it is not boxed through the base class.

Read in the Tier 1 listing of .NET 10.0.14: ReadValidValue<Int128> is inlined
into GetFieldValue<Int128>; on .NET 8 it stays a call. Not measured.

2.0 item 9 (#370).

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

2740 of 2995 branches covered (91.49%)

Branch coverage included in aggregate %.

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

55 existing lines in 14 files now uncovered.

4299 of 4563 relevant lines covered (94.21%)

1062879.99 hits per line

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

92.99
/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,240 ✔
12

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

15

16
    public string ColumnName { get; }
238,657 ✔
17
    public DuckDBType DuckDBType { get; }
89,238,132 ✔
18
    private protected unsafe void* DataPointer { get; private set; }
96,388,296 ✔
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,979,384 ✔
24

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

30
        DuckDBType = columnType;
56,914 ✔
31
        ColumnName = columnName;
56,914 ✔
32
    }
56,914 ✔
33

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

42
        return ValidityMask.IsValid(validityMaskPointer, offset);
15,007,232 ✔
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,530 ✔
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,022 ✔
59
        {
60
            return NullableHandler<T>.Read(this, offset);
326,142 ✔
61
        }
62

63
        if (IsValid(offset))
54,391,880 ✔
64
        {
65
            return ReadValidValue<T>(offset);
54,391,688 ✔
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,240,543 ✔
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,242 ✔
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,498,750 ✔
109
        {
110
            return Unsafe.Read<T>((byte*)DataPointer + offset * (ulong)Unsafe.SizeOf<T>());
18,549,955 ✔
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,948,801 ✔
115
        if (typeof(T) == typeof(byte))     return (T)(object)ReadByte(offset);
65,961,085 ✔
116
        if (typeof(T) == typeof(short))    return (T)(object)ReadInt16(offset);
65,948,783 ✔
117
        if (typeof(T) == typeof(int))      return (T)(object)ReadInt32(offset);
65,970,921 ✔
118
        if (typeof(T) == typeof(long))     return (T)(object)ReadInt64(offset);
66,129,871 ✔
119
        if (typeof(T) == typeof(float))    return (T)(object)ReadFloat(offset);
65,630,640 ✔
120
        if (typeof(T) == typeof(double))   return (T)(object)ReadDouble(offset);
65,743,557 ✔
121
        if (typeof(T) == typeof(decimal))  return (T)(object)ReadDecimal(offset);
67,109,004 ✔
122
        if (typeof(T) == typeof(DateTime)) return (T)(object)ReadDateTime(offset);
88,516,987 ✔
123
        if (typeof(T) == typeof(DateTimeOffset)) return (T)(object)ReadDateTimeOffset(offset);
40,491,690 ✔
124
        if (typeof(T) == typeof(DateOnly)) return (T)(object)ReadDateOnly(offset);
49,117,644 ✔
125
        if (typeof(T) == typeof(TimeOnly)) return (T)(object)ReadTimeOnly(offset);
27,543,294 ✔
126
        if (typeof(T) == typeof(Guid))     return (T)(object)ReadGuid(offset);
27,614,937 ✔
127
        if (typeof(T) == typeof(string))   return (T)(object)ReadString(offset);
27,025,021 ✔
128
        if (typeof(T) == typeof(Stream))   return (T)(object)ReadStream(offset);
22,774,503 ✔
129
        if (typeof(T) == typeof(object))   return (T)GetValueCore(offset, ClrType);
22,577,781 ✔
130

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

134
    public object GetValue(ulong offset)
135
    {
136
        if (!IsValid(offset)) return null!;
12,665,443 !
137
        return GetValueCore(offset, ClrType);
12,665,443 ✔
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,351,321 ✔
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);
114 ✔
157

158
    protected virtual Type GetColumnType()
159
    {
160
        return DuckDBType switch
20,926 !
161
        {
20,926 ✔
162
            DuckDBType.Invalid => throw new DuckDBException($"Invalid type for column {ColumnName}"),
×
163
            DuckDBType.Boolean => typeof(bool),
255 ✔
164
            DuckDBType.TinyInt => typeof(sbyte),
159 ✔
165
            DuckDBType.SmallInt => typeof(short),
186 ✔
166
            DuckDBType.Integer => typeof(int),
1,908 ✔
167
            DuckDBType.BigInt => typeof(long),
618 ✔
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 (2.0 item 9; BigInteger before). VARINT has no bound and stays BigInteger.
20,926 ✔
181
            DuckDBType.HugeInt => typeof(Int128),
261 ✔
182
            DuckDBType.UnsignedHugeInt => typeof(UInt128),
189 ✔
183
            DuckDBType.Varchar => typeof(string),
1,044 ✔
184
            DuckDBType.Decimal => typeof(decimal),
840 ✔
185
            DuckDBType.TimestampS => typeof(DateTime),
48 ✔
186
            DuckDBType.TimestampMs => typeof(DateTime),
48 ✔
187
            DuckDBType.TimestampNs => typeof(DateTime),
93 ✔
188
            DuckDBType.Blob => typeof(Stream),
30 ✔
189
            DuckDBType.Enum => typeof(string),
192 ✔
190
            DuckDBType.Uuid => typeof(Guid),
48 ✔
191
            DuckDBType.Struct => typeof(Dictionary<string, object>),
54 ✔
192
            DuckDBType.Bit => typeof(string),
21 ✔
193
            DuckDBType.TimestampTz => typeof(DateTime),
48 ✔
194
            DuckDBType.VarInt => typeof(BigInteger),
13,225 ✔
195
            DuckDBType.SqlNull => typeof(object),
9 ✔
196
            _ => throw new ArgumentException($"Unrecognised type {DuckDBType} ({(int)DuckDBType}) for column {ColumnName}")
6 ✔
197
        };
20,926 ✔
198
    }
199

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

241
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
242
    protected unsafe T GetFieldData<T>(ulong offset) where T : unmanaged => *((T*)DataPointer + offset);
71,270,649 ✔
243

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

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

252
    // A FLOAT, DOUBLE or DECIMAL with a fraction, read as an integer type (2.0 item 5).
253
    [MethodImpl(MethodImplOptions.NoInlining)]
254
    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 ✔
255

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

266
    public virtual void Dispose()
267
    {
268
    }
39,889 ✔
269

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

278
    private static class NullableHandler<T>
279
    {
280
        private static readonly Type? underlyingType;
281

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

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

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

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

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

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

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

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

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

UNCOV
332
            var valueType = underlyingType;
×
333

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

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

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