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

torand / FasterSQL / 17211276322

25 Aug 2025 02:07PM UTC coverage: 68.208% (-0.2%) from 68.417%
17211276322

push

github

torand
refactor: sonar cloud issues

288 of 590 branches covered (48.81%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

34 existing lines in 1 file now uncovered.

1615 of 2200 relevant lines covered (73.41%)

4.0 hits per line

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

75.08
/src/main/java/io/github/torand/fastersql/statement/SelectStatement.java
1
/*
2
 * Copyright (c) 2024-2025 Tore Eide Andersen
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package io.github.torand.fastersql.statement;
17

18
import io.github.torand.fastersql.alias.Alias;
19
import io.github.torand.fastersql.alias.ColumnAlias;
20
import io.github.torand.fastersql.dialect.AnsiIsoDialect;
21
import io.github.torand.fastersql.dialect.Dialect;
22
import io.github.torand.fastersql.expression.Expression;
23
import io.github.torand.fastersql.function.aggregate.AggregateFunction;
24
import io.github.torand.fastersql.join.Join;
25
import io.github.torand.fastersql.model.Column;
26
import io.github.torand.fastersql.model.Table;
27
import io.github.torand.fastersql.order.Order;
28
import io.github.torand.fastersql.predicate.OptionalPredicate;
29
import io.github.torand.fastersql.predicate.Predicate;
30
import io.github.torand.fastersql.projection.Projection;
31
import io.github.torand.fastersql.relation.Relation;
32
import io.github.torand.fastersql.setoperation.SetOperation;
33
import io.github.torand.fastersql.setoperation.SetOperator;
34
import io.github.torand.fastersql.sql.Context;
35
import io.github.torand.fastersql.sql.Sql;
36
import io.github.torand.fastersql.subquery.Subquery;
37
import io.github.torand.javacommons.collection.CollectionHelper;
38

39
import java.util.HashSet;
40
import java.util.LinkedList;
41
import java.util.List;
42
import java.util.Objects;
43
import java.util.Optional;
44
import java.util.Set;
45
import java.util.function.Function;
46
import java.util.function.Supplier;
47
import java.util.stream.Stream;
48

49
import static io.github.torand.fastersql.dialect.Capability.LIMIT_OFFSET;
50
import static io.github.torand.fastersql.dialect.Capability.SELECT_FOR_UPDATE;
51
import static io.github.torand.fastersql.sql.Command.SELECT;
52
import static io.github.torand.fastersql.statement.Helpers.unwrapSuppliers;
53
import static io.github.torand.javacommons.collection.CollectionHelper.asList;
54
import static io.github.torand.javacommons.collection.CollectionHelper.concat;
55
import static io.github.torand.javacommons.collection.CollectionHelper.isEmpty;
56
import static io.github.torand.javacommons.collection.CollectionHelper.nonEmpty;
57
import static io.github.torand.javacommons.contract.Requires.require;
58
import static io.github.torand.javacommons.contract.Requires.requireNonEmpty;
59
import static io.github.torand.javacommons.functional.Functions.castTo;
60
import static io.github.torand.javacommons.functional.Optionals.mapSafely;
61
import static io.github.torand.javacommons.functional.Predicates.instanceOf;
62
import static io.github.torand.javacommons.stream.StreamHelper.streamSafely;
63
import static java.util.Collections.emptyList;
64
import static java.util.Objects.nonNull;
65
import static java.util.Objects.requireNonNull;
66
import static java.util.function.Predicate.not;
67
import static java.util.stream.Collectors.joining;
68
import static java.util.stream.Collectors.toSet;
69

70
/**
71
 * Implements a SELECT statement.
72
 */
73
public class SelectStatement implements PreparableStatement {
74
    private final List<Projection> projections;
75
    private final List<Relation> relations;
76
    private final List<Join> joins;
77
    private final List<Predicate> wherePredicates;
78
    private final List<Column> groups;
79
    private final List<Predicate> havingPredicates;
80
    private final List<Order> orders;
81
    private final boolean distinct;
82
    private final Long limit;
83
    private final Long offset;
84
    private final boolean forUpdate;
85

86
    SelectStatement(List<Projection> projections, List<Relation> relations, List<Join> joins, List<Predicate> wherePredicates, List<Column> groups, List<Predicate> havingPredicates, List<Order> orders, boolean distinct, Long limit, Long offset, boolean forUpdate) {
2✔
87
        this.projections = asList(projections);
4✔
88
        this.relations = asList(relations);
4✔
89
        this.joins = asList(joins);
4✔
90
        this.wherePredicates = asList(wherePredicates);
4✔
91
        this.groups = asList(groups);
4✔
92
        this.havingPredicates = asList(havingPredicates);
4✔
93
        this.orders = asList(orders);
4✔
94
        this.distinct = distinct;
3✔
95
        this.limit = limit;
3✔
96
        this.offset = offset;
3✔
97
        this.forUpdate = forUpdate;
3✔
98
    }
1✔
99

100
    /**
101
     * Adds one or more JOIN clauses.
102
     * @param joins the JOIN clauses.
103
     * @return the modified statement.
104
     */
105
    public SelectStatement join(Join... joins) {
106
        requireNonEmpty(joins, "No joins specified");
6✔
107
        require(() -> streamSafely(relations).noneMatch(instanceOf(Subquery.class)), "Can't combine a subquery FROM clause with joins");
13✔
108

109
        List<Join> concatenated = concat(this.joins, joins);
5✔
110
        return new SelectStatement(projections, relations, concatenated, wherePredicates, groups, havingPredicates, orders, distinct, limit, offset, forUpdate);
25✔
111
    }
112

113
    /**
114
     * Adds a LEFT OUTER JOIN clause.
115
     * @param join the JOIN clause.
116
     * @return the modified statement.
117
     */
118
    public SelectStatement leftOuterJoin(Join join) {
UNCOV
119
        requireNonNull(join, "No join specified");
×
UNCOV
120
        return join(join.leftOuter());
×
121
    }
122

123
    /**
124
     * Adds a RIGHT OUTER JOIN clause.
125
     * @param join the JOIN clause.
126
     * @return the modified statement.
127
     */
128
    public SelectStatement rightOuterJoin(Join join) {
129
        requireNonNull(join, "No join specified");
4✔
130
        return join(join.rightOuter());
10✔
131
    }
132

133
    /**
134
     * Adds a FULL OUTER JOIN clause.
135
     * @param join the JOIN clause.
136
     * @return the modified statement.
137
     */
138
    public SelectStatement fullOuterJoin(Join join) {
139
        requireNonNull(join, "No join specified");
4✔
140
        return join(join.fullOuter());
10✔
141
    }
142

143
    /**
144
     * Adds one or more JOIN clauses, if the condition is true.
145
     * @param condition the condition.
146
     * @param joinSuppliers the suppliers of JOIN clauses.
147
     * @return the modified statement.
148
     */
149
    @SafeVarargs
150
    public final SelectStatement joinIf(boolean condition, Supplier<Join>... joinSuppliers) {
151
        requireNonEmpty(joinSuppliers, "No join suppliers specified");
×
152
        require(() -> streamSafely(relations).noneMatch(instanceOf(Subquery.class)), "Can't combine a subquery FROM clause with joins");
×
153
        if (condition) {
×
UNCOV
154
            List<Join> concatenated = concat(this.joins, unwrapSuppliers(joinSuppliers));
×
155
            return new SelectStatement(projections, relations, concatenated, wherePredicates, groups, havingPredicates, orders, distinct, limit, offset, forUpdate);
×
156
        } else {
UNCOV
157
            return this;
×
158
        }
159
    }
160

161
    /**
162
     * Adds one or more predicates to the WHERE clause.
163
     * @param predicates the predicates.
164
     * @return the modified statement.
165
     */
166
    public SelectStatement where(Predicate... predicates) {
167
        requireNonEmpty(predicates, "No WHERE predicates specified");
6✔
168

169
        List<Predicate> concatenated = concat(this.wherePredicates, predicates);
5✔
170
        return new SelectStatement(projections, relations, joins, concatenated, groups, havingPredicates, orders, distinct, limit, offset, forUpdate);
25✔
171
    }
172

173
    /**
174
     * Adds optional predicates to the WHERE clause if the wrapped predicates are present.
175
     * @param maybePredicates the optional predicates.
176
     * @return the modified statement.
177
     */
178
    public final SelectStatement where(OptionalPredicate... maybePredicates) {
179
        requireNonEmpty(maybePredicates, "No optional WHERE predicates specified");
6✔
180

181
        List<Predicate> concatenated = concat(this.wherePredicates, OptionalPredicate.unwrap(maybePredicates));
6✔
182
        return new SelectStatement(projections, relations, joins, concatenated, groups, havingPredicates, orders, distinct, limit, offset, forUpdate);
25✔
183
    }
184

185
    /**
186
     * Adds supplied predicates to the WHERE clause, if the condition is true.
187
     * @param condition the condition.
188
     * @param predicateSuppliers the suppliers providing predicates
189
     * @return the modified statement.
190
     */
191
    @SafeVarargs
192
    public final SelectStatement whereIf(boolean condition, Supplier<Predicate>... predicateSuppliers) {
193
        requireNonEmpty(predicateSuppliers, "No WHERE predicate suppliers specified");
×
194
        if (condition) {
×
UNCOV
195
            List<Predicate> concatenated = concat(this.wherePredicates, unwrapSuppliers(predicateSuppliers));
×
196
            return new SelectStatement(projections, relations, joins, concatenated, groups, havingPredicates, orders, distinct, limit, offset, forUpdate);
×
197
        } else {
UNCOV
198
            return this;
×
199
        }
200
    }
201

202
    /**
203
     * Adds one or more columns as groups to the GROUP BY clause.
204
     * @param groups the groups
205
     * @return the modified statement.
206
     */
207
    public SelectStatement groupBy(Column... groups) {
208
        requireNonEmpty(groups, "No groups specified");
6✔
209

210
        List<Column> concatenated = concat(this.groups, groups);
5✔
211
        return new SelectStatement(projections, relations, joins, wherePredicates, concatenated, havingPredicates, orders, distinct, limit, offset, forUpdate);
25✔
212
    }
213

214
    /**
215
     * Adds one or more predicates to the HAVING clause.
216
     * @param predicates the predicates.
217
     * @return the modified statement.
218
     */
219
    public SelectStatement having(Predicate... predicates) {
220
        requireNonEmpty(predicates, "No HAVING predicates specified");
6✔
221

222
        List<Predicate> concatenated = concat(this.havingPredicates, predicates);
5✔
223
        return new SelectStatement(projections, relations, joins, wherePredicates, groups, concatenated, orders, distinct, limit, offset, forUpdate);
25✔
224
    }
225

226
    /**
227
     * Adds optional predicates to the HAVING clause if the wrapped predicates are present.
228
     * @param maybePredicates the optional predicates.
229
     * @return the modified statement.
230
     */
231
    public final SelectStatement having(OptionalPredicate... maybePredicates) {
232
        requireNonEmpty(maybePredicates, "No optional HAVING predicates specified");
×
233

UNCOV
234
        List<Predicate> concatenated = concat(this.havingPredicates, OptionalPredicate.unwrap(maybePredicates));
×
UNCOV
235
        return new SelectStatement(projections, relations, joins, wherePredicates, groups, concatenated, orders, distinct, limit, offset, forUpdate);
×
236
    }
237

238
    /**
239
     * Adds one or more predicates to the HAVING clause, if the condition is true.
240
     * @param condition the condition.
241
     * @param predicateSuppliers the suppliers providing predicates.
242
     * @return the modified statement.
243
     */
244
    @SafeVarargs
245
    public final SelectStatement havingIf(boolean condition, Supplier<Predicate>... predicateSuppliers) {
246
        requireNonEmpty(predicateSuppliers, "No HAVING predicate suppliers specified");
×
247
        if (condition) {
×
UNCOV
248
            List<Predicate> concatenated = concat(this.havingPredicates, unwrapSuppliers(predicateSuppliers));
×
249
            return new SelectStatement(projections, relations, joins, wherePredicates, groups, concatenated, orders, distinct, limit, offset, forUpdate);
×
250
        } else {
UNCOV
251
            return this;
×
252
        }
253
    }
254

255
    /**
256
     * Adds one or more ORDER clauses.
257
     * @param orders the ORDER clauses.
258
     * @return the modified statement.
259
     */
260
    public SelectStatement orderBy(Order... orders) {
261
        requireNonEmpty(orders, "No orders specified");
6✔
262
        List<Order> concatenated = concat(this.orders, orders);
5✔
263
        return new SelectStatement(projections, relations, joins, wherePredicates, groups, havingPredicates, concatenated, distinct, limit, offset, forUpdate);
25✔
264
    }
265

266
    /**
267
     * Adds a LIMIT clause.
268
     * @param limit the limit.
269
     * @return the modified statement.
270
     */
271
    public SelectStatement limit(long limit) {
272
        return new SelectStatement(projections, relations, joins, wherePredicates, groups, havingPredicates, orders, distinct, limit, offset, forUpdate);
26✔
273
    }
274

275
    /**
276
     * Adds a OFFSET clause.
277
     * @param offset the offset.
278
     * @return the modified statement.
279
     */
280
    public SelectStatement offset(long offset) {
281
        return new SelectStatement(projections, relations, joins, wherePredicates, groups, havingPredicates, orders, distinct, limit, offset, forUpdate);
26✔
282
    }
283

284
    /**
285
     * Adds a FOR UPDATE clause.
286
     * @return the modified statement.
287
     */
288
    public SelectStatement forUpdate() {
289
        return new SelectStatement(projections, relations, joins, wherePredicates, groups, havingPredicates, orders, distinct, limit, offset, true);
25✔
290
    }
291

292
    /**
293
     * Creates a UNION set operation between this and the specified statement.
294
     * @param other the other SELECT statement
295
     * @return the SELECT set operation statement.
296
     */
297
    public SelectSetOpStatement union(SelectStatement other) {
UNCOV
298
        SetOperation setOperation = new SetOperation(other, SetOperator.UNION);
×
UNCOV
299
        return new SelectSetOpStatement(this, asList(setOperation), emptyList());
×
300
    }
301

302
    /**
303
     * Creates a UNION ALL set operation between this and the specified statement.
304
     * @param other the other SELECT statement
305
     * @return the SELECT set operation statement.
306
     */
307
    public SelectSetOpStatement unionAll(SelectStatement other) {
UNCOV
308
        SetOperation setOperation = new SetOperation(other, SetOperator.UNION).all();
×
UNCOV
309
        return new SelectSetOpStatement(this, asList(setOperation), emptyList());
×
310
    }
311

312
    /**
313
     * Creates an INTERSECT set operation between this and the specified statement.
314
     * Note that INTERSECT has precedence over UNION and EXCEPT.
315
     * @param other the other SELECT statement
316
     * @return the SELECT set operation statement.
317
     */
318
    public SelectSetOpStatement intersect(SelectStatement other) {
319
        SetOperation setOperation = new SetOperation(other, SetOperator.INTERSECT);
6✔
320
        return new SelectSetOpStatement(this, asList(setOperation), emptyList());
13✔
321
    }
322

323
    /**
324
     * Creates an INTERSECT ALL set operation between this and the specified statement.
325
     * Note that INTERSECT has precedence over UNION and EXCEPT.
326
     * @param other the other SELECT statement
327
     * @return the SELECT set operation statement.
328
     */
329
    public SelectSetOpStatement intersectAll(SelectStatement other) {
UNCOV
330
        SetOperation setOperation = new SetOperation(other, SetOperator.INTERSECT).all();
×
UNCOV
331
        return new SelectSetOpStatement(this, asList(setOperation), emptyList());
×
332
    }
333

334
    /**
335
     * Creates an EXCEPT set operation between this and the specified statement.
336
     * @param other the other SELECT statement
337
     * @return the SELECT set operation statement.
338
     */
339
    public SelectSetOpStatement except(SelectStatement other) {
UNCOV
340
        SetOperation setOperation = new SetOperation(other, SetOperator.EXCEPT);
×
UNCOV
341
        return new SelectSetOpStatement(this, asList(setOperation), emptyList());
×
342
    }
343

344
    /**
345
     * Creates an EXCEPT ALL set operation between this and the specified statement.
346
     * @param other the other SELECT statement
347
     * @return the SELECT set operation statement.
348
     */
349
    public SelectSetOpStatement exceptAll(SelectStatement other) {
UNCOV
350
        SetOperation setOperation = new SetOperation(other, SetOperator.EXCEPT).all();
×
UNCOV
351
        return new SelectSetOpStatement(this, asList(setOperation), emptyList());
×
352
    }
353

354
    @Override
355
    public String sql(Context context) {
356
        final Context localContext = context
2✔
357
            .withCommand(SELECT)
2✔
358
            .withOuterStatement(this);
2✔
359

360
        final Dialect dialect = localContext.getDialect();
3✔
361

362
        validate(localContext);
3✔
363

364
        StringBuilder sb = new StringBuilder();
4✔
365
        sb.append("select ");
4✔
366
        if (distinct) {
3✔
367
            sb.append("distinct ");
4✔
368
        }
369

370
        sb.append(streamSafely(projections)
8✔
371
            .map(p -> p.sql(localContext) + (p.alias().isEmpty() ? "" : " " + p.alias().map(a -> a.sql(localContext)).get()))
25✔
372
            .collect(joining(", ")));
3✔
373

374
        sb.append(" from ");
4✔
375

376
        // Tables that are joined with should not be specified in the FROM clause
377
        Set<Table> joinedTables = streamSafely(joins).map(Join::joined).collect(toSet());
9✔
378

379
        sb.append(streamSafely(relations)
8✔
380
            .filter(not(joinedTables::contains))
7✔
381
            .map(t -> t.sql(localContext))
6✔
382
            .collect(joining(", ")));
3✔
383

384
        if (nonEmpty(joins)) {
4✔
385
            sb.append(" ");
4✔
386
            sb.append(streamSafely(joins)
8✔
387
                .map(j -> j.sql(localContext))
6✔
388
                .collect(joining(" ")));
3✔
389
        }
390

391
        if (nonEmpty(wherePredicates)) {
4✔
392
            sb.append(" where ");
4✔
393
            sb.append(streamSafely(wherePredicates)
8✔
394
                .map(p -> p.sql(localContext))
6✔
395
                .collect(joining(" and ")));
3✔
396
        }
397

398
        if (nonEmpty(groups)) {
4✔
399
            sb.append(" group by ");
4✔
400
            sb.append(streamSafely(groups)
8✔
401
                .map(g -> g.sql(localContext))
6✔
402
                .collect(joining(", ")));
3✔
403
        }
404

405
        if (nonEmpty(havingPredicates)) {
4✔
406
            sb.append(" having ");
4✔
407
            sb.append(streamSafely(havingPredicates)
8✔
408
                .map(p -> p.sql(localContext))
6✔
409
                .collect(joining(" and ")));
3✔
410
        }
411

412
        if (nonEmpty(orders)) {
4✔
413
            sb.append(" order by ");
4✔
414
            sb.append(streamSafely(orders)
8✔
415
                .map(o -> o.sql(localContext))
6✔
416
                .collect(joining(", ")));
3✔
417
        }
418

419
        sb = addLimitOffsetSql(dialect, sb);
5✔
420

421
        if (forUpdate) {
3✔
422
            sb.append(" for update");
4✔
423
        }
424

425
        return sb.toString();
3✔
426
    }
427

428
    Stream<Projection> projections() {
429
        return streamSafely(projections);
4✔
430
    }
431

432
    private Long rowFrom() {
433
        return mapSafely(offset, o -> o + 1);
12✔
434
    }
435

436
    private Long rowTo() {
437
        return mapSafely(limit, l -> (nonNull(offset) ? offset : 0) + l);
20!
438
    }
439

440
    private StringBuilder addLimitOffsetSql(Dialect dialect, StringBuilder querySql) {
441
        if (nonNull(offset) || nonNull(limit)) {
8!
442
            if (dialect.supports(LIMIT_OFFSET)) {
4✔
443
                String offsetClause = nonNull(offset) ? " " + dialect.formatRowOffsetClause().orElseThrow(() -> new FasterSQLException(getDialectRef(dialect) + " has no row offset clause")) : "";
14!
444
                String limitClause = nonNull(limit) ? " " + dialect.formatRowLimitClause().orElseThrow(() -> new FasterSQLException(getDialectRef(dialect) + " has no row limit clause")) : "";
14!
445

446
                if (dialect.offsetBeforeLimit()) {
3✔
447
                    return querySql.append(offsetClause).append(limitClause);
6✔
448
                } else {
449
                    return querySql.append(limitClause).append(offsetClause);
6✔
450
                }
451
            } else {
452
                return addLimitOffsetFallbackSql(dialect, querySql, rowFrom(), rowTo());
9✔
453
            }
454
        }
455

456
        return querySql;
2✔
457
    }
458

459
    private StringBuilder addLimitOffsetFallbackSql(Dialect dialect, StringBuilder innerSql, Long rowFrom, Long rowTo) {
460
        final String ROWNUM = "{ROWNUM}";
2✔
461
        final String selectAllFrom = "select * from";
2✔
462

463
        String rowNum = dialect.formatRowNumLiteral()
5✔
464
            .orElseThrow(() -> new FasterSQLException(getDialectRef(dialect) + " has no row number literal"));
3✔
465

466
        if (nonNull(rowFrom) && nonNull(rowTo)) {
6!
467
            String limitSql = "select ORIGINAL.*, %s ROW_NO from ( %s ) ORIGINAL where %s <= ?".formatted(ROWNUM, innerSql, ROWNUM);
17✔
468
            String offsetSql = "%s ( %s ) where ROW_NO >= ?".formatted(selectAllFrom, limitSql);
13✔
469
            return new StringBuilder(offsetSql.replace(ROWNUM, rowNum));
8✔
470
        } else if (nonNull(rowFrom)) {
×
UNCOV
471
            String offsetSql = "%s ( %s ) where %s >= ?".formatted(selectAllFrom, innerSql, ROWNUM);
×
UNCOV
472
            return new StringBuilder(offsetSql.replace(ROWNUM, rowNum));
×
UNCOV
473
        } else if (nonNull(rowTo)) {
×
UNCOV
474
            String limitSql = "%s ( %s ) where %s <= ?".formatted(selectAllFrom, innerSql, ROWNUM);
×
UNCOV
475
            return new StringBuilder(limitSql.replace(ROWNUM, rowNum));
×
476
        }
477

UNCOV
478
        return innerSql;
×
479
    }
480

481
    @Override
482
    public Stream<Object> params(Context context) {
483
        List<Object> params = new LinkedList<>();
4✔
484

485
        streamSafely(projections).flatMap(p -> p.params(context)).forEach(params::add);
16✔
486
        streamSafely(relations).flatMap(r -> r.params(context)).forEach(params::add);
16✔
487
        streamSafely(wherePredicates).flatMap(p -> p.params(context)).forEach(params::add);
16✔
488
        streamSafely(havingPredicates).flatMap(p -> p.params(context)).forEach(params::add);
16✔
489

490
        addLimitOffsetParams(context.getDialect(), params);
5✔
491

492
        return params.stream();
3✔
493
    }
494

495
    private void addLimitOffsetParams(Dialect dialect, List<Object> params) {
496
        if (dialect.supports(LIMIT_OFFSET)) {
4✔
497
            List<Long> limitOffsetParams = Stream.of(limit, offset).filter(Objects::nonNull).toList();
17✔
498
            if (dialect.offsetBeforeLimit()) {
3✔
499
                limitOffsetParams = CollectionHelper.reverse(limitOffsetParams);
3✔
500
            }
501
            params.addAll(limitOffsetParams);
4✔
502
        } else {
1✔
503
            if (nonNull(limit)) {
4!
504
                params.add(rowTo());
5✔
505
            }
506
            if (nonNull(offset)) {
4!
507
                params.add(rowFrom());
5✔
508
            }
509
        }
510
    }
1✔
511

512
    private void validate(Context context) {
513
        if (isEmpty(relations)) {
4!
UNCOV
514
            throw new IllegalStateException("No FROM clause specified");
×
515
        }
516

517
        Stream<Column> projectedColumns = streamSafely(projections)
4✔
518
            .filter(instanceOf(Expression.class))
3✔
519
            .map(castTo(Expression.class))
3✔
520
            .flatMap(Expression::columnRefs);
2✔
521
        validateColumnTableRelations(context, projectedColumns);
4✔
522

523
        if (nonEmpty(joins)) {
4✔
524
            validateColumnTableRelations(context, streamSafely(joins).flatMap(Join::columnRefs));
8✔
525
        }
526

527
        if (nonEmpty(orders)) {
4✔
528
            Set<String> orderableAliases = streamSafely(projections)
4✔
529
                .map(Projection::alias)
2✔
530
                .flatMap(Optional::stream)
2✔
531
                .map(Alias::name)
1✔
532
                .collect(toSet());
4✔
533

534
            streamSafely(orders)
4✔
535
                .flatMap(Sql::aliasRefs)
2✔
536
                .map(ColumnAlias::name)
3✔
537
                .filter(a -> !orderableAliases.contains(a))
7!
538
                .findFirst()
2✔
539
                .ifPresent(a -> {
1✔
UNCOV
540
                    throw new IllegalStateException("ORDER BY column alias " + a + " is not specified in the SELECT clause");
×
541
                });
542
        }
543

544
        validateColumnTableRelations(context, streamSafely(wherePredicates).flatMap(Predicate::columnRefs));
8✔
545
        validateColumnTableRelations(context, streamSafely(groups));
6✔
546
        validateColumnTableRelations(context, streamSafely(havingPredicates).flatMap(Predicate::columnRefs));
8✔
547
        validateColumnTableRelations(context, streamSafely(orders).flatMap(Order::columnRefs));
8✔
548

549
        if (forUpdate) {
3✔
550
            if (!context.getDialect().supports(SELECT_FOR_UPDATE)) {
5!
UNCOV
551
                throw new UnsupportedOperationException("%s does not support the SELECT ... FOR UPDATE clause".formatted(getDialectRef(context.getDialect())));
×
552
            }
553

554
            if (distinct || nonEmpty(groups) || streamSafely(projections).anyMatch(instanceOf(AggregateFunction.class))) {
14!
UNCOV
555
                throw new IllegalStateException("SELECT ... FOR UPDATE can't be used with DISTINCT, GROUP BY or aggregates");
×
556
            }
557

558
            List<String> projectedTables = streamSafely(projections)
4✔
559
                .filter(instanceOf(Expression.class))
3✔
560
                .map(castTo(Expression.class))
3✔
561
                .flatMap(Expression::columnRefs)
2✔
562
                .map(cr -> cr.table().name())
5✔
563
                .distinct()
1✔
564
                .toList();
2✔
565

566
            if (projectedTables.size() != 1) {
4!
UNCOV
567
                throw new IllegalStateException("SELECT ... FOR UPDATE can be used for a single projected table only. Projected tables in this statement are: %s".formatted(projectedTables));
×
568
            }
569
        }
570
    }
1✔
571

572
    private void validateColumnTableRelations(Context context, Stream<Column> columns) {
573
        Function<Relation, Stream<Table>> filterTables = r -> r instanceof Table table ? Stream.of(table) : Stream.empty();
13✔
574

575
        Set<String> outerTableNames = new HashSet<>();
4✔
576
        if (nonEmpty(context.getOuterStatements())) {
4!
577
            context.getOuterStatements().forEach(os -> streamSafely(os.relations).flatMap(filterTables).map(Table::name).forEach(outerTableNames::add));
20✔
578
            context.getOuterStatements().forEach(os -> streamSafely(os.joins).map(Join::joined).map(Table::name).forEach(outerTableNames::add));
19✔
579
        }
580

581
        Set<String> tableNames = Stream.concat(streamSafely(relations).flatMap(filterTables), streamSafely(joins).map(Join::joined))
12✔
582
            .map(Table::name)
1✔
583
            .collect(toSet());
4✔
584

585
        columns
4✔
586
            .filter(c -> !outerTableNames.contains(c.table().name()) && !tableNames.contains(c.table().name()))
9!
587
            .findFirst()
2✔
588
            .ifPresent(c -> {
1✔
UNCOV
589
                throw new IllegalStateException("Column " + c.name() + " belongs to table " + c.table().name() + ", but is not specified in a FROM or JOIN clause");
×
590
            });
591
    }
1✔
592

593
    @Override
594
    public String toString() {
UNCOV
595
        return toString(new AnsiIsoDialect());
×
596
    }
597

598
    private String getDialectRef(Dialect dialect) {
UNCOV
599
        return "Dialect " + dialect.getProductName();
×
600
    }
601
}
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