|
7 | 7 |
|
8 | 8 | import static org.junit.Assert.assertEquals; |
9 | 9 |
|
10 | | -import org.postgresql.PGConnection; |
| 10 | +import org.postgresql.PGProperty; |
11 | 11 | import org.postgresql.core.ServerVersion; |
12 | 12 | import org.postgresql.test.TestUtil; |
13 | 13 |
|
| 14 | +import org.checkerframework.checker.nullness.qual.Nullable; |
| 15 | +import org.junit.AfterClass; |
14 | 16 | import org.junit.BeforeClass; |
15 | 17 | import org.junit.Test; |
| 18 | +import org.junit.runner.RunWith; |
| 19 | +import org.junit.runners.Parameterized; |
16 | 20 |
|
17 | 21 | import java.sql.CallableStatement; |
| 22 | +import java.sql.Connection; |
18 | 23 | import java.sql.ResultSet; |
19 | 24 | import java.sql.SQLException; |
20 | 25 | import java.sql.Types; |
| 26 | +import java.util.ArrayList; |
| 27 | +import java.util.Collection; |
| 28 | +import java.util.Properties; |
21 | 29 |
|
| 30 | +@RunWith(Parameterized.class) |
22 | 31 | 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; |
26 | 67 | } |
27 | 68 |
|
28 | 69 | @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 | + } |
32 | 76 |
|
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"); |
35 | 89 |
|
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 | + } |
43 | 97 |
|
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 | + } |
49 | 104 | } |
50 | 105 |
|
51 | 106 | @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); |
56 | 111 | } |
57 | 112 |
|
58 | 113 | @Test |
59 | | - public void testRefCursorWithFetchSize() throws SQLException { |
60 | | - ((PGConnection)con).setDefaultFetchSize(50); |
| 114 | + public void fetchAllRows() throws SQLException { |
61 | 115 | int cnt = 0; |
62 | | - try (CallableStatement call = con.prepareCall("{? = call test_blob()}")) { |
| 116 | + try (CallableStatement call = con.prepareCall("{? = call test_blob(?)}")) { |
63 | 117 | con.setAutoCommit(false); // ref cursors only work if auto commit is off |
64 | 118 | call.registerOutParameter(1, Types.REF_CURSOR); |
| 119 | + call.setInt(2, numRows); |
65 | 120 | 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); |
69 | 127 | } |
70 | 128 | } |
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(); |
83 | 129 | try (ResultSet rs = (ResultSet) call.getObject(1)) { |
84 | | - while (rs.next()) { |
85 | | - cnt++; |
| 130 | + if (resultSetFetchSize != null) { |
| 131 | + rs.setFetchSize(resultSetFetchSize); |
86 | 132 | } |
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)) { |
109 | 133 | while (rs.next()) { |
110 | 134 | cnt++; |
111 | 135 | } |
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; |
117 | 154 | } |
118 | 155 | } |
119 | 156 | } |
120 | | - |
121 | 157 | } |
0 commit comments