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

jhannes / fluent-jdbc / #207

10 Feb 2026 01:24PM UTC coverage: 91.533% (-0.06%) from 91.59%
#207

push

jhannes-test
[maven-release-plugin] prepare release fluent-jdbc-0.7.0

1200 of 1311 relevant lines covered (91.53%)

0.92 hits per line

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

90.48
/src/main/java/org/fluentjdbc/DatabaseStatement.java
1
package org.fluentjdbc;
2

3
import org.fluentjdbc.util.ExceptionUtil;
4
import org.slf4j.Logger;
5
import org.slf4j.LoggerFactory;
6
import org.slf4j.MDC;
7

8
import javax.annotation.Nullable;
9
import javax.annotation.ParametersAreNonnullByDefault;
10
import java.io.InputStream;
11
import java.io.Reader;
12
import java.math.BigDecimal;
13
import java.sql.Connection;
14
import java.sql.Date;
15
import java.sql.PreparedStatement;
16
import java.sql.SQLException;
17
import java.sql.Timestamp;
18
import java.time.Instant;
19
import java.time.LocalDate;
20
import java.time.OffsetDateTime;
21
import java.time.ZonedDateTime;
22
import java.util.Collection;
23
import java.util.List;
24
import java.util.Objects;
25
import java.util.UUID;
26
import java.util.function.Function;
27
import java.util.stream.Collectors;
28
import java.util.stream.Stream;
29

30
/**
31
 * Allows the execution of arbitrary SQL statements with parameters and returns the ResultSet.
32
 * Will convert parameters to the statement using {@link #bindParameter(PreparedStatement, int, Object)},
33
 * which supports many more data types than JDBC supports natively. Returns the result via
34
 * {@link #list(Connection, DatabaseResult.RowMapper)}, {@link #singleObject(Connection, DatabaseResult.RowMapper)}
35
 * and {@link #stream(Connection, DatabaseResult.RowMapper)}, which uses {@link DatabaseRow} to convert
36
 * ResultSet types.
37
 */
38
@ParametersAreNonnullByDefault
39
public class DatabaseStatement {
40

41
    @FunctionalInterface
42
    public interface PreparedStatementFunction<T> {
43
        T apply(PreparedStatement stmt) throws SQLException;
44
    }
45

46

47
    protected static final Logger logger = LoggerFactory.getLogger(DatabaseStatement.class);
1✔
48
    private final String tableName;
49
    private final String statement;
50
    private final Collection<?> parameters;
51
    private final DatabaseTableOperationReporter reporter;
52

53
    public DatabaseStatement(String tableName, String statement, Collection<?> parameters, DatabaseTableOperationReporter reporter) {
1✔
54
        this.tableName = tableName;
1✔
55
        this.statement = statement;
1✔
56
        this.parameters = parameters;
1✔
57
        this.reporter = reporter;
1✔
58
    }
1✔
59

60
    public String getStatement() {
61
        return statement;
×
62
    }
63

64
    /**
65
     * sets all parameters on the statement, calling {@link #bindParameter(PreparedStatement, int, Object)} to
66
     * convert each one
67
     */
68
    public static int bindParameters(PreparedStatement stmt, Collection<?> parameters) throws SQLException {
69
        return bindParameters(stmt, parameters, 1);
1✔
70
    }
71

72
    /**
73
     * sets all parameters on the statement, calling {@link #bindParameter(PreparedStatement, int, Object)} to
74
     * convert each one
75
     */
76
    public static int bindParameters(PreparedStatement stmt, Collection<?> parameters, int start) throws SQLException {
77
        int index = start;
1✔
78
        for (Object parameter : parameters) {
1✔
79
            bindParameter(stmt, index++, parameter);
1✔
80
        }
1✔
81
        return index;
1✔
82
    }
83

84
    /**
85
     * Calls the correct {@link PreparedStatement} <code>setXXX</code> method based on the type of the parameter.
86
     * Supports {@link Instant}, {@link ZonedDateTime}, {@link OffsetDateTime}, {@link LocalDate}, {@link String},
87
     * {@link List} of String or Integer, {@link Enum}, {@link UUID}, {@link Double}
88
     */
89
    public static void bindParameter(PreparedStatement stmt, int index, @Nullable Object parameter) throws SQLException {
90
        if (parameter instanceof Instant) {
1✔
91
            stmt.setTimestamp(index, (Timestamp) toDatabaseType(parameter, stmt.getConnection()));
1✔
92
        } else if (parameter instanceof ZonedDateTime) {
1✔
93
            stmt.setTimestamp(index, (Timestamp) toDatabaseType(parameter, stmt.getConnection()));
1✔
94
        } else if (parameter instanceof OffsetDateTime) {
1✔
95
            stmt.setTimestamp(index, (Timestamp) toDatabaseType(parameter, stmt.getConnection()));
1✔
96
        } else if (parameter instanceof LocalDate) {
1✔
97
            stmt.setDate(index, (Date) toDatabaseType(parameter, stmt.getConnection()));
1✔
98
        } else if (parameter instanceof CharSequence) {
1✔
99
            stmt.setString(index, (String) toDatabaseType(parameter, stmt.getConnection()));
1✔
100
        } else if (parameter instanceof Character) {
1✔
101
            stmt.setString(index, String.valueOf(parameter));
×
102
        } else if (parameter instanceof Enum<?>) {
1✔
103
            stmt.setString(index, (String) toDatabaseType(parameter, stmt.getConnection()));
1✔
104
        } else if (parameter instanceof Collection<?>) {
1✔
105
            //noinspection rawtypes
106
            Object[] elements = ((Collection) parameter).toArray();
1✔
107
            if (elements.length == 0) {
1✔
108
                stmt.setArray(index, stmt.getConnection().createArrayOf(null, elements));
1✔
109
            } else if (elements[0] instanceof Integer) {
1✔
110
                stmt.setArray(index, stmt.getConnection().createArrayOf("integer", elements));
1✔
111
            } else if (elements[0] instanceof String) {
1✔
112
                stmt.setArray(index, stmt.getConnection().createArrayOf("varchar", elements));
1✔
113
            } else {
114
                throw new IllegalArgumentException("Not supported: Arrays of " + elements[0].getClass());
×
115
            }
116
        } else if (parameter instanceof InputStream) {
1✔
117
            stmt.setBinaryStream(index, ((InputStream) parameter));
1✔
118
        } else if (parameter instanceof Reader) {
1✔
119
            stmt.setCharacterStream(index, ((Reader) parameter));
1✔
120
        } else {
121
            stmt.setObject(index, toDatabaseType(parameter, stmt.getConnection()));
1✔
122
        }
123
    }
1✔
124

125
    /**
126
     * Converts parameter to canonical database type.
127
     * Supports {@link Instant}, {@link ZonedDateTime}, {@link OffsetDateTime}, {@link LocalDate}, {@link String},
128
     * {@link Enum}, {@link UUID}, {@link Double}
129
     */
130
    public static Object toDatabaseType(@Nullable Object parameter, Connection connection) {
131
        if (parameter instanceof Instant) {
1✔
132
            return Timestamp.from((Instant) parameter);
1✔
133
        } else if (parameter instanceof ZonedDateTime) {
1✔
134
            return Timestamp.from(Instant.from((ZonedDateTime) parameter));
1✔
135
        } else if (parameter instanceof OffsetDateTime) {
1✔
136
            return Timestamp.from(Instant.from((OffsetDateTime) parameter));
1✔
137
        } else if (parameter instanceof LocalDate) {
1✔
138
            return Date.valueOf((LocalDate) parameter);
1✔
139
        } else if (parameter instanceof UUID) {
1✔
140
            if (isSqlServer(connection)) {
1✔
141
                return parameter.toString().toUpperCase();
×
142
            } else if (isOracle(connection)) {
1✔
143
                return parameter.toString();
×
144
            } else {
145
                return parameter;
1✔
146
            }
147
        } else if (parameter instanceof Double) {
1✔
148
            return BigDecimal.valueOf(((Number) parameter).doubleValue());
1✔
149
        } else if (parameter instanceof CharSequence) {
1✔
150
            return parameter.toString();
1✔
151
        } else if (parameter instanceof Enum<?>) {
1✔
152
            return parameter.toString();
1✔
153
        } else {
154
            return parameter;
1✔
155
        }
156
    }
157

158
    /**
159
     * Binds the parameters and calls {@link PreparedStatement#addBatch()}.
160
     *
161
     * @see #bindParameter(PreparedStatement, int, Object)
162
     */
163
    public static <T> void addBatch(PreparedStatement statement, Iterable<T> objects, Collection<Function<T, ?>> columnValueExtractors) throws SQLException {
164
        for (T object : objects) {
1✔
165
            int columnIndex = 1;
1✔
166
            for (Function<T, ?> f : columnValueExtractors) {
1✔
167
                bindParameter(statement, columnIndex++, f.apply(object));
1✔
168
            }
1✔
169
            statement.addBatch();
1✔
170
        }
1✔
171
    }
1✔
172

173
    /**
174
     * Returns true if the database connection is to SQL server
175
     */
176
    private static boolean isSqlServer(Connection connection) {
177
        return connection.getClass().getName().startsWith("net.sourceforge.jtds.jdbc") ||
1✔
178
               connection.getClass().getName().startsWith("com.microsoft.sqlserver.jdbc");
1✔
179
    }
180

181
    /**
182
     * Returns true if the database connection is to Oracle
183
     */
184
    private static boolean isOracle(Connection connection) {
185
        return connection.getClass().getName().startsWith("oracle.jdbc");
1✔
186
    }
187

188
    /**
189
     * Calls {@link Connection#prepareStatement(String)} with the statement,
190
     * {@link #bindParameters(PreparedStatement, Collection)}, converting each parameter in the process
191
     * and executes the statement
192
     */
193
    public int executeUpdate(Connection connection) {
194
        return execute(connection, PreparedStatement::executeUpdate);
1✔
195
    }
196

197
    /**
198
     * Create a string like <code>?, ?, ?</code> with the parameterCount number of '?'
199
     */
200
    public static String parameterString(int parameterCount) {
201
        StringBuilder parameterString = new StringBuilder("?");
1✔
202
        for (int i = 1; i < parameterCount; i++) {
1✔
203
            parameterString.append(", ?");
1✔
204
        }
205
        return parameterString.toString();
1✔
206
    }
207

208
    /**
209
     * Returns true if the object value equals the specified field name in the database. Converts
210
     * {@link #toDatabaseType(Object, Connection)} to decrease number of false positives
211
     */
212
    public static boolean dbValuesAreEqual(Object value, DatabaseRow row, String field, Connection connection) throws SQLException {
213
        Object canonicalValue = toDatabaseType(value, connection);
1✔
214
        Object dbValue;
215
        if (canonicalValue instanceof Timestamp) {
1✔
216
            dbValue = row.getTimestamp(field);
1✔
217
        } else if (canonicalValue instanceof Integer) {
1✔
218
            dbValue = row.getInt(field);
1✔
219
        } else {
220
            dbValue = row.getObject(field);
1✔
221
        }
222
        return Objects.equals(canonicalValue, toDatabaseType(dbValue, connection));
1✔
223
    }
224

225
    /**
226
     * If the query returns no rows, returns {@link SingleRow#absent}, if exactly one row is returned, maps it and return it,
227
     * if more than one is returned, throws `IllegalStateException`
228
     *
229
     * @param connection Database connection
230
     * @param mapper     Function object to map a single returned row to a object
231
     * @return the mapped row if one row is returned, {@link SingleRow#absent} otherwise
232
     * @throws MultipleRowsReturnedException if more than one row was matched the query
233
     */
234
    public <T> SingleRow<T> singleObject(Connection connection, DatabaseResult.RowMapper<T> mapper) {
235
        return query(connection, result -> result.single(
1✔
236
                mapper,
237
                () -> new NoRowsReturnedException(statement, parameters),
×
238
                () -> new MultipleRowsReturnedException(statement, parameters)
1✔
239
        ));
240
    }
241

242
    /**
243
     * Execute the query and map each return value over the {@link DatabaseResult.RowMapper} function to return a list. Example:
244
     * <pre>
245
     *     List&lt;Instant&gt; creationTimes = table.where("status", status).list(row -&gt; row.getInstant("created_at"))
246
     * </pre>
247
     */
248
    public <OBJECT> List<OBJECT> list(Connection connection, DatabaseResult.RowMapper<OBJECT> mapper) {
249
        return stream(connection, mapper).collect(Collectors.toList());
×
250
    }
251

252

253
    /**
254
     * Executes the <code>SELECT * FROM ...</code> statement and calls back to
255
     * {@link DatabaseResult.RowConsumer} for each returned row
256
     */
257
    public void forEach(Connection connection, DatabaseResult.RowConsumer consumer) {
258
        query(connection, result -> {
1✔
259
            result.forEach(consumer);
1✔
260
            return null;
1✔
261
        });
262
    }
1✔
263

264

265
    /**
266
     * Execute the query and map each return value over the {@link DatabaseResult.RowMapper} function to return a stream. Example:
267
     * <pre>
268
     *     table.where("status", status).stream(row -&gt; row.getInstant("created_at"))
269
     * </pre>
270
     */
271
    public <OBJECT> Stream<OBJECT> stream(Connection connection, DatabaseResult.RowMapper<OBJECT> mapper) {
272
        long startTime = System.currentTimeMillis();
1✔
273
        try {
274
            PreparedStatement stmt = prepareStatement(connection);
1✔
275
            DatabaseResult result = new DatabaseResult(stmt);
1✔
276
            return result.stream(mapper, statement);
1✔
277
        } catch (SQLException e) {
1✔
278
            MDC.put("fluentjdbc.tablename", tableName);
1✔
279
            throw ExceptionUtil.softenCheckedException(e);
×
280
        } finally {
281
            reporter.reportQuery(this, System.currentTimeMillis() - startTime);
1✔
282
        }
283
    }
284

285
    /**
286
     * Calls {@link Connection#prepareStatement(String)} with the statement and
287
     * {@link #bindParameters(PreparedStatement, Collection)}
288
     */
289
    public PreparedStatement prepareStatement(Connection connection) throws SQLException {
290
        logger.trace(statement);
1✔
291
        PreparedStatement stmt = connection.prepareStatement(statement);
1✔
292
        bindParameters(stmt, parameters);
1✔
293
        return stmt;
1✔
294
    }
295

296
    /**
297
     * Calls {@link Connection#prepareStatement(String)} with the statement,
298
     * {@link #bindParameters(PreparedStatement, Collection)}, converting each parameter in the process
299
     * and executes the argument function with the statement
300
     */
301
    public <T> T execute(Connection connection, PreparedStatementFunction<T> f) {
302
        long startTime = System.currentTimeMillis();
1✔
303
        try (PreparedStatement stmt = prepareStatement(connection)) {
1✔
304
            return f.apply(stmt);
1✔
305
        } catch (SQLException e) {
1✔
306
            MDC.put("fluentjdbc.tablename", tableName);
1✔
307
            throw ExceptionUtil.softenCheckedException(e);
×
308
        } finally {
309
            reporter.reportQuery(this, System.currentTimeMillis() - startTime);
1✔
310
        }
311
    }
312

313
    /**
314
     * Calls {@link Connection#prepareStatement(String, String[])} with the statement and columnNames,
315
     * {@link #bindParameters(PreparedStatement, Collection)}, converting each parameter in the process
316
     * and executes the argument function with the statement
317
     */
318
    public <T> T execute(Connection connection, PreparedStatementFunction<T> f, String[] columnNames) {
319
        long startTime = System.currentTimeMillis();
1✔
320
        logger.trace(statement);
1✔
321
        try (PreparedStatement stmt = connection.prepareStatement(statement, columnNames)) {
1✔
322
            bindParameters(stmt, parameters);
1✔
323
            return f.apply(stmt);
1✔
324
        } catch (SQLException e) {
×
325
            MDC.put("fluentjdbc.tablename", tableName);
×
326
            throw ExceptionUtil.softenCheckedException(e);
×
327
        } finally {
328
            reporter.reportQuery(this, System.currentTimeMillis() - startTime);
1✔
329
        }
330
    }
331

332
    public <T> T query(Connection connection, DatabaseResult.DatabaseResultMapper<T> resultMapper) {
333
        return execute(connection, stmt -> {
1✔
334
            try (DatabaseResult result = new DatabaseResult(stmt)) {
1✔
335
                return resultMapper.apply(result);
1✔
336
            }
337
        });
338
    }
339

340
}
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