Skip to content

Commit 85f8581

Browse files
committed
fix: close refcursors when underlying cursor==null instead of relying on defaultRowFetchSize
See #2227 See #2371
1 parent 12541c4 commit 85f8581

File tree

7 files changed

+166
-102
lines changed

7 files changed

+166
-102
lines changed

docs/_posts/2022-02-01-42.3.2-release.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ version: 42.3.2
77
---
88

99
**Notable changes**
10+
### Known issues
11+
- Regression since 42.3.2: "cursor <unnamed portal 1> does not exist" when using ResultSet.setFetchSize from CallableStatement, fixed in 42.3.5 (see [PG #2377](https://github.com/pgjdbc/pgjdbc/pull/2377))
12+
1013
### Security
1114
- CVE-2022-21724 pgjdbc instantiates plugin instances based on class names provided via authenticationPluginClassName,
1215
sslhostnameverifier, socketFactory, sslfactory, sslpasswordcallback connection properties.

docs/_posts/2022-02-15-42.3.3-release.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ version: 42.3.3
77
---
88
**Notable changes**
99

10+
### Known issues
11+
- Regression since 42.3.2: "cursor <unnamed portal 1> does not exist" when using ResultSet.setFetchSize from CallableStatement, fixed in 42.3.5 (see [PG #2377](https://github.com/pgjdbc/pgjdbc/pull/2377))
12+
1013
### Changed
1114
- fix: Removed loggerFile and loggerLevel configuration. While the properties still exist.
1215
They can no longer be used to configure the driver logging. Instead use java.util.logging

docs/_posts/2022-04-15-42.3.4-release.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ version: 42.3.4
77
---
88

99
**Notable changes**
10+
### Known issues
11+
- Regression since 42.3.2: "cursor <unnamed portal 1> does not exist" when using ResultSet.setFetchSize from CallableStatement, fixed in 42.3.5 (see [PG #2377](https://github.com/pgjdbc/pgjdbc/pull/2377))
1012

1113
### Changed
1214
- fix: change name of build cache [PR 2471](https://github.com/pgjdbc/pgjdbc/pull/2471)

docs/_posts/2022-05-04-42.3.5-release.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ categories:
66
version: 42.3.5
77
---
88
**Notable changes**
9+
### Known issues
10+
- Regression since 42.3.2: "cursor <unnamed portal 1> does not exist" when using ResultSet.setFetchSize from CallableStatement, fixed in 42.3.5 (see [PG #2377](https://github.com/pgjdbc/pgjdbc/pull/2377))
911

1012
### Changed
1113
- test: polish TimestampUtilsTest

pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -286,23 +286,12 @@ public java.net.URL getURL(String columnName) throws SQLException {
286286
// we have updatable cursors it must be readonly.
287287
ResultSet rs =
288288
connection.execSQLQuery(sb.toString(), resultsettype, ResultSet.CONCUR_READ_ONLY);
289-
290-
/*
291-
If the user has set a fetch size we can't close the cursor yet.
292-
Issue https://github.com/pgjdbc/pgjdbc/issues/2227
293-
*/
294-
if (connection.getDefaultFetchSize() == 0 ) {
295-
/*
296-
// In long running transactions these backend cursors take up memory space
297-
// we could close in rs.close(), but if the transaction is closed before the result set,
298-
// then the cursor no longer exists
299-
*/
300-
sb.setLength(0);
301-
sb.append("CLOSE ");
302-
Utils.escapeIdentifier(sb, cursorName);
303-
connection.execSQLUpdate(sb.toString());
304-
}
305289
((PgResultSet) rs).setRefCursor(cursorName);
290+
// In long-running transactions these backend cursors take up memory space
291+
// we could close in rs.close(), but if the transaction is closed before the result set,
292+
// then
293+
// the cursor no longer exists
294+
((PgResultSet) rs).closeRefCursor();
306295
return rs;
307296
}
308297
if ("hstore".equals(type)) {
@@ -2154,6 +2143,10 @@ public boolean next() throws SQLException {
21542143
connection.getQueryExecutor()
21552144
.fetch(cursor, new CursorResultHandler(), fetchRows, adaptiveFetch);
21562145

2146+
// .fetch(...) could update this.cursor, and cursor==null means
2147+
// there are no more rows to fetch
2148+
closeRefCursor();
2149+
21572150
// After fetch, update last used fetch size (could be useful for adaptive fetch).
21582151
lastUsedFetchSize = fetchRows;
21592152

@@ -2192,24 +2185,30 @@ protected void closeInternally() throws SQLException {
21922185
rows = null;
21932186
JdbcBlackHole.close(deleteStatement);
21942187
deleteStatement = null;
2188+
if (cursor != null) {
2189+
cursor.close();
2190+
cursor = null;
2191+
}
2192+
closeRefCursor();
2193+
}
21952194

2196-
/* this used to be closed right after reading all of the rows,
2197-
however if fetchSize is set ony fetchSize rows will be read and then
2198-
the cursor will be closed
2199-
We only need to worry about closing it if the transaction is still open
2200-
if it is in error it will get closed eventually. (maybe not ?)
2201-
*/
2202-
if ( refCursorName != null && fetchSize != 0) {
2195+
/**
2196+
* Closes {@code <unnamed portal 1>} if no more fetch calls expected ({@code cursor==null})
2197+
* @throws SQLException if portal close fails
2198+
*/
2199+
private void closeRefCursor() throws SQLException {
2200+
String refCursorName = this.refCursorName;
2201+
if (refCursorName == null || cursor != null) {
2202+
return;
2203+
}
2204+
try {
22032205
if (connection.getTransactionState() == TransactionState.OPEN) {
22042206
StringBuilder sb = new StringBuilder("CLOSE ");
2205-
Utils.escapeIdentifier(sb, castNonNull(refCursorName));
2207+
Utils.escapeIdentifier(sb, refCursorName);
22062208
connection.execSQLUpdate(sb.toString());
2207-
refCursorName = null;
22082209
}
2209-
}
2210-
if (cursor != null) {
2211-
cursor.close();
2212-
cursor = null;
2210+
} finally {
2211+
this.refCursorName = null;
22132212
}
22142213
}
22152214

pgjdbc/src/test/java/org/postgresql/test/TestUtil.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,13 @@ public static void dropType(Connection con, String type) throws SQLException {
654654
dropObject(con, "TYPE", type);
655655
}
656656

657+
/*
658+
* Drops a function with a given signature.
659+
*/
660+
public static void dropFunction(Connection con, String name, String arguments) throws SQLException {
661+
dropObject(con, "FUNCTION", name + "(" + arguments + ")");
662+
}
663+
657664
private static void dropObject(Connection con, String type, String name) throws SQLException {
658665
Statement stmt = con.createStatement();
659666
try {
@@ -1163,7 +1170,19 @@ private static void waitStopReplicationSlot(Connection connection, String slotNa
11631170
}
11641171
}
11651172

1173+
/**
1174+
* Executes given SQL via {@link Statement#execute(String)} on a given connection.
1175+
* @deprecated prefer {@link #execute(Connection, String)} since it yields easier for read code
1176+
*/
1177+
@Deprecated
11661178
public static void execute(String sql, Connection connection) throws SQLException {
1179+
execute(connection, sql);
1180+
}
1181+
1182+
/**
1183+
* Executes given SQL via {@link Statement#execute(String)} on a given connection.
1184+
*/
1185+
public static void execute(Connection connection, String sql) throws SQLException {
11671186
try (Statement stmt = connection.createStatement()) {
11681187
stmt.execute(sql);
11691188
}

pgjdbc/src/test/java/org/postgresql/test/jdbc2/RefCursorFetchTest.java

Lines changed: 109 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7,115 +7,151 @@
77

88
import static org.junit.Assert.assertEquals;
99

10-
import org.postgresql.PGConnection;
10+
import org.postgresql.PGProperty;
1111
import org.postgresql.core.ServerVersion;
1212
import org.postgresql.test.TestUtil;
1313

14+
import org.checkerframework.checker.nullness.qual.Nullable;
15+
import org.junit.AfterClass;
1416
import org.junit.BeforeClass;
1517
import org.junit.Test;
18+
import org.junit.runner.RunWith;
19+
import org.junit.runners.Parameterized;
1620

1721
import java.sql.CallableStatement;
22+
import java.sql.Connection;
1823
import java.sql.ResultSet;
1924
import java.sql.SQLException;
2025
import java.sql.Types;
26+
import java.util.ArrayList;
27+
import java.util.Collection;
28+
import java.util.Properties;
2129

30+
@RunWith(Parameterized.class)
2231
public class RefCursorFetchTest extends BaseTest4 {
23-
@BeforeClass
24-
public static void checkVersion() throws SQLException {
25-
TestUtil.assumeHaveMinimumServerVersion(ServerVersion.v9_0);
32+
private final int numRows;
33+
private final @Nullable Integer defaultFetchSize;
34+
private final @Nullable Integer resultSetFetchSize;
35+
private final AutoCommit autoCommit;
36+
private final boolean commitAfterExecute;
37+
38+
public RefCursorFetchTest(BinaryMode binaryMode, int numRows,
39+
@Nullable Integer defaultFetchSize,
40+
@Nullable Integer resultSetFetchSize,
41+
AutoCommit autoCommit, boolean commitAfterExecute) {
42+
this.numRows = numRows;
43+
this.defaultFetchSize = defaultFetchSize;
44+
this.resultSetFetchSize = resultSetFetchSize;
45+
this.autoCommit = autoCommit;
46+
this.commitAfterExecute = commitAfterExecute;
47+
setBinaryMode(binaryMode);
48+
}
49+
50+
@Parameterized.Parameters(name = "binary = {0}, numRows = {1}, defaultFetchSize = {2}, resultSetFetchSize = {3}, autoCommit = {4}, commitAfterExecute = {5}")
51+
public static Iterable<Object[]> data() {
52+
Collection<Object[]> ids = new ArrayList<>();
53+
for (BinaryMode binaryMode : BinaryMode.values()) {
54+
for (int numRows : new int[]{0, 10, 101}) {
55+
for (Integer defaultFetchSize : new Integer[]{null, 0, 9, 50}) {
56+
for (AutoCommit autoCommit : AutoCommit.values()) {
57+
for (boolean commitAfterExecute : new boolean[]{true, false}) {
58+
for (Integer resultSetFetchSize : new Integer[]{null, 0, 9, 50}) {
59+
ids.add(new Object[]{binaryMode, numRows, defaultFetchSize, resultSetFetchSize, autoCommit, commitAfterExecute});
60+
}
61+
}
62+
}
63+
}
64+
}
65+
}
66+
return ids;
2667
}
2768

2869
@Override
29-
public void setUp() throws Exception {
30-
super.setUp();
31-
assumeCallableStatementsSupported();
70+
protected void updateProperties(Properties props) {
71+
super.updateProperties(props);
72+
if (defaultFetchSize != null) {
73+
PGProperty.DEFAULT_ROW_FETCH_SIZE.set(props, defaultFetchSize);
74+
}
75+
}
3276

33-
TestUtil.dropTable(con, "test_blob");
34-
TestUtil.createTable(con, "test_blob", "content bytea");
77+
@BeforeClass
78+
public static void beforeClass() throws Exception {
79+
TestUtil.assumeHaveMinimumServerVersion(ServerVersion.v9_0);
80+
try (Connection con = TestUtil.openDB();) {
81+
TestUtil.createTable(con, "test_blob", "content bytea");
82+
TestUtil.execute(con, "");
83+
TestUtil.execute(con, "--create function to read data\n"
84+
+ "CREATE OR REPLACE FUNCTION test_blob(p_cur OUT REFCURSOR, p_limit int4) AS $body$\n"
85+
+ "BEGIN\n"
86+
+ "OPEN p_cur FOR SELECT content FROM test_blob LIMIT p_limit;\n"
87+
+ "END;\n"
88+
+ "$body$ LANGUAGE plpgsql STABLE");
3589

36-
// Create a function to read the blob data
37-
TestUtil.execute("CREATE OR REPLACE FUNCTION test_blob (p_cur OUT REFCURSOR) AS\n"
38-
+ "$body$\n"
39-
+ " BEGIN\n"
40-
+ " OPEN p_cur FOR SELECT content FROM test_blob;\n"
41-
+ " END;\n"
42-
+ "$body$ LANGUAGE plpgsql STABLE", con);
90+
TestUtil.execute(con,"--generate 101 rows with 4096 bytes:\n"
91+
+ "insert into test_blob\n"
92+
+ "select(select decode(string_agg(lpad(to_hex(width_bucket(random(), 0, 1, 256) - 1), 2, '0'), ''), 'hex')"
93+
+ " FROM generate_series(1, 4096))\n"
94+
+ "from generate_series (1, 200)");
95+
}
96+
}
4397

44-
// Generate 101 rows with 4096 bytes
45-
TestUtil.execute("INSERT INTO test_blob (content)\n"
46-
+ "SELECT (SELECT decode(string_agg(lpad(to_hex(width_bucket(random(), 0, 1, 256) - 1), 2, '0'), ''), 'hex')\n"
47-
+ " FROM generate_series(1, 4096)) AS content\n"
48-
+ "FROM generate_series (1, 101)", con);
98+
@AfterClass
99+
public static void afterClass() throws Exception {
100+
try (Connection con = TestUtil.openDB();) {
101+
TestUtil.dropTable(con, "test_blob");
102+
TestUtil.dropFunction(con,"test_blob", "REFCURSOR, int4");
103+
}
49104
}
50105

51106
@Override
52-
public void tearDown() throws SQLException {
53-
TestUtil.execute("DROP FUNCTION IF EXISTS test_blob (OUT REFCURSOR)", con);
54-
TestUtil.dropTable(con, "test_blob");
55-
super.tearDown();
107+
public void setUp() throws Exception {
108+
super.setUp();
109+
assumeCallableStatementsSupported();
110+
con.setAutoCommit(autoCommit == AutoCommit.YES);
56111
}
57112

58113
@Test
59-
public void testRefCursorWithFetchSize() throws SQLException {
60-
((PGConnection)con).setDefaultFetchSize(50);
114+
public void fetchAllRows() throws SQLException {
61115
int cnt = 0;
62-
try (CallableStatement call = con.prepareCall("{? = call test_blob()}")) {
116+
try (CallableStatement call = con.prepareCall("{? = call test_blob(?)}")) {
63117
con.setAutoCommit(false); // ref cursors only work if auto commit is off
64118
call.registerOutParameter(1, Types.REF_CURSOR);
119+
call.setInt(2, numRows);
65120
call.execute();
66-
try (ResultSet rs = (ResultSet) call.getObject(1)) {
67-
while (rs.next()) {
68-
cnt++;
121+
if (commitAfterExecute) {
122+
if (autoCommit == AutoCommit.NO) {
123+
con.commit();
124+
} else {
125+
con.setAutoCommit(false);
126+
con.setAutoCommit(true);
69127
}
70128
}
71-
assertEquals(101, cnt);
72-
}
73-
}
74-
75-
@Test
76-
public void testRefCursorWithOutFetchSize() throws SQLException {
77-
assumeCallableStatementsSupported();
78-
int cnt = 0;
79-
try (CallableStatement call = con.prepareCall("{? = call test_blob()}")) {
80-
con.setAutoCommit(false); // ref cursors only work if auto commit is off
81-
call.registerOutParameter(1, Types.REF_CURSOR);
82-
call.execute();
83129
try (ResultSet rs = (ResultSet) call.getObject(1)) {
84-
while (rs.next()) {
85-
cnt++;
130+
if (resultSetFetchSize != null) {
131+
rs.setFetchSize(resultSetFetchSize);
86132
}
87-
}
88-
assertEquals(101, cnt);
89-
}
90-
}
91-
92-
/*
93-
test to make sure that close in the result set does not attempt to get rid of the non-existent
94-
portal
95-
*/
96-
@Test
97-
public void testRefCursorWithFetchSizeNoTransaction() throws SQLException {
98-
assumeCallableStatementsSupported();
99-
((PGConnection)con).setDefaultFetchSize(50);
100-
int cnt = 0;
101-
try (CallableStatement call = con.prepareCall("{? = call test_blob()}")) {
102-
con.setAutoCommit(false); // ref cursors only work if auto commit is off
103-
call.registerOutParameter(1, Types.REF_CURSOR);
104-
call.execute();
105-
// end the transaction here, which will get rid of the refcursor
106-
con.setAutoCommit(true);
107-
// we should be able to read the first 50 as they were read before the tx was ended
108-
try (ResultSet rs = (ResultSet) call.getObject(1)) {
109133
while (rs.next()) {
110134
cnt++;
111135
}
112-
} catch (SQLException ex) {
113-
// should get an exception here as we try to read more but the portal is gone
114-
assertEquals("34000", ex.getSQLState());
115-
} finally {
116-
assertEquals(50, cnt);
136+
assertEquals("number of rows from test_blob(...) call", numRows, cnt);
137+
} catch (SQLException e) {
138+
if (commitAfterExecute && "34000".equals(e.getSQLState())) {
139+
// Transaction commit closes refcursor, so the fetch call is expected to fail
140+
// File: postgres.c, Routine: exec_execute_message, Line: 2070
141+
// Server SQLState: 34000
142+
int expectedRows =
143+
defaultFetchSize != null && defaultFetchSize != 0 ? Math.min(defaultFetchSize, numRows) : numRows;
144+
assertEquals(
145+
"The transaction was committed before processing the results,"
146+
+ " so expecting ResultSet to buffer fetchSize=" + defaultFetchSize + " rows out of "
147+
+ numRows,
148+
expectedRows,
149+
cnt
150+
);
151+
return;
152+
}
153+
throw e;
117154
}
118155
}
119156
}
120-
121157
}

0 commit comments

Comments
 (0)