• 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

88.37
/DuckDB.NET.Data/DuckDBConnection.ScalarFunction.cs
1
using DuckDB.NET.Data.Connection;
2
using DuckDB.NET.Data.DataChunk.Reader;
3
using DuckDB.NET.Data.DataChunk.Writer;
4
using System.Runtime.CompilerServices;
5
using System.Runtime.InteropServices;
6

7
namespace DuckDB.NET.Data;
8

9
partial class DuckDBConnection
10
{
11
    public void RegisterScalarFunction<TResult>(string name, Action<IDuckDBDataWriter, ulong> action, ScalarFunctionOptions? options = null)
12
    {
13
        RegisterScalarMethod(name, (_, w, index) => action(w, index), TypeExtensions.GetLogicalType<TResult>(), varargs: false, options);
12✔
14
    }
6✔
15

16
    public void RegisterScalarFunction<T, TResult>(string name, Action<IReadOnlyList<IDuckDBDataReader>, IDuckDBDataWriter, ulong> action, ScalarFunctionOptions? options = null, bool @params = false)
17
    {
18
        RegisterScalarMethod(name, action, TypeExtensions.GetLogicalType<TResult>(), @params, options, TypeExtensions.GetLogicalType<T>());
57✔
19
    }
57✔
20

21
    public void RegisterScalarFunction<T1, T2, TResult>(string name, Action<IReadOnlyList<IDuckDBDataReader>, IDuckDBDataWriter, ulong> action, ScalarFunctionOptions? options = null)
22
    {
23
        RegisterScalarMethod(name, action, TypeExtensions.GetLogicalType<TResult>(), varargs: false, options,
21✔
24
            TypeExtensions.GetLogicalType<T1>(), TypeExtensions.GetLogicalType<T2>());
21✔
25
    }
21✔
26

27
    public void RegisterScalarFunction<T1, T2, T3, TResult>(string name, Action<IReadOnlyList<IDuckDBDataReader>, IDuckDBDataWriter, ulong> action, ScalarFunctionOptions? options = null)
28
    {
29
        RegisterScalarMethod(name, action, TypeExtensions.GetLogicalType<TResult>(), varargs: false, options,
6✔
30
            TypeExtensions.GetLogicalType<T1>(), TypeExtensions.GetLogicalType<T2>(), TypeExtensions.GetLogicalType<T3>());
6✔
31
    }
6✔
32

33
    public void RegisterScalarFunction<T1, T2, T3, T4, TResult>(string name, Action<IReadOnlyList<IDuckDBDataReader>, IDuckDBDataWriter, ulong> action, ScalarFunctionOptions? options = null)
34
    {
NEW
35
        RegisterScalarMethod(name, action, TypeExtensions.GetLogicalType<TResult>(), varargs: false, options,
×
NEW
36
            TypeExtensions.GetLogicalType<T1>(), TypeExtensions.GetLogicalType<T2>(),
×
NEW
37
            TypeExtensions.GetLogicalType<T3>(), TypeExtensions.GetLogicalType<T4>());
×
UNCOV
38
    }
×
39

40
    private unsafe void RegisterScalarMethod(string name, Action<IReadOnlyList<IDuckDBDataReader>, IDuckDBDataWriter, ulong> action, DuckDBLogicalType returnType,
41
                                             bool varargs, ScalarFunctionOptions? options, params DuckDBLogicalType[] parameterTypes)
42
    {
43
        var function = NativeMethods.ScalarFunction.DuckDBCreateScalarFunction();
90✔
44
        NativeMethods.ScalarFunction.DuckDBScalarFunctionSetName(function, name);
90✔
45

46
        if (varargs)
90✔
47
        {
48
            if (parameterTypes.Length != 1)
24!
49
            {
50
                throw new InvalidOperationException("Cannot use params with multiple parameters");
×
51
            }
52

53
            NativeMethods.ScalarFunction.DuckDBScalarFunctionSetVarargs(function, parameterTypes[0]);
24✔
54
            parameterTypes[0].Dispose();
24✔
55
        }
56
        else
57
        {
58
            foreach (var type in parameterTypes)
318✔
59
            {
60
                NativeMethods.ScalarFunction.DuckDBScalarFunctionAddParameter(function, type);
93✔
61
                type.Dispose();
93✔
62
            }
63
        }
64

65
        // Functions with parameters default to pure; parameterless functions (e.g. random()) default to volatile
66
        var defaultPure = parameterTypes.Length > 0;
90✔
67
        if (!(options?.IsPureFunction ?? defaultPure))
90✔
68
        {
69
            NativeMethods.ScalarFunction.DuckDBScalarFunctionSetVolatile(function);
15✔
70
        }
71

72
        if (options?.HandlesNulls == true)
90✔
73
        {
74
            NativeMethods.ScalarFunction.DuckDBScalarFunctionSetSpecialHandling(function);
18✔
75
        }
76

77
        NativeMethods.ScalarFunction.DuckDBScalarFunctionSetReturnType(function, returnType);
90✔
78
        NativeMethods.ScalarFunction.DuckDBScalarFunctionSetFunction(function, &ScalarFunctionCallback);
90✔
79

80
        var info = new ScalarFunctionInfo(returnType, action);
90✔
81

82
        NativeMethods.ScalarFunction.DuckDBScalarFunctionSetExtraInfo(function, info.ToHandle(), &DestroyExtraInfo);
90✔
83

84
        var state = NativeMethods.ScalarFunction.DuckDBRegisterScalarFunction(NativeConnection, function);
90✔
85

86
        NativeMethods.ScalarFunction.DuckDBDestroyScalarFunction(ref function);
90✔
87

88
        if (!state.IsSuccess())
90!
89
        {
90
            throw new InvalidOperationException($"Error registering user defined scalar function: {name}");
×
91
        }
92
    }
90✔
93

94
    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
95
    private static void ScalarFunctionCallback(IntPtr info, IntPtr chunk, IntPtr outputVector)
96
    {
97
        VectorDataReaderBase[] readers = [];
246✔
98
        VectorDataWriterBase? writer = null;
246✔
99

100
        try
101
        {
102
            var dataChunk = new DuckDBDataChunk(chunk);
246✔
103

104
            var chunkSize = NativeMethods.DataChunks.DuckDBDataChunkGetSize(dataChunk);
246✔
105
            var handle = GCHandle.FromIntPtr(NativeMethods.ScalarFunction.DuckDBScalarFunctionGetExtraInfo(info));
246✔
106

107
            if (handle.Target is not ScalarFunctionInfo functionInfo)
246!
108
            {
109
                throw new InvalidOperationException("User defined scalar function execution failed. Function extra info is null");
×
110
            }
111

112
            readers = new VectorDataReaderBase[NativeMethods.DataChunks.DuckDBDataChunkGetColumnCount(dataChunk)];
246✔
113

114
            for (var index = 0; index < readers.Length; index++)
1,206✔
115
            {
116
                var vector = NativeMethods.DataChunks.DuckDBDataChunkGetVector(dataChunk, index);
357✔
117
                using var logicalType = NativeMethods.Vectors.DuckDBVectorGetColumnType(vector);
357✔
118
                readers[index] = VectorDataReaderFactory.CreateReader(vector, logicalType);
357✔
119
            }
120

121
            writer = VectorDataWriterFactory.CreateWriter(outputVector, functionInfo.ReturnType);
246✔
122

123
            functionInfo.Action(readers, writer, chunkSize);
246✔
124
        }
228✔
125
        catch (Exception ex)
18✔
126
        {
127
            NativeMethods.ScalarFunction.DuckDBScalarFunctionSetError(info, ex.Message);
18✔
128
        }
18✔
129
        finally
130
        {
131
            foreach (var reader in readers)
1,206✔
132
            {
133
                reader.Dispose();
357✔
134
            }
135

136
            writer?.Dispose();
246✔
137
        }
246✔
138
    }
246✔
139

140
    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
141
    private static void DestroyExtraInfo(IntPtr pointer) => pointer.FreeHandle();
285✔
142
}
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