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

torand / FasterSQL / 12893661530

21 Jan 2025 06:40PM UTC coverage: 50.829%. Remained the same
12893661530

push

github

torand
refactor: rename expression -> condition

104 of 304 branches covered (34.21%)

Branch coverage included in aggregate %.

81 of 211 new or added lines in 24 files covered. (38.39%)

3 existing lines in 2 files now uncovered.

601 of 1083 relevant lines covered (55.49%)

2.91 hits per line

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

67.56
/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.condition.Condition;
23
import io.github.torand.fastersql.condition.OptionalCondition;
24
import io.github.torand.fastersql.function.FieldFunction;
25
import io.github.torand.fastersql.function.aggregate.AggregateFunction;
26
import io.github.torand.fastersql.order.Order;
27
import io.github.torand.fastersql.projection.Projection;
28
import io.github.torand.fastersql.subquery.Subquery;
29
import io.github.torand.fastersql.util.functional.Optionals;
30

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

37
import static io.github.torand.fastersql.Command.SELECT;
38
import static io.github.torand.fastersql.dialect.Capability.LIMIT_OFFSET;
39
import static io.github.torand.fastersql.statement.Helpers.unwrapSuppliers;
40
import static io.github.torand.fastersql.util.collection.CollectionHelper.*;
41
import static io.github.torand.fastersql.util.contract.Requires.require;
42
import static io.github.torand.fastersql.util.contract.Requires.requireNonEmpty;
43
import static io.github.torand.fastersql.util.functional.Functions.castTo;
44
import static io.github.torand.fastersql.util.functional.Optionals.mapIfNonNull;
45
import static io.github.torand.fastersql.util.functional.Predicates.instanceOf;
46
import static io.github.torand.fastersql.util.functional.Predicates.not;
47
import static io.github.torand.fastersql.util.lang.StringHelper.isBlank;
48
import static io.github.torand.fastersql.util.lang.StringHelper.nonBlank;
49
import static java.util.Objects.*;
50
import static java.util.stream.Collectors.joining;
51
import static java.util.stream.Collectors.toSet;
52

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

66
    SelectStatement(List<Projection> projections, List<Table<?>> tables, List<Join> joins, Subquery subqueryFrom, List<Condition> conditions, List<Field> groups, List<Order> orders, boolean distinct, Long limit, Long offset, boolean forUpdate) {
2✔
67
        this.projections = asList(projections);
4✔
68
        this.tables = asList(tables);
4✔
69
        this.joins = asList(joins);
4✔
70
        this.subqueryFrom = subqueryFrom;
3✔
71
        this.conditions = asList(conditions);
4✔
72
        this.groups = asList(groups);
4✔
73
        this.orders = asList(orders);
4✔
74
        this.distinct = distinct;
3✔
75
        this.limit = limit;
3✔
76
        this.offset = offset;
3✔
77
        this.forUpdate = forUpdate;
3✔
78
    }
1✔
79

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

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

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

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

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

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

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

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

131
        List<Condition> concatenated = concat(this.conditions, OptionalCondition.unwrap(maybeConditions));
6✔
132
        return new SelectStatement(projections, tables, joins, subqueryFrom, concatenated, groups, orders, distinct, limit, offset, forUpdate);
25✔
133
    }
134

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

283
        return innerSql;
×
284
    }
285

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

290
        if (nonNull(subqueryFrom)) {
4✔
291
            params.addAll(subqueryFrom.params(context));
7✔
292
        }
293

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

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

311
        return params;
2✔
312
    }
313

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

319
        List<Field> projectedFields = streamSafely(projections)
4✔
320
            .filter(instanceOf(Field.class))
3✔
321
            .map(castTo(Field.class))
2✔
322
            .toList();
2✔
323
        validateFieldTableRelations(streamSafely(projectedFields));
4✔
324

325
        List<Field> projectedFunctionFields = streamSafely(projections)
4✔
326
            .filter(instanceOf(FieldFunction.class))
3✔
327
            .map(castTo(FieldFunction.class))
3✔
328
            .map(FieldFunction::field)
2✔
329
            .flatMap(Optionals::stream)
1✔
330
            .toList();
2✔
331
        validateFieldTableRelations(streamSafely(projectedFunctionFields));
4✔
332

333
        if (nonNull(joins)) {
4!
334
            validateFieldTableRelations(streamSafely(joins).flatMap(Join::fields));
7✔
335
        }
336

337
        validateFieldTableRelations(streamSafely(conditions).flatMap(Condition::fields));
7✔
338
        validateFieldTableRelations(streamSafely(groups));
5✔
339
        validateFieldTableRelations(streamSafely(orders).flatMap(Order::fields));
7✔
340

341
        if (forUpdate) {
3!
342
            if (distinct || nonEmpty(groups) || streamSafely(projections).anyMatch(instanceOf(AggregateFunction.class))) {
×
343
                throw new IllegalStateException("SELECT ... FOR UPDATE can't be used with DISTINCT, GROUP BY or aggregates");
×
344
            }
345
        }
346
    }
1✔
347

348
    private void validateFieldTableRelations(Stream<Field> fields) {
349
        Set<String> tableNames = Stream.concat(streamSafely(tables), streamSafely(joins).map(Join::joined))
10✔
350
            .map(Table::name)
1✔
351
            .collect(toSet());
4✔
352

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