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

Giorgi / DuckDB.NET / 22734550158

05 Mar 2026 08:06PM UTC coverage: 89.491%. Remained the same
22734550158

push

github

Giorgi
Add NULL handling support for scalar functions

Replace `isPureFunction` bool with `ScalarFunctionOptions`
record that supports `IsPureFunction` and `HandlesNulls`.

- Add `ScalarFunctionOptions` with `IsPureFunction` and
  `HandlesNulls` properties
- Bind `duckdb_scalar_function_set_special_handling` native
  method to enable NULL passthrough
- Update simplified scalar function extensions to validate
  nullable parameter types when `HandlesNulls` is true
- Propagate NULL correctly: nullable types get default,
  non-nullable types throw with a clear error message
- Update existing tests to use new options API

1287 of 1499 branches covered (85.86%)

Branch coverage included in aggregate %.

41 of 49 new or added lines in 3 files covered. (83.67%)

2 existing lines in 2 files now uncovered.

2630 of 2878 relevant lines covered (91.38%)

446353.59 hits per line

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

89.72
/DuckDB.NET.Data/DuckDBConnection.ScalarFunction.Extensions.cs
1
using System.Linq;
2
using DuckDB.NET.Data.DataChunk.Reader;
3
using DuckDB.NET.Data.DataChunk.Writer;
4
using System.Runtime.CompilerServices;
5

6
namespace DuckDB.NET.Data;
7

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

23
        public void RegisterScalarFunction<T, TResult>(string name, Func<T, TResult> func, ScalarFunctionOptions? options = null)
24
        {
25
            ValidateHandlesNulls(options, typeof(T));
24✔
26
            var handlesNulls = options?.HandlesNulls ?? false;
21✔
27

28
            connection.RegisterScalarFunction<T, TResult>(name, WrapScalarFunction<TResult>((readers, index) =>
21✔
29
                func(ReadValue<T>(readers[0], index, handlesNulls))), options);
363✔
30
        }
21✔
31

32
        public void RegisterScalarFunction<T, TResult>(string name, Func<T[], TResult> func, ScalarFunctionOptions? options = null)
33
        {
34
            ValidateHandlesNulls(options, typeof(T));
21✔
35
            var handlesNulls = options?.HandlesNulls ?? false;
21✔
36

37
            connection.RegisterScalarFunction<T, TResult>(name,
21✔
38
                WrapVarargsScalarFunction(func, handlesNulls), options, @params: true);
21✔
39
        }
21✔
40

41
        public void RegisterScalarFunction<T1, T2, TResult>(string name, Func<T1, T2, TResult> func, ScalarFunctionOptions? options = null)
42
        {
43
            ValidateHandlesNulls(options, typeof(T1), typeof(T2));
15✔
44
            var handlesNulls = options?.HandlesNulls ?? false;
15✔
45

46
            connection.RegisterScalarFunction<T1, T2, TResult>(name, WrapScalarFunction<TResult>((readers, index) =>
15✔
47
                func(ReadValue<T1>(readers[0], index, handlesNulls), ReadValue<T2>(readers[1], index, handlesNulls))), options);
39✔
48
        }
15✔
49

50
        public void RegisterScalarFunction<T1, T2, T3, TResult>(string name, Func<T1, T2, T3, TResult> func, ScalarFunctionOptions? options = null)
51
        {
52
            ValidateHandlesNulls(options, typeof(T1), typeof(T2), typeof(T3));
6✔
53
            var handlesNulls = options?.HandlesNulls ?? false;
6!
54

55
            connection.RegisterScalarFunction<T1, T2, T3, TResult>(name, WrapScalarFunction<TResult>((readers, index) =>
6✔
56
                func(ReadValue<T1>(readers[0], index, handlesNulls), ReadValue<T2>(readers[1], index, handlesNulls),
42✔
57
                     ReadValue<T3>(readers[2], index, handlesNulls))), options);
42✔
58
        }
6✔
59

60
        public void RegisterScalarFunction<T1, T2, T3, T4, TResult>(string name, Func<T1, T2, T3, T4, TResult> func, ScalarFunctionOptions? options = null)
61
        {
NEW
62
            ValidateHandlesNulls(options, typeof(T1), typeof(T2), typeof(T3), typeof(T4));
×
NEW
63
            var handlesNulls = options?.HandlesNulls ?? false;
×
64

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

71
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
72
    private static T ReadValue<T>(IDuckDBDataReader reader, ulong index, bool handlesNulls)
73
    {
74
        if (typeof(T) == typeof(object))
120,573✔
75
        {
76
            if (handlesNulls && !reader.IsValid(index)) return default!;
45!
77
            return (T)reader.GetValue(index);
45✔
78
        }
79

80
        if (handlesNulls && !reader.IsValid(index))
120,528✔
81
        {
82
            if (default(T) is null) return default!;
33✔
83
            ThrowNullReceivedByNonNullableParam<T>();
9✔
84
        }
85

86
        return reader.GetValue<T>(index);
120,507✔
87
    }
88

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

96
    private static Action<IReadOnlyList<IDuckDBDataReader>, IDuckDBDataWriter, ulong> WrapScalarFunction<TResult>(Func<IReadOnlyList<IDuckDBDataReader>, ulong, TResult> perRowFunc)
97
    {
98
        return (readers, writer, rowCount) =>
42✔
99
        {
42✔
100
            for (ulong index = 0; index < rowCount; index++)
912✔
101
            {
42✔
102
                var result = perRowFunc(readers, index);
402✔
103

42✔
104
                writer.WriteValue(result, index);
393✔
105
            }
42✔
106
        };
96✔
107
    }
108

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

21✔
115
            for (ulong index = 0; index < rowCount; index++)
210,240✔
116
            {
21✔
117
                for (int r = 0; r < readers.Count; r++)
450,216✔
118
                {
21✔
119
                    args[r] = ReadValue<T>(readers[r], index, handlesNulls);
120,075✔
120
                }
21✔
121

21✔
122
                writer.WriteValue(func(args), index);
105,033✔
123
            }
21✔
124
        };
108✔
125
    }
126

127
    //If HandlesNulls is true, at least one parameter type must be nullable to allow null values to be passed in.
128
    private static void ValidateHandlesNulls(ScalarFunctionOptions? options, params Type[] parameterTypes)
129
    {
130
        if (options?.HandlesNulls == true && !parameterTypes.Any(t => t.AllowsNullValue(out _, out _)))
84✔
131
            throw new ArgumentException("HandlesNulls requires at least one nullable parameter type (use int? instead of int).");
3✔
132
    }
63✔
133
}
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