• 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

93.85
/src/main/java/org/fluentjdbc/DatabaseResult.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.CheckReturnValue;
8
import javax.annotation.Nonnull;
9
import javax.annotation.ParametersAreNonnullByDefault;
10
import java.sql.Connection;
11
import java.sql.PreparedStatement;
12
import java.sql.ResultSet;
13
import java.sql.ResultSetMetaData;
14
import java.sql.SQLException;
15
import java.util.ArrayList;
16
import java.util.HashMap;
17
import java.util.List;
18
import java.util.Map;
19
import java.util.Set;
20
import java.util.Spliterators;
21
import java.util.function.Supplier;
22
import java.util.stream.Stream;
23
import java.util.stream.StreamSupport;
24

25
/**
26
 * Collects together a {@link PreparedStatement} and the resulting {@link ResultSet},
27
 * as well as metaData to map column names to collect indexes from {@link ResultSetMetaData}.
28
 * Use {@link #list(RowMapper)}, {@link #forEach(RowConsumer)} and {@link #single}
29
 * to process the {@link ResultSet}
30
 */
31
@ParametersAreNonnullByDefault
32
public class DatabaseResult implements AutoCloseable {
33

34
    private final static Logger logger = LoggerFactory.getLogger(DatabaseResult.class);
1✔
35

36
    /**
37
     * Used to execute statements on the whole DatabaseResult. Like
38
     * {@link java.util.function.Function}, but allows {@link SQLException} to be
39
     * thrown from {@link #apply(DatabaseResult)}
40
     */
41
    @FunctionalInterface
42
    public interface DatabaseResultMapper<T> {
43

44
        @CheckReturnValue
45
        T apply(DatabaseResult result) throws SQLException;
46
    }
47

48
    /**
49
     * Functional interface for {@link #single(RowMapper, Supplier, Supplier)} and {@link #list(RowMapper)}.
50
     * Like {@link java.util.function.Function}, but allows {@link SQLException} to be
51
     * thrown from {@link #mapRow(DatabaseRow)}
52
     */
53
    @FunctionalInterface
54
    public interface RowMapper<T> {
55

56
        @CheckReturnValue
57
        T mapRow(DatabaseRow row) throws SQLException;
58
    }
59

60
    /**
61
     * Functional interface for {@link #forEach}. Like {@link java.util.function.Consumer},
62
     * but allows {@link SQLException} to be thrown from {@link #apply}
63
     */
64
    @FunctionalInterface
65
    public interface RowConsumer {
66
        void apply(DatabaseRow row) throws SQLException;
67
    }
68

69
    private final PreparedStatement statement;
70
    protected final ResultSet resultSet;
71
    protected final Map<String, Integer> columnIndexes;
72
    protected final Map<String, Map<String, Integer>> tableColumnIndexes;
73
    private final Map<DatabaseTableAlias, Integer> keys;
74

75
    DatabaseResult(PreparedStatement statement, ResultSet resultSet, Map<String, Integer> columnIndexes, Map<String, Map<String, Integer>> aliasColumnIndexes, Map<DatabaseTableAlias, Integer> keys) {
1✔
76
        this.statement = statement;
1✔
77
        this.resultSet = resultSet;
1✔
78
        this.columnIndexes = columnIndexes;
1✔
79
        this.tableColumnIndexes = aliasColumnIndexes;
1✔
80
        this.keys = keys;
1✔
81
    }
1✔
82

83
    public DatabaseResult(PreparedStatement statement) throws SQLException {
84
        this(statement, statement.executeQuery());
1✔
85
    }
1✔
86

87
    public DatabaseResult(PreparedStatement statement, ResultSet resultSet) throws SQLException {
88
        this(statement, resultSet, new HashMap<>(), new HashMap<>(), new HashMap<>());
1✔
89
        ResultSetMetaData metaData = resultSet.getMetaData();
1✔
90
        for (int i = 1; i <= metaData.getColumnCount(); i++) {
1✔
91
            String columnName = metaData.getColumnName(i).toUpperCase();
1✔
92
            String tableName = metaData.getTableName(i).toUpperCase();
1✔
93
            if (!tableName.isEmpty()) {
1✔
94
                if (!tableColumnIndexes.containsKey(tableName)) {
1✔
95
                    tableColumnIndexes.put(tableName, new HashMap<>());
1✔
96
                }
97
                if (tableColumnIndexes.get(tableName).containsKey(columnName)) {
1✔
98
                    logger.warn("Duplicate column {}.{} in query result", tableName, columnName);
×
99
                } else {
100
                    tableColumnIndexes.get(tableName).put(columnName, i);
1✔
101
                }
102
            }
103

104
            if (!columnIndexes.containsKey(columnName)) {
1✔
105
                columnIndexes.put(columnName, i);
1✔
106
            } else {
107
                logger.debug("Duplicate column {} in query result", columnName);
1✔
108
            }
109
        }
110
    }
1✔
111

112
    @Override
113
    public void close() throws SQLException {
114
        resultSet.close();
1✔
115
    }
1✔
116

117
    /**
118
     * Position the underlying {@link ResultSet} on the next row
119
     */
120
    @CheckReturnValue
121
    public boolean next() throws SQLException {
122
        return resultSet.next();
1✔
123
    }
124

125
    /**
126
     * Call mapper for each row in the {@link ResultSet} and return the result as
127
     * a List. If {@link RowMapper} throws {@link SQLException}, processing is aborted
128
     * and the exception is rethrown
129
     *
130
     * @param mapper Function to be called for each row
131
     */
132
    @CheckReturnValue
133
    public <T> List<T> list(RowMapper<T> mapper) throws SQLException {
134
        List<T> result = new ArrayList<>();
1✔
135
        while (next()) {
1✔
136
            result.add(mapper.mapRow(row()));
1✔
137
        }
138
        return result;
1✔
139
    }
140

141
    /**
142
     * Returns a {@link Stream} which iterates over all rows in the {@link ResultSet} and apply a
143
     * {@link RowMapper} to each.
144
     * 
145
     * @see DatabaseTableQueryBuilder#stream(Connection, RowMapper)
146
     *
147
     * @param mapper Function to be called for each row
148
     * @param query The SQL that was used to generate this {@link DatabaseResult}. Used for logging
149
     */
150
    @CheckReturnValue
151
    public <T> Stream<T> stream(RowMapper<T> mapper, String query) throws SQLException {
152
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator(mapper, query), 0), false);
1✔
153
    }
154

155
    /**
156
     * Returns an {@link Iterator} which iterates over all rows in the {@link ResultSet} and apply a
157
     * {@link RowMapper} to each.
158
     *
159
     * @param mapper Function to be called for each row
160
     * @param query The SQL that was used to generate this {@link DatabaseResult}. Used for logging
161
     */
162
    @CheckReturnValue
163
    public <T> Iterator<T> iterator(RowMapper<T> mapper, String query) throws SQLException {
164
        return new Iterator<>(mapper, query);
1✔
165
    }
166

167
    /**
168
     * Call {@link RowConsumer} for each row in the {@link ResultSet}.
169
     * If {@link RowMapper} throws {@link SQLException}, processing is aborted and the exception is
170
     * rethrown
171
     */
172
    public void forEach(RowConsumer consumer) throws SQLException {
173
        while (next()) {
1✔
174
            consumer.apply(row());
1✔
175
        }
176
    }
1✔
177

178
    /**
179
     * Call {@link RowConsumer} for the first row in the {@link ResultSet}. Should only be used
180
     * where the query parameter ensure that no more than one row can be returned, ie the query
181
     * should include a unique key.
182
     *
183
     * @return the mapped row. If no rows were returned, returns {@link SingleRow#absent}
184
     * @throws MultipleRowsReturnedException if more than one row was returned
185
     * @throws SQLException if the {@link RowMapper} throws
186
     */
187
    @Nonnull
188
    @CheckReturnValue
189
    <T> SingleRow<T> single(RowMapper<T> mapper, Supplier<RuntimeException> noMatchException, Supplier<MultipleRowsReturnedException> multipleRowsException) throws SQLException {
190
        if (!next()) {
1✔
191
            return SingleRow.absent(noMatchException);
1✔
192
        }
193
        T result = mapper.mapRow(row());
1✔
194
        if (next()) {
1✔
195
            throw multipleRowsException.get();
1✔
196
        }
197
        return SingleRow.of(result);
1✔
198
    }
199

200
    /**
201
     * Returns a {@link DatabaseRow} for the current row, allowing mapping retrieval and conversion
202
     * of data in all columns
203
     */
204
    @CheckReturnValue
205
    public DatabaseRow row() {
206
        return new DatabaseRow(this.resultSet, this.columnIndexes, this.tableColumnIndexes, this.keys);
1✔
207
    }
208

209
    public Set<String> getColumnNames() {
NEW
210
        return columnIndexes.keySet();
×
211
    }
212

213
    private class Iterator<T> implements java.util.Iterator<T> {
214
        private final RowMapper<T> mapper;
215
        private final long startTime;
216
        private final String query;
217
        private boolean hasNext;
218

219
        public Iterator(RowMapper<T> mapper, String query) throws SQLException {
1✔
220
            this.mapper = mapper;
1✔
221
            this.startTime = System.currentTimeMillis();
1✔
222
            this.query = query;
1✔
223
            hasNext = resultSet.next();
1✔
224
        }
1✔
225

226
        @Override
227
        public boolean hasNext() {
228
            return hasNext;
1✔
229
        }
230

231
        @Override
232
        public T next() {
233
            try {
234
                T o = mapper.mapRow(row());
1✔
235
                hasNext = resultSet.next();
1✔
236
                if (!hasNext) {
1✔
237
                    logger.debug("time={}s query=\"{}\"", (System.currentTimeMillis()- startTime)/1000.0, query);
1✔
238
                    close();
1✔
239
                }
240
                return o;
1✔
241
            } catch (SQLException e) {
×
242
                throw ExceptionUtil.softenCheckedException(e);
×
243
            }
244
        }
245

246
        protected void close() throws SQLException {
247
            resultSet.close();
1✔
248
            statement.close();
1✔
249
        }
1✔
250

251
        @SuppressWarnings("deprecation")
252
        @Override
253
        protected void finalize() throws Throwable {
254
            close();
1✔
255
        }
1✔
256
    }
257
}
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