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

jhannes / fluent-jdbc / #198

06 Oct 2025 10:27PM UTC coverage: 91.711% (-0.5%) from 92.242%
#198

push

jhannes-test
improved ergonomics of select builders

88 of 100 new or added lines in 13 files covered. (88.0%)

2 existing lines in 1 file now uncovered.

1195 of 1303 relevant lines covered (91.71%)

0.92 hits per line

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

98.11
/src/main/java/org/fluentjdbc/DatabaseRow.java
1
package org.fluentjdbc;
2

3
import javax.annotation.CheckReturnValue;
4
import java.io.InputStream;
5
import java.io.Reader;
6
import java.math.BigDecimal;
7
import java.sql.Array;
8
import java.sql.Date;
9
import java.sql.ResultSet;
10
import java.sql.SQLException;
11
import java.sql.Timestamp;
12
import java.time.Instant;
13
import java.time.LocalDate;
14
import java.time.OffsetDateTime;
15
import java.time.ZoneId;
16
import java.time.ZonedDateTime;
17
import java.util.Arrays;
18
import java.util.List;
19
import java.util.Map;
20
import java.util.Optional;
21
import java.util.Set;
22
import java.util.UUID;
23

24
/**
25
 * Retrieves column values for a single row returned by a query, if necessary, calculating
26
 * the column position and doing necessary conversion. {@link DatabaseRow} serves as an
27
 * encapsulation of a {@link ResultSet} for a single row, with the additional support for
28
 * converting {@link Instant}s, {@link ZonedDateTime}s {@link OffsetDateTime}s, {@link LocalDate}s,
29
 * {@link UUID}s, and {@link Enum}s. {@link #getColumnIndex(String)} supports joins for most database
30
 * drivers by reading column names from {@link java.sql.ResultSetMetaData}. {@link #table(String)}
31
 * and {@link #table(DatabaseTableAlias)} returns a DatabaseRow where all offsets are relative to the
32
 * specified table.
33
 *
34
 * <h2>Example usage:</h2>
35
 * <pre>
36
 * DatabaseTable organizations = new DatabaseTableImpl("organizations");
37
 * DatabaseTable persons = new DatabaseTableImpl("persons");
38
 * DatabaseTable memberships = new DatabaseTableImpl("memberships");
39
 *
40
 * DatabaseTableAlias m = memberships.alias("m");
41
 * DatabaseTableAlias p = persons.alias("p");
42
 * DatabaseTableAlias o = organizations.alias("o");
43
 *
44
 * List&lt;Member&lt; result = m.join(m.column("person_id"), p.column("id"))
45
 *         .join(m.column("organization_id"), o.column("id"))
46
 *         .list(connection, row -&gt;s {
47
 *             Member member = new Member();
48
 *             member.setName(row.table(p).getString("name"));
49
 *             member.setBirthDate(row.table(p).getLocalDate("birthdate"));
50
 *             member.setOrganizationName(row.table(o).getString("name"));
51
 *             member.setStatus(row.table(m).getEnum(MembershipStatus.class, "name"));
52
 *             return person;
53
 *         }
54
 * </pre>
55
 */
56
@CheckReturnValue
57
public class DatabaseRow {
58

59
    private final Map<String, Integer> columnIndexes;
60
    private final Map<String, Map<String, Integer>> tableColumnIndexes;
61
    private final Map<DatabaseTableAlias, Integer> keys;
62
    protected final ResultSet rs;
63

64
    protected DatabaseRow(ResultSet rs, Map<String, Integer> columnIndexes, Map<String, Map<String, Integer>> tableColumnIndexes, Map<DatabaseTableAlias, Integer> keys) {
1✔
65
        this.rs = rs;
1✔
66
        this.columnIndexes = columnIndexes;
1✔
67
        this.tableColumnIndexes = tableColumnIndexes;
1✔
68
        this.keys = keys;
1✔
69
    }
1✔
70

71
    public Set<String> getColumnNames() {
NEW
72
        return columnIndexes.keySet();
×
73
    }
74

75
    /**
76
     * Returns the underlying database-representation for the specified column
77
     */
78
    public Object getObject(String column) throws SQLException {
79
        return rs.getObject(getColumnIndex(column));
1✔
80
    }
81

82
    /**
83
     * Returns the value of the specified column on this row as a string
84
     */
85
    public String getString(String column) throws SQLException {
86
        return rs.getString(getColumnIndex(column));
1✔
87
    }
88

89
    /**
90
     * Returns the long value of the specified column on this row. If the
91
     * column value is null, returns null (unlike {@link ResultSet#getLong(int)}
92
     *
93
     * @see #getColumnIndex
94
     */
95
    public Long getLong(String column) throws SQLException {
96
        long result = rs.getLong(getColumnIndex(column));
1✔
97
        return rs.wasNull() ? null : result;
1✔
98
    }
99

100
    /**
101
     * Returns the Integer value of the specified column on this row. If the
102
     * column value is null, returns null (unlike {@link ResultSet#getInt(int)}
103
     *
104
     * @see #getColumnIndex
105
     */
106
    public Integer getInt(String column) throws SQLException {
107
        int result = rs.getInt(getColumnIndex(column));
1✔
108
        return rs.wasNull() ? null : result;
1✔
109
    }
110

111
    /**
112
     * Returns the Double value of the specified column on this row. If the
113
     * column value is null, returns null (unlike {@link ResultSet#getDouble(int)}
114
     *
115
     * @see #getColumnIndex
116
     */
117
    public Double getDouble(String column) throws SQLException {
118
        double result = rs.getDouble(getColumnIndex(column));
1✔
119
        return !rs.wasNull() ? result : null;
1✔
120
    }
121

122
    /**
123
     * Returns the value of the specified column on this row as a boolean
124
     *
125
     * @see #getColumnIndex
126
     */
127
    public boolean getBoolean(String column) throws SQLException {
128
        return rs.getBoolean(getColumnIndex(column));
1✔
129
    }
130

131
    /**
132
     * Returns the value of the specified column on this row as a timestamp
133
     *
134
     * @see #getColumnIndex
135
     */
136
    public Timestamp getTimestamp(String column) throws SQLException {
137
        return rs.getTimestamp(getColumnIndex(column));
1✔
138
    }
139

140
    /**
141
     * Returns the value of the specified column on this row as an Instant
142
     *
143
     * @see #getColumnIndex
144
     */
145
    public Instant getInstant(String column) throws SQLException {
146
        Timestamp timestamp = getTimestamp(column);
1✔
147
        return timestamp != null ? timestamp.toInstant() : null;
1✔
148
    }
149

150
    /**
151
     * Returns the value of the specified column on this row as a ZonedDateTime
152
     *
153
     * @see #getColumnIndex
154
     */
155
    public ZonedDateTime getZonedDateTime(String fieldName) throws SQLException {
156
        Instant instant = getInstant(fieldName);
1✔
157
        return instant != null ? instant.atZone(ZoneId.systemDefault()) : null;
1✔
158
    }
159

160
    /**
161
     * Returns the value of the specified column on this row as a OffsetDateTime
162
     *
163
     * @see #getColumnIndex
164
     */
165
    public OffsetDateTime getOffsetDateTime(String fieldName) throws SQLException {
166
        ZonedDateTime dateTime = getZonedDateTime(fieldName);
1✔
167
        return dateTime != null ? dateTime.toOffsetDateTime() : null;
1✔
168
    }
169

170
    /**
171
     * Returns the value of the specified column on this row as a LocalDate
172
     *
173
     * @see #getColumnIndex
174
     */
175
    public LocalDate getLocalDate(String column) throws SQLException {
176
        Date date = rs.getDate(getColumnIndex(column));
1✔
177
        return date != null ? date.toLocalDate() : null;
1✔
178
    }
179

180
    /**
181
     * Returns the value of the specified column on this row as a String converted to {@link UUID}
182
     *
183
     * @see #getColumnIndex
184
     */
185
    public UUID getUUID(String fieldName) throws SQLException {
186
        String result = getString(fieldName);
1✔
187
        return result != null ? UUID.fromString(result) : null;
1✔
188
    }
189

190
    /**
191
     * Returns the value of the specified column on this row as a binary stream. Used with
192
     * BLOB (Binary Large Objects) and bytea (PostgreSQL) data types
193
     *
194
     * @see #getColumnIndex
195
     */
196
    public InputStream getInputStream(String fieldName) throws SQLException {
197
        return rs.getBinaryStream(getColumnIndex(fieldName));
1✔
198
    }
199

200
    /**
201
     * Returns the value of the specified column on this row as a reader. Used with
202
     * CLOB (Character Large Objects) and text (PostgreSQL) data types
203
     *
204
     * @see #getColumnIndex
205
     */
206
    public Reader getReader(String fieldName) throws SQLException {
207
        return rs.getCharacterStream(getColumnIndex(fieldName));
1✔
208
    }
209

210
    /**
211
     * Returns the value of the specified column on this row as a BigDecimal
212
     *
213
     * @see #getColumnIndex
214
     */
215
    public BigDecimal getBigDecimal(String column) throws SQLException {
216
        return rs.getBigDecimal(getColumnIndex(column));
1✔
217
    }
218

219
    /**
220
     * Returns the value of the specified column on this row as a List of Integers,
221
     * if the underlying type is Array
222
     *
223
     * @see #getColumnIndex
224
     */
225
    public List<Integer> getIntList(String columnName) throws SQLException {
226
        return toList(rs.getArray(getColumnIndex(columnName)), Integer.class);
1✔
227
    }
228

229
    /**
230
     * Returns the value of the specified column on this row as a List of String,
231
     * if the underlying type is Array
232
     *
233
     * @see #getColumnIndex
234
     */
235
    public List<String> getStringList(String columnName) throws SQLException {
236
        return toList(rs.getArray(getColumnIndex(columnName)), String.class);
1✔
237
    }
238

239
    private <T> List<T> toList(Array array, Class<T> arrayType) throws SQLException {
240
        if (array == null) {
1✔
241
            return null;
1✔
242
        }
243
        //noinspection unchecked
244
        T[] javaArray = (T[]) array.getArray();
1✔
245
        if (javaArray.length > 0 && !arrayType.isAssignableFrom(javaArray[0].getClass())) {
1✔
246
            throw new ClassCastException("Can't convert " + javaArray[0].getClass() + " to " + arrayType);
1✔
247
        }
248
        return Arrays.asList(javaArray);
1✔
249
    }
250

251
    /**
252
     * Returns the value of the specified column on this row as an Enum of the specified type.
253
     * Retrieves the column value as String and converts it to the specified enum
254
     *
255
     * @see #getColumnIndex
256
     * @throws IllegalArgumentException if the specified enum type has
257
     *         no constant with the specified name, or the specified
258
     *         class object does not represent an enum type
259
     */
260
    public <T extends Enum<T>> T getEnum(Class<T> enumClass, String fieldName) throws SQLException {
261
        String value = getString(fieldName);
1✔
262
        return value != null ? Enum.valueOf(enumClass, value) : null;
1✔
263
    }
264

265
    /**
266
     * Returns the numeric index of the specified column in the current context. If {@link #table}
267
     * has been called to specify a table or table alias in a join statement, this method can resolve
268
     * ambiguous column names
269
     *
270
     * @return the index to be used with {@link ResultSet#getObject(int)} etc
271
     * @throws IllegalArgumentException if the fieldName was not present in the ResultSet
272
     */
273
    protected Integer getColumnIndex(String fieldName) {
274
        if (!columnIndexes.containsKey(fieldName.toUpperCase())) {
1✔
275
            throw new IllegalArgumentException("Column {" + fieldName + "} is not present in " + columnIndexes.keySet());
1✔
276
        }
277
        return columnIndexes.get(fieldName.toUpperCase());
1✔
278
    }
279

280
    /**
281
     * Extracts a {@link DatabaseRow} for the specified {@link DatabaseTableAlias} and maps it over the
282
     * {@link DatabaseResult.RowMapper} function to return an object mapped from the
283
     * part of the {@link DatabaseRow} belonging to the specified alias. If the alias was the result
284
     * of an outer join that didn't return data, this method will instead return {@link Optional#empty()}
285
     *
286
     * @see DatabaseJoinedQueryBuilder
287
     */
288
    public <T> Optional<T> table(DatabaseTableAlias alias, DatabaseResult.RowMapper<T> function) throws SQLException {
289
        DatabaseRow table = table(alias);
1✔
290
        return table != null ? Optional.of(function.mapRow(table)) : Optional.empty();
1✔
291
    }
292

293
    /**
294
     * Extracts a {@link DatabaseRow} for the specified {@link DatabaseTableAlias} belonging to the
295
     * specified alias. If the alias was the result of an outer join that didn't return data, this
296
     * method will instead return <code>null</code>
297
     *
298
     * @see DatabaseJoinedQueryBuilder
299
     * @return A {@link DatabaseRow} associated with the specified alias, or null if the alias was
300
     *          part of an outer join that didn't return data
301
     */
302
    public DatabaseRow table(DatabaseTableAlias alias) throws SQLException {
303
        if (keys.containsKey(alias) && rs.getObject(keys.get(alias)) == null) {
1✔
304
            return null;
1✔
305
        }
306
        return table(alias.getAlias());
1✔
307
    }
308

309
    /**
310
     * Extracts a {@link DatabaseRow} for the specified {@link DatabaseTableAlias} belonging to the
311
     * specified alias. If the alias was the result of an outer join that didn't return data, this
312
     * method will instead return <code>null</code>
313
     *
314
     * @see DatabaseJoinedQueryBuilder
315
     * @return A {@link DatabaseRow} associated with the specified alias, or null if the alias was
316
     *          part of an outer join that didn't return data
317
     */
318
    public DatabaseRow table(DbContextTableAlias alias) throws SQLException {
319
        return table(alias.getTableAlias());
1✔
320
    }
321

322
    /**
323
     * Extracts a {@link DatabaseRow} for the specified {@link DatabaseTableAlias} belonging to the
324
     * specified alias.
325
     *
326
     * @see DatabaseJoinedQueryBuilder
327
     * @throws IllegalArgumentException if the specified table wasn't part of the <code>SELECT ... FROM ...</code>
328
     * clause
329
     */
330
    public DatabaseRow table(String table) {
331
        Map<String, Integer> columnIndexes = tableColumnIndexes.get(table.toUpperCase());
1✔
332
        if (columnIndexes == null) {
1✔
333
            throw new IllegalArgumentException("Unknown table " + table.toUpperCase() + " in " + tableColumnIndexes.keySet());
1✔
334
        }
335
        return new DatabaseRow(rs, tableColumnIndexes.get(table.toUpperCase()), tableColumnIndexes, this.keys);
1✔
336
    }
337
}
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

© 2025 Coveralls, Inc