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

Giorgi / DuckDB.NET / 18406925343

10 Oct 2025 12:46PM UTC coverage: 88.061%. First build
18406925343

Pull #296

github

web-flow
Merge 39d6c8fd2 into fd45dbbc9
Pull Request #296: Add ClassMap-based type-safe appender API to DuckDB.NET

1154 of 1361 branches covered (84.79%)

Branch coverage included in aggregate %.

106 of 155 new or added lines in 4 files covered. (68.39%)

2239 of 2492 relevant lines covered (89.85%)

587198.28 hits per line

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

56.94
/DuckDB.NET.Data/DuckDBMappedAppender.cs
1
using System;
2
using System.Collections.Generic;
3
using DuckDB.NET.Data.Mapping;
4
using DuckDB.NET.Native;
5

6
namespace DuckDB.NET.Data;
7

8
/// <summary>
9
/// A type-safe appender that uses ClassMap to validate type mappings.
10
/// </summary>
11
/// <typeparam name="T">The type being appended</typeparam>
12
/// <typeparam name="TMap">The ClassMap type defining the mappings</typeparam>
13
public class DuckDBMappedAppender<T, TMap> : IDisposable where TMap : DuckDBClassMap<T>, new()
14
{
15
    private readonly DuckDBAppender appender;
16
    private readonly TMap classMap;
17
    private readonly PropertyMapping<T>[] orderedMappings;
18

19
    internal DuckDBMappedAppender(DuckDBAppender appender)
9✔
20
    {
21
        this.appender = appender;
9✔
22
        classMap = new TMap();
9✔
23
        
24
        // Validate mappings match the table structure
25
        var mappings = classMap.PropertyMappings;
9✔
26
        if (mappings.Count == 0)
9!
27
        {
NEW
28
            throw new InvalidOperationException($"ClassMap {typeof(TMap).Name} has no property mappings defined");
×
29
        }
30

31
        var columnTypes = appender.LogicalTypes;
9✔
32
        if (mappings.Count != columnTypes.Count)
9!
33
        {
NEW
34
            throw new InvalidOperationException($"ClassMap {typeof(TMap).Name} has {mappings.Count} mappings but table has {columnTypes.Count} columns");
×
35
        }
36

37
        // Validate each mapping
38
        orderedMappings = new PropertyMapping<T>[mappings.Count];
9✔
39
        for (int i = 0; i < mappings.Count; i++)
78✔
40
        {
41
            var mapping = mappings[i];
33✔
42
            orderedMappings[i] = mapping;
33✔
43

44
            // Skip validation for Default and Null mappings
45
            if (mapping.MappingType != PropertyMappingType.Property)
33✔
46
            {
47
                continue;
48
            }
49

50
            // Get the actual column type from the appender
51
            var columnType = NativeMethods.LogicalType.DuckDBGetTypeId(columnTypes[i]);
27✔
52
            var expectedType = GetExpectedDuckDBType(mapping.PropertyType);
27✔
53

54
            if (expectedType != columnType)
27✔
55
            {
56
                throw new InvalidOperationException(
3✔
57
                    $"Type mismatch for property '{mapping.PropertyName}': " +
3✔
58
                    $"Property type is {mapping.PropertyType.Name} (maps to {expectedType}) " +
3✔
59
                    $"but column {i} is {columnType}");
3✔
60
            }
61
        }
62
    }
6✔
63

64
    /// <summary>
65
    /// Appends multiple records to the table.
66
    /// </summary>
67
    /// <param name="records">The records to append</param>
68
    public void AppendRecords(IEnumerable<T> records)
69
    {
70
        if (records == null)
6!
71
        {
NEW
72
            throw new ArgumentNullException(nameof(records));
×
73
        }
74

75
        foreach (var record in records)
36✔
76
        {
77
            AppendRecord(record);
12✔
78
        }
79
    }
6✔
80

81
    private void AppendRecord(T record)
82
    {
83
        if (record == null)
12!
84
        {
NEW
85
            throw new ArgumentNullException(nameof(record));
×
86
        }
87

88
        var row = appender.CreateRow();
12✔
89

90
        foreach (var mapping in orderedMappings)
120✔
91
        {
92
            switch (mapping.MappingType)
48✔
93
            {
94
                case PropertyMappingType.Property:
95
                    var value = mapping.Getter(record);
36✔
96
                    AppendValue(row, value);
36✔
97
                    break;
36✔
98
                case PropertyMappingType.Default:
99
                    row.AppendDefault();
6✔
100
                    break;
6✔
101
                case PropertyMappingType.Null:
102
                    row.AppendNullValue();
6✔
103
                    break;
104
            }
105
        }
106

107
        row.EndRow();
12✔
108
    }
12✔
109

110
    private static DuckDBType GetExpectedDuckDBType(Type type)
111
    {
112
        // Handle nullable types
113
        var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
27✔
114

115
        return underlyingType switch
27!
116
        {
27✔
117
            Type t when t == typeof(bool) => DuckDBType.Boolean,
27!
118
            Type t when t == typeof(sbyte) => DuckDBType.TinyInt,
27!
119
            Type t when t == typeof(short) => DuckDBType.SmallInt,
27!
120
            Type t when t == typeof(int) => DuckDBType.Integer,
36✔
121
            Type t when t == typeof(long) => DuckDBType.BigInt,
18!
122
            Type t when t == typeof(byte) => DuckDBType.UnsignedTinyInt,
18!
123
            Type t when t == typeof(ushort) => DuckDBType.UnsignedSmallInt,
18!
124
            Type t when t == typeof(uint) => DuckDBType.UnsignedInteger,
18!
125
            Type t when t == typeof(ulong) => DuckDBType.UnsignedBigInt,
18!
126
            Type t when t == typeof(float) => DuckDBType.Float,
21✔
127
            Type t when t == typeof(double) => DuckDBType.Double,
15!
128
            Type t when t == typeof(decimal) => DuckDBType.Decimal,
15!
129
            Type t when t == typeof(string) => DuckDBType.Varchar,
24✔
130
            Type t when t == typeof(DateTime) => DuckDBType.Timestamp,
12!
NEW
131
            Type t when t == typeof(DateTimeOffset) => DuckDBType.TimestampTz,
×
NEW
132
            Type t when t == typeof(TimeSpan) => DuckDBType.Interval,
×
NEW
133
            Type t when t == typeof(Guid) => DuckDBType.Uuid,
×
134
#if NET6_0_OR_GREATER
27✔
NEW
135
            Type t when t == typeof(DateOnly) => DuckDBType.Date,
×
NEW
136
            Type t when t == typeof(TimeOnly) => DuckDBType.Time,
×
137
#endif
27✔
NEW
138
            _ => throw new NotSupportedException($"Type {type.Name} is not supported for mapping")
×
139
        };
27✔
140
    }
141

142
    private static void AppendValue(IDuckDBAppenderRow row, object? value)
143
    {
144
        if (value == null)
36!
145
        {
NEW
146
            row.AppendNullValue();
×
NEW
147
            return;
×
148
        }
149

150
        switch (value)
151
        {
152
            case bool boolValue:
NEW
153
                row.AppendValue(boolValue);
×
NEW
154
                break;
×
155
            case sbyte sbyteValue:
NEW
156
                row.AppendValue(sbyteValue);
×
NEW
157
                break;
×
158
            case short shortValue:
NEW
159
                row.AppendValue(shortValue);
×
NEW
160
                break;
×
161
            case int intValue:
162
                row.AppendValue(intValue);
12✔
163
                break;
12✔
164
            case long longValue:
NEW
165
                row.AppendValue(longValue);
×
NEW
166
                break;
×
167
            case byte byteValue:
NEW
168
                row.AppendValue(byteValue);
×
NEW
169
                break;
×
170
            case ushort ushortValue:
NEW
171
                row.AppendValue(ushortValue);
×
NEW
172
                break;
×
173
            case uint uintValue:
NEW
174
                row.AppendValue(uintValue);
×
NEW
175
                break;
×
176
            case ulong ulongValue:
NEW
177
                row.AppendValue(ulongValue);
×
NEW
178
                break;
×
179
            case float floatValue:
180
                row.AppendValue(floatValue);
6✔
181
                break;
6✔
182
            case double doubleValue:
NEW
183
                row.AppendValue(doubleValue);
×
NEW
184
                break;
×
185
            case decimal decimalValue:
NEW
186
                row.AppendValue(decimalValue);
×
NEW
187
                break;
×
188
            case string stringValue:
189
                row.AppendValue(stringValue);
12✔
190
                break;
12✔
191
            case DateTime dateTimeValue:
192
                row.AppendValue(dateTimeValue);
6✔
193
                break;
6✔
194
            case DateTimeOffset dateTimeOffsetValue:
NEW
195
                row.AppendValue(dateTimeOffsetValue);
×
NEW
196
                break;
×
197
            case TimeSpan timeSpanValue:
NEW
198
                row.AppendValue(timeSpanValue);
×
NEW
199
                break;
×
200
            case Guid guidValue:
NEW
201
                row.AppendValue(guidValue);
×
NEW
202
                break;
×
203
#if NET6_0_OR_GREATER
204
            case DateOnly dateOnlyValue:
NEW
205
                row.AppendValue((DateOnly?)dateOnlyValue);
×
NEW
206
                break;
×
207
            case TimeOnly timeOnlyValue:
NEW
208
                row.AppendValue((TimeOnly?)timeOnlyValue);
×
NEW
209
                break;
×
210
#endif
211
            default:
NEW
212
                throw new NotSupportedException($"Type {value.GetType().Name} is not supported for appending");
×
213
        }
214
    }
215

216
    /// <summary>
217
    /// Closes the appender and flushes any remaining data.
218
    /// </summary>
219
    public void Close()
220
    {
NEW
221
        appender.Close();
×
NEW
222
    }
×
223

224
    /// <summary>
225
    /// Disposes the appender.
226
    /// </summary>
227
    public void Dispose()
228
    {
229
        appender.Dispose();
6✔
230
    }
6✔
231
}
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

© 2025 Coveralls, Inc