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

jhannes / fluent-jdbc / #178

09 Aug 2024 12:30PM UTC coverage: 92.992% (-1.2%) from 94.146%
#178

push

jhannes
added DatabaseQueryBuilder.singleInt and fixed DatabaseQueryBuilder.singleLong

1 of 1 new or added line in 1 file covered. (100.0%)

24 existing lines in 3 files now uncovered.

1181 of 1270 relevant lines covered (92.99%)

0.93 hits per line

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

92.44
/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

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

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

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

45

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

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

57
    /**
58
     * sets all parameters on the statement, calling {@link #bindParameter(PreparedStatement, int, Object)} to
59
     * convert each one
60
     */
61
    public static int bindParameters(PreparedStatement stmt, Collection<?> parameters) throws SQLException {
62
        return bindParameters(stmt, parameters, 1);
1✔
63
    }
64

65
    /**
66
     * sets all parameters on the statement, calling {@link #bindParameter(PreparedStatement, int, Object)} to
67
     * convert each one
68
     */
69
    public static int bindParameters(PreparedStatement stmt, Collection<?> parameters, int start) throws SQLException {
70
        int index = start;
1✔
71
        for (Object parameter : parameters) {
1✔
72
            bindParameter(stmt, index++, parameter);
1✔
73
        }
1✔
74
        return index;
1✔
75
    }
76

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

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

149
    /**
150
     * Binds the parameters and calls {@link PreparedStatement#addBatch()}.
151
     *
152
     * @see #bindParameter(PreparedStatement, int, Object)
153
     */
154
    public static <T> void addBatch(PreparedStatement statement, Iterable<T> objects, Collection<Function<T, ?>> columnValueExtractors) throws SQLException {
155
        for (T object : objects) {
1✔
156
            int columnIndex = 1;
1✔
157
            for (Function<T, ?> f : columnValueExtractors) {
1✔
158
                bindParameter(statement, columnIndex++, f.apply(object));
1✔
159
            }
1✔
160
            statement.addBatch();
1✔
161
        }
1✔
162
    }
1✔
163

164
    /**
165
     * Returns true if the database connection is to SQL server
166
     */
167
    private static boolean isSqlServer(Connection connection) {
168
        return connection.getClass().getName().startsWith("net.sourceforge.jtds.jdbc") ||
1✔
169
               connection.getClass().getName().startsWith("com.microsoft.sqlserver.jdbc");
1✔
170
    }
171

172
    /**
173
     * Returns true if the database connection is to Oracle
174
     */
175
    private static boolean isOracle(Connection connection) {
176
        return connection.getClass().getName().startsWith("oracle.jdbc");
1✔
177
    }
178

179
    /**
180
     * Calls {@link Connection#prepareStatement(String)} with the statement,
181
     * {@link #bindParameters(PreparedStatement, Collection)}, converting each parameter in the process
182
     * and executes the statement
183
     */
184
    public int executeUpdate(Connection connection) {
185
        return execute(connection, PreparedStatement::executeUpdate);
1✔
186
    }
187

188
    /**
189
     * Create a string like <code>?, ?, ?</code> with the parameterCount number of '?'
190
     */
191
    public static String parameterString(int parameterCount) {
192
        StringBuilder parameterString = new StringBuilder("?");
1✔
193
        for (int i = 1; i < parameterCount; i++) {
1✔
194
            parameterString.append(", ?");
1✔
195
        }
196
        return parameterString.toString();
1✔
197
    }
198

199
    /**
200
     * Returns true if the object value equals the specified field name in the database. Converts
201
     * {@link #toDatabaseType(Object, Connection)} to decrease number of false positives
202
     */
203
    public static boolean dbValuesAreEqual(Object value, DatabaseRow row, String field, Connection connection) throws SQLException {
204
        Object canonicalValue = toDatabaseType(value, connection);
1✔
205
        Object dbValue;
206
        if (canonicalValue instanceof Timestamp) {
1✔
207
            dbValue = row.getTimestamp(field);
1✔
208
        } else if (canonicalValue instanceof Integer) {
1✔
209
            dbValue = row.getInt(field);
1✔
210
        } else {
211
            dbValue = row.getObject(field);
1✔
212
        }
213
        return Objects.equals(canonicalValue, toDatabaseType(dbValue, connection));
1✔
214
    }
215

216
    /**
217
     * If the query returns no rows, returns {@link SingleRow#absent}, if exactly one row is returned, maps it and return it,
218
     * if more than one is returned, throws `IllegalStateException`
219
     *
220
     * @param connection Database connection
221
     * @param mapper     Function object to map a single returned row to a object
222
     * @return the mapped row if one row is returned, {@link SingleRow#absent} otherwise
223
     * @throws MultipleRowsReturnedException if more than one row was matched the query
224
     */
225
    public <T> SingleRow<T> singleObject(Connection connection, DatabaseResult.RowMapper<T> mapper) {
226
        return query(connection, result -> result.single(
1✔
227
                mapper,
UNCOV
228
                () -> new NoRowsReturnedException(statement, parameters),
×
229
                () -> new MultipleRowsReturnedException(statement, parameters)
1✔
230
        ));
231
    }
232

233
    /**
234
     * Execute the query and map each return value over the {@link DatabaseResult.RowMapper} function to return a list. Example:
235
     * <pre>
236
     *     List&lt;Instant&gt; creationTimes = table.where("status", status).list(row -&gt; row.getInstant("created_at"))
237
     * </pre>
238
     */
239
    public <OBJECT> List<OBJECT> list(Connection connection, DatabaseResult.RowMapper<OBJECT> mapper) {
UNCOV
240
        return stream(connection, mapper).collect(Collectors.toList());
×
241
    }
242

243

244
    /**
245
     * Executes the <code>SELECT * FROM ...</code> statement and calls back to
246
     * {@link DatabaseResult.RowConsumer} for each returned row
247
     */
248
    public void forEach(Connection connection, DatabaseResult.RowConsumer consumer) {
249
        query(connection, result -> {
1✔
250
            result.forEach(consumer);
1✔
251
            return null;
1✔
252
        });
253
    }
1✔
254

255

256
    /**
257
     * Execute the query and map each return value over the {@link DatabaseResult.RowMapper} function to return a stream. Example:
258
     * <pre>
259
     *     table.where("status", status).stream(row -&gt; row.getInstant("created_at"))
260
     * </pre>
261
     */
262
    public <OBJECT> Stream<OBJECT> stream(Connection connection, DatabaseResult.RowMapper<OBJECT> mapper) {
263
        long startTime = System.currentTimeMillis();
1✔
264
        try {
265
            logger.trace(statement);
1✔
266
            PreparedStatement stmt = connection.prepareStatement(statement);
1✔
267
            bindParameters(stmt, parameters);
1✔
268
            DatabaseResult result = new DatabaseResult(stmt, stmt.executeQuery());
1✔
269
            return result.stream(mapper, statement);
1✔
270
        } catch (SQLException e) {
1✔
UNCOV
271
            throw ExceptionUtil.softenCheckedException(e);
×
272
        } finally {
273
            reporter.reportQuery(statement, System.currentTimeMillis() - startTime);
1✔
274
        }
275
    }
276

277
    /**
278
     * Calls {@link Connection#prepareStatement(String)} with the statement,
279
     * {@link #bindParameters(PreparedStatement, Collection)}, converting each parameter in the process
280
     * and executes the argument function with the statement
281
     */
282
    public <T> T execute(Connection connection, PreparedStatementFunction<T> f) {
283
        long startTime = System.currentTimeMillis();
1✔
284
        logger.trace(statement);
1✔
285
        try (PreparedStatement stmt = connection.prepareStatement(statement)) {
1✔
286
            bindParameters(stmt, parameters);
1✔
287
            return f.apply(stmt);
1✔
288
        } catch (SQLException e) {
1✔
UNCOV
289
            throw ExceptionUtil.softenCheckedException(e);
×
290
        } finally {
291
            reporter.reportQuery(statement, System.currentTimeMillis() - startTime);
1✔
292
        }
293
    }
294

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

313
    public <T> T query(Connection connection, DatabaseResult.DatabaseResultMapper<T> resultMapper) {
314
        return execute(connection, stmt -> {
1✔
315
            try (DatabaseResult result = new DatabaseResult(stmt, stmt.executeQuery())) {
1✔
316
                return resultMapper.apply(result);
1✔
317
            }
318
        });
319
    }
320

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