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

torand / FasterSQL / 12970217277

26 Jan 2025 01:31AM UTC coverage: 49.458%. Remained the same
12970217277

push

github

torand
refactor: condition -> predicate

108 of 322 branches covered (33.54%)

Branch coverage included in aggregate %.

58 of 137 new or added lines in 12 files covered. (42.34%)

3 existing lines in 2 files now uncovered.

622 of 1154 relevant lines covered (53.9%)

2.85 hits per line

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

66.82
/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.*;
40
import static io.github.torand.fastersql.util.contract.Requires.require;
41
import static io.github.torand.fastersql.util.contract.Requires.requireNonEmpty;
42
import static io.github.torand.fastersql.util.functional.Functions.castTo;
43
import static io.github.torand.fastersql.util.functional.Optionals.mapIfNonNull;
44
import static io.github.torand.fastersql.util.functional.Predicates.instanceOf;
45
import static io.github.torand.fastersql.util.functional.Predicates.not;
46
import static io.github.torand.fastersql.util.lang.StringHelper.isBlank;
47
import static io.github.torand.fastersql.util.lang.StringHelper.nonBlank;
48
import static java.util.Objects.*;
49
import static java.util.stream.Collectors.joining;
50
import static java.util.stream.Collectors.toSet;
51

52
public class SelectStatement extends PreparableStatement {
53
    private final List<Projection> projections;
54
    private final List<Table<?>> tables;
55
    private final List<Join> joins;
56
    private final Subquery subqueryFrom;
57
    private final List<Predicate> predicates;
58
    private final List<Field> groups;
59
    private final List<Order> orders;
60
    private final boolean distinct;
61
    private final Long limit;
62
    private final Long offset;
63
    private final boolean forUpdate;
64

65
    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✔
66
        this.projections = asList(projections);
4✔
67
        this.tables = asList(tables);
4✔
68
        this.joins = asList(joins);
4✔
69
        this.subqueryFrom = subqueryFrom;
3✔
70
        this.predicates = asList(predicates);
4✔
71
        this.groups = asList(groups);
4✔
72
        this.orders = asList(orders);
4✔
73
        this.distinct = distinct;
3✔
74
        this.limit = limit;
3✔
75
        this.offset = offset;
3✔
76
        this.forUpdate = forUpdate;
3✔
77
    }
1✔
78

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

83
        List<Join> concatenated = concat(this.joins, joins);
5✔
84
        return new SelectStatement(projections, tables, concatenated, subqueryFrom, predicates, groups, orders, distinct, limit, offset, forUpdate);
25✔
85
    }
86

87
    public SelectStatement leftOuterJoin(Join join) {
88
        requireNonNull(join, "No join specified");
×
89
        return join(join.leftOuter());
×
90
    }
91

92
    public SelectStatement rightOuterJoin(Join join) {
93
        requireNonNull(join, "No join specified");
×
94
        return join(join.rightOuter());
×
95
    }
96

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

109
    /**
110
     * Adds one or more predicates to the where clause.
111
     * @param predicates the predicates to add
112
     * @return updated statement, for method chaining
113
     */
114
    public SelectStatement where(Predicate... predicates) {
115
        requireNonEmpty(predicates, "No predicates specified");
6✔
116

117
        List<Predicate> concatenated = concat(this.predicates, predicates);
5✔
118
        return new SelectStatement(projections, tables, joins, subqueryFrom, concatenated, groups, orders, distinct, limit, offset, forUpdate);
25✔
119
    }
120

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

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

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

151
    public SelectStatement groupBy(Field... groups) {
152
        requireNonEmpty(groups, "No groups specified");
6✔
153

154
        List<Field> concatenated = concat(this.groups, groups);
5✔
155
        return new SelectStatement(projections, tables, joins, subqueryFrom, predicates, concatenated, orders, distinct, limit, offset, forUpdate);
25✔
156
    }
157

158
    public SelectStatement orderBy(Order... orders) {
159
        requireNonEmpty(orders, "No orders specified");
6✔
160

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

165
    public SelectStatement limit(long limit) {
166
        return new SelectStatement(projections, tables, joins, subqueryFrom, predicates, groups, orders, distinct, limit, offset, forUpdate);
26✔
167
    }
168

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

173
    public SelectStatement forUpdate() {
NEW
174
        return new SelectStatement(projections, tables, joins, subqueryFrom, predicates, groups, orders, distinct, limit, offset, forUpdate);
×
175
    }
176

177
    @Override
178
    public String sql(Context context) {
179
        final Context localContext = context.withCommand(SELECT);
4✔
180
        validate();
2✔
181

182
        StringBuilder sb = new StringBuilder();
4✔
183
        sb.append("select ");
4✔
184
        if (distinct) {
3!
185
            sb.append("distinct ");
×
186
        }
187

188
        sb.append(streamSafely(projections)
8✔
189
            .map(p -> p.sql(localContext) + (isBlank(p.alias()) ? "" : " " + p.alias()))
16✔
190
            .collect(joining(", ")));
3✔
191

192
        sb.append(" from ");
4✔
193

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

204
            sb.append(streamSafely(tables)
8✔
205
                .filter(not(joinedTables::contains))
7✔
206
                .map(t -> t.sql(localContext))
6✔
207
                .collect(joining(", ")));
3✔
208

209
            if (nonEmpty(joins)) {
4✔
210
                sb.append(" ");
4✔
211
                sb.append(streamSafely(joins)
8✔
212
                    .map(j -> j.sql(localContext))
6✔
213
                    .collect(joining(" ")));
3✔
214
            }
215
        }
216

217
        if (nonEmpty(predicates)) {
4✔
218
            sb.append(" where ");
4✔
219
            sb.append(streamSafely(predicates)
8✔
220
                .map(e -> e.sql(localContext))
6✔
221
                .collect(joining(" and ")));
3✔
222
        }
223

224
        if (nonEmpty(groups)) {
4✔
225
            sb.append(" group by ");
4✔
226
            sb.append(streamSafely(groups)
8✔
227
                .map(g -> g.sql(localContext))
6✔
228
                .collect(joining(", ")));
3✔
229
        }
230

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

238
        if (nonNull(offset) || nonNull(limit)) {
8!
239
            if (context.getDialect().supports(LIMIT_OFFSET)) {
5!
240
                if (nonNull(limit)) {
4!
241
                    sb.append(" limit ?");
4✔
242
                }
243
                if (nonNull(offset)) {
4!
244
                    sb.append(" offset ?");
5✔
245
                }
246
            } else {
247
                sb = addLimitOffsetFallback(context, sb, rowFrom(), rowTo());
×
248
            }
249
        }
250

251
        if (forUpdate) {
3!
252
            sb.append(" for update");
×
253
        }
254

255
        return sb.toString();
3✔
256
    }
257

258
    private Long rowFrom() {
259
        return mapIfNonNull(offset, o -> o + 1);
×
260
    }
261

262
    private Long rowTo() {
263
        return mapIfNonNull(limit, l -> (nonNull(offset) ? offset : 0) + l);
×
264
    }
265

266
    private StringBuilder addLimitOffsetFallback(Context context, StringBuilder innerSql, Long rowFrom, Long rowTo) {
267
        String rowNum = context.getDialect().getRowNumLiteral()
×
268
            .orElseThrow(() -> new RuntimeException("Dialect " + context.getDialect().getProductName() + " has no ROWNUM literal"));
×
269

270
        if (nonNull(rowFrom) && nonNull(rowTo)) {
×
271
            String limitSql = "select original.*, {ROWNUM} row_no from ( " + innerSql.toString() + " ) original where {ROWNUM} <= ?";
×
272
            String offsetSql = "select * from ( " + limitSql + " ) where row_no >= ?";
×
273
            return new StringBuilder(offsetSql.replace("{ROWNUM}", rowNum));
×
274
        } else if (nonNull(rowFrom)) {
×
275
            String offsetSql = "select * from ( " + innerSql.toString() + " ) where {ROWNUM} >= ?";
×
276
            return new StringBuilder(offsetSql.replace("{ROWNUM}", rowNum));
×
277
        } else if (nonNull(rowTo)) {
×
278
            String limitSql = "select * from ( " + innerSql.toString() + " ) where {ROWNUM} <= ?";
×
279
            return new StringBuilder(limitSql.replace("{ROWNUM}", rowNum));
×
280
        }
281

282
        return innerSql;
×
283
    }
284

285
    @Override
286
    public List<Object> params(Context context) {
287
        List<Object> params = new LinkedList<>();
4✔
288

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

291
        if (nonNull(subqueryFrom)) {
4✔
292
            subqueryFrom.params(context).forEach(params::add);
10✔
293
        }
294

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

297
        if (nonNull(limit)) {
4✔
298
            if (context.getDialect().supports(LIMIT_OFFSET)) {
5!
299
                params.add(limit);
6✔
300
            } else {
301
                params.add(rowTo());
×
302
            }
303
        }
304
        if (nonNull(offset)) {
4✔
305
            if (context.getDialect().supports(LIMIT_OFFSET)) {
5!
306
                params.add(offset);
6✔
307
            } else {
308
                params.add(rowFrom());
×
309
            }
310
        }
311

312
        return params;
2✔
313
    }
314

315
    private void validate() {
316
        if (isEmpty(tables) && isNull(subqueryFrom)) {
8!
317
            throw new IllegalStateException("No FROM clause specified");
×
318
        }
319

320
        List<Field> projectedFields = streamSafely(projections)
4✔
321
            .filter(instanceOf(Expression.class))
3✔
322
            .map(castTo(Expression.class))
3✔
323
            .flatMap(Expression::fieldRefs)
1✔
324
            .toList();
2✔
325
        validateFieldTableRelations(streamSafely(projectedFields));
4✔
326

327
        if (nonNull(joins)) {
4!
328
            validateFieldTableRelations(streamSafely(joins).flatMap(Join::fieldRefs));
7✔
329
        }
330

331
        validateFieldTableRelations(streamSafely(predicates).flatMap(Predicate::fieldRefs));
7✔
332
        validateFieldTableRelations(streamSafely(groups));
5✔
333
        validateFieldTableRelations(streamSafely(orders).flatMap(Order::fieldRefs));
7✔
334

335
        if (forUpdate) {
3!
336
            if (distinct || nonEmpty(groups) || streamSafely(projections).anyMatch(instanceOf(AggregateFunction.class))) {
×
337
                throw new IllegalStateException("SELECT ... FOR UPDATE can't be used with DISTINCT, GROUP BY or aggregates");
×
338
            }
339
        }
340
    }
1✔
341

342
    private void validateFieldTableRelations(Stream<Field> fields) {
343
        Set<String> tableNames = Stream.concat(streamSafely(tables), streamSafely(joins).map(Join::joined))
10✔
344
            .map(Table::name)
1✔
345
            .collect(toSet());
4✔
346

347
        fields
3✔
348
            .filter(f -> !tableNames.contains(f.table().name()))
9!
349
            .findFirst()
2✔
350
            .ifPresent(f -> {
1✔
351
                throw new IllegalStateException("Field " + f.name() + " belongs to table " + f.table().name() + ", but is not specified in a FROM or JOIN clause");
×
352
            });
353
    }
1✔
354
}
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