3333import java .sql .ResultSet ;
3434import java .sql .SQLException ;
3535import java .sql .Statement ;
36+ import java .util .ArrayList ;
3637import java .util .Collection ;
3738import java .util .List ;
3839import java .util .Map ;
@@ -46,10 +47,14 @@ public abstract class SqlExecutingSelector<FACTORY extends SqlFactory, SELECTOR
4647{
4748 private static final Logger LOGGER = LogHelper .getLogger (SqlExecutingSelector .class , "Log warnings about SQL exceptions" );
4849
50+ // Warn when this many (or more) rows are pulled into a Java collection; suggests switching to a streaming method
51+ private static final int LARGE_RESULT_THRESHOLD = 10_000 ;
52+
4953 int _maxRows = Table .ALL_ROWS ;
5054 protected long _offset = Table .NO_OFFSET ;
5155 @ Nullable Map <String , Object > _namedParameters = null ;
52- private ConnectionFactory _connectionFactory = super ::getConnection ;
56+ private @ Nullable ConnectionFactory _connectionFactory = null ; // null means "no explicit choice"; see getEffectiveConnectionFactory()
57+ private boolean _jdbcCachingExplicitlySet = false ;
5358 private Integer _fetchSize = null ; // By default, use the standard fetch size
5459
5560 private @ Nullable AsyncQueryRequest <?> _asyncRequest = null ;
@@ -79,21 +84,51 @@ public interface ConnectionFactory
7984 @ Override
8085 public Connection getConnection () throws SQLException
8186 {
82- return _connectionFactory .get ();
87+ return getEffectiveConnectionFactory ().get ();
88+ }
89+
90+ /**
91+ * Determines which {@link ConnectionFactory} to use for this query. When a caller has explicitly chosen a caching
92+ * behavior via {@link #setJdbcCaching(boolean)} or supplied a Connection at construction time, that choice is
93+ * honored. Otherwise, JDBC caching is disabled by default: we ask the dialect for a streaming ConnectionFactory so
94+ * the driver won't buffer the entire ResultSet in memory. The dialect returns null (meaning "use the shared
95+ * Connection with the driver's default caching") when a transaction is active, the dialect is not PostgreSQL, or the
96+ * statement is not a SELECT, so this default is safe by construction. Resolving lazily here (rather than at
97+ * construction) ensures the transaction check reflects the state at execution time.
98+ */
99+ private ConnectionFactory getEffectiveConnectionFactory ()
100+ {
101+ // Honor an explicit setJdbcCaching() call (which populated _connectionFactory)...
102+ if (_jdbcCachingExplicitlySet )
103+ return _connectionFactory ;
104+
105+ // ...or a Connection supplied at construction time (super::getConnection returns the stashed _conn)
106+ if (null != _conn )
107+ return super ::getConnection ;
108+
109+ ConnectionFactory factory = getScope ().getSqlDialect ().getConnectionFactory (false , getScope (),
110+ new SQLFragment ("SELECT FakeColumn FROM FakeTable" ) /* SqlExecutingSelector always generates SELECT statements */ );
111+
112+ return null != factory ? factory : super ::getConnection ;
83113 }
84114
85115 /**
86116 * <p>Calling this method with cache=false ensures that the JDBC driver will not cache the produced ResultSet in
87117 * memory, which is useful when potentially working with very large (e.g., > 100MB) ResultSets. Calling it with
88- * cache=true (the default setting) ensures the JDBC driver's default caching behavior.</p>
118+ * cache=true ensures the JDBC driver's default caching behavior.</p>
89119 *
90120 * <p>By default, the PostgreSQL JDBC driver caches every ResultSet in its entirety. This can lead to
91121 * OutOfMemoryErrors when working with very large ResultSets. When the underlying database is PostgreSQL, calling
92122 * this method with false instructs this SqlExecutingSelector to use an unshared Connection and configure it with
93123 * special settings that disable the driver caching. The trade-off is that the underlying database query will not
94124 * use the shared Connection that other code on the thread (up or down the call stack) may be using, making
95- * Connection exhaustion more likely; that's why JDBC caching is on by default. Calling this method is not
96- * compatible with passing in an explicit Connection to the constructor.</p>
125+ * Connection exhaustion more likely. Calling this method is not compatible with passing in an explicit Connection to
126+ * the constructor.</p>
127+ *
128+ * <p>Note that when neither this method nor an explicit Connection is supplied, JDBC caching is disabled by default
129+ * whenever it's safe to do so (PostgreSQL, no active transaction, SELECT statement) — see
130+ * {@link #getEffectiveConnectionFactory()}. Callers that require the driver's default caching behavior (e.g., to
131+ * share the thread's Connection) must therefore opt in explicitly by calling this method with cache=true.</p>
97132 *
98133 * <p>When the underlying database is not PostgreSQL, calling this method has no effect, other than validating that
99134 * the stashed Connection is null.</p>
@@ -109,10 +144,32 @@ public SELECTOR setJdbcCaching(boolean cache)
109144 ConnectionFactory factory = getScope ().getSqlDialect ().getConnectionFactory (cache , getScope (),
110145 new SQLFragment ("SELECT FakeColumn FROM FakeTable" ) /* SqlExecutingSelector always generates SELECT statements */ );
111146 _connectionFactory = null != factory ? factory : super ::getConnection ;
147+ _jdbcCachingExplicitlySet = true ;
112148
113149 return getThis ();
114150 }
115151
152+ /**
153+ * Overridden to warn when a large number of rows is pulled into a Java collection. Loading many rows into memory
154+ * (here plus, potentially, in the JDBC driver's buffer) is a common source of OutOfMemoryErrors; callers should
155+ * generally prefer a streaming method — {@link #forEach}, {@link #forEachBatch}, or {@link #uncachedStream} — that
156+ * processes rows without materializing them all at once. {@code getArray}, {@code getCollection},
157+ * {@code getMapArray}, and {@code getMapCollection} all delegate here, so they're covered as well.
158+ */
159+ @ Override
160+ public @ NotNull <E > ArrayList <E > getArrayList (Class <E > clazz )
161+ {
162+ ArrayList <E > result = super .getArrayList (clazz );
163+
164+ if (result .size () >= LARGE_RESULT_THRESHOLD )
165+ {
166+ LOGGER .warn ("{} rows loaded into a collection via {}. Consider switching to forEach(), forEachBatch(), or uncachedStream() to reduce memory usage. SQL: {}" ,
167+ result .size (), getClass ().getSimpleName (), getSqlFactory (false ).getSql (), new Throwable ("Stack trace for large collection load" ));
168+ }
169+
170+ return result ;
171+ }
172+
116173 /**
117174 * Set a ResultSet fetch size that differs from the default value (1,000 rows on PostgreSQL). This is normally a
118175 * fine fetch size, but not when dealing with rows containing large TEXT or BYTEA columns.
0 commit comments