Skip to content

Commit d197ea1

Browse files
authored
Feature/storm 76 2 (#80)
Use fetchSize to limit memory consumption for large (streaming) results sets. (#76)
1 parent f64f0f3 commit d197ea1

22 files changed

Lines changed: 608 additions & 109 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
**Key benefits:**
1414

1515
- **Minimal code**: Define entities with simple records/data classes and query with concise, readable syntax; no boilerplate.
16-
- **Parameterized by default**: String interpolations are automatically converted to bound parameters, making queries SQL injection safe by design.
16+
- **Parameterized by default**: String interpolations are automatically converted to bind variables, making queries SQL injection safe by design.
1717
- **Close to SQL**: Storm embraces SQL rather than abstracting it away, keeping you in control of your database operations.
1818
- **Type-safe**: Storm's DSL mirrors SQL, providing a type-safe, intuitive experience that makes queries easy to write and read while reducing the risk of runtime errors.
1919
- **Direct Database Interaction**: Storm translates method calls directly into database operations, offering a transparent and straightforward experience. It eliminates inefficiencies like the N+1 query problem for predictable and efficient interactions.

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import TabItem from '@theme/TabItem';
1313
**Key benefits:**
1414

1515
- **Minimal code**: Define entities with simple records/data classes and query with concise, readable syntax, no boilerplate.
16-
- **Parameterized by default**: String interpolations are automatically converted to bound parameters, making queries SQL injection safe by design.
16+
- **Parameterized by default**: String interpolations are automatically converted to bind variables, making queries SQL injection safe by design.
1717
- **Close to SQL**: Storm embraces SQL rather than abstracting it away, keeping you in control of your database operations.
1818
- **Type-safe**: Storm's DSL mirrors SQL, providing a type-safe, intuitive experience that makes queries easy to write and read while reducing the risk of runtime errors.
1919
- **Direct Database Interaction**: Storm translates method calls directly into database operations, offering a transparent and straightforward experience. It eliminates inefficiencies like the N+1 query problem for predictable and efficient interactions.

docs/testing.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,11 @@ All three methods are scoped: only SQL statements generated within the block are
283283

284284
Each captured statement is represented as a `CapturedStatement` record with three fields:
285285

286-
| Field | Type | Description |
287-
|--------------|-------------------|------------------------------------------------------|
286+
| Field | Type | Description |
287+
|--------------|-------------------|---------------------------------------------------------------------------------|
288288
| `operation` | `Operation` | The SQL operation type: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, or `UNDEFINED`. |
289-
| `statement` | `String` | The SQL text with `?` placeholders for bound parameters. |
290-
| `parameters` | `List<Object>` | The bound parameter values in order. |
289+
| `statement` | `String` | The SQL text with `?` placeholders for bind variables. |
290+
| `parameters` | `List<Object>` | The bound parameter values in order. |
291291

292292
Query the capture results using `count()`, `statements()`, or their filtered variants:
293293

storm-core/src/main/java/st/orm/core/template/SqlDialect.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,4 +467,30 @@ default boolean useCatalogAsSchema() {
467467
return false;
468468
}
469469

470+
/**
471+
* Returns the default JDBC fetch size hint for queries.
472+
*
473+
* <p>The returned value is passed to {@link PreparedStatement#setFetchSize(int)} to control how many rows the
474+
* JDBC driver fetches from the database at a time. A value of {@code 0} (the default) leaves the fetch size
475+
* unset, deferring to driver defaults.</p>
476+
*
477+
* @return the default fetch size.
478+
* @since 1.10
479+
*/
480+
default int defaultFetchSize() {
481+
return 0;
482+
}
483+
484+
/**
485+
* Returns whether the default fetch size should only be applied for streaming result consumption
486+
* (i.e., {@code getResultStream()} and flow-based access), not for eager methods like
487+
* {@code getResultList()} or {@code getSingleResult()}.
488+
*
489+
* @return {@code true} if fetch size should only apply to streaming access.
490+
* @since 1.10
491+
*/
492+
default boolean streamOnlyFetchSize() {
493+
return false;
494+
}
495+
470496
}

storm-core/src/main/java/st/orm/core/template/impl/PreparedQueryImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@ public PreparedQueryImpl(@Nonnull RefFactory refFactory,
4646
@Nullable Class<? extends Data> affectedType,
4747
boolean versionAware,
4848
boolean managed,
49+
int defaultFetchSize,
50+
boolean streamOnlyFetchSize,
4951
@Nonnull Function<Throwable, PersistenceException> exceptionTransformer) {
50-
super(refFactory, ignore -> statement, bindVarsHandle, affectedType, versionAware, managed, false, exceptionTransformer);
52+
super(refFactory, ignore -> statement, bindVarsHandle, affectedType, versionAware, managed, false, defaultFetchSize, streamOnlyFetchSize, exceptionTransformer);
5153
this.refFactory = refFactory;
5254
this.statement = statement;
5355
this.bindVarsHandle = bindVarsHandle;

storm-core/src/main/java/st/orm/core/template/impl/PreparedStatementTemplateImpl.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ private static TemplateProcessor createDataSourceProcessor(@Nonnull DataSource d
178178
preparedStatement = transactionContext.getDecorator(PreparedStatement.class)
179179
.decorate(preparedStatement);
180180
}
181+
if (!dialect.streamOnlyFetchSize() && dialect.defaultFetchSize() != 0) {
182+
preparedStatement.setFetchSize(dialect.defaultFetchSize());
183+
}
181184
if (bindVariables == null) {
182185
setParameters(preparedStatement, parameters, dialect);
183186
} else {
@@ -229,6 +232,9 @@ private static TemplateProcessor createConnectionProcessor(@Nonnull Connection c
229232
preparedStatement = transactionContext.getDecorator(PreparedStatement.class)
230233
.decorate(preparedStatement);
231234
}
235+
if (!dialect.streamOnlyFetchSize() && dialect.defaultFetchSize() != 0) {
236+
preparedStatement.setFetchSize(dialect.defaultFetchSize());
237+
}
232238
if (bindVariables == null) {
233239
setParameters(preparedStatement, parameters, dialect);
234240
} else {
@@ -492,13 +498,16 @@ public Query create(@Nonnull TemplateString template) {
492498
try {
493499
var sql = sqlTemplate().process(template);
494500
var bindVariables = sql.bindVariables().orElse(null);
501+
SqlDialect dialect = providerFilter != null
502+
? getSqlDialect(providerFilter, config)
503+
: getSqlDialect(config);
495504
return new QueryImpl(refFactory, unsafe -> {
496505
try {
497506
return templateProcessor.process(sql, unsafe);
498507
} catch (SQLException e) {
499508
throw new PersistenceException(e);
500509
}
501-
}, bindVariables == null ? null : bindVariables.getHandle(), sql.affectedType().orElse(null), sql.versionAware(), false, getExceptionTransformer(sql));
510+
}, bindVariables == null ? null : bindVariables.getHandle(), sql.affectedType().orElse(null), sql.versionAware(), false, false, dialect.defaultFetchSize(), dialect.streamOnlyFetchSize(), getExceptionTransformer(sql));
502511
} catch (SqlTemplateException e) {
503512
throw new PersistenceException(e);
504513
}

storm-core/src/main/java/st/orm/core/template/impl/QueryImpl.java

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
import java.time.ZoneOffset;
3939
import java.time.ZonedDateTime;
4040
import java.util.Calendar;
41+
import java.util.List;
4142
import java.util.Objects;
43+
import java.util.Optional;
4244
import java.util.TimeZone;
4345
import java.util.UUID;
4446
import java.util.function.Function;
@@ -67,14 +69,16 @@ class QueryImpl implements Query {
6769
private final Class<? extends Data> affectedType;
6870
private final boolean unsafe;
6971
private final boolean managed;
72+
private final int defaultFetchSize;
73+
private final boolean streamOnlyFetchSize;
7074
private final Function<Throwable, PersistenceException> exceptionTransformer;
7175

7276
QueryImpl(@Nonnull RefFactory refFactory,
7377
@Nonnull Function<Boolean, PreparedStatement> statement,
7478
@Nullable BindVarsHandle bindVarsHandle,
7579
boolean versionAware,
7680
@Nonnull Function<Throwable, PersistenceException> exceptionTransformer) {
77-
this(refFactory, statement, bindVarsHandle, null, versionAware, false, false, exceptionTransformer);
81+
this(refFactory, statement, bindVarsHandle, null, versionAware, false, false, 0, false, exceptionTransformer);
7882
}
7983

8084
QueryImpl(@Nonnull RefFactory refFactory,
@@ -83,7 +87,7 @@ class QueryImpl implements Query {
8387
boolean versionAware,
8488
boolean unsafe,
8589
@Nonnull Function<Throwable, PersistenceException> exceptionTransformer) {
86-
this(refFactory, statement, bindVarsHandle, null, versionAware, false, unsafe, exceptionTransformer);
90+
this(refFactory, statement, bindVarsHandle, null, versionAware, false, unsafe, 0, false, exceptionTransformer);
8791
}
8892

8993
QueryImpl(@Nonnull RefFactory refFactory,
@@ -93,7 +97,7 @@ class QueryImpl implements Query {
9397
boolean versionAware,
9498
boolean unsafe,
9599
@Nonnull Function<Throwable, PersistenceException> exceptionTransformer) {
96-
this(refFactory, statement, bindVarsHandle, affectedType, versionAware, false, unsafe, exceptionTransformer);
100+
this(refFactory, statement, bindVarsHandle, affectedType, versionAware, false, unsafe, 0, false, exceptionTransformer);
97101
}
98102

99103
QueryImpl(@Nonnull RefFactory refFactory,
@@ -104,13 +108,28 @@ class QueryImpl implements Query {
104108
boolean managed,
105109
boolean unsafe,
106110
@Nonnull Function<Throwable, PersistenceException> exceptionTransformer) {
111+
this(refFactory, statement, bindVarsHandle, affectedType, versionAware, managed, unsafe, 0, false, exceptionTransformer);
112+
}
113+
114+
QueryImpl(@Nonnull RefFactory refFactory,
115+
@Nonnull Function<Boolean, PreparedStatement> statement,
116+
@Nullable BindVarsHandle bindVarsHandle,
117+
@Nullable Class<? extends Data> affectedType,
118+
boolean versionAware,
119+
boolean managed,
120+
boolean unsafe,
121+
int defaultFetchSize,
122+
boolean streamOnlyFetchSize,
123+
@Nonnull Function<Throwable, PersistenceException> exceptionTransformer) {
107124
this.refFactory = refFactory;
108125
this.statement = statement;
109126
this.bindVarsHandle = bindVarsHandle;
110127
this.versionAware = versionAware;
111128
this.affectedType = affectedType;
112129
this.managed = managed;
113130
this.unsafe = unsafe;
131+
this.defaultFetchSize = defaultFetchSize;
132+
this.streamOnlyFetchSize = streamOnlyFetchSize;
114133
this.exceptionTransformer = exceptionTransformer;
115134
}
116135

@@ -128,7 +147,7 @@ class QueryImpl implements Query {
128147
*/
129148
@Override
130149
public PreparedQuery prepare() {
131-
return MonitoredResource.wrap(new PreparedQueryImpl(refFactory, statement.apply(unsafe), bindVarsHandle, affectedType, versionAware, managed, exceptionTransformer));
150+
return MonitoredResource.wrap(new PreparedQueryImpl(refFactory, statement.apply(unsafe), bindVarsHandle, affectedType, versionAware, managed, defaultFetchSize, streamOnlyFetchSize, exceptionTransformer));
132151
}
133152

134153
/**
@@ -139,7 +158,7 @@ public PreparedQuery prepare() {
139158
*/
140159
@Override
141160
public Query managed() {
142-
return new QueryImpl(refFactory, statement, bindVarsHandle, affectedType, versionAware, true, unsafe, exceptionTransformer);
161+
return new QueryImpl(refFactory, statement, bindVarsHandle, affectedType, versionAware, true, unsafe, defaultFetchSize, streamOnlyFetchSize, exceptionTransformer);
143162
}
144163

145164
/**
@@ -150,13 +169,23 @@ public Query managed() {
150169
*/
151170
@Override
152171
public Query unsafe() {
153-
return new QueryImpl(refFactory, statement, bindVarsHandle, affectedType, versionAware, managed, true, exceptionTransformer);
172+
return new QueryImpl(refFactory, statement, bindVarsHandle, affectedType, versionAware, managed, true, defaultFetchSize, streamOnlyFetchSize, exceptionTransformer);
173+
}
174+
175+
private QueryImpl withoutFetchSize() {
176+
return new QueryImpl(refFactory, statement, bindVarsHandle, affectedType, versionAware, managed, unsafe, 0, false, exceptionTransformer);
154177
}
155178

156179
private PreparedStatement getStatement() {
157180
return statement.apply(unsafe);
158181
}
159182

183+
private void applyFetchSize(@Nonnull PreparedStatement statement) throws SQLException {
184+
if (defaultFetchSize != 0) {
185+
statement.setFetchSize(defaultFetchSize);
186+
}
187+
}
188+
160189
protected boolean closeStatement() {
161190
return true;
162191
}
@@ -186,6 +215,7 @@ public Stream<Object[]> getResultStream() {
186215
PreparedStatement statement = getStatement();
187216
boolean close = true;
188217
try {
218+
applyFetchSize(statement);
189219
ResultSet resultSet = statement.executeQuery();
190220
try {
191221
int columnCount = resultSet.getMetaData().getColumnCount();
@@ -240,6 +270,7 @@ public <T> Stream<T> getResultStream(@Nonnull Class<T> type) {
240270
boolean close = true;
241271
try {
242272
try {
273+
applyFetchSize(statement);
243274
ResultSet resultSet = statement.executeQuery();
244275
int columnCount = resultSet.getMetaData().getColumnCount();
245276
var mapper = getObjectMapper(columnCount, type, refFactory)
@@ -289,6 +320,62 @@ public <T extends Data> Stream<Ref<T>> getRefStream(@Nonnull Class<T> type, @Non
289320
.map(pk -> pk == null ? null : interner.intern(refFactory.create(type, pk)));
290321
}
291322

323+
@Override
324+
public Object[] getSingleResult() {
325+
return streamOnlyFetchSize && defaultFetchSize != 0
326+
? withoutFetchSize().getSingleResult()
327+
: Query.super.getSingleResult();
328+
}
329+
330+
@Override
331+
public <T> T getSingleResult(@Nonnull Class<T> type) {
332+
return streamOnlyFetchSize && defaultFetchSize != 0
333+
? withoutFetchSize().getSingleResult(type)
334+
: Query.super.getSingleResult(type);
335+
}
336+
337+
@Override
338+
public Optional<Object[]> getOptionalResult() {
339+
return streamOnlyFetchSize && defaultFetchSize != 0
340+
? withoutFetchSize().getOptionalResult()
341+
: Query.super.getOptionalResult();
342+
}
343+
344+
@Override
345+
public <T> Optional<T> getOptionalResult(@Nonnull Class<T> type) {
346+
return streamOnlyFetchSize && defaultFetchSize != 0
347+
? withoutFetchSize().getOptionalResult(type)
348+
: Query.super.getOptionalResult(type);
349+
}
350+
351+
@Override
352+
public List<Object[]> getResultList() {
353+
return streamOnlyFetchSize && defaultFetchSize != 0
354+
? withoutFetchSize().getResultList()
355+
: Query.super.getResultList();
356+
}
357+
358+
@Override
359+
public <T> List<T> getResultList(@Nonnull Class<T> type) {
360+
return streamOnlyFetchSize && defaultFetchSize != 0
361+
? withoutFetchSize().getResultList(type)
362+
: Query.super.getResultList(type);
363+
}
364+
365+
@Override
366+
public <T extends Data> List<Ref<T>> getRefList(@Nonnull Class<T> type, @Nonnull Class<?> pkType) {
367+
return streamOnlyFetchSize && defaultFetchSize != 0
368+
? withoutFetchSize().getRefList(type, pkType)
369+
: Query.super.getRefList(type, pkType);
370+
}
371+
372+
@Override
373+
public long getResultCount() {
374+
return streamOnlyFetchSize && defaultFetchSize != 0
375+
? withoutFetchSize().getResultCount()
376+
: Query.super.getResultCount();
377+
}
378+
292379
protected void close(@Nonnull ResultSet resultSet, @Nonnull PreparedStatement statement) {
293380
try {
294381
try {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2024 - 2026 the original author or authors.
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+
* https://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 st.orm.core.spi;
17+
18+
import jakarta.annotation.Nonnull;
19+
import st.orm.StormConfig;
20+
import st.orm.core.template.SqlDialect;
21+
22+
/**
23+
* Test dialect provider that returns a non-zero default fetch size.
24+
* Ensures the fetch size code paths in PreparedStatementTemplateImpl and QueryImpl are exercised during tests.
25+
*/
26+
@Orderable.BeforeAny
27+
public class FetchSizeSqlDialectProviderImpl implements SqlDialectProvider {
28+
29+
@Override
30+
public SqlDialect getSqlDialect(@Nonnull StormConfig config) {
31+
return new DefaultSqlDialect(config) {
32+
@Override
33+
public String name() {
34+
return "FetchSizeTest";
35+
}
36+
37+
@Override
38+
public int defaultFetchSize() {
39+
return 100;
40+
}
41+
};
42+
}
43+
}

0 commit comments

Comments
 (0)