• 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

43.69
/src/FluentCommand/Query/SelectBuilder.cs
1
using FluentCommand.Extensions;
2
using FluentCommand.Query.Generators;
3
using FluentCommand.Reflection;
4

5
namespace FluentCommand.Query;
6

7
/// <summary>
8
/// Provides a builder for constructing SQL SELECT statements with fluent, chainable methods.
9
/// </summary>
10
public class SelectBuilder : SelectBuilder<SelectBuilder>
11
{
12
    /// <summary>
13
    /// Initializes a new instance of the <see cref="SelectBuilder"/> class.
14
    /// </summary>
15
    /// <param name="queryGenerator">The <see cref="IQueryGenerator"/> used to generate SQL expressions.</param>
16
    /// <param name="parameters">The list of <see cref="QueryParameter"/> objects for the query.</param>
17
    /// <param name="logicalOperator">The logical operator (<see cref="LogicalOperators"/>) to combine WHERE expressions. Defaults to <see cref="LogicalOperators.And"/>.</param>
18
    public SelectBuilder(
19
        IQueryGenerator queryGenerator,
20
        List<QueryParameter> parameters,
21
        LogicalOperators logicalOperator = LogicalOperators.And)
22
        : base(queryGenerator, parameters, logicalOperator)
×
23
    {
24
    }
×
25
}
26

27
/// <summary>
28
/// Provides a generic base class for building SQL SELECT statements with fluent, chainable methods.
29
/// </summary>
30
/// <typeparam name="TBuilder">The type of the builder for fluent chaining.</typeparam>
31
public abstract class SelectBuilder<TBuilder> : WhereBuilder<TBuilder>
32
    where TBuilder : SelectBuilder<TBuilder>
33
{
34
    /// <summary>
35
    /// Initializes a new instance of the <see cref="SelectBuilder{TBuilder}"/> class.
36
    /// </summary>
37
    /// <param name="queryGenerator">The <see cref="IQueryGenerator"/> used to generate SQL expressions.</param>
38
    /// <param name="parameters">The list of <see cref="QueryParameter"/> objects for the query.</param>
39
    /// <param name="logicalOperator">The logical operator (<see cref="LogicalOperators"/>) to combine WHERE expressions. Defaults to <see cref="LogicalOperators.And"/>.</param>
40
    protected SelectBuilder(
41
        IQueryGenerator queryGenerator,
42
        List<QueryParameter> parameters,
43
        LogicalOperators logicalOperator = LogicalOperators.And)
44
        : base(queryGenerator, parameters, logicalOperator)
53✔
45
    {
46
    }
53✔
47

48
    /// <summary>
49
    /// Gets the collection of select column expressions for the SELECT statement.
50
    /// </summary>
51
    /// <value>
52
    /// A <see cref="HashSet{ColumnExpression}"/> containing the select column expressions.
53
    /// </value>
54
    protected HashSet<ColumnExpression> SelectExpressions { get; } = new();
55

56
    /// <summary>
57
    /// Gets the collection of FROM table expressions for the SELECT statement.
58
    /// </summary>
59
    /// <value>
60
    /// A <see cref="HashSet{TableExpression}"/> containing the FROM table expressions.
61
    /// </value>
62
    protected HashSet<TableExpression> FromExpressions { get; } = new();
63

64
    /// <summary>
65
    /// Gets the collection of sort expressions for the ORDER BY clause.
66
    /// </summary>
67
    /// <value>
68
    /// A <see cref="HashSet{SortExpression}"/> containing the sort expressions.
69
    /// </value>
70
    protected HashSet<SortExpression> SortExpressions { get; } = new();
71

72
    /// <summary>
73
    /// Gets the collection of group by expressions for the GROUP BY clause.
74
    /// </summary>
75
    /// <value>
76
    /// A <see cref="HashSet{GroupExpression}"/> containing the group by expressions.
77
    /// </value>
78
    protected HashSet<GroupExpression> GroupExpressions { get; } = new();
79

80
    /// <summary>
81
    /// Gets the collection of join expressions for the SELECT statement.
82
    /// </summary>
83
    /// <value>
84
    /// A <see cref="HashSet{JoinExpression}"/> containing the join expressions.
85
    /// </value>
86
    protected HashSet<JoinExpression> JoinExpressions { get; } = new();
87

88
    /// <summary>
89
    /// Gets the collection of limit expressions for the SELECT statement.
90
    /// </summary>
91
    /// <value>
92
    /// A <see cref="HashSet{LimitExpression}"/> containing the limit expressions.
93
    /// </value>
94
    protected HashSet<LimitExpression> LimitExpressions { get; } = new();
95

96
    /// <summary>
97
    /// Adds a column expression with the specified name to the SELECT clause.
98
    /// </summary>
99
    /// <param name="columnName">The name of the column.</param>
100
    /// <param name="tableAlias">The alias of the table (optional).</param>
101
    /// <param name="columnAlias">The alias for the column (optional).</param>
102
    /// <returns>
103
    /// The same builder instance for method chaining.
104
    /// </returns>
105
    public TBuilder Column(
106
        string columnName,
107
        string? tableAlias = null,
108
        string? columnAlias = null)
109
    {
110
        var selectClause = new ColumnExpression(columnName, tableAlias, columnAlias);
98✔
111

112
        SelectExpressions.Add(selectClause);
98✔
113

114
        return (TBuilder)this;
98✔
115
    }
116

117
    /// <summary>
118
    /// Conditionally adds a column expression with the specified name to the SELECT clause.
119
    /// </summary>
120
    /// <param name="columnName">The name of the column.</param>
121
    /// <param name="tableAlias">The alias of the table (optional).</param>
122
    /// <param name="columnAlias">The alias for the column (optional).</param>
123
    /// <param name="condition">A function that determines whether to add the column, based on the column name. If <c>null</c>, the column is always added.</param>
124
    /// <returns>
125
    /// The same builder instance for method chaining.
126
    /// </returns>
127
    public TBuilder ColumnIf(
128
        string columnName,
129
        string? tableAlias = null,
130
        string? columnAlias = null,
131
        Func<string, bool>? condition = null)
132
    {
133
        if (condition != null && !condition(columnName))
×
134
            return (TBuilder)this;
×
135

136
        return Column(columnName, tableAlias, columnAlias);
×
137
    }
138

139
    /// <summary>
140
    /// Adds a column expression for each of the specified column names to the SELECT clause.
141
    /// </summary>
142
    /// <param name="columnNames">The collection of column names.</param>
143
    /// <param name="tableAlias">The alias of the table (optional).</param>
144
    /// <returns>
145
    /// The same builder instance for method chaining.
146
    /// </returns>
147
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="columnNames"/> is <c>null</c>.</exception>
148
    public virtual TBuilder Columns(
149
        IEnumerable<string> columnNames,
150
        string? tableAlias = null)
151
    {
152
        ArgumentNullException.ThrowIfNull(columnNames);
×
153

UNCOV
154
        foreach (var column in columnNames)
×
155
            Column(column, tableAlias);
×
156

UNCOV
157
        return (TBuilder)this;
×
158
    }
159

160
    /// <summary>
161
    /// Adds a COUNT aggregate expression using the specified column name to the SELECT clause.
162
    /// </summary>
163
    /// <param name="columnName">The name of the column (default is <c>*</c>).</param>
164
    /// <param name="tableAlias">The alias of the table (optional).</param>
165
    /// <param name="columnAlias">The alias for the column (optional).</param>
166
    /// <returns>
167
    /// The same builder instance for method chaining.
168
    /// </returns>
169
    public TBuilder Count(
170
        string columnName = "*",
171
        string? tableAlias = null,
172
        string? columnAlias = null)
173
    {
174
        var selectClause = new AggregateExpression(AggregateFunctions.Count, columnName, tableAlias, columnAlias);
3✔
175

176
        SelectExpressions.Add(selectClause);
3✔
177

178
        return (TBuilder)this;
3✔
179
    }
180

181
    /// <summary>
182
    /// Adds an aggregate expression using the specified function and column name to the SELECT clause.
183
    /// </summary>
184
    /// <param name="function">The aggregate function to use (e.g., <see cref="AggregateFunctions.Sum"/>).</param>
185
    /// <param name="columnName">The name of the column.</param>
186
    /// <param name="tableAlias">The alias of the table (optional).</param>
187
    /// <param name="columnAlias">The alias for the column (optional).</param>
188
    /// <returns>
189
    /// The same builder instance for method chaining.
190
    /// </returns>
191
    public TBuilder Aggregate(
192
        AggregateFunctions function,
193
        string columnName,
194
        string? tableAlias = null,
195
        string? columnAlias = null)
196
    {
197
        var selectClause = new AggregateExpression(function, columnName, tableAlias, columnAlias);
3✔
198

199
        SelectExpressions.Add(selectClause);
3✔
200

201
        return (TBuilder)this;
3✔
202
    }
203

204
    /// <summary>
205
    /// Adds a FROM clause to the query.
206
    /// </summary>
207
    /// <param name="tableName">The name of the table.</param>
208
    /// <param name="tableSchema">The schema of the table (optional).</param>
209
    /// <param name="tableAlias">The alias of the table (optional).</param>
210
    /// <returns>
211
    /// The same builder instance for method chaining.
212
    /// </returns>
213
    public virtual TBuilder From(
214
        string tableName,
215
        string? tableSchema = null,
216
        string? tableAlias = null)
217
    {
218
        var fromClause = new TableExpression(tableName, tableSchema, tableAlias);
50✔
219

220
        FromExpressions.Add(fromClause);
50✔
221

222
        return (TBuilder)this;
50✔
223
    }
224

225
    /// <summary>
226
    /// Adds a FROM clause to the query using the specified entity type.
227
    /// </summary>
228
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
229
    /// <param name="tableAlias">The alias of the table (optional).</param>
230
    /// <returns>
231
    /// The same builder instance for method chaining.
232
    /// </returns>
233
    public TBuilder From<TEntity>(
234
        string? tableAlias = null)
235
    {
UNCOV
236
        var typeAccessor = TypeAccessor.GetAccessor<TEntity>();
×
237

UNCOV
238
        var fromClause = new TableExpression(typeAccessor.TableName, typeAccessor.TableSchema, tableAlias);
×
239

UNCOV
240
        FromExpressions.Add(fromClause);
×
241

UNCOV
242
        return (TBuilder)this;
×
243
    }
244

245
    /// <summary>
246
    /// Adds a raw FROM clause to the query.
247
    /// </summary>
248
    /// <param name="fromClause">The raw SQL FROM clause.</param>
249
    /// <returns>
250
    /// The same builder instance for method chaining.
251
    /// </returns>
252
    public TBuilder FromRaw(string fromClause)
253
    {
254
        if (fromClause.HasValue())
3!
255
            FromExpressions.Add(new TableExpression(fromClause, IsRaw: true));
3✔
256

257
        return (TBuilder)this;
3✔
258
    }
259

260
    /// <summary>
261
    /// Adds a JOIN clause to the query using the specified builder action.
262
    /// </summary>
263
    /// <param name="builder">An action that configures the join using a <see cref="JoinBuilder"/>.</param>
264
    /// <returns>
265
    /// The same builder instance for method chaining.
266
    /// </returns>
267
    public TBuilder Join(Action<JoinBuilder> builder)
268
    {
269
        var innerBuilder = new JoinBuilder(QueryGenerator, Parameters);
1✔
270
        builder(innerBuilder);
1✔
271

272
        JoinExpressions.Add(innerBuilder.BuildExpression());
1✔
273

274
        return (TBuilder)this;
1✔
275
    }
276

277
    /// <summary>
278
    /// Adds an ORDER BY clause with the specified column name and sort direction.
279
    /// </summary>
280
    /// <param name="columnName">The name of the column to sort by.</param>
281
    /// <param name="sortDirection">The sort direction (default is <see cref="SortDirections.Ascending"/>).</param>
282
    /// <returns>
283
    /// The same builder instance for method chaining.
284
    /// </returns>
285
    public TBuilder OrderBy(
286
        string columnName,
287
        SortDirections sortDirection = SortDirections.Ascending)
288
    {
289
        return OrderBy(columnName, null, sortDirection);
4✔
290
    }
291

292
    /// <summary>
293
    /// Adds an ORDER BY clause with the specified column name, sort direction, and table alias.
294
    /// </summary>
295
    /// <param name="columnName">The name of the column to sort by.</param>
296
    /// <param name="tableAlias">The alias of the table (optional).</param>
297
    /// <param name="sortDirection">The sort direction (default is <see cref="SortDirections.Ascending"/>).</param>
298
    /// <returns>
299
    /// The same builder instance for method chaining.
300
    /// </returns>
301
    public TBuilder OrderBy(
302
        string columnName,
303
        string? tableAlias,
304
        SortDirections sortDirection = SortDirections.Ascending)
305
    {
306
        var orderClause = new SortExpression(columnName, tableAlias, sortDirection);
34✔
307

308
        SortExpressions.Add(orderClause);
34✔
309

310
        return (TBuilder)this;
34✔
311
    }
312

313
    /// <summary>
314
    /// Conditionally adds an ORDER BY clause with the specified column name and sort direction.
315
    /// </summary>
316
    /// <param name="columnName">The name of the column to sort by.</param>
317
    /// <param name="sortDirection">The sort direction (default is <see cref="SortDirections.Ascending"/>).</param>
318
    /// <param name="condition">A function that determines whether to add the ORDER BY clause, based on the column name. If <c>null</c>, the clause is always added.</param>
319
    /// <returns>
320
    /// The same builder instance for method chaining.
321
    /// </returns>
322
    public TBuilder OrderByIf(
323
        string columnName,
324
        SortDirections sortDirection = SortDirections.Ascending,
325
        Func<string, bool>? condition = null)
326
    {
UNCOV
327
        return OrderByIf(columnName, null, sortDirection, condition);
×
328
    }
329

330
    /// <summary>
331
    /// Conditionally adds an ORDER BY clause with the specified column name, sort direction, and table alias.
332
    /// </summary>
333
    /// <param name="columnName">The name of the column to sort by.</param>
334
    /// <param name="tableAlias">The alias of the table (optional).</param>
335
    /// <param name="sortDirection">The sort direction (default is <see cref="SortDirections.Ascending"/>).</param>
336
    /// <param name="condition">A function that determines whether to add the ORDER BY clause, based on the column name. If <c>null</c>, the clause is always added.</param>
337
    /// <returns>
338
    /// The same builder instance for method chaining.
339
    /// </returns>
340
    public TBuilder OrderByIf(
341
        string columnName,
342
        string? tableAlias,
343
        SortDirections sortDirection = SortDirections.Ascending,
344
        Func<string, bool>? condition = null)
345
    {
UNCOV
346
        if (condition != null && !condition(columnName))
×
347
            return (TBuilder)this;
×
348

UNCOV
349
        return OrderBy(columnName, tableAlias, sortDirection);
×
350
    }
351

352
    /// <summary>
353
    /// Adds a raw ORDER BY clause to the query.
354
    /// </summary>
355
    /// <param name="sortExpression">The raw SQL ORDER BY clause.</param>
356
    /// <returns>
357
    /// The same builder instance for method chaining.
358
    /// </returns>
359
    public TBuilder OrderByRaw(string sortExpression)
360
    {
UNCOV
361
        if (sortExpression.HasValue())
×
362
            SortExpressions.Add(new SortExpression(sortExpression, IsRaw: true));
×
363

UNCOV
364
        return (TBuilder)this;
×
365
    }
366

367
    /// <summary>
368
    /// Conditionally adds a raw ORDER BY clause to the query.
369
    /// </summary>
370
    /// <param name="sortExpression">The raw SQL ORDER BY clause.</param>
371
    /// <param name="condition">A function that determines whether to add the ORDER BY clause, based on the expression. If <c>null</c>, the clause is always added.</param>
372
    /// <returns>
373
    /// The same builder instance for method chaining.
374
    /// </returns>
375
    public TBuilder OrderByRawIf(
376
        string sortExpression,
377
        Func<string, bool>? condition = null)
378
    {
UNCOV
379
        if (condition != null && !condition(sortExpression))
×
380
            return (TBuilder)this;
×
381

UNCOV
382
        return OrderByRaw(sortExpression);
×
383
    }
384

385
    /// <summary>
386
    /// Adds multiple raw ORDER BY clauses to the query.
387
    /// </summary>
388
    /// <param name="sortExpressions">A collection of raw SQL ORDER BY clauses.</param>
389
    /// <returns>
390
    /// The same builder instance for method chaining.
391
    /// </returns>
392
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="sortExpressions"/> is <c>null</c>.</exception>
393
    public TBuilder OrderByRaw(IEnumerable<string> sortExpressions)
394
    {
UNCOV
395
        ArgumentNullException.ThrowIfNull(sortExpressions);
×
396

397
        foreach (var sortExpression in sortExpressions)
×
UNCOV
398
            SortExpressions.Add(new SortExpression(sortExpression, IsRaw: true));
×
399

400
        return (TBuilder)this;
×
401
    }
402

403
    /// <summary>
404
    /// Adds a GROUP BY clause with the specified column name.
405
    /// </summary>
406
    /// <param name="columnName">The name of the column to group by.</param>
407
    /// <param name="tableAlias">The alias of the table (optional).</param>
408
    /// <returns>
409
    /// The same builder instance for method chaining.
410
    /// </returns>
411
    public TBuilder GroupBy(
412
        string columnName,
413
        string? tableAlias = null)
414
    {
415
        var orderClause = new GroupExpression(columnName, tableAlias);
3✔
416

417
        GroupExpressions.Add(orderClause);
3✔
418

419
        return (TBuilder)this;
3✔
420
    }
421

422
    /// <summary>
423
    /// Adds a LIMIT clause with the specified offset and size.
424
    /// </summary>
425
    /// <param name="offset">The number of rows to skip before starting to return rows.</param>
426
    /// <param name="size">The maximum number of rows to return.</param>
427
    /// <returns>
428
    /// The same builder instance for method chaining.
429
    /// </returns>
430
    public TBuilder Limit(int offset = 0, int size = 0)
431
    {
432
        // no paging
433
        if (size <= 0)
20!
UNCOV
434
            return (TBuilder)this;
×
435

436
        var limitClause = new LimitExpression(offset, size);
20✔
437
        LimitExpressions.Add(limitClause);
20✔
438

439
        return (TBuilder)this;
20✔
440
    }
441

442
    /// <summary>
443
    /// Adds a LIMIT clause for paging with the specified page number and page size.
444
    /// </summary>
445
    /// <param name="page">The page number (1-based).</param>
446
    /// <param name="pageSize">The number of rows per page.</param>
447
    /// <returns>
448
    /// The same builder instance for method chaining.
449
    /// </returns>
450
    public TBuilder Page(int page = 0, int pageSize = 0)
451
    {
452
        // no paging
UNCOV
453
        if (pageSize <= 0 || page <= 0)
×
UNCOV
454
            return (TBuilder)this;
×
455

456
        int offset = Math.Max(pageSize * (page - 1), 0);
×
UNCOV
457
        var limitClause = new LimitExpression(offset, pageSize);
×
458

459
        LimitExpressions.Add(limitClause);
×
460

461
        return (TBuilder)this;
×
462
    }
463

464
    /// <summary>
465
    /// Builds the SQL SELECT statement using the current configuration.
466
    /// </summary>
467
    /// <returns>
468
    /// A <see cref="QueryStatement"/> containing the SQL SELECT statement and its parameters.
469
    /// </returns>
470
    public override QueryStatement? BuildStatement()
471
    {
472
        var selectStatement = new SelectStatement(
53✔
473
            SelectExpressions,
53✔
474
            FromExpressions,
53✔
475
            JoinExpressions,
53✔
476
            WhereExpressions,
53✔
477
            SortExpressions,
53✔
478
            GroupExpressions,
53✔
479
            LimitExpressions,
53✔
480
            CommentExpressions);
53✔
481

482
        var statement = QueryGenerator.BuildSelect(selectStatement);
53✔
483

484
        return new QueryStatement(statement, Parameters);
53✔
485
    }
486
}
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