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

loresoft / FluentCommand / 26559531993

28 May 2026 06:51AM UTC coverage: 54.902% (+0.5%) from 54.377%
26559531993

push

github

pwelter34
Add UPSERT support to query builder and SQL generators

1405 of 3347 branches covered (41.98%)

Branch coverage included in aggregate %.

179 of 227 new or added lines in 7 files covered. (78.85%)

4341 of 7119 relevant lines covered (60.98%)

308.3 hits per line

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

70.55
/src/FluentCommand/Query/Generators/SqlServerGenerator.cs
1
using FluentCommand.Extensions;
2
using FluentCommand.Internal;
3

4
namespace FluentCommand.Query.Generators;
5

6
/// <summary>
7
/// Provides a SQL generator for SQL Server, implementing SQL statement and expression generation
8
/// with SQL Server-specific syntax and conventions.
9
/// </summary>
10
public class SqlServerGenerator : IQueryGenerator
11
{
12
    /// <summary>
13
    /// Builds a SQL SELECT statement for SQL Server, including support for JOIN, WHERE, GROUP BY, ORDER BY, LIMIT, and comments.
14
    /// </summary>
15
    /// <param name="selectStatement">The <see cref="SelectStatement"/> containing the SELECT statement configuration.</param>
16
    /// <returns>A SQL SELECT statement string for SQL Server.</returns>
17
    /// <exception cref="ArgumentException">Thrown if no table is specified in <paramref name="selectStatement"/>.</exception>
18
    public virtual string BuildSelect(SelectStatement selectStatement)
19
    {
20
        if (selectStatement.FromExpressions == null || selectStatement.FromExpressions.Count == 0)
51!
21
            throw new ArgumentException("No table specified to select from", nameof(selectStatement));
×
22

23
        var selectBuilder = StringBuilderCache.Acquire();
51✔
24

25
        if (selectStatement.CommentExpressions?.Count > 0)
51!
26
        {
27
            selectBuilder
17✔
28
                .AppendJoin(Environment.NewLine, selectStatement.CommentExpressions)
17✔
29
                .AppendLine();
17✔
30
        }
31

32
        selectBuilder
51✔
33
            .Append("SELECT ");
51✔
34

35
        if (selectStatement.SelectExpressions?.Count > 0)
51!
36
            selectBuilder.AppendJoin(", ", selectStatement.SelectExpressions.Select(SelectExpression));
26✔
37
        else
38
            selectBuilder.Append('*');
25✔
39

40
        selectBuilder
51✔
41
            .AppendLine()
51✔
42
            .Append("FROM ")
51✔
43
            .AppendJoin(", ", selectStatement.FromExpressions.Select(TableExpression));
51✔
44

45
        if (selectStatement.JoinExpressions?.Count > 0)
51!
46
        {
47
            foreach (var joinExpression in selectStatement.JoinExpressions)
36✔
48
            {
49
                selectBuilder
11✔
50
                    .AppendLine()
11✔
51
                    .Append(JoinExpression(joinExpression));
11✔
52
            }
53
        }
54

55
        if (selectStatement.WhereExpressions?.Count > 0)
51!
56
        {
57
            selectBuilder
32✔
58
                .AppendLine()
32✔
59
                .Append("WHERE ")
32✔
60
                .Append('(')
32✔
61
                .AppendJoin(" AND ", selectStatement.WhereExpressions.Select(WhereExpression))
32✔
62
                .Append(')');
32✔
63
        }
64

65
        if (selectStatement.GroupExpressions?.Count > 0)
51!
66
        {
67
            selectBuilder
3✔
68
                .AppendLine()
3✔
69
                .Append("GROUP BY ")
3✔
70
                .AppendJoin(", ", selectStatement.GroupExpressions.Select(GroupExpression));
3✔
71
        }
72

73
        if (selectStatement.SortExpressions?.Count > 0)
51!
74
        {
75
            selectBuilder
28✔
76
                .AppendLine()
28✔
77
                .Append("ORDER BY ")
28✔
78
                .AppendJoin(", ", selectStatement.SortExpressions.Select(SortExpression));
28✔
79
        }
80

81
        if (selectStatement.LimitExpressions?.Count > 0)
51!
82
        {
83
            selectBuilder
18✔
84
                .AppendLine()
18✔
85
                .AppendJoin(" ", selectStatement.LimitExpressions.Select(LimitExpression));
18✔
86
        }
87

88
        selectBuilder.AppendLine(";");
51✔
89

90
        return StringBuilderCache.ToString(selectBuilder);
51✔
91
    }
92

93
    /// <summary>
94
    /// Builds a SQL INSERT statement for SQL Server, including support for OUTPUT and comments.
95
    /// </summary>
96
    /// <param name="insertStatement">The <see cref="InsertStatement"/> containing the INSERT statement configuration.</param>
97
    /// <returns>A SQL INSERT statement string for SQL Server.</returns>
98
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="insertStatement"/> is <c>null</c>.</exception>
99
    /// <exception cref="ArgumentException">Thrown if the table or values are not specified.</exception>
100
    public virtual string BuildInsert(InsertStatement insertStatement)
101
    {
102
        if (insertStatement is null)
6!
103
            throw new ArgumentNullException(nameof(insertStatement));
×
104

105
        if (insertStatement.TableExpression == null)
6!
106
            throw new ArgumentException("No table specified to insert into", nameof(insertStatement));
×
107

108
        if (insertStatement.ValueExpressions == null || insertStatement.ValueExpressions.Count == 0)
6!
109
            throw new ArgumentException("No values specified for insert", nameof(insertStatement));
×
110

111
        var insertBuilder = StringBuilderCache.Acquire();
6✔
112

113
        if (insertStatement.CommentExpressions?.Count > 0)
6!
114
        {
115
            insertBuilder
4✔
116
                .AppendJoin(Environment.NewLine, insertStatement.CommentExpressions)
4✔
117
                .AppendLine();
4✔
118
        }
119

120
        var table = TableExpression(insertStatement.TableExpression);
6✔
121
        insertBuilder
6✔
122
            .Append("INSERT INTO ")
6✔
123
            .Append(table);
6✔
124

125
        if (insertStatement.ColumnExpressions?.Count > 0)
6!
126
        {
127
            insertBuilder
6✔
128
                .Append(" (")
6✔
129
                .AppendJoin(", ", insertStatement.ColumnExpressions.Select(ColumnExpression))
6✔
130
                .Append(')');
6✔
131
        }
132

133
        if (insertStatement.OutputExpressions?.Count > 0)
6!
134
        {
135

136
            insertBuilder
5✔
137
                .AppendLine()
5✔
138
                .Append("OUTPUT ")
5✔
139
                .AppendJoin(", ", insertStatement.OutputExpressions.Select(c => ColumnExpression(c, "INSERTED")));
5✔
140
        }
141

142
        insertBuilder
6✔
143
            .AppendLine()
6✔
144
            .Append("VALUES ")
6✔
145
            .Append('(')
6✔
146
            .AppendJoin(", ", insertStatement.ValueExpressions)
6✔
147
            .Append(");");
6✔
148

149
        return StringBuilderCache.ToString(insertBuilder);
6✔
150
    }
151

152
    /// <summary>
153
    /// Builds a SQL UPSERT statement for SQL Server using MERGE syntax, including support for OUTPUT and comments.
154
    /// </summary>
155
    /// <param name="upsertStatement">The <see cref="UpsertStatement"/> containing the UPSERT statement configuration.</param>
156
    /// <returns>A SQL UPSERT statement string for SQL Server.</returns>
157
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="upsertStatement"/> is <c>null</c>.</exception>
158
    /// <exception cref="ArgumentException">Thrown if the table, values, keys, or update values are not specified.</exception>
159
    public virtual string BuildUpsert(UpsertStatement upsertStatement)
160
    {
161
        ValidateUpsert(upsertStatement);
9✔
162

163
        var upsertBuilder = StringBuilderCache.Acquire();
9✔
164

165
        if (upsertStatement.CommentExpressions?.Count > 0)
9!
166
        {
NEW
167
            upsertBuilder
×
NEW
168
                .AppendJoin(Environment.NewLine, upsertStatement.CommentExpressions)
×
NEW
169
                .AppendLine();
×
170
        }
171

172
        var table = TableExpression(upsertStatement.TableExpression);
9✔
173
        var sourceColumns = upsertStatement.ColumnExpressions.Select(c => ColumnExpression(c)).ToArray();
9✔
174

175
        upsertBuilder
9✔
176
            .Append("MERGE INTO ")
9✔
177
            .Append(table)
9✔
178
            .AppendLine(" AS TARGET")
9✔
179
            .Append("USING (VALUES (")
9✔
180
            .AppendJoin(", ", upsertStatement.ValueExpressions)
9✔
181
            .AppendLine(")) AS SOURCE")
9✔
182
            .Append("(")
9✔
183
            .AppendJoin(", ", sourceColumns)
9✔
184
            .AppendLine(")")
9✔
185
            .Append("ON ")
9✔
186
            .AppendJoin(" AND ", upsertStatement.KeyExpressions.Select(k => $"TARGET.{ColumnExpression(k)} = SOURCE.{ColumnExpression(k)}"))
9✔
187
            .AppendLine()
9✔
188
            .AppendLine("WHEN MATCHED THEN")
9✔
189
            .Append("UPDATE SET ")
9✔
190
            .AppendJoin(", ", upsertStatement.UpdateExpressions.Select(u => $"TARGET.{ColumnExpression(u)} = SOURCE.{ColumnExpression(u)}"))
9✔
191
            .AppendLine()
9✔
192
            .AppendLine("WHEN NOT MATCHED THEN")
9✔
193
            .Append("INSERT (")
9✔
194
            .AppendJoin(", ", sourceColumns)
9✔
195
            .AppendLine(")")
9✔
196
            .Append("VALUES (")
9✔
197
            .AppendJoin(", ", sourceColumns.Select(c => $"SOURCE.{c}"))
9✔
198
            .Append(')');
9✔
199

200
        if (upsertStatement.OutputExpressions?.Count > 0)
9!
201
        {
202
            upsertBuilder
4✔
203
                .AppendLine()
4✔
204
                .Append("OUTPUT ")
4✔
205
                .AppendJoin(", ", upsertStatement.OutputExpressions.Select(c => ColumnExpression(c, "INSERTED")));
4✔
206
        }
207

208
        upsertBuilder.AppendLine(";");
9✔
209

210
        return StringBuilderCache.ToString(upsertBuilder);
9✔
211
    }
212

213
    /// <summary>
214
    /// Builds a SQL UPDATE statement for SQL Server, including support for OUTPUT, FROM, JOIN, WHERE, and comments.
215
    /// </summary>
216
    /// <param name="updateStatement">The <see cref="UpdateStatement"/> containing the UPDATE statement configuration.</param>
217
    /// <returns>A SQL UPDATE statement string for SQL Server.</returns>
218
    /// <exception cref="ArgumentException">Thrown if the table or update values are not specified.</exception>
219
    public virtual string BuildUpdate(UpdateStatement updateStatement)
220
    {
221
        if (updateStatement.UpdateExpressions == null || updateStatement.UpdateExpressions.Count == 0)
4!
222
            throw new ArgumentException("No values specified for update", nameof(updateStatement));
×
223

224
        var updateBuilder = StringBuilderCache.Acquire();
4✔
225

226
        if (updateStatement.CommentExpressions?.Count > 0)
4!
227
        {
228
            updateBuilder
2✔
229
                .AppendJoin(Environment.NewLine, updateStatement.CommentExpressions)
2✔
230
                .AppendLine();
2✔
231
        }
232

233
        var table = TableExpression(updateStatement.TableExpression);
4✔
234

235
        updateBuilder
4✔
236
            .Append("UPDATE ")
4✔
237
            .Append(table)
4✔
238
            .AppendLine()
4✔
239
            .Append("SET ")
4✔
240
            .AppendJoin(", ", updateStatement.UpdateExpressions.Select(UpdateExpression));
4✔
241

242
        if (updateStatement.OutputExpressions?.Count > 0)
4!
243
        {
244
            updateBuilder
4✔
245
                .AppendLine()
4✔
246
                .Append("OUTPUT ")
4✔
247
                .AppendJoin(", ", updateStatement.OutputExpressions.Select(c => ColumnExpression(c, "INSERTED")));
4✔
248
        }
249

250
        if (updateStatement.FromExpressions?.Count > 0)
4!
251
        {
252
            updateBuilder
1✔
253
                .AppendLine()
1✔
254
                .Append("FROM ")
1✔
255
                .AppendJoin(", ", updateStatement.FromExpressions.Select(TableExpression));
1✔
256
        }
257

258
        if (updateStatement.JoinExpressions?.Count > 0)
4!
259
        {
260
            foreach (var joinExpression in updateStatement.JoinExpressions)
4✔
261
            {
262
                updateBuilder
1✔
263
                    .AppendLine()
1✔
264
                    .Append(JoinExpression(joinExpression));
1✔
265
            }
266
        }
267

268
        if (updateStatement.WhereExpressions?.Count > 0)
4!
269
        {
270
            updateBuilder
4✔
271
                .AppendLine()
4✔
272
                .Append("WHERE ")
4✔
273
                .Append('(')
4✔
274
                .AppendJoin(" AND ", updateStatement.WhereExpressions.Select(WhereExpression))
4✔
275
                .Append(')');
4✔
276
        }
277

278
        updateBuilder.AppendLine(";");
4✔
279

280
        return StringBuilderCache.ToString(updateBuilder);
4✔
281
    }
282

283
    /// <summary>
284
    /// Builds a SQL DELETE statement for SQL Server, including support for OUTPUT, FROM, JOIN, WHERE, and comments.
285
    /// </summary>
286
    /// <param name="deleteStatement">The <see cref="DeleteStatement"/> containing the DELETE statement configuration.</param>
287
    /// <returns>A SQL DELETE statement string for SQL Server.</returns>
288
    /// <exception cref="ArgumentException">Thrown if the table is not specified.</exception>
289
    public virtual string BuildDelete(DeleteStatement deleteStatement)
290
    {
291
        if (deleteStatement.TableExpression == null)
4!
292
            throw new ArgumentException("No table specified to delete from", nameof(deleteStatement));
×
293

294
        var deleteBuilder = StringBuilderCache.Acquire();
4✔
295

296
        if (deleteStatement.CommentExpressions?.Count > 0)
4!
297
        {
298
            deleteBuilder
3✔
299
                .AppendJoin(Environment.NewLine, deleteStatement.CommentExpressions)
3✔
300
                .AppendLine();
3✔
301
        }
302

303
        var table = TableExpression(deleteStatement.TableExpression);
4✔
304

305
        deleteBuilder
4✔
306
            .Append("DELETE FROM ")
4✔
307
            .Append(table);
4✔
308

309
        if (deleteStatement.OutputExpressions?.Count > 0)
4!
310
        {
311
            deleteBuilder
4✔
312
                .AppendLine()
4✔
313
                .Append("OUTPUT ")
4✔
314
                .AppendJoin(", ", deleteStatement.OutputExpressions.Select(c => ColumnExpression(c, "DELETED")));
4✔
315
        }
316

317
        if (deleteStatement.FromExpressions?.Count > 0)
4!
318
        {
319
            deleteBuilder
1✔
320
                .AppendLine()
1✔
321
                .Append("FROM ")
1✔
322
                .AppendJoin(", ", deleteStatement.FromExpressions.Select(TableExpression));
1✔
323
        }
324

325
        if (deleteStatement.JoinExpressions?.Count > 0)
4!
326
        {
327
            foreach (var joinExpression in deleteStatement.JoinExpressions)
4✔
328
            {
329
                deleteBuilder
1✔
330
                    .AppendLine()
1✔
331
                    .Append(JoinExpression(joinExpression));
1✔
332
            }
333
        }
334

335
        if (deleteStatement.WhereExpressions?.Count > 0)
4!
336
        {
337
            deleteBuilder
4✔
338
                .AppendLine()
4✔
339
                .Append("WHERE ")
4✔
340
                .Append('(')
4✔
341
                .AppendJoin(" AND ", deleteStatement.WhereExpressions.Select(WhereExpression))
4✔
342
                .Append(')');
4✔
343
        }
344

345
        deleteBuilder.AppendLine(";");
4✔
346

347
        return StringBuilderCache.ToString(deleteBuilder);
4✔
348
    }
349

350
    /// <summary>
351
    /// Builds a SQL WHERE clause from the specified collection of <see cref="WhereExpression"/> objects.
352
    /// </summary>
353
    /// <param name="whereExpressions">A collection of <see cref="WhereExpression"/> objects representing WHERE conditions.</param>
354
    /// <returns>A SQL WHERE clause string, or <c>null</c> if no expressions are provided.</returns>
355
    public virtual string? BuildWhere(IReadOnlyCollection<WhereExpression> whereExpressions)
356
    {
357
        if (whereExpressions == null || whereExpressions.Count == 0)
×
358
            return null;
×
359

360
        var whereBuilder = StringBuilderCache.Acquire();
×
361

362
        if (whereExpressions?.Count > 0)
×
363
        {
364
            whereBuilder
×
365
                .Append('(')
×
366
                .AppendJoin(" AND ", whereExpressions.Select(WhereExpression))
×
367
                .Append(')');
×
368
        }
369

370
        return StringBuilderCache.ToString(whereBuilder);
×
371
    }
372

373
    /// <summary>
374
    /// Builds a SQL ORDER BY clause from the specified collection of <see cref="SortExpression"/> objects.
375
    /// </summary>
376
    /// <param name="sortExpressions">A collection of <see cref="SortExpression"/> objects representing sort conditions.</param>
377
    /// <returns>A SQL ORDER BY clause string, or <c>null</c> if no expressions are provided.</returns>
378
    public virtual string? BuildOrder(IReadOnlyCollection<SortExpression> sortExpressions)
379
    {
380
        if (sortExpressions == null || sortExpressions.Count == 0)
×
381
            return null;
×
382

383
        var orderBuilder = StringBuilderCache.Acquire();
×
384

385
        if (sortExpressions?.Count > 0)
×
386
        {
387
            orderBuilder
×
388
                .AppendJoin(", ", sortExpressions.Select(SortExpression));
×
389
        }
390

391
        return StringBuilderCache.ToString(orderBuilder);
×
392
    }
393

394
    /// <summary>
395
    /// Builds a SQL comment expression.
396
    /// </summary>
397
    /// <param name="comment">The comment text.</param>
398
    /// <returns>A SQL comment string.</returns>
399
    public virtual string CommentExpression(string comment)
400
    {
401
        return $"/* {comment} */";
36✔
402
    }
403

404
    /// <summary>
405
    /// Builds a SQL SELECT column or aggregate expression.
406
    /// </summary>
407
    /// <param name="columnExpression">
408
    /// The <see cref="FluentCommand.Query.Generators.ColumnExpression"/> or
409
    /// <see cref="FluentCommand.Query.Generators.AggregateExpression"/> to select.
410
    /// </param>
411
    public virtual string SelectExpression(ColumnExpression columnExpression)
412
    {
413
        if (columnExpression is AggregateExpression aggregateExpression)
104✔
414
            return AggregateExpression(aggregateExpression);
6✔
415

416
        return ColumnExpression(columnExpression);
98✔
417
    }
418

419
    /// <summary>
420
    /// Builds a SQL column expression from the specified <see cref="FluentCommand.Query.Generators.ColumnExpression"/>.
421
    /// </summary>
422
    /// <param name="columnExpression">The <see cref="FluentCommand.Query.Generators.ColumnExpression"/> representing the column.</param>
423
    /// <returns>A SQL column expression string.</returns>
424
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="columnExpression"/> is <c>null</c>.</exception>
425
    /// <exception cref="ArgumentException">Thrown if the column name is not specified.</exception>
426
    public virtual string ColumnExpression(ColumnExpression columnExpression)
427
    {
428
        if (columnExpression is null)
899!
429
            throw new ArgumentNullException(nameof(columnExpression));
×
430

431
        if (columnExpression.ColumnName.IsNullOrWhiteSpace())
899!
432
            throw new ArgumentException($"'{nameof(columnExpression.ColumnName)}' property cannot be null or empty.", nameof(columnExpression));
×
433

434
        if (columnExpression.IsRaw)
899!
435
            return columnExpression.ColumnName;
×
436

437
        var quotedName = QuoteIdentifier(columnExpression.ColumnName);
899✔
438

439
        var clause = columnExpression.TableAlias.HasValue()
899✔
440
            ? $"{QuoteIdentifier(columnExpression.TableAlias)}.{quotedName}"
899✔
441
            : quotedName;
899✔
442

443
        if (columnExpression.ColumnAlias.HasValue())
899✔
444
            clause += $" AS {QuoteIdentifier(columnExpression.ColumnAlias)}";
7✔
445

446
        return clause;
899✔
447
    }
448

449
    /// <summary>
450
    /// Builds a SQL aggregate expression (e.g., COUNT, SUM) from the specified <see cref="AggregateExpression"/>.
451
    /// </summary>
452
    /// <param name="aggregateExpression">The <see cref="AggregateExpression"/> representing the aggregate function.</param>
453
    /// <returns>A SQL aggregate expression string.</returns>
454
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="aggregateExpression"/> is <c>null</c>.</exception>
455
    public virtual string AggregateExpression(AggregateExpression aggregateExpression)
456
    {
457
        if (aggregateExpression is null)
6!
458
            throw new ArgumentNullException(nameof(aggregateExpression));
×
459

460
        if (aggregateExpression.IsRaw)
6!
461
            return aggregateExpression.ColumnName;
×
462

463
        var selectClause = ColumnExpression(aggregateExpression);
6✔
464

465
        return aggregateExpression.Aggregate switch
6!
466
        {
6✔
467
            AggregateFunctions.Average => $"AVG({selectClause})",
×
468
            AggregateFunctions.Count => $"COUNT({selectClause})",
3✔
469
            AggregateFunctions.Max => $"MAX({selectClause})",
×
470
            AggregateFunctions.Min => $"MIN({selectClause})",
×
471
            AggregateFunctions.Sum => $"SUM({selectClause})",
3✔
472
            _ => throw new NotImplementedException(),
×
473
        };
6✔
474
    }
475

476
    /// <summary>
477
    /// Builds a SQL table expression from the specified <see cref="TableExpression"/>.
478
    /// </summary>
479
    /// <param name="tableExpression">The <see cref="TableExpression"/> representing the table.</param>
480
    /// <returns>A SQL table expression string.</returns>
481
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="tableExpression"/> is <c>null</c>.</exception>
482
    /// <exception cref="ArgumentException">Thrown if the table name is not specified.</exception>
483
    public virtual string TableExpression(TableExpression tableExpression)
484
    {
485
        if (tableExpression is null)
116!
486
            throw new ArgumentNullException(nameof(tableExpression));
×
487

488
        if (tableExpression.TableName.IsNullOrWhiteSpace())
116!
489
            throw new ArgumentException($"'{nameof(tableExpression.TableName)}' property cannot be null or empty.", nameof(tableExpression));
×
490

491
        if (tableExpression.IsRaw)
116✔
492
            return tableExpression.TableName;
3✔
493

494
        var quotedName = QuoteIdentifier(tableExpression.TableName);
113✔
495

496
        var fromClause = tableExpression.TableSchema.HasValue()
113✔
497
            ? $"{QuoteIdentifier(tableExpression.TableSchema)}.{quotedName}"
113✔
498
            : quotedName;
113✔
499

500
        if (tableExpression.TableAlias.HasValue())
113✔
501
            fromClause += $" AS {QuoteIdentifier(tableExpression.TableAlias)}";
20✔
502

503
        return fromClause;
113✔
504
    }
505

506
    /// <summary>
507
    /// Builds a SQL sort expression from the specified <see cref="SortExpression"/>.
508
    /// </summary>
509
    /// <param name="sortExpression">The <see cref="SortExpression"/> representing the sort condition.</param>
510
    /// <returns>A SQL sort expression string.</returns>
511
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="sortExpression"/> is <c>null</c>.</exception>
512
    /// <exception cref="ArgumentException">Thrown if the column name is not specified.</exception>
513
    public virtual string SortExpression(SortExpression sortExpression)
514
    {
515
        if (sortExpression is null)
32!
516
            throw new ArgumentNullException(nameof(sortExpression));
×
517

518
        if (sortExpression.ColumnName.IsNullOrWhiteSpace())
32!
519
            throw new ArgumentException($"'{nameof(sortExpression.ColumnName)}' property cannot be null or empty.", nameof(sortExpression));
×
520

521
        if (sortExpression.IsRaw)
32!
522
            return sortExpression.ColumnName;
×
523

524
        var quotedName = ColumnExpression(sortExpression);
32✔
525

526
        return sortExpression.SortDirection == SortDirections.Ascending
32✔
527
            ? $"{quotedName} ASC"
32✔
528
            : $"{quotedName} DESC";
32✔
529
    }
530

531
    /// <summary>
532
    /// Builds a SQL GROUP BY expression from the specified <see cref="GroupExpression"/>.
533
    /// </summary>
534
    /// <param name="groupExpression">The <see cref="GroupExpression"/> representing the group by condition.</param>
535
    /// <returns>A SQL GROUP BY expression string.</returns>
536
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="groupExpression"/> is <c>null</c>.</exception>
537
    public virtual string GroupExpression(GroupExpression groupExpression)
538
    {
539
        if (groupExpression is null)
3!
540
            throw new ArgumentNullException(nameof(groupExpression));
×
541

542
        return ColumnExpression(groupExpression);
3✔
543
    }
544

545
    /// <summary>
546
    /// Builds a SQL WHERE expression from the specified <see cref="WhereExpression"/>.
547
    /// </summary>
548
    /// <param name="whereExpression">The <see cref="WhereExpression"/> representing the condition.</param>
549
    /// <returns>A SQL WHERE expression string.</returns>
550
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="whereExpression"/> is <c>null</c>.</exception>
551
    /// <exception cref="ArgumentException">Thrown if required properties are not specified.</exception>
552
    public virtual string WhereExpression(WhereExpression whereExpression)
553
    {
554
        if (whereExpression is null)
54!
555
            throw new ArgumentNullException(nameof(whereExpression));
×
556

557
        if (whereExpression.ColumnName.IsNullOrWhiteSpace())
54!
558
            throw new ArgumentException($"'{nameof(whereExpression.ColumnName)}' property cannot be null or empty.", nameof(whereExpression));
×
559

560
        if (whereExpression.IsRaw)
54✔
561
            return whereExpression.ColumnName;
6✔
562

563
        var parameterlessFilters = new[] { FilterOperators.IsNull, FilterOperators.IsNotNull };
48✔
564
        if (!parameterlessFilters.Contains(whereExpression.FilterOperator) && whereExpression.ParameterName.IsNullOrWhiteSpace())
48!
565
            throw new ArgumentException($"'{nameof(whereExpression.ParameterName)}' property cannot be null or empty.", nameof(whereExpression));
×
566

567
        var columnName = ColumnExpression(whereExpression);
48✔
568

569
        return whereExpression.FilterOperator switch
48!
570
        {
48✔
571
            FilterOperators.StartsWith => $"{columnName} LIKE {whereExpression.ParameterName} + '%'",
×
572
            FilterOperators.EndsWith => $"{columnName} LIKE '%' + {whereExpression.ParameterName}",
×
573
            FilterOperators.Contains => $"{columnName} LIKE '%' + {whereExpression.ParameterName} + '%'",
8✔
574
            FilterOperators.Equal => $"{columnName} = {whereExpression.ParameterName}",
26✔
575
            FilterOperators.NotEqual => $"{columnName} != {whereExpression.ParameterName}",
3✔
576
            FilterOperators.LessThan => $"{columnName} < {whereExpression.ParameterName}",
×
577
            FilterOperators.LessThanOrEqual => $"{columnName} <= {whereExpression.ParameterName}",
×
578
            FilterOperators.GreaterThan => $"{columnName} > {whereExpression.ParameterName}",
1✔
579
            FilterOperators.GreaterThanOrEqual => $"{columnName} >= {whereExpression.ParameterName}",
2✔
580
            FilterOperators.IsNull => $"{columnName} IS NULL",
×
581
            FilterOperators.IsNotNull => $"{columnName} IS NOT NULL",
×
582
            FilterOperators.In => $"{columnName} IN ({whereExpression.ParameterName})",
8✔
583
            _ => $"{columnName} = {whereExpression.ParameterName}",
×
584
        };
48✔
585
    }
586

587
    /// <summary>
588
    /// Builds a logical SQL expression (e.g., AND/OR group) from the specified WHERE expressions and logical operator.
589
    /// </summary>
590
    /// <param name="whereExpressions">A collection of <see cref="WhereExpression"/> objects representing conditions.</param>
591
    /// <param name="logicalOperator">The <see cref="LogicalOperators"/> value to combine the expressions.</param>
592
    /// <returns>A logical SQL expression string, or an empty string if no expressions are provided.</returns>
593
    public virtual string LogicalExpression(IReadOnlyCollection<WhereExpression> whereExpressions, LogicalOperators logicalOperator)
594
    {
595
        if (whereExpressions == null || whereExpressions.Count == 0)
7!
596
            return string.Empty;
×
597

598
        var stringBuilder = StringBuilderCache.Acquire();
7✔
599
        var logical = logicalOperator == LogicalOperators.And ? " AND " : " OR ";
7✔
600

601
        stringBuilder
7✔
602
            .Append('(')
7✔
603
            .AppendJoin(logical, whereExpressions.Select(WhereExpression))
7✔
604
            .Append(')' );
7✔
605

606
        return StringBuilderCache.ToString(stringBuilder);
7✔
607
    }
608

609
    /// <summary>
610
    /// Builds a SQL LIMIT/OFFSET expression for SQL Server.
611
    /// </summary>
612
    /// <param name="limitExpression">The <see cref="LimitExpression"/> representing the limit and offset.</param>
613
    /// <returns>A SQL LIMIT/OFFSET expression string for SQL Server, or an empty string if not applicable.</returns>
614
    public virtual string LimitExpression(LimitExpression limitExpression)
615
    {
616
        if (limitExpression is null || limitExpression.Size == 0)
12!
617
            return string.Empty;
×
618

619
        return $"OFFSET {limitExpression.Offset} ROWS FETCH NEXT {limitExpression.Size} ROWS ONLY";
12✔
620
    }
621

622
    /// <summary>
623
    /// Builds a SQL update expression from the specified <see cref="UpdateExpression"/>.
624
    /// </summary>
625
    /// <param name="updateExpression">The <see cref="UpdateExpression"/> representing the update operation.</param>
626
    /// <returns>A SQL update expression string.</returns>
627
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="updateExpression"/> is <c>null</c>.</exception>
628
    /// <exception cref="ArgumentException">Thrown if required properties are not specified.</exception>
629
    public virtual string UpdateExpression(UpdateExpression updateExpression)
630
    {
631
        if (updateExpression is null)
11!
632
            throw new ArgumentNullException(nameof(updateExpression));
×
633

634
        if (updateExpression.ColumnName.IsNullOrWhiteSpace())
11!
635
            throw new ArgumentException($"'{nameof(updateExpression.ColumnName)}' cannot be null or empty.", nameof(updateExpression));
×
636

637
        if (updateExpression.IsRaw)
11!
638
            return updateExpression.ColumnName;
×
639

640
        if (updateExpression.ParameterName.IsNullOrWhiteSpace())
11!
641
            throw new ArgumentException($"'{nameof(updateExpression.ParameterName)}' cannot be null or empty.", nameof(updateExpression));
×
642

643
        var quotedName = ColumnExpression(updateExpression);
11✔
644

645
        return $"{quotedName} = {updateExpression.ParameterName}";
11✔
646
    }
647

648
    /// <summary>
649
    /// Validates that an UPSERT statement has the required table, values, keys, and update expressions.
650
    /// </summary>
651
    /// <param name="upsertStatement">The <see cref="UpsertStatement"/> to validate.</param>
652
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="upsertStatement"/> is <c>null</c>.</exception>
653
    /// <exception cref="ArgumentException">Thrown if required upsert configuration is missing.</exception>
654
    protected static void ValidateUpsert(UpsertStatement upsertStatement)
655
    {
656
        if (upsertStatement is null)
23!
NEW
657
            throw new ArgumentNullException(nameof(upsertStatement));
×
658

659
        if (upsertStatement.TableExpression == null)
23!
NEW
660
            throw new ArgumentException("No table specified to upsert into", nameof(upsertStatement));
×
661

662
        if (upsertStatement.ColumnExpressions == null || upsertStatement.ColumnExpressions.Count == 0)
23!
NEW
663
            throw new ArgumentException("No columns specified for upsert", nameof(upsertStatement));
×
664

665
        if (upsertStatement.ValueExpressions == null || upsertStatement.ValueExpressions.Count == 0)
23!
NEW
666
            throw new ArgumentException("No values specified for upsert", nameof(upsertStatement));
×
667

668
        if (upsertStatement.KeyExpressions == null || upsertStatement.KeyExpressions.Count == 0)
23!
NEW
669
            throw new ArgumentException("No keys specified for upsert", nameof(upsertStatement));
×
670

671
        if (upsertStatement.UpdateExpressions == null || upsertStatement.UpdateExpressions.Count == 0)
23!
NEW
672
            throw new ArgumentException("No update values specified for upsert", nameof(upsertStatement));
×
673
    }
23✔
674

675
    /// <summary>
676
    /// Builds a SQL JOIN expression from the specified <see cref="JoinExpression"/>.
677
    /// </summary>
678
    /// <param name="joinExpression">The <see cref="JoinExpression"/> representing the join operation.</param>
679
    /// <returns>A SQL JOIN expression string.</returns>
680
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="joinExpression"/> is <c>null</c>.</exception>
681
    /// <exception cref="ArgumentException">Thrown if required properties are not specified.</exception>
682
    public virtual string JoinExpression(JoinExpression joinExpression)
683
    {
684
        if (joinExpression is null)
13!
685
            throw new ArgumentNullException(nameof(joinExpression));
×
686

687
        var leftColumn = ColumnExpression(new ColumnExpression(joinExpression.LeftColumnName, joinExpression.LeftTableAlias));
13✔
688
        var rightColumn = ColumnExpression(new ColumnExpression(joinExpression.RightColumnName, joinExpression.RightTableAlias));
13✔
689
        var rightTable = TableExpression(new TableExpression(joinExpression.RightTableName, joinExpression.RightTableSchema, joinExpression.RightTableAlias));
13✔
690

691
        var joinType = joinExpression.JoinType switch
13!
692
        {
13✔
693
            JoinTypes.Inner => "INNER JOIN",
12✔
694
            JoinTypes.Left => "LEFT OUTER JOIN",
1✔
695
            JoinTypes.Right => "RIGHT OUTER JOIN",
×
696
            _ => throw new NotImplementedException(),
×
697
        };
13✔
698

699
        return $"{joinType} {rightTable} ON {leftColumn} = {rightColumn}";
13✔
700
    }
701

702
    /// <summary>
703
    /// Quotes an identifier (such as a table or column name) for SQL Server, using square brackets.
704
    /// </summary>
705
    /// <param name="name">The identifier to quote.</param>
706
    /// <returns>The quoted identifier, or the original name if quoting is not required.</returns>
707
    public virtual string QuoteIdentifier(string name)
708
    {
709
        if (name.IsNullOrWhiteSpace())
692!
710
            return string.Empty;
×
711

712
        if (name == "*")
692✔
713
            return name;
1✔
714

715
        if (name.StartsWith('[') && name.EndsWith(']'))
691!
716
            return name;
×
717

718
        return "[" + name.Replace("]", "]]") + "]";
691✔
719
    }
720

721
    /// <summary>
722
    /// Parses a quoted identifier and returns the unquoted name for SQL Server.
723
    /// </summary>
724
    /// <param name="name">The quoted identifier.</param>
725
    /// <returns>The unquoted identifier name.</returns>
726
    public virtual string ParseIdentifier(string name)
727
    {
728
        if (name.StartsWith('[') && name.EndsWith(']'))
×
729
            return name[1..^1];
×
730

731
        return name;
×
732
    }
733

734
    /// <summary>
735
    /// Builds a SQL column expression with a specific table alias.
736
    /// </summary>
737
    /// <param name="columnExpression">The <see cref="Generators.ColumnExpression"/> representing the column.</param>
738
    /// <param name="tableAlias">The table alias to use if not already set.</param>
739
    /// <returns>A SQL column expression string with the specified table alias.</returns>
740
    private string ColumnExpression(ColumnExpression columnExpression, string tableAlias)
741
    {
742
        var column = columnExpression with
17!
743
        {
17✔
744
            TableAlias = columnExpression.TableAlias ?? tableAlias
17✔
745
        };
17✔
746

747
        return ColumnExpression(column);
17✔
748
    }
749
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc