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

torand / FasterSQL / 17194256933

24 Aug 2025 09:56PM UTC coverage: 68.417% (+0.3%) from 68.15%
17194256933

push

github

torand
refactor: sonar cloud issues

299 of 598 branches covered (50.0%)

Branch coverage included in aggregate %.

35 of 40 new or added lines in 6 files covered. (87.5%)

1 existing line in 1 file now uncovered.

1616 of 2201 relevant lines covered (73.42%)

4.01 hits per line

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

76.77
/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

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

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

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

84
    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✔
85
        this.projections = asList(projections);
4✔
86
        this.relations = asList(relations);
4✔
87
        this.joins = asList(joins);
4✔
88
        this.wherePredicates = asList(wherePredicates);
4✔
89
        this.groups = asList(groups);
4✔
90
        this.havingPredicates = asList(havingPredicates);
4✔
91
        this.orders = asList(orders);
4✔
92
        this.distinct = distinct;
3✔
93
        this.limit = limit;
3✔
94
        this.offset = offset;
3✔
95
        this.forUpdate = forUpdate;
3✔
96
    }
1✔
97

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

358
        final Dialect dialect = localContext.getDialect();
3✔
359

360
        validate(context);
3✔
361

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

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

372
        sb.append(" from ");
4✔
373

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

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

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

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

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

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

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

417
        if (nonNull(offset) || nonNull(limit)) {
8!
418
            if (dialect.supports(LIMIT_OFFSET)) {
4✔
419
                String offsetClause = nonNull(offset) ? " " + dialect.formatRowOffsetClause().orElseThrow(() -> new FasterSQLException(getDialectRef(dialect) + " has no row offset clause")) : "";
14!
420
                String limitClause = nonNull(limit) ? " " + dialect.formatRowLimitClause().orElseThrow(() -> new FasterSQLException(getDialectRef(dialect) + " has no row limit clause")) : "";
14!
421

422
                if (dialect.offsetBeforeLimit()) {
3✔
423
                    sb.append(offsetClause).append(limitClause);
7✔
424
                } else {
425
                    sb.append(limitClause).append(offsetClause);
6✔
426
                }
427
            } else {
1✔
428
                sb = addLimitOffsetFallback(localContext, sb, rowFrom(), rowTo());
9✔
429
            }
430
        }
431

432
        if (forUpdate) {
3✔
433
            sb.append(" for update");
4✔
434
        }
435

436
        return sb.toString();
3✔
437
    }
438

439
    Stream<Projection> projections() {
440
        return streamSafely(projections);
4✔
441
    }
442

443
    private Long rowFrom() {
444
        return mapSafely(offset, o -> o + 1);
12✔
445
    }
446

447
    private Long rowTo() {
448
        return mapSafely(limit, l -> (nonNull(offset) ? offset : 0) + l);
20!
449
    }
450

451
    private StringBuilder addLimitOffsetFallback(Context context, StringBuilder innerSql, Long rowFrom, Long rowTo) {
452
        final String ROWNUM = "{ROWNUM}";
2✔
453
        final String selectAllFrom = "select * from";
2✔
454

455
        String rowNum = context.getDialect().formatRowNumLiteral()
6✔
456
            .orElseThrow(() -> new FasterSQLException(getDialectRef(context.getDialect()) + " has no row number literal"));
3✔
457

458
        if (nonNull(rowFrom) && nonNull(rowTo)) {
6!
459
            String limitSql = "select ORIGINAL.*, %s ROW_NO from ( %s ) ORIGINAL where %s <= ?".formatted(ROWNUM, innerSql, ROWNUM);
17✔
460
            String offsetSql = "%s ( %s ) where ROW_NO >= ?".formatted(selectAllFrom, limitSql);
13✔
461
            return new StringBuilder(offsetSql.replace(ROWNUM, rowNum));
8✔
462
        } else if (nonNull(rowFrom)) {
×
463
            String offsetSql = "%s ( %s ) where %s >= ?".formatted(selectAllFrom, innerSql, ROWNUM);
×
464
            return new StringBuilder(offsetSql.replace(ROWNUM, rowNum));
×
465
        } else if (nonNull(rowTo)) {
×
466
            String limitSql = "%s ( %s ) where %s <= ?".formatted(selectAllFrom, innerSql, ROWNUM);
×
467
            return new StringBuilder(limitSql.replace(ROWNUM, rowNum));
×
468
        }
469

470
        return innerSql;
×
471
    }
472

473
    @Override
474
    public Stream<Object> params(Context context) {
475
        List<Object> params = new LinkedList<>();
4✔
476

477
        streamSafely(projections).flatMap(p -> p.params(context)).forEach(params::add);
16✔
478

479
        streamSafely(relations).flatMap(r -> r.params(context)).forEach(params::add);
16✔
480

481
        streamSafely(wherePredicates).flatMap(p -> p.params(context)).forEach(params::add);
16✔
482

483
        streamSafely(havingPredicates).flatMap(p -> p.params(context)).forEach(params::add);
16✔
484

485
        if (context.getDialect().supports(LIMIT_OFFSET)) {
5✔
486
            if (context.getDialect().offsetBeforeLimit()) {
4✔
487
                if (nonNull(offset)) {
4✔
488
                    params.add(offset);
5✔
489
                }
490
                if (nonNull(limit)) {
4✔
491
                    params.add(limit);
6✔
492
                }
493
            } else {
494
                if (nonNull(limit)) {
4✔
495
                    params.add(limit);
5✔
496
                }
497
                if (nonNull(offset)) {
4✔
498
                    params.add(offset);
6✔
499
                }
500
            }
501
        } else {
502
            if (nonNull(limit)) {
4!
503
                params.add(rowTo());
5✔
504
            }
505
            if (nonNull(offset)) {
4!
506
                params.add(rowFrom());
5✔
507
            }
508
        }
509

510
        return params.stream();
3✔
511
    }
512

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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