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

loresoft / FluentCommand / 26594986582

28 May 2026 06:28PM UTC coverage: 55.553%. First build
26594986582

push

github

pwelter34
Move JSON support, add docs and examples

1358 of 3215 branches covered (42.24%)

Branch coverage included in aggregate %.

103 of 234 new or added lines in 9 files covered. (44.02%)

4389 of 7130 relevant lines covered (61.56%)

312.88 hits per line

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

56.0
/src/FluentCommand/Query/UpsertBuilder.cs
1
using System.Text.Json;
2
using System.Text.Json.Serialization.Metadata;
3

4
using FluentCommand.Query.Generators;
5

6
namespace FluentCommand.Query;
7

8
/// <summary>
9
/// Provides a builder for constructing SQL UPSERT statements with fluent, chainable methods.
10
/// </summary>
11
public class UpsertBuilder : UpsertBuilder<UpsertBuilder>
12
{
13
    /// <summary>
14
    /// Initializes a new instance of the <see cref="UpsertBuilder"/> class.
15
    /// </summary>
16
    /// <param name="queryGenerator">The <see cref="IQueryGenerator"/> used to generate SQL expressions.</param>
17
    /// <param name="parameters">The list of <see cref="QueryParameter"/> objects for the query.</param>
18
    public UpsertBuilder(
19
        IQueryGenerator queryGenerator,
20
        List<QueryParameter> parameters)
21
        : base(queryGenerator, parameters)
14✔
22
    { }
14✔
23
}
24

25
/// <summary>
26
/// Provides a generic base class for building SQL UPSERT statements with fluent, chainable methods.
27
/// </summary>
28
/// <typeparam name="TBuilder">The type of the builder for fluent chaining.</typeparam>
29
public abstract class UpsertBuilder<TBuilder> : StatementBuilder<TBuilder>
30
    where TBuilder : UpsertBuilder<TBuilder>
31
{
32
    /// <summary>
33
    /// Initializes a new instance of the <see cref="UpsertBuilder{TBuilder}"/> class.
34
    /// </summary>
35
    /// <param name="queryGenerator">The <see cref="IQueryGenerator"/> used to generate SQL expressions.</param>
36
    /// <param name="parameters">The list of <see cref="QueryParameter"/> objects for the query.</param>
37
    protected UpsertBuilder(
38
        IQueryGenerator queryGenerator,
39
        List<QueryParameter> parameters)
40
        : base(queryGenerator, parameters)
27✔
41
    {
42
    }
27✔
43

44
    /// <summary>
45
    /// Gets the collection of column expressions for the UPSERT statement.
46
    /// </summary>
47
    protected HashSet<ColumnExpression> ColumnExpressions { get; } = new();
48

49
    /// <summary>
50
    /// Gets the collection of value expressions for the UPSERT statement.
51
    /// </summary>
52
    protected HashSet<string> ValueExpressions { get; } = new();
53

54
    /// <summary>
55
    /// Gets the collection of key column expressions for the UPSERT statement.
56
    /// </summary>
57
    protected HashSet<ColumnExpression> KeyExpressions { get; } = new();
58

59
    /// <summary>
60
    /// Gets the collection of output column expressions for the UPSERT statement.
61
    /// </summary>
62
    protected HashSet<ColumnExpression> OutputExpressions { get; } = new();
63

64
    /// <summary>
65
    /// Gets the target table expression for the UPSERT statement.
66
    /// </summary>
67
    protected TableExpression? TableExpression { get; private set; }
68

69
    /// <summary>
70
    /// Sets the target table to insert into or update.
71
    /// </summary>
72
    /// <param name="tableName">The name of the table.</param>
73
    /// <param name="tableSchema">The schema of the table (optional).</param>
74
    /// <param name="tableAlias">The alias for the table (optional).</param>
75
    /// <returns>The same builder instance for method chaining.</returns>
76
    public TBuilder Into(
77
        string tableName,
78
        string? tableSchema = null,
79
        string? tableAlias = null)
80
    {
81
        TableExpression = new TableExpression(tableName, tableSchema, tableAlias);
27✔
82

83
        return (TBuilder)this;
27✔
84
    }
85

86
    /// <summary>
87
    /// Adds a value for the specified column name and value.
88
    /// </summary>
89
    /// <typeparam name="TValue">The type of the value.</typeparam>
90
    /// <param name="columnName">The name of the column.</param>
91
    /// <param name="parameterValue">The value to insert or update for the column.</param>
92
    /// <returns>The same builder instance for method chaining.</returns>
93
    public TBuilder Value<TValue>(
94
        string columnName,
95
        TValue? parameterValue)
96
    {
97
        return Value(columnName, parameterValue, typeof(TValue));
55✔
98
    }
99

100
    /// <summary>
101
    /// Adds a value for the specified column name, value, and type.
102
    /// </summary>
103
    /// <param name="columnName">The name of the column.</param>
104
    /// <param name="parameterValue">The value to insert or update for the column.</param>
105
    /// <param name="parameterType">The type of the parameter value.</param>
106
    /// <returns>The same builder instance for method chaining.</returns>
107
    /// <exception cref="ArgumentException">Thrown if <paramref name="columnName"/> is null or empty.</exception>
108
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="parameterType"/> is <c>null</c>.</exception>
109
    public TBuilder Value(
110
        string columnName,
111
        object? parameterValue,
112
        Type parameterType)
113
    {
114
        if (string.IsNullOrWhiteSpace(columnName))
166!
115
            throw new ArgumentException($"'{nameof(columnName)}' cannot be null or empty.", nameof(columnName));
×
116

117
        ArgumentNullException.ThrowIfNull(parameterType);
166✔
118

119
        var paramterName = NextParameter();
166✔
120

121
        var columnExpression = new ColumnExpression(columnName);
166✔
122

123
        ColumnExpressions.Add(columnExpression);
166✔
124
        ValueExpressions.Add(paramterName);
166✔
125

126
        Parameters.Add(new QueryParameter(paramterName, parameterValue, parameterType));
166✔
127

128
        return (TBuilder)this;
166✔
129
    }
130

131
    /// <summary>
132
    /// Conditionally adds a value for the specified column name and value if the condition is met.
133
    /// </summary>
134
    /// <typeparam name="TValue">The type of the value.</typeparam>
135
    /// <param name="columnName">The name of the column.</param>
136
    /// <param name="parameterValue">The value to insert or update for the column.</param>
137
    /// <param name="condition">A function that determines whether to add the value, based on the column name and value.</param>
138
    /// <returns>The same builder instance for method chaining.</returns>
139
    public TBuilder ValueIf<TValue>(
140
        string columnName,
141
        TValue? parameterValue,
142
        Func<string, TValue?, bool> condition)
143
    {
144
        if (condition != null && !condition(columnName, parameterValue))
×
145
            return (TBuilder)this;
×
146

147
        return Value(columnName, parameterValue);
×
148
    }
149

150
    /// <summary>
151
    /// Adds a value for the specified column name with the value serialized as JSON using the specified <paramref name="options" />.
152
    /// </summary>
153
    /// <typeparam name="TValue">The type of the value.</typeparam>
154
    /// <param name="columnName">The name of the column.</param>
155
    /// <param name="parameterValue">The value to serialize as JSON and insert or update for the column.</param>
156
    /// <param name="options">The <see cref="JsonSerializerOptions"/> to use when serializing.</param>
157
    /// <returns>The same builder instance for method chaining.</returns>
158
    public TBuilder ValueJson<TValue>(
159
        string columnName,
160
        TValue? parameterValue,
161
        JsonSerializerOptions? options = null)
162
    {
163
        var json = parameterValue is not null
4✔
164
            ? JsonSerializer.Serialize(parameterValue, options)
4✔
165
            : null;
4✔
166

167
        return Value(columnName, json);
4✔
168
    }
169

170
    /// <summary>
171
    /// Adds a value for the specified column name with the value serialized as JSON using the specified <paramref name="jsonTypeInfo" />.
172
    /// </summary>
173
    /// <typeparam name="TValue">The type of the value.</typeparam>
174
    /// <param name="columnName">The name of the column.</param>
175
    /// <param name="parameterValue">The value to serialize as JSON and insert or update for the column.</param>
176
    /// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo{T}"/> to use when serializing.</param>
177
    /// <returns>The same builder instance for method chaining.</returns>
178
    public TBuilder ValueJson<TValue>(
179
        string columnName,
180
        TValue? parameterValue,
181
        JsonTypeInfo<TValue> jsonTypeInfo)
182
    {
NEW
183
        ArgumentNullException.ThrowIfNull(jsonTypeInfo);
×
184

NEW
185
        var json = parameterValue is not null
×
NEW
186
            ? JsonSerializer.Serialize(parameterValue, jsonTypeInfo)
×
NEW
187
            : null;
×
188

NEW
189
        return Value(columnName, json);
×
190
    }
191

192

193
    /// <summary>
194
    /// Adds a key column used to determine whether a row already exists.
195
    /// </summary>
196
    /// <param name="columnName">The key column name.</param>
197
    /// <returns>The same builder instance for method chaining.</returns>
198
    /// <exception cref="ArgumentException">Thrown if <paramref name="columnName"/> is null or empty.</exception>
199
    public TBuilder Key(string columnName)
200
    {
201
        ArgumentException.ThrowIfNullOrWhiteSpace(columnName);
34✔
202

203
        KeyExpressions.Add(new ColumnExpression(columnName));
34✔
204

205
        return (TBuilder)this;
34✔
206
    }
207

208
    /// <summary>
209
    /// Conditionally adds a key column used to determine whether a row already exists.
210
    /// </summary>
211
    /// <param name="columnName">The key column name.</param>
212
    /// <param name="condition">A function that determines whether to add the key column.</param>
213
    /// <returns>The same builder instance for method chaining.</returns>
214
    public TBuilder KeyIf(
215
        string columnName,
216
        Func<string, bool>? condition = null)
217
    {
218
        if (condition != null && !condition(columnName))
×
219
            return (TBuilder)this;
×
220

221
        return Key(columnName);
×
222
    }
223

224
    /// <summary>
225
    /// Adds an OUTPUT clause for the specified column names.
226
    /// </summary>
227
    /// <param name="columnNames">The collection of column names to include in the OUTPUT clause.</param>
228
    /// <param name="tableAlias">The alias for the table (optional).</param>
229
    /// <returns>The same builder instance for method chaining.</returns>
230
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="columnNames"/> is <c>null</c>.</exception>
231
    public TBuilder Output(
232
        IEnumerable<string> columnNames,
233
        string? tableAlias = null)
234
    {
NEW
235
        ArgumentNullException.ThrowIfNull(columnNames);
×
236

237
        foreach (var column in columnNames)
×
238
            Output(column, tableAlias);
×
239

240
        return (TBuilder)this;
×
241
    }
242

243
    /// <summary>
244
    /// Adds an OUTPUT clause for the specified column name.
245
    /// </summary>
246
    /// <param name="columnName">The name of the column to include in the OUTPUT clause.</param>
247
    /// <param name="tableAlias">The alias for the table (optional).</param>
248
    /// <param name="columnAlias">The alias for the output column (optional).</param>
249
    /// <returns>The same builder instance for method chaining.</returns>
250
    public TBuilder Output(
251
        string columnName,
252
        string? tableAlias = null,
253
        string? columnAlias = null)
254
    {
255
        var outputClause = new ColumnExpression(columnName, tableAlias, columnAlias);
10✔
256

257
        OutputExpressions.Add(outputClause);
10✔
258

259
        return (TBuilder)this;
10✔
260
    }
261

262
    /// <summary>
263
    /// Conditionally adds an OUTPUT clause for the specified column name if the condition is met.
264
    /// </summary>
265
    /// <param name="columnName">The name of the column to include in the OUTPUT clause.</param>
266
    /// <param name="tableAlias">The alias for the table (optional).</param>
267
    /// <param name="columnAlias">The alias for the output column (optional).</param>
268
    /// <param name="condition">A function that determines whether to add the OUTPUT clause.</param>
269
    /// <returns>The same builder instance for method chaining.</returns>
270
    public TBuilder OutputIf(
271
        string columnName,
272
        string? tableAlias = null,
273
        string? columnAlias = null,
274
        Func<string, bool>? condition = null)
275
    {
276
        if (condition != null && !condition(columnName))
×
277
            return (TBuilder)this;
×
278

279
        return Output(columnName, tableAlias, columnAlias);
×
280
    }
281

282
    /// <summary>
283
    /// Builds the SQL UPSERT statement using the current configuration.
284
    /// </summary>
285
    /// <returns>A <see cref="QueryStatement"/> containing the SQL UPSERT statement and its parameters.</returns>
286
    public override QueryStatement? BuildStatement()
287
    {
288
        if (TableExpression is null)
27!
289
            throw new InvalidOperationException("Table must be specified before building an upsert statement.");
×
290

291
        if (ValueExpressions.Count == 0)
27!
292
            throw new InvalidOperationException("Values must be specified before building an upsert statement.");
×
293

294
        if (KeyExpressions.Count == 0)
27!
295
            throw new InvalidOperationException("Keys must be specified before building an upsert statement.");
×
296

297
        var updateExpressions = BuildUpdateExpressions();
27✔
298
        if (updateExpressions.Count == 0)
27!
299
            throw new InvalidOperationException("At least one non-key value must be specified before building an upsert statement.");
×
300

301
        var upsertStatement = new UpsertStatement(
27✔
302
            TableExpression,
27✔
303
            ColumnExpressions,
27✔
304
            ValueExpressions,
27✔
305
            KeyExpressions,
27✔
306
            updateExpressions,
27✔
307
            OutputExpressions,
27✔
308
            CommentExpressions);
27✔
309

310
        var statement = QueryGenerator.BuildUpsert(upsertStatement);
27✔
311

312
        return new QueryStatement(statement, Parameters);
27✔
313
    }
314

315
    private IReadOnlyCollection<UpdateExpression> BuildUpdateExpressions()
316
    {
317
        var keyColumns = new HashSet<string>(KeyExpressions.Select(static k => k.ColumnName), StringComparer.OrdinalIgnoreCase);
27✔
318

319
        return ColumnExpressions
27✔
320
            .Where(c => !keyColumns.Contains(c.ColumnName))
27✔
321
            .Select(c => new UpdateExpression(c.ColumnName, string.Empty, c.TableAlias, c.IsRaw))
27✔
322
            .ToArray();
27✔
323
    }
324
}
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