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

loresoft / FluentCommand / 29209785244

12 Jul 2026 09:27PM UTC coverage: 65.698%. Remained the same
29209785244

push

github

pwelter34
Add negated string filter operators

1833 of 3582 branches covered (51.17%)

Branch coverage included in aggregate %.

23 of 34 new or added lines in 3 files covered. (67.65%)

2 existing lines in 2 files now uncovered.

5673 of 7843 relevant lines covered (72.33%)

298.97 hits per line

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

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

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

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

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

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

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

45
        if (selectStatement.JoinExpressions?.Count > 0)
55!
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)
55!
56
        {
57
            selectBuilder
33✔
58
                .AppendLine()
33✔
59
                .Append("WHERE ")
33✔
60
                .Append('(')
33✔
61
                .AppendJoin(" AND ", selectStatement.WhereExpressions.Select(WhereExpression))
33✔
62
                .Append(')');
33✔
63
        }
64

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

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

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

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

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

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

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

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

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

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

132
        if (insertStatement.OutputExpressions?.Count > 0)
17!
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
17✔
142
            .AppendLine()
17✔
143
            .Append("VALUES ")
17✔
144
            .Append('(')
17✔
145
            .AppendJoin(", ", insertStatement.ValueExpressions)
17✔
146
            .Append(");");
17✔
147

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

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

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

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

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

199
        if (upsertStatement.OutputExpressions?.Count > 0)
17!
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(";");
17✔
208

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

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

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

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

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

241
        if (updateStatement.OutputExpressions?.Count > 0)
11!
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)
11!
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)
11!
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)
11!
268
        {
269
            updateBuilder
8✔
270
                .AppendLine()
8✔
271
                .Append("WHERE ")
8✔
272
                .Append('(')
8✔
273
                .AppendJoin(" AND ", updateStatement.WhereExpressions.Select(WhereExpression))
8✔
274
                .Append(')');
8✔
275
        }
276

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

279
        return StringBuilderCache.ToString(updateBuilder);
11✔
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!
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
    {
356
        if (whereExpressions == null || whereExpressions.Count == 0)
2!
357
            return null;
×
358

359
        var whereBuilder = StringBuilderCache.Acquire();
2✔
360

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

369
        return StringBuilderCache.ToString(whereBuilder);
2✔
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} */";
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)
107✔
413
            return AggregateExpression(aggregateExpression);
6✔
414

415
        return ColumnExpression(columnExpression);
101✔
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);
1,006✔
428

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

432
        if (columnExpression.IsRaw)
1,006!
433
            return columnExpression.ColumnName;
×
434

435
        var quotedName = QuoteIdentifier(columnExpression.ColumnName);
1,006✔
436

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

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

444
        return clause;
1,006✔
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);
152✔
483

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

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

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

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

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

499
        return fromClause;
149✔
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);
35✔
512

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

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

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

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

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

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

562
        return whereExpression.FilterOperator switch
50!
563
        {
50✔
564
            FilterOperators.StartsWith => $"{columnName} LIKE {whereExpression.ParameterName} + '%'",
×
565
            FilterOperators.NotStartsWith => $"{columnName} NOT LIKE {whereExpression.ParameterName} + '%'",
1✔
UNCOV
566
            FilterOperators.EndsWith => $"{columnName} LIKE '%' + {whereExpression.ParameterName}",
×
567
            FilterOperators.NotEndsWith => $"{columnName} NOT LIKE '%' + {whereExpression.ParameterName}",
1✔
568
            FilterOperators.Contains => $"{columnName} LIKE '%' + {whereExpression.ParameterName} + '%'",
8✔
569
            FilterOperators.NotContains => $"{columnName} NOT LIKE '%' + {whereExpression.ParameterName} + '%'",
1✔
570
            FilterOperators.Equal => $"{columnName} = {whereExpression.ParameterName}",
25✔
571
            FilterOperators.NotEqual => $"{columnName} != {whereExpression.ParameterName}",
2✔
572
            FilterOperators.LessThan => $"{columnName} < {whereExpression.ParameterName}",
×
573
            FilterOperators.LessThanOrEqual => $"{columnName} <= {whereExpression.ParameterName}",
×
574
            FilterOperators.GreaterThan => $"{columnName} > {whereExpression.ParameterName}",
1✔
575
            FilterOperators.GreaterThanOrEqual => $"{columnName} >= {whereExpression.ParameterName}",
2✔
576
            FilterOperators.IsNull => $"{columnName} IS NULL",
1✔
577
            FilterOperators.IsNotNull => $"{columnName} IS NOT NULL",
×
578
            FilterOperators.In => $"{columnName} IN ({whereExpression.ParameterName})",
8✔
579
            _ => $"{columnName} = {whereExpression.ParameterName}",
×
580
        };
50✔
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)
9!
592
            return string.Empty;
×
593

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

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

602
        return StringBuilderCache.ToString(stringBuilder);
9✔
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)
13!
613
            return string.Empty;
×
614

615
        return $"OFFSET {limitExpression.Offset} ROWS FETCH NEXT {limitExpression.Size} ROWS ONLY";
13✔
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);
20✔
628

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

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

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

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

640
        return $"{quotedName} = {updateExpression.ParameterName}";
20✔
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);
33✔
652

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

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

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

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

665
        if (upsertStatement.UpdateExpressions == null || upsertStatement.UpdateExpressions.Count == 0)
33!
666
            throw new ArgumentException("No update values specified for upsert", nameof(upsertStatement));
×
667
    }
33✔
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);
13✔
679

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

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

692
        return $"{joinType} {rightTable} ON {leftColumn} = {rightColumn}";
13✔
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())
804!
703
            return string.Empty;
×
704

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

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

711
        return "[" + name.Replace("]", "]]") + "]";
803✔
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
17!
736
        {
17✔
737
            TableAlias = columnExpression.TableAlias ?? tableAlias
17✔
738
        };
17✔
739

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