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

loresoft / FluentCommand / 26594173245

28 May 2026 06:28PM UTC coverage: 55.553% (+0.7%) from 54.902%
26594173245

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%)

371 existing lines in 26 files now uncovered.

4389 of 7130 relevant lines covered (61.56%)

312.89 hits per line

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

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

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

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

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

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

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

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

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

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

90
        return StringBuilderCache.ToString(selectBuilder);
53✔
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);
8✔
103

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

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

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

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

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

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

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

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

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

148
        return StringBuilderCache.ToString(insertBuilder);
8✔
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);
11✔
161

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

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

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

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

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

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

209
        return StringBuilderCache.ToString(upsertBuilder);
11✔
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)
6!
UNCOV
221
            throw new ArgumentException("No values specified for update", nameof(updateStatement));
×
222

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

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

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

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

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

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

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

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

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

279
        return StringBuilderCache.ToString(updateBuilder);
6✔
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)
4!
UNCOV
291
            throw new ArgumentException("No table specified to delete from", nameof(deleteStatement));
×
292

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

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

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

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

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

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

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

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

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

346
        return StringBuilderCache.ToString(deleteBuilder);
4✔
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
    {
UNCOV
356
        if (whereExpressions == null || whereExpressions.Count == 0)
×
357
            return null;
×
358

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

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

UNCOV
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
    {
UNCOV
379
        if (sortExpressions == null || sortExpressions.Count == 0)
×
380
            return null;
×
381

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

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

UNCOV
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} */";
36✔
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)
104✔
413
            return AggregateExpression(aggregateExpression);
6✔
414

415
        return ColumnExpression(columnExpression);
98✔
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);
936✔
428

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

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

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

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

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

444
        return clause;
936✔
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✔
UNCOV
464
            AggregateFunctions.Average => $"AVG({selectClause})",
×
465
            AggregateFunctions.Count => $"COUNT({selectClause})",
3✔
UNCOV
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);
130✔
483

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

487
        if (tableExpression.IsRaw)
130✔
488
            return tableExpression.TableName;
3✔
489

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

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

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

499
        return fromClause;
127✔
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);
34✔
512

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

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

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

521
        return sortExpression.SortDirection == SortDirections.Ascending
34✔
522
            ? $"{quotedName} ASC"
34✔
523
            : $"{quotedName} DESC";
34✔
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);
57✔
549

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

553
        if (whereExpression.IsRaw)
57✔
554
            return whereExpression.ColumnName;
6✔
555

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

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

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

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

591
        var stringBuilder = StringBuilderCache.Acquire();
7✔
592
        var logical = logicalOperator == LogicalOperators.And ? " AND " : " OR ";
7✔
593

594
        stringBuilder
7✔
595
            .Append('(')
7✔
596
            .AppendJoin(logical, whereExpressions.Select(WhereExpression))
7✔
597
            .Append(')' );
7✔
598

599
        return StringBuilderCache.ToString(stringBuilder);
7✔
600
    }
601

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

612
        return $"OFFSET {limitExpression.Offset} ROWS FETCH NEXT {limitExpression.Size} ROWS ONLY";
12✔
613
    }
614

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

626
        if (updateExpression.ColumnName.IsNullOrWhiteSpace())
15!
UNCOV
627
            throw new ArgumentException($"'{nameof(updateExpression.ColumnName)}' cannot be null or empty.", nameof(updateExpression));
×
628

629
        if (updateExpression.IsRaw)
15!
UNCOV
630
            return updateExpression.ColumnName;
×
631

632
        if (updateExpression.ParameterName.IsNullOrWhiteSpace())
15!
UNCOV
633
            throw new ArgumentException($"'{nameof(updateExpression.ParameterName)}' cannot be null or empty.", nameof(updateExpression));
×
634

635
        var quotedName = ColumnExpression(updateExpression);
15✔
636

637
        return $"{quotedName} = {updateExpression.ParameterName}";
15✔
638
    }
639

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

650
        if (upsertStatement.TableExpression == null)
27!
UNCOV
651
            throw new ArgumentException("No table specified to upsert into", nameof(upsertStatement));
×
652

653
        if (upsertStatement.ColumnExpressions == null || upsertStatement.ColumnExpressions.Count == 0)
27!
UNCOV
654
            throw new ArgumentException("No columns specified for upsert", nameof(upsertStatement));
×
655

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

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

662
        if (upsertStatement.UpdateExpressions == null || upsertStatement.UpdateExpressions.Count == 0)
27!
663
            throw new ArgumentException("No update values specified for upsert", nameof(upsertStatement));
×
664
    }
27✔
665

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

677
        var leftColumn = ColumnExpression(new ColumnExpression(joinExpression.LeftColumnName, joinExpression.LeftTableAlias));
13✔
678
        var rightColumn = ColumnExpression(new ColumnExpression(joinExpression.RightColumnName, joinExpression.RightTableAlias));
13✔
679
        var rightTable = TableExpression(new TableExpression(joinExpression.RightTableName, joinExpression.RightTableSchema, joinExpression.RightTableAlias));
13✔
680

681
        var joinType = joinExpression.JoinType switch
13!
682
        {
13✔
683
            JoinTypes.Inner => "INNER JOIN",
12✔
684
            JoinTypes.Left => "LEFT OUTER JOIN",
1✔
685
            JoinTypes.Right => "RIGHT OUTER JOIN",
×
UNCOV
686
            _ => throw new NotImplementedException(),
×
687
        };
13✔
688

689
        return $"{joinType} {rightTable} ON {leftColumn} = {rightColumn}";
13✔
690
    }
691

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

702
        if (name == "*")
716✔
703
            return name;
1✔
704

705
        if (name.StartsWith('[') && name.EndsWith(']'))
715!
UNCOV
706
            return name;
×
707

708
        return "[" + name.Replace("]", "]]") + "]";
715✔
709
    }
710

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

UNCOV
721
        return name;
×
722
    }
723

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

737
        return ColumnExpression(column);
17✔
738
    }
739
}
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