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

loresoft / FluentCommand / 36390489643

28 Sep 2026 07:13AM UTC coverage: 61.25% (-4.7%) from 65.903%
36390489643

Pull #788

github

web-flow
Merge cbe8fb84b into 96088ce32
Pull Request #788: Bump coverlet.MTP from 10.0.1 to 10.1.0

1599 of 3466 branches covered (46.13%)

Branch coverage included in aggregate %.

5397 of 7956 relevant lines covered (67.84%)

295.03 hits per line

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

66.2
/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)
40 !
21
            throw new ArgumentException("No table specified to select from", nameof(selectStatement));
×
22

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

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

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

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

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

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

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

65
        if (selectStatement.GroupExpressions?.Count > 0)
40 !
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)
40 !
74
        {
75
            selectBuilder
19 ✔
76
                .AppendLine()
19 ✔
77
                .Append("ORDER BY ")
19 ✔
78
                .AppendJoin(", ", selectStatement.SortExpressions.Select(SortExpression));
19 ✔
79
        }
80

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

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

90
        return StringBuilderCache.ToString(selectBuilder);
40 ✔
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
        ArgumentNullException.ThrowIfNull(insertStatement);
7 ✔
103

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

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

110
        var insertBuilder = StringBuilderCache.Acquire();
7 ✔
111

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

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

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

132
        if (insertStatement.OutputExpressions?.Count > 0)
7 !
133
        {
134

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

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

148
        return StringBuilderCache.ToString(insertBuilder);
7 ✔
149
    }
150

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

162
        var upsertBuilder = StringBuilderCache.Acquire();
8 ✔
163

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

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

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

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

207
        upsertBuilder.AppendLine(";");
8 ✔
208

209
        return StringBuilderCache.ToString(upsertBuilder);
8 ✔
210
    }
211

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

223
        var updateBuilder = StringBuilderCache.Acquire();
3 ✔
224

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

232
        var table = TableExpression(updateStatement.TableExpression);
3 ✔
233

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

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

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

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

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

277
        updateBuilder.AppendLine(";");
3 ✔
278

279
        return StringBuilderCache.ToString(updateBuilder);
3 ✔
280
    }
281

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

293
        var deleteBuilder = StringBuilderCache.Acquire();
1 ✔
294

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

302
        var table = TableExpression(deleteStatement.TableExpression);
1 ✔
303

304
        deleteBuilder
1 ✔
305
            .Append("DELETE FROM ")
1 ✔
306
            .Append(table);
1 ✔
307

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

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

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

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

344
        deleteBuilder.AppendLine(";");
1 ✔
345

346
        return StringBuilderCache.ToString(deleteBuilder);
1 ✔
347
    }
348

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

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

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

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

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

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

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

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

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

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

415
        return ColumnExpression(columnExpression);
31 ✔
416
    }
417

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

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

432
        if (columnExpression.IsRaw)
756 !
433
            return columnExpression.ColumnName;
×
434

435
        var quotedName = QuoteIdentifier(columnExpression.ColumnName);
756 ✔
436

437
        var clause = columnExpression.TableAlias.HasValue()
756 ✔
438
            ? $"{QuoteIdentifier(columnExpression.TableAlias)}.{quotedName}"
756 ✔
439
            : quotedName;
756 ✔
440

441
        if (columnExpression.ColumnAlias.HasValue())
756 ✔
442
            clause += $" AS {QuoteIdentifier(columnExpression.ColumnAlias)}";
3 ✔
443

444
        return clause;
756 ✔
445
    }
446

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

457
        if (aggregateExpression.IsRaw)
6 !
458
            return aggregateExpression.ColumnName;
×
459

460
        var selectClause = ColumnExpression(aggregateExpression);
6 ✔
461

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

473
    /// <summary>
474
    /// Builds a SQL table expression from the specified <see cref="TableExpression"/>.
475
    /// </summary>
476
    /// <param name="tableExpression">The <see cref="TableExpression"/> representing the table.</param>
477
    /// <returns>A SQL table expression string.</returns>
478
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="tableExpression"/> is <c>null</c>.</exception>
479
    /// <exception cref="ArgumentException">Thrown if the table name is not specified.</exception>
480
    public virtual string TableExpression(TableExpression tableExpression)
481
    {
482
        ArgumentNullException.ThrowIfNull(tableExpression);
96 ✔
483

484
        if (tableExpression.TableName.IsNullOrWhiteSpace())
96 !
485
            throw new ArgumentException($"'{nameof(tableExpression.TableName)}' property cannot be null or empty.", nameof(tableExpression));
×
486

487
        if (tableExpression.IsRaw)
96 ✔
488
            return tableExpression.TableName;
1 ✔
489

490
        var quotedName = QuoteIdentifier(tableExpression.TableName);
95 ✔
491

492
        var fromClause = tableExpression.TableSchema.HasValue()
95 ✔
493
            ? $"{QuoteIdentifier(tableExpression.TableSchema)}.{quotedName}"
95 ✔
494
            : quotedName;
95 ✔
495

496
        if (tableExpression.TableAlias.HasValue())
95 ✔
497
            fromClause += $" AS {QuoteIdentifier(tableExpression.TableAlias)}";
12 ✔
498

499
        return fromClause;
95 ✔
500
    }
501

502
    /// <summary>
503
    /// Builds a SQL sort expression from the specified <see cref="SortExpression"/>.
504
    /// </summary>
505
    /// <param name="sortExpression">The <see cref="SortExpression"/> representing the sort condition.</param>
506
    /// <returns>A SQL sort expression string.</returns>
507
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="sortExpression"/> is <c>null</c>.</exception>
508
    /// <exception cref="ArgumentException">Thrown if the column name is not specified.</exception>
509
    public virtual string SortExpression(SortExpression sortExpression)
510
    {
511
        ArgumentNullException.ThrowIfNull(sortExpression);
19 ✔
512

513
        if (sortExpression.ColumnName.IsNullOrWhiteSpace())
19 !
514
            throw new ArgumentException($"'{nameof(sortExpression.ColumnName)}' property cannot be null or empty.", nameof(sortExpression));
×
515

516
        if (sortExpression.IsRaw)
19 !
517
            return sortExpression.ColumnName;
×
518

519
        var quotedName = ColumnExpression(sortExpression);
19 ✔
520

521
        return sortExpression.SortDirection == SortDirections.Ascending
19 !
522
            ? $"{quotedName} ASC"
19 ✔
523
            : $"{quotedName} DESC";
19 ✔
524
    }
525

526
    /// <summary>
527
    /// Builds a SQL GROUP BY expression from the specified <see cref="GroupExpression"/>.
528
    /// </summary>
529
    /// <param name="groupExpression">The <see cref="GroupExpression"/> representing the group by condition.</param>
530
    /// <returns>A SQL GROUP BY expression string.</returns>
531
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="groupExpression"/> is <c>null</c>.</exception>
532
    public virtual string GroupExpression(GroupExpression groupExpression)
533
    {
534
        ArgumentNullException.ThrowIfNull(groupExpression);
3 ✔
535

536
        return ColumnExpression(groupExpression);
3 ✔
537
    }
538

539
    /// <summary>
540
    /// Builds a SQL WHERE expression from the specified <see cref="WhereExpression"/>.
541
    /// </summary>
542
    /// <param name="whereExpression">The <see cref="WhereExpression"/> representing the condition.</param>
543
    /// <returns>A SQL WHERE expression string.</returns>
544
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="whereExpression"/> is <c>null</c>.</exception>
545
    /// <exception cref="ArgumentException">Thrown if required properties are not specified.</exception>
546
    public virtual string WhereExpression(WhereExpression whereExpression)
547
    {
548
        ArgumentNullException.ThrowIfNull(whereExpression);
16 ✔
549

550
        if (whereExpression.ColumnName.IsNullOrWhiteSpace())
16 !
551
            throw new ArgumentException($"'{nameof(whereExpression.ColumnName)}' property cannot be null or empty.", nameof(whereExpression));
×
552

553
        if (whereExpression.IsRaw)
16 ✔
554
            return whereExpression.ColumnName;
1 ✔
555

556
        var parameterlessFilters = new[] { FilterOperators.IsNull, FilterOperators.IsNotNull };
15 ✔
557
        if (!parameterlessFilters.Contains(whereExpression.FilterOperator) && whereExpression.ParameterName.IsNullOrWhiteSpace())
15 !
558
            throw new ArgumentException($"'{nameof(whereExpression.ParameterName)}' property cannot be null or empty.", nameof(whereExpression));
×
559

560
        var columnName = ColumnExpression(whereExpression);
15 ✔
561

562
        return whereExpression.FilterOperator switch
15 !
563
        {
15 ✔
564
            FilterOperators.StartsWith => $"{columnName} LIKE {whereExpression.ParameterName} + '%'",
×
565
            FilterOperators.NotStartsWith => $"{columnName} NOT LIKE {whereExpression.ParameterName} + '%'",
×
566
            FilterOperators.EndsWith => $"{columnName} LIKE '%' + {whereExpression.ParameterName}",
×
567
            FilterOperators.NotEndsWith => $"{columnName} NOT LIKE '%' + {whereExpression.ParameterName}",
×
568
            FilterOperators.Contains => $"{columnName} LIKE '%' + {whereExpression.ParameterName} + '%'",
2 ✔
569
            FilterOperators.NotContains => $"{columnName} NOT LIKE '%' + {whereExpression.ParameterName} + '%'",
×
570
            FilterOperators.Equal => $"{columnName} = {whereExpression.ParameterName}",
9 ✔
571
            FilterOperators.NotEqual => $"{columnName} != {whereExpression.ParameterName}",
1 ✔
572
            FilterOperators.LessThan => $"{columnName} < {whereExpression.ParameterName}",
×
573
            FilterOperators.LessThanOrEqual => $"{columnName} <= {whereExpression.ParameterName}",
×
574
            FilterOperators.GreaterThan => $"{columnName} > {whereExpression.ParameterName}",
×
575
            FilterOperators.GreaterThanOrEqual => $"{columnName} >= {whereExpression.ParameterName}",
×
576
            FilterOperators.IsNull => $"{columnName} IS NULL",
×
577
            FilterOperators.IsNotNull => $"{columnName} IS NOT NULL",
×
578
            FilterOperators.In => $"{columnName} IN ({whereExpression.ParameterName})",
3 ✔
579
            _ => $"{columnName} = {whereExpression.ParameterName}",
×
580
        };
15 ✔
581
    }
582

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

594
        var stringBuilder = StringBuilderCache.Acquire();
3 ✔
595
        var logical = logicalOperator == LogicalOperators.And ? " AND " : " OR ";
3 !
596

597
        stringBuilder
3 ✔
598
            .Append('(')
3 ✔
599
            .AppendJoin(logical, whereExpressions.Select(WhereExpression))
3 ✔
600
            .Append(')' );
3 ✔
601

602
        return StringBuilderCache.ToString(stringBuilder);
3 ✔
603
    }
604

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

615
        return $"OFFSET {limitExpression.Offset} ROWS FETCH NEXT {limitExpression.Size} ROWS ONLY";
11 ✔
616
    }
617

618
    /// <summary>
619
    /// Builds a SQL update expression from the specified <see cref="UpdateExpression"/>.
620
    /// </summary>
621
    /// <param name="updateExpression">The <see cref="UpdateExpression"/> representing the update operation.</param>
622
    /// <returns>A SQL update expression string.</returns>
623
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="updateExpression"/> is <c>null</c>.</exception>
624
    /// <exception cref="ArgumentException">Thrown if required properties are not specified.</exception>
625
    public virtual string UpdateExpression(UpdateExpression updateExpression)
626
    {
627
        ArgumentNullException.ThrowIfNull(updateExpression);
7 ✔
628

629
        if (updateExpression.ColumnName.IsNullOrWhiteSpace())
7 !
630
            throw new ArgumentException($"'{nameof(updateExpression.ColumnName)}' cannot be null or empty.", nameof(updateExpression));
×
631

632
        if (updateExpression.IsRaw)
7 !
633
            return updateExpression.ColumnName;
×
634

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

638
        var quotedName = ColumnExpression(updateExpression);
7 ✔
639

640
        return $"{quotedName} = {updateExpression.ParameterName}";
7 ✔
641
    }
642

643
    /// <summary>
644
    /// Validates that an UPSERT statement has the required table, values, keys, and update expressions.
645
    /// </summary>
646
    /// <param name="upsertStatement">The <see cref="UpsertStatement"/> to validate.</param>
647
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="upsertStatement"/> is <c>null</c>.</exception>
648
    /// <exception cref="ArgumentException">Thrown if required upsert configuration is missing.</exception>
649
    protected static void ValidateUpsert(UpsertStatement upsertStatement)
650
    {
651
        ArgumentNullException.ThrowIfNull(upsertStatement);
22 ✔
652

653
        if (upsertStatement.TableExpression == null)
22 !
654
            throw new ArgumentException("No table specified to upsert into", nameof(upsertStatement));
×
655

656
        if (upsertStatement.ColumnExpressions == null || upsertStatement.ColumnExpressions.Count == 0)
22 !
657
            throw new ArgumentException("No columns specified for upsert", nameof(upsertStatement));
×
658

659
        if (upsertStatement.ValueExpressions == null || upsertStatement.ValueExpressions.Count == 0)
22 !
660
            throw new ArgumentException("No values specified for upsert", nameof(upsertStatement));
×
661

662
        if (upsertStatement.KeyExpressions == null || upsertStatement.KeyExpressions.Count == 0)
22 !
663
            throw new ArgumentException("No keys specified for upsert", nameof(upsertStatement));
×
664

665
        if (upsertStatement.UpdateExpressions == null || upsertStatement.UpdateExpressions.Count == 0)
22 !
666
            throw new ArgumentException("No update values specified for upsert", nameof(upsertStatement));
×
667
    }
22 ✔
668

669
    /// <summary>
670
    /// Builds a SQL JOIN expression from the specified <see cref="JoinExpression"/>.
671
    /// </summary>
672
    /// <param name="joinExpression">The <see cref="JoinExpression"/> representing the join operation.</param>
673
    /// <returns>A SQL JOIN expression string.</returns>
674
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="joinExpression"/> is <c>null</c>.</exception>
675
    /// <exception cref="ArgumentException">Thrown if required properties are not specified.</exception>
676
    public virtual string JoinExpression(JoinExpression joinExpression)
677
    {
678
        ArgumentNullException.ThrowIfNull(joinExpression);
8 ✔
679

680
        var leftColumn = ColumnExpression(new ColumnExpression(joinExpression.LeftColumnName, joinExpression.LeftTableAlias));
8 ✔
681
        var rightColumn = ColumnExpression(new ColumnExpression(joinExpression.RightColumnName, joinExpression.RightTableAlias));
8 ✔
682
        var rightTable = TableExpression(new TableExpression(joinExpression.RightTableName, joinExpression.RightTableSchema, joinExpression.RightTableAlias));
8 ✔
683

684
        var joinType = joinExpression.JoinType switch
8 !
685
        {
8 ✔
686
            JoinTypes.Inner => "INNER JOIN",
8 ✔
687
            JoinTypes.Left => "LEFT OUTER JOIN",
×
688
            JoinTypes.Right => "RIGHT OUTER JOIN",
×
689
            _ => throw new NotImplementedException(),
×
690
        };
8 ✔
691

692
        return $"{joinType} {rightTable} ON {leftColumn} = {rightColumn}";
8 ✔
693
    }
694

695
    /// <summary>
696
    /// Quotes an identifier (such as a table or column name) for SQL Server, using square brackets.
697
    /// </summary>
698
    /// <param name="name">The identifier to quote.</param>
699
    /// <returns>The quoted identifier, or the original name if quoting is not required.</returns>
700
    public virtual string QuoteIdentifier(string name)
701
    {
702
        if (name.IsNullOrWhiteSpace())
430 !
703
            return string.Empty;
×
704

705
        if (name == "*")
430 ✔
706
            return name;
1 ✔
707

708
        if (name.StartsWith('[') && name.EndsWith(']'))
429 !
709
            return name;
×
710

711
        return "[" + name.Replace("]", "]]") + "]";
429 ✔
712
    }
713

714
    /// <summary>
715
    /// Parses a quoted identifier and returns the unquoted name for SQL Server.
716
    /// </summary>
717
    /// <param name="name">The quoted identifier.</param>
718
    /// <returns>The unquoted identifier name.</returns>
719
    public virtual string ParseIdentifier(string name)
720
    {
721
        if (name.StartsWith('[') && name.EndsWith(']'))
×
722
            return name[1..^1];
×
723

724
        return name;
×
725
    }
726

727
    /// <summary>
728
    /// Builds a SQL column expression with a specific table alias.
729
    /// </summary>
730
    /// <param name="columnExpression">The <see cref="Generators.ColumnExpression"/> representing the column.</param>
731
    /// <param name="tableAlias">The table alias to use if not already set.</param>
732
    /// <returns>A SQL column expression string with the specified table alias.</returns>
733
    private string ColumnExpression(ColumnExpression columnExpression, string tableAlias)
734
    {
735
        var column = columnExpression with
9 !
736
        {
9 ✔
737
            TableAlias = columnExpression.TableAlias ?? tableAlias
9 ✔
738
        };
9 ✔
739

740
        return ColumnExpression(column);
9 ✔
741
    }
742
}
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