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

torand / FasterSQL / 13389227868

18 Feb 2025 11:10AM UTC coverage: 59.667% (+0.9%) from 58.785%
13389227868

push

github

torand
test: H2 specific testing of select and insert clauses

157 of 346 branches covered (45.38%)

Branch coverage included in aggregate %.

846 of 1335 relevant lines covered (63.37%)

3.35 hits per line

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

75.63
/src/main/java/io/github/torand/fastersql/statement/SelectStatement.java
1
/*
2
 * Copyright (c) 2024 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.Context;
19
import io.github.torand.fastersql.Field;
20
import io.github.torand.fastersql.Join;
21
import io.github.torand.fastersql.Table;
22
import io.github.torand.fastersql.expression.Expression;
23
import io.github.torand.fastersql.function.aggregate.AggregateFunction;
24
import io.github.torand.fastersql.order.Order;
25
import io.github.torand.fastersql.predicate.OptionalPredicate;
26
import io.github.torand.fastersql.predicate.Predicate;
27
import io.github.torand.fastersql.projection.Projection;
28
import io.github.torand.fastersql.subquery.Subquery;
29

30
import java.util.LinkedList;
31
import java.util.List;
32
import java.util.Set;
33
import java.util.function.Supplier;
34
import java.util.stream.Stream;
35

36
import static io.github.torand.fastersql.Command.SELECT;
37
import static io.github.torand.fastersql.dialect.Capability.LIMIT_OFFSET;
38
import static io.github.torand.fastersql.statement.Helpers.unwrapSuppliers;
39
import static io.github.torand.fastersql.util.collection.CollectionHelper.asList;
40
import static io.github.torand.fastersql.util.collection.CollectionHelper.concat;
41
import static io.github.torand.fastersql.util.collection.CollectionHelper.isEmpty;
42
import static io.github.torand.fastersql.util.collection.CollectionHelper.nonEmpty;
43
import static io.github.torand.fastersql.util.collection.CollectionHelper.streamSafely;
44
import static io.github.torand.fastersql.util.contract.Requires.require;
45
import static io.github.torand.fastersql.util.contract.Requires.requireNonEmpty;
46
import static io.github.torand.fastersql.util.functional.Functions.castTo;
47
import static io.github.torand.fastersql.util.functional.Optionals.mapIfNonNull;
48
import static io.github.torand.fastersql.util.functional.Predicates.instanceOf;
49
import static io.github.torand.fastersql.util.functional.Predicates.not;
50
import static io.github.torand.fastersql.util.lang.StringHelper.isBlank;
51
import static io.github.torand.fastersql.util.lang.StringHelper.nonBlank;
52
import static java.util.Objects.isNull;
53
import static java.util.Objects.nonNull;
54
import static java.util.Objects.requireNonNull;
55
import static java.util.stream.Collectors.joining;
56
import static java.util.stream.Collectors.toSet;
57

58
public class SelectStatement extends PreparableStatement {
59
    private final List<Projection> projections;
60
    private final List<Table<?>> tables;
61
    private final List<Join> joins;
62
    private final Subquery subqueryFrom;
63
    private final List<Predicate> predicates;
64
    private final List<Field> groups;
65
    private final List<Order> orders;
66
    private final boolean distinct;
67
    private final Long limit;
68
    private final Long offset;
69
    private final boolean forUpdate;
70

71
    SelectStatement(List<Projection> projections, List<Table<?>> tables, List<Join> joins, Subquery subqueryFrom, List<Predicate> predicates, List<Field> groups, List<Order> orders, boolean distinct, Long limit, Long offset, boolean forUpdate) {
2✔
72
        this.projections = asList(projections);
4✔
73
        this.tables = asList(tables);
4✔
74
        this.joins = asList(joins);
4✔
75
        this.subqueryFrom = subqueryFrom;
3✔
76
        this.predicates = asList(predicates);
4✔
77
        this.groups = asList(groups);
4✔
78
        this.orders = asList(orders);
4✔
79
        this.distinct = distinct;
3✔
80
        this.limit = limit;
3✔
81
        this.offset = offset;
3✔
82
        this.forUpdate = forUpdate;
3✔
83
    }
1✔
84

85
    public SelectStatement join(Join... joins) {
86
        requireNonEmpty(joins, "No joins specified");
6✔
87
        require(() -> isNull(subqueryFrom), "Can't combine a subquery FROM clause with joins");
10✔
88

89
        List<Join> concatenated = concat(this.joins, joins);
5✔
90
        return new SelectStatement(projections, tables, concatenated, subqueryFrom, predicates, groups, orders, distinct, limit, offset, forUpdate);
25✔
91
    }
92

93
    public SelectStatement leftOuterJoin(Join join) {
94
        requireNonNull(join, "No join specified");
×
95
        return join(join.leftOuter());
×
96
    }
97

98
    public SelectStatement rightOuterJoin(Join join) {
99
        requireNonNull(join, "No join specified");
×
100
        return join(join.rightOuter());
×
101
    }
102

103
    @SafeVarargs
104
    public final SelectStatement joinIf(boolean condition, Supplier<Join>... joinSuppliers) {
105
        requireNonEmpty(joinSuppliers, "No join suppliers specified");
×
106
        require(() -> isNull(subqueryFrom), "Can't combine a subquery FROM clause with joins");
×
107
        if (condition) {
×
108
            List<Join> concatenated = concat(this.joins, unwrapSuppliers(joinSuppliers));
×
109
            return new SelectStatement(projections, tables, concatenated, subqueryFrom, predicates, groups, orders, distinct, limit, offset, forUpdate);
×
110
        } else {
111
            return this;
×
112
        }
113
    }
114

115
    /**
116
     * Adds one or more predicates to the where clause.
117
     * @param predicates the predicates to add
118
     * @return updated statement, for method chaining
119
     */
120
    public SelectStatement where(Predicate... predicates) {
121
        requireNonEmpty(predicates, "No predicates specified");
6✔
122

123
        List<Predicate> concatenated = concat(this.predicates, predicates);
5✔
124
        return new SelectStatement(projections, tables, joins, subqueryFrom, concatenated, groups, orders, distinct, limit, offset, forUpdate);
25✔
125
    }
126

127
    /**
128
     * Same as other method of same name, but only adds to the where clause predicates that are present.
129
     * @param maybePredicates the predicate that may be present or not
130
     * @return updated statement, for method chaining
131
     */
132
    @SafeVarargs
133
    public final SelectStatement where(OptionalPredicate... maybePredicates) {
134
        requireNonEmpty(maybePredicates, "No optional predicates specified");
6✔
135

136
        List<Predicate> concatenated = concat(this.predicates, OptionalPredicate.unwrap(maybePredicates));
6✔
137
        return new SelectStatement(projections, tables, joins, subqueryFrom, concatenated, groups, orders, distinct, limit, offset, forUpdate);
25✔
138
    }
139

140
    /**
141
     * Adds one or more predicates to the where clause, if a predicate is true.
142
     * @param condition the condition that must be true for predicates to be added
143
     * @param predicateSuppliers the suppliers providing predicates to add
144
     * @return updatet statement, for method chaining
145
     */
146
    @SafeVarargs
147
    public final SelectStatement whereIf(boolean condition, Supplier<Predicate>... predicateSuppliers) {
148
        requireNonEmpty(predicateSuppliers, "No predicate suppliers specified");
×
149
        if (condition) {
×
150
            List<Predicate> concatenated = concat(this.predicates, unwrapSuppliers(predicateSuppliers));
×
151
            return new SelectStatement(projections, tables, joins, subqueryFrom, concatenated, groups, orders, distinct, limit, offset, forUpdate);
×
152
        } else {
153
            return this;
×
154
        }
155
    }
156

157
    public SelectStatement groupBy(Field... groups) {
158
        requireNonEmpty(groups, "No groups specified");
6✔
159

160
        List<Field> concatenated = concat(this.groups, groups);
5✔
161
        return new SelectStatement(projections, tables, joins, subqueryFrom, predicates, concatenated, orders, distinct, limit, offset, forUpdate);
25✔
162
    }
163

164
    public SelectStatement orderBy(Order... orders) {
165
        requireNonEmpty(orders, "No orders specified");
6✔
166

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

171
    public SelectStatement limit(long limit) {
172
        return new SelectStatement(projections, tables, joins, subqueryFrom, predicates, groups, orders, distinct, limit, offset, forUpdate);
26✔
173
    }
174

175
    public SelectStatement offset(long offset) {
176
        return new SelectStatement(projections, tables, joins, subqueryFrom, predicates, groups, orders, distinct, limit, offset, forUpdate);
26✔
177
    }
178

179
    public SelectStatement forUpdate() {
180
        return new SelectStatement(projections, tables, joins, subqueryFrom, predicates, groups, orders, distinct, limit, offset, forUpdate);
×
181
    }
182

183
    @Override
184
    public String sql(Context context) {
185
        final Context localContext = context.withCommand(SELECT);
4✔
186
        validate();
2✔
187

188
        StringBuilder sb = new StringBuilder();
4✔
189
        sb.append("select ");
4✔
190
        if (distinct) {
3!
191
            sb.append("distinct ");
×
192
        }
193

194
        sb.append(streamSafely(projections)
8✔
195
            .map(p -> p.sql(localContext) + (isBlank(p.alias()) ? "" : " " + p.alias()))
16✔
196
            .collect(joining(", ")));
3✔
197

198
        sb.append(" from ");
4✔
199

200
        if (nonNull(subqueryFrom)) {
4✔
201
            sb.append(subqueryFrom.sql(context));
7✔
202
            if (nonBlank(subqueryFrom.alias())) {
5!
203
                sb.append(" ");
4✔
204
                sb.append(subqueryFrom.alias());
7✔
205
            }
206
        } else {
207
            // Tables that are joined with should not be specified in the FROM clause
208
            Set<Table<?>> joinedTables = streamSafely(joins).map(Join::joined).collect(toSet());
9✔
209

210
            sb.append(streamSafely(tables)
8✔
211
                .filter(not(joinedTables::contains))
7✔
212
                .map(t -> t.sql(localContext))
6✔
213
                .collect(joining(", ")));
3✔
214

215
            if (nonEmpty(joins)) {
4✔
216
                sb.append(" ");
4✔
217
                sb.append(streamSafely(joins)
8✔
218
                    .map(j -> j.sql(localContext))
6✔
219
                    .collect(joining(" ")));
3✔
220
            }
221
        }
222

223
        if (nonEmpty(predicates)) {
4✔
224
            sb.append(" where ");
4✔
225
            sb.append(streamSafely(predicates)
8✔
226
                .map(e -> e.sql(localContext))
6✔
227
                .collect(joining(" and ")));
3✔
228
        }
229

230
        if (nonEmpty(groups)) {
4✔
231
            sb.append(" group by ");
4✔
232
            sb.append(streamSafely(groups)
8✔
233
                .map(g -> g.sql(localContext))
6✔
234
                .collect(joining(", ")));
3✔
235
        }
236

237
        if (nonEmpty(orders)) {
4✔
238
            sb.append(" order by ");
4✔
239
            sb.append(streamSafely(orders)
8✔
240
                .map(o -> o.sql(localContext))
6✔
241
                .collect(joining(", ")));
3✔
242
        }
243

244
        if (nonNull(offset) || nonNull(limit)) {
8!
245
            if (context.getDialect().supports(LIMIT_OFFSET)) {
5✔
246
                String offsetClause = nonNull(offset) ? " " + context.getDialect().formatRowOffsetClause().orElseThrow(() -> new RuntimeException("Dialect " + context.getDialect().getProductName() + " has no row offset clause")) : "";
14!
247
                String limitClause = nonNull(limit) ? " " + context.getDialect().formatRowLimitClause().orElseThrow(() -> new RuntimeException("Dialect " + context.getDialect().getProductName() + " has no row limit clause")) : "";
14!
248

249
                if (context.getDialect().offsetBeforeLimit()) {
4✔
250
                    sb.append(offsetClause).append(limitClause);
7✔
251
                } else {
252
                    sb.append(limitClause).append(offsetClause);
6✔
253
                }
254
            } else {
1✔
255
                sb = addLimitOffsetFallback(context, sb, rowFrom(), rowTo());
9✔
256
            }
257
        }
258

259
        if (forUpdate) {
3!
260
            sb.append(" for update");
×
261
        }
262

263
        return sb.toString();
3✔
264
    }
265

266
    private Long rowFrom() {
267
        return mapIfNonNull(offset, o -> o + 1);
12✔
268
    }
269

270
    private Long rowTo() {
271
        return mapIfNonNull(limit, l -> (nonNull(offset) ? offset : 0) + l);
20!
272
    }
273

274
    private StringBuilder addLimitOffsetFallback(Context context, StringBuilder innerSql, Long rowFrom, Long rowTo) {
275
        String rowNum = context.getDialect().formatRowNumLiteral()
5✔
276
            .orElseThrow(() -> new RuntimeException("Dialect " + context.getDialect().getProductName() + " has no ROWNUM literal"));
3✔
277

278
        if (nonNull(rowFrom) && nonNull(rowTo)) {
6!
279
            String limitSql = "select ORIGINAL.*, {ROWNUM} ROW_NO from ( " + innerSql.toString() + " ) ORIGINAL where {ROWNUM} <= ?";
4✔
280
            String offsetSql = "select * from ( " + limitSql + " ) where ROW_NO >= ?";
3✔
281
            return new StringBuilder(offsetSql.replace("{ROWNUM}", rowNum));
8✔
282
        } else if (nonNull(rowFrom)) {
×
283
            String offsetSql = "select * from ( " + innerSql.toString() + " ) where {ROWNUM} >= ?";
×
284
            return new StringBuilder(offsetSql.replace("{ROWNUM}", rowNum));
×
285
        } else if (nonNull(rowTo)) {
×
286
            String limitSql = "select * from ( " + innerSql.toString() + " ) where {ROWNUM} <= ?";
×
287
            return new StringBuilder(limitSql.replace("{ROWNUM}", rowNum));
×
288
        }
289

290
        return innerSql;
×
291
    }
292

293
    @Override
294
    public List<Object> params(Context context) {
295
        List<Object> params = new LinkedList<>();
4✔
296

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

299
        if (nonNull(subqueryFrom)) {
4✔
300
            subqueryFrom.params(context).forEach(params::add);
10✔
301
        }
302

303
        streamSafely(predicates).flatMap(e -> e.params(context)).forEach(params::add);
16✔
304

305
        if (context.getDialect().supports(LIMIT_OFFSET)) {
5✔
306
            if (context.getDialect().offsetBeforeLimit()) {
4✔
307
                if (nonNull(offset)) {
4✔
308
                    params.add(offset);
5✔
309
                }
310
                if (nonNull(limit)) {
4✔
311
                    params.add(limit);
6✔
312
                }
313
            } else {
314
                if (nonNull(limit)) {
4✔
315
                    params.add(limit);
5✔
316
                }
317
                if (nonNull(offset)) {
4✔
318
                    params.add(offset);
6✔
319
                }
320
            }
321
        } else {
322
            if (nonNull(limit)) {
4!
323
                params.add(rowTo());
5✔
324
            }
325
            if (nonNull(offset)) {
4!
326
                params.add(rowFrom());
5✔
327
            }
328
        }
329

330
        return params;
2✔
331
    }
332

333
    private void validate() {
334
        if (isEmpty(tables) && isNull(subqueryFrom)) {
8!
335
            throw new IllegalStateException("No FROM clause specified");
×
336
        }
337

338
        List<Field> projectedFields = streamSafely(projections)
4✔
339
            .filter(instanceOf(Expression.class))
3✔
340
            .map(castTo(Expression.class))
3✔
341
            .flatMap(Expression::fieldRefs)
1✔
342
            .toList();
2✔
343
        validateFieldTableRelations(streamSafely(projectedFields));
4✔
344

345
        if (nonNull(joins)) {
4!
346
            validateFieldTableRelations(streamSafely(joins).flatMap(Join::fieldRefs));
7✔
347
        }
348

349
        validateFieldTableRelations(streamSafely(predicates).flatMap(Predicate::fieldRefs));
7✔
350
        validateFieldTableRelations(streamSafely(groups));
5✔
351
        validateFieldTableRelations(streamSafely(orders).flatMap(Order::fieldRefs));
7✔
352

353
        if (forUpdate) {
3!
354
            if (distinct || nonEmpty(groups) || streamSafely(projections).anyMatch(instanceOf(AggregateFunction.class))) {
×
355
                throw new IllegalStateException("SELECT ... FOR UPDATE can't be used with DISTINCT, GROUP BY or aggregates");
×
356
            }
357
        }
358
    }
1✔
359

360
    private void validateFieldTableRelations(Stream<Field> fields) {
361
        Set<String> tableNames = Stream.concat(streamSafely(tables), streamSafely(joins).map(Join::joined))
10✔
362
            .map(Table::name)
1✔
363
            .collect(toSet());
4✔
364

365
        fields
3✔
366
            .filter(f -> !tableNames.contains(f.table().name()))
9!
367
            .findFirst()
2✔
368
            .ifPresent(f -> {
1✔
369
                throw new IllegalStateException("Field " + f.name() + " belongs to table " + f.table().name() + ", but is not specified in a FROM or JOIN clause");
×
370
            });
371
    }
1✔
372
}
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