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

Giorgi / DuckDB.NET / 28017109290

23 Jun 2026 09:41AM UTC coverage: 89.361% (-0.4%) from 89.738%
28017109290

push

github

Giorgi
Merge branch 'main' into nightly-builds

1330 of 1545 branches covered (86.08%)

Branch coverage included in aggregate %.

362 of 413 new or added lines in 32 files covered. (87.65%)

4 existing lines in 3 files now uncovered.

2769 of 3042 relevant lines covered (91.03%)

434285.19 hits per line

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

88.38
/DuckDB.NET.Data/DuckDBConnection.TableFunction.cs
1
using DuckDB.NET.Data.Common;
2
using DuckDB.NET.Data.Connection;
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
public record ColumnInfo(string Name, Type Type);
1,836✔
10

11
public record CardinalityHint(ulong Value, bool IsExact = false);
36✔
12

13
public record ProjectedColumn(int Index, string Name, Type Type);
207✔
14

NEW
15
public record TableFunction
×
16
{
17
    public IReadOnlyList<ColumnInfo> Columns { get; }
378✔
18
    public CardinalityHint? Cardinality { get; }
189✔
19
    internal IEnumerable? Data { get; }
189✔
20
    internal Func<IReadOnlyList<ProjectedColumn>, IEnumerable>? DataFactory { get; }
189✔
21

22
    public TableFunction(IReadOnlyList<ColumnInfo> columns, IEnumerable data, CardinalityHint? cardinality = null)
135✔
23
    {
24
        Columns = columns;
135✔
25
        Data = data;
135✔
26
        Cardinality = cardinality;
135✔
27
    }
135✔
28

29
    internal TableFunction(IReadOnlyList<ColumnInfo> columns, Func<IReadOnlyList<ProjectedColumn>, IEnumerable> dataFactory, CardinalityHint? cardinality = null)
54✔
30
    {
31
        Columns = columns;
54✔
32
        DataFactory = dataFactory;
54✔
33
        Cardinality = cardinality;
54✔
34
    }
54✔
35
}
36

37
partial class DuckDBConnection
38
{
39
    public void RegisterTableFunction(string name, Func<TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback)
40
    {
41
        RegisterTableFunctionInternal(name, (_) => resultCallback(), mapperCallback, Array.Empty<Type>());
126✔
42
    }
63✔
43

44
    public void RegisterTableFunction<T>(string name, Func<IReadOnlyList<IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback)
45
    {
46
        RegisterTableFunctionInternal(name, resultCallback, mapperCallback, typeof(T));
24✔
47
    }
24✔
48

49
    public void RegisterTableFunction<T1, T2>(string name, Func<IReadOnlyList<IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback)
50
    {
51
        RegisterTableFunctionInternal(name, resultCallback, mapperCallback, typeof(T1), typeof(T2));
9✔
52
    }
9✔
53

54
    public void RegisterTableFunction<T1, T2, T3>(string name, Func<IReadOnlyList<IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback)
55
    {
56
        RegisterTableFunctionInternal(name, resultCallback, mapperCallback, typeof(T1), typeof(T2), typeof(T3));
3✔
57
    }
3✔
58

59
    public void RegisterTableFunction<T1, T2, T3, T4>(string name, Func<IReadOnlyList<IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback)
60
    {
61
        RegisterTableFunctionInternal(name, resultCallback, mapperCallback, typeof(T1), typeof(T2), typeof(T3), typeof(T4));
3✔
62
    }
3✔
63

64
    public void RegisterTableFunction<T1, T2, T3, T4, T5>(string name, Func<IReadOnlyList<IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback)
65
    {
66
        RegisterTableFunctionInternal(name, resultCallback, mapperCallback, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5));
3✔
67
    }
3✔
68

69
    public void RegisterTableFunction<T1, T2, T3, T4, T5, T6>(string name, Func<IReadOnlyList<IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback)
70
    {
71
        RegisterTableFunctionInternal(name, resultCallback, mapperCallback, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6));
×
72
    }
×
73

74
    public void RegisterTableFunction<T1, T2, T3, T4, T5, T6, T7>(string name, Func<IReadOnlyList<IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback)
75
    {
76
        RegisterTableFunctionInternal(name, resultCallback, mapperCallback, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7));
×
77
    }
×
78

79
    public void RegisterTableFunction<T1, T2, T3, T4, T5, T6, T7, T8>(string name, Func<IReadOnlyList<IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback)
80
    {
81
        RegisterTableFunctionInternal(name, resultCallback, mapperCallback, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8));
×
82
    }
×
83

84
    public void RegisterTableFunction(string name, Func<IReadOnlyList<IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback, params DuckDBType[] parameterTypes)
85
    {
86
        var logicalTypes = Array.ConvertAll(parameterTypes, NativeMethods.LogicalType.DuckDBCreateLogicalType);
18✔
87
        RegisterTableFunctionInternal(name, (positional, _) => resultCallback(positional), mapperCallback, logicalTypes, []);
36✔
88
    }
18✔
89

90
    private void RegisterTableFunctionInternal(string name, Func<IReadOnlyList<IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback, params Type[] parameterTypes)
91
    {
92
        var logicalTypes = Array.ConvertAll(parameterTypes, TypeExtensions.GetLogicalType);
105✔
93
        RegisterTableFunctionInternal(name, (positional, _) => resultCallback(positional), mapperCallback, logicalTypes, []);
210✔
94
    }
105✔
95

96
    internal unsafe void RegisterTableFunctionInternal(string name, Func<IReadOnlyList<IDuckDBValueReader>, IReadOnlyDictionary<string, IDuckDBValueReader>, TableFunction> resultCallback, Action<object?, IDuckDBDataWriter[], ulong> mapperCallback, DuckDBLogicalType[] positionalLogicalTypes, NamedParameterDefinition[] namedParameters)
97
    {
98
        var function = NativeMethods.TableFunction.DuckDBCreateTableFunction();
183✔
99
        NativeMethods.TableFunction.DuckDBTableFunctionSetName(function, name);
183✔
100

101
        foreach (var logicalType in positionalLogicalTypes)
702✔
102
        {
103
            NativeMethods.TableFunction.DuckDBTableFunctionAddParameter(function, logicalType);
168✔
104
            logicalType.Dispose();
168✔
105
        }
106

107
        foreach (var param in namedParameters)
402✔
108
        {
109
            using var logicalType = param.Type.GetLogicalType();
18✔
110
            NativeMethods.TableFunction.DuckDBTableFunctionAddNamedParameter(function, param.Name, logicalType);
18✔
111
        }
112

113
        var tableFunctionInfo = new TableFunctionInfo(resultCallback, mapperCallback, Array.ConvertAll(namedParameters, p => p.Name));
201✔
114

115
        NativeMethods.TableFunction.DuckDBTableFunctionSetBind(function, &Bind);
183✔
116
        NativeMethods.TableFunction.DuckDBTableFunctionSetInit(function, &Init);
183✔
117
        NativeMethods.TableFunction.DuckDBTableFunctionSetFunction(function, &TableFunction);
183✔
118
        NativeMethods.TableFunction.DuckDBTableFunctionSetExtraInfo(function, tableFunctionInfo.ToHandle(), &DestroyExtraInfo);
183✔
119
        NativeMethods.TableFunction.DuckDBTableFunctionSupportsProjectionPushdown(function, true);
183✔
120

121
        var state = NativeMethods.TableFunction.DuckDBRegisterTableFunction(NativeConnection, function);
183✔
122

123
        NativeMethods.TableFunction.DuckDBDestroyTableFunction(ref function);
183✔
124

125
        if (!state.IsSuccess())
183!
126
        {
127
            throw new InvalidOperationException($"Error registering user defined table function: {name}");
×
128
        }
129
    }
183✔
130

131
    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
132
    private static unsafe void Bind(IntPtr info)
133
    {
134
        IDuckDBValueReader[] parameters = [];
201✔
135
        Dictionary<string, IDuckDBValueReader> named = [];
201✔
136
        try
137
        {
138
            var handle = GCHandle.FromIntPtr(NativeMethods.TableFunction.DuckDBBindGetExtraInfo(info));
201✔
139

140
            if (handle.Target is not TableFunctionInfo functionInfo)
201!
141
            {
142
                throw new InvalidOperationException("User defined table function bind failed. Bind extra info is null");
×
143
            }
144

145
            parameters = new IDuckDBValueReader[NativeMethods.TableFunction.DuckDBBindGetParameterCount(info)];
201✔
146

147
            for (var i = 0; i < parameters.Length; i++)
774✔
148
            {
149
                var value = NativeMethods.TableFunction.DuckDBBindGetParameter(info, (ulong)i);
186✔
150
                parameters[i] = value;
186✔
151
            }
152

153
            // When a named parameter is omitted in SQL, duckdb_bind_get_named_parameter returns a null pointer.
154
            // We substitute it with NullValueReader so CompileValueReader's IsNull() check
155
            // correctly handles it (returns default for nullable, throws for non-nullable).
156
            foreach (var paramName in functionInfo.NamedParameterNames)
462✔
157
            {
158
                var value = NativeMethods.TableFunction.DuckDBBindGetNamedParameter(info, paramName);
30✔
159
                named[paramName] = value.IsInvalid ? NullValueReader.Instance : value;
30✔
160
            }
161

162
            var tableFunctionData = functionInfo.Bind(parameters, named);
201✔
163

164
            foreach (var columnInfo in tableFunctionData.Columns)
1,110✔
165
            {
166
                using var logicalType = columnInfo.Type.GetLogicalType();
366✔
167
                NativeMethods.TableFunction.DuckDBBindAddResultColumn(info, columnInfo.Name, logicalType);
366✔
168
            }
169

170
            if (tableFunctionData.Cardinality is { } cardinality)
189✔
171
            {
172
                NativeMethods.TableFunction.DuckDBBindSetCardinality(info, cardinality.Value, isExact: cardinality.IsExact);
12✔
173
            }
174

175
            var connectionId = UdfExceptionStore.GetTableFunctionBindConnectionId(info);
189✔
176
            // ReSharper disable once GenericEnumeratorNotDisposed This is disposed in TableFunctionBindData.Dispose
177
            var eagerEnumerator = tableFunctionData.Data?.GetEnumerator();
189✔
178
            var bindData = new TableFunctionBindData(tableFunctionData.Columns, eagerEnumerator, tableFunctionData.DataFactory, connectionId);
189✔
179

180
            NativeMethods.TableFunction.DuckDBBindSetBindData(info, bindData.ToHandle(), &DestroyExtraInfo);
189✔
181
        }
189✔
182
        catch (Exception ex)
12✔
183
        {
184
            try
185
            {
186
                var connectionId = UdfExceptionStore.GetTableFunctionBindConnectionId(info);
12✔
187
                UdfExceptionStore.Store(connectionId, ex);
12✔
188
            }
12✔
NEW
189
            catch
×
190
            {
191
                // If we can't get the connection ID, we still report the error message
NEW
192
            }
×
193

194
            NativeMethods.TableFunction.DuckDBBindSetError(info, ex.Message);
12✔
195
        }
12✔
196
        finally
197
        {
198
            foreach (var parameter in parameters)
774✔
199
            {
200
                (parameter as IDisposable)?.Dispose();
186!
201
            }
202

203
            foreach (var namedParam in named.Values)
462✔
204
            {
205
                (namedParam as IDisposable)?.Dispose();
30✔
206
            }
207
        }
201✔
208
    }
201✔
209

210
    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
211
    private static unsafe void Init(IntPtr info)
212
    {
213
        TableFunctionBindData? bindData = null;
183✔
214
        try
215
        {
216
            var bindHandle = GCHandle.FromIntPtr(NativeMethods.TableFunction.DuckDBInitGetBindData(info));
183✔
217
            if (bindHandle.Target is not TableFunctionBindData data)
183!
218
            {
NEW
219
                throw new InvalidOperationException("User defined table function init failed. Bind data is null");
×
220
            }
221
            bindData = data;
183✔
222

223
            var count = (int)NativeMethods.TableFunction.DuckDBInitGetColumnCount(info);
183✔
224
            var projected = new int[count];
183✔
225
            for (var i = 0; i < count; i++)
948✔
226
            {
227
                projected[i] = (int)NativeMethods.TableFunction.DuckDBInitGetColumnIndex(info, (ulong)i);
291✔
228
            }
229

230
            IEnumerator? factoryEnumerator = null;
183✔
231
            if (bindData.DataFactory is { } factory)
183✔
232
            {
233
                var projectedColumns = new ProjectedColumn[count];
54✔
234
                for (var i = 0; i < count; i++)
300✔
235
                {
236
                    var column = bindData.Columns[projected[i]];
96✔
237
                    projectedColumns[i] = new ProjectedColumn(projected[i], column.Name, column.Type);
96✔
238
                }
239
                // ReSharper disable once GenericEnumeratorNotDisposed This is disposed in TableFunctionInitData.Dispose
240
                factoryEnumerator = factory(projectedColumns).GetEnumerator();
54✔
241
            }
242

243
            var initData = new TableFunctionInitData(projected, factoryEnumerator);
177✔
244
            NativeMethods.TableFunction.DuckDBInitSetInitData(info, initData.ToHandle(), &DestroyExtraInfo);
177✔
245
        }
177✔
246
        catch (Exception ex)
6✔
247
        {
248
            if (bindData is not null)
6✔
249
            {
250
                UdfExceptionStore.Store(bindData.ConnectionId, ex);
6✔
251
            }
252
            NativeMethods.TableFunction.DuckDBInitSetError(info, ex.Message);
6✔
253
        }
6✔
254
    }
183✔
255

256
    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
257
    private static void TableFunction(IntPtr info, IntPtr chunk)
258
    {
259
        IDuckDBDataWriter[] writers = [];
345✔
260
        DuckDBLogicalType[] logicalTypes = [];
345✔
261
        try
262
        {
263
            var bindData = GCHandle.FromIntPtr(NativeMethods.TableFunction.DuckDBFunctionGetBindData(info));
345✔
264
            var extraInfo = GCHandle.FromIntPtr(NativeMethods.TableFunction.DuckDBFunctionGetExtraInfo(info));
345✔
265
            var initHandle = GCHandle.FromIntPtr(NativeMethods.TableFunction.DuckDBFunctionGetInitData(info));
345✔
266

267
            if (bindData.Target is not TableFunctionBindData tableFunctionBindData)
345!
268
            {
269
                throw new InvalidOperationException("User defined table function failed. Function bind data is null");
×
270
            }
271

272
            if (extraInfo.Target is not TableFunctionInfo tableFunctionInfo)
345!
273
            {
274
                throw new InvalidOperationException("User defined table function failed. Function extra info is null");
×
275
            }
276

277
            if (initHandle.Target is not TableFunctionInitData tableFunctionInitData)
345!
278
            {
NEW
279
                throw new InvalidOperationException("User defined table function failed. Function init data is null");
×
280
            }
281

282
            var dataChunk = new DuckDBDataChunk(chunk);
345✔
283

284
            var totalColumnCount = tableFunctionBindData.Columns.Count;
345✔
285
            var projectedCount = tableFunctionInitData.Projected.Length;
345✔
286

287
            writers = new IDuckDBDataWriter[totalColumnCount];
345✔
288
            Array.Fill(writers, NullDataWriter.Instance);
345✔
289

290
            logicalTypes = new DuckDBLogicalType[projectedCount];
345✔
291

292
            for (var columnIndex = 0; columnIndex < projectedCount; columnIndex++)
1,824✔
293
            {
294
                var originalColumnIndex = tableFunctionInitData.Projected[columnIndex];
567✔
295
                var column = tableFunctionBindData.Columns[originalColumnIndex];
567✔
296
                var vector = NativeMethods.DataChunks.DuckDBDataChunkGetVector(dataChunk, columnIndex);
567✔
297

298
                logicalTypes[columnIndex] = column.Type.GetLogicalType();
567✔
299
                writers[originalColumnIndex] = VectorDataWriterFactory.CreateWriter(vector, logicalTypes[columnIndex]);
567✔
300
            }
301

302
            var enumerator = tableFunctionInitData.FactoryEnumerator ?? tableFunctionBindData.DataEnumerator;
345✔
303
            if (enumerator is null)
345!
304
            {
NEW
305
                throw new InvalidOperationException("User defined table function failed. No data source available");
×
306
            }
307

308
            ulong size = 0;
345✔
309

310
            for (; size < DuckDBGlobalData.VectorSize; size++)
32,751✔
311
            {
312
                if (enumerator.MoveNext())
16,542✔
313
                {
314
                    tableFunctionInfo.Mapper(enumerator.Current, writers, size);
16,209✔
315
                }
316
                else
317
                {
318
                    break;
319
                }
320
            }
321

322
            NativeMethods.DataChunks.DuckDBDataChunkSetSize(dataChunk, size);
339✔
323
        }
339✔
324
        catch (Exception ex)
6✔
325
        {
326
            try
327
            {
328
                var bindData = GCHandle.FromIntPtr(NativeMethods.TableFunction.DuckDBFunctionGetBindData(info));
6✔
329
                if (bindData.Target is TableFunctionBindData tableFunctionBindData)
6✔
330
                {
331
                    UdfExceptionStore.Store(tableFunctionBindData.ConnectionId, ex);
6✔
332
                }
333
            }
6✔
NEW
334
            catch
×
335
            {
336
                // If we can't get the connection ID, we still report the error message
NEW
337
            }
×
338

339
            NativeMethods.TableFunction.DuckDBFunctionSetError(info, ex.Message);
6✔
340
        }
6✔
341
        finally
342
        {
343
            foreach (var writer in writers)
2,088✔
344
            {
345
                (writer as IDisposable)?.Dispose();
699✔
346
            }
347

348
            foreach (var logicalType in logicalTypes)
1,824✔
349
            {
350
                logicalType?.Dispose();
567!
351
            }
352
        }
345✔
353
    }
345✔
354

355
    private class NullValueReader : IDuckDBValueReader
356
    {
357
        public static readonly NullValueReader Instance = new();
3✔
358
        public bool IsNull() => true;
9✔
359
        public T GetValue<T>() => throw new InvalidOperationException("Cannot read value from a null parameter.");
×
360
    }
361
}
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