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

Giorgi / DuckDB.NET / 22870989532

09 Mar 2026 07:28PM UTC coverage: 89.342% (+0.09%) from 89.254%
22870989532

push

github

Giorgi
Infer scalar function null handling from parameter nullability

Use NullabilityInfoContext to detect nullable parameters and
automatically set HandlesNulls, removing the need for callers
to pass ScalarFunctionOptions. Add strict/non-strict GetValue
modes so reference types return null instead of throwing in
non-strict contexts (e.g. scalar function callbacks).

1253 of 1463 branches covered (85.65%)

Branch coverage included in aggregate %.

43 of 48 new or added lines in 6 files covered. (89.58%)

1 existing line in 1 file now uncovered.

2645 of 2900 relevant lines covered (91.21%)

445447.62 hits per line

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

93.14
/DuckDB.NET.Data/DuckDBConnection.ScalarFunction.Extensions.cs
1
using DuckDB.NET.Data.DataChunk.Reader;
2
using DuckDB.NET.Data.DataChunk.Writer;
3
using System.Diagnostics.CodeAnalysis;
4
using System.Linq;
5
using System.Reflection;
6
using System.Runtime.CompilerServices;
7

8
namespace DuckDB.NET.Data;
9

10
public static class DuckDBConnectionScalarFunctionExtensions
11
{
12
    extension(DuckDBConnection connection)
13
    {
14
        public void RegisterScalarFunction<TResult>(string name, Func<TResult> func)
15
        {
16
            connection.RegisterScalarFunction<TResult>(name, (writer, rowCount) =>
3✔
17
            {
3✔
18
                for (ulong index = 0; index < rowCount; index++)
36✔
19
                {
3✔
20
                    writer.WriteValue(func(), index);
15✔
21
                }
3✔
22
            });
6✔
23
        }
3✔
24

25
        public void RegisterScalarFunction<T, TResult>(string name, Func<T, TResult> func)
26
        {
27
            var (nullability, anyNullable) = InferParameterNullability(func);
27✔
28

29
            connection.RegisterScalarFunction<T, TResult>(name, WrapScalarFunction<TResult>((readers, index) =>
27✔
30
                func(ReadValue<T>(readers[0], index, nullability[0], anyNullable))), new() { HandlesNulls = anyNullable });
369✔
31
        }
27✔
32

33
        public void RegisterScalarFunction<T, TResult>(string name, Func<T[], TResult> func)
34
        {
35
            var elementNullable = InferArrayElementNullability(func);
21✔
36

37
            connection.RegisterScalarFunction<T, TResult>(name,
21✔
38
                WrapVarargsScalarFunction(func, elementNullable), new() { HandlesNulls = elementNullable }, @params: true);
21✔
39
        }
21✔
40

41
        public void RegisterScalarFunction<T1, T2, TResult>(string name, Func<T1, T2, TResult> func)
42
        {
43
            var (nullability, anyNullable) = InferParameterNullability(func);
18✔
44

45
            connection.RegisterScalarFunction<T1, T2, TResult>(name, WrapScalarFunction<TResult>((readers, index) =>
18✔
46
                func(ReadValue<T1>(readers[0], index, nullability[0], anyNullable), ReadValue<T2>(readers[1], index, nullability[1], anyNullable))),
33✔
47
                new() { HandlesNulls = anyNullable });
18✔
48
        }
18✔
49

50
        public void RegisterScalarFunction<T1, T2, T3, TResult>(string name, Func<T1, T2, T3, TResult> func)
51
        {
52
            var (nullability, anyNullable) = InferParameterNullability(func);
6✔
53

54
            connection.RegisterScalarFunction<T1, T2, T3, TResult>(name, WrapScalarFunction<TResult>((readers, index) =>
6✔
55
                func(ReadValue<T1>(readers[0], index, nullability[0], anyNullable), ReadValue<T2>(readers[1], index, nullability[1], anyNullable),
36✔
56
                     ReadValue<T3>(readers[2], index, nullability[2], anyNullable))),
36✔
57
                new() { HandlesNulls = anyNullable });
6✔
58
        }
6✔
59

60
        public void RegisterScalarFunction<T1, T2, T3, T4, TResult>(string name, Func<T1, T2, T3, T4, TResult> func)
61
        {
NEW
62
            var (nullability, anyNullable) = InferParameterNullability(func);
×
63

64
            connection.RegisterScalarFunction<T1, T2, T3, T4, TResult>(name, WrapScalarFunction<TResult>((readers, index) =>
×
NEW
65
                func(ReadValue<T1>(readers[0], index, nullability[0], anyNullable), ReadValue<T2>(readers[1], index, nullability[1], anyNullable),
×
NEW
66
                     ReadValue<T3>(readers[2], index, nullability[2], anyNullable), ReadValue<T4>(readers[3], index, nullability[3], anyNullable))),
×
NEW
67
                new() { HandlesNulls = anyNullable });
×
UNCOV
68
        }
×
69
    }
70

71
    // checksNulls: true when any parameter in the function is nullable (special handling active).
72
    // Needed because set_special_handling is function-level — DuckDB sends NULLs for ALL params,
73
    // so non-nullable params must also check and throw a descriptive error.
74
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
75
    private static T ReadValue<T>(IDuckDBDataReader reader, ulong index, bool isNullable, bool checksNulls)
76
    {
77
        if (checksNulls && !reader.IsValid(index))
30,597✔
78
        {
79
            if (isNullable) return default!;
48✔
80
            ThrowNullReceivedByNonNullableParam<T>();
18✔
81
        }
82

83
        return typeof(T) == typeof(object) ? (T)reader.GetValue(index) : reader.GetValue<T>(index);
30,564✔
84
    }
85

86
    [DoesNotReturn]
87
    private static void ThrowNullReceivedByNonNullableParam<T>()
88
    {
89
        throw new InvalidOperationException(
18✔
90
            $"Scalar function parameter of type '{typeof(T).Name}' received NULL. " +
18✔
91
            $"Use '{typeof(T).Name}?' to handle NULL values.");
18✔
92
    }
93

94
    private static Action<IReadOnlyList<IDuckDBDataReader>, IDuckDBDataWriter, ulong> WrapScalarFunction<TResult>(Func<IReadOnlyList<IDuckDBDataReader>, ulong, TResult> perRowFunc)
95
    {
96
        return (readers, writer, rowCount) =>
51✔
97
        {
51✔
98
            for (ulong index = 0; index < rowCount; index++)
930✔
99
            {
51✔
100
                var result = perRowFunc(readers, index);
411✔
101

51✔
102
                writer.WriteValue(result, index);
393✔
103
            }
51✔
104
        };
105✔
105
    }
106

107
    private static Action<IReadOnlyList<IDuckDBDataReader>, IDuckDBDataWriter, ulong> WrapVarargsScalarFunction<T, TResult>(Func<T[], TResult> func, bool elementNullable)
108
    {
109
        return (readers, writer, rowCount) =>
21✔
110
        {
21✔
111
            var args = new T[readers.Count];
48✔
112

21✔
113
            for (ulong index = 0; index < rowCount; index++)
30,174✔
114
            {
21✔
115
                for (int r = 0; r < readers.Count; r++)
90,258✔
116
                {
21✔
117
                    args[r] = ReadValue<T>(readers[r], index, elementNullable, elementNullable);
30,090✔
118
                }
21✔
119

21✔
120
                writer.WriteValue(func(args), index);
15,039✔
121
            }
21✔
122
        };
69✔
123
    }
124

125
    private static (bool[] perParam, bool anyNullable) InferParameterNullability(Delegate func)
126
    {
127
        var context = new NullabilityInfoContext();
51✔
128
        var parameters = func.Method.GetParameters();
51✔
129
        var result = parameters.Select(info => IsNullableParameter(context, info)).ToArray();
132✔
130

131
        return (result, result.Any(static x => x));
126✔
132
    }
133

134
    private static bool InferArrayElementNullability(Delegate func)
135
    {
136
        var parameter = func.Method.GetParameters()[0];
21✔
137
        var elementType = parameter.ParameterType.GetElementType();
21✔
138

139
        // Nullable<T> value types: detectable without attributes
140
        if (elementType != null && Nullable.GetUnderlyingType(elementType) != null)
21✔
141
            return true;
3✔
142

143
        // Reference types: check nullable annotation
144
        var context = new NullabilityInfoContext();
18✔
145
        var info = context.Create(parameter);
18✔
146
        return info.ElementType?.ReadState == NullabilityState.Nullable;
18!
147
    }
148

149
    private static bool IsNullableParameter(NullabilityInfoContext context, ParameterInfo parameter)
150
    {
151
        // Nullable<T> value types are always nullable
152
        if (Nullable.GetUnderlyingType(parameter.ParameterType) != null)
81✔
153
            return true;
12✔
154

155
        // Reference types: check nullable annotation
156
        var info = context.Create(parameter);
69✔
157
        return info.ReadState == NullabilityState.Nullable;
69✔
158
    }
159

160
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc