Skip to content

Commit e1f02fe

Browse files
authored
fix: pg_catalog tables were not replaced for information_schema queries (#494)
Queries in read/write transactions that refer to the information schema were retried using a single use read-only transaction, but not with the replaced pg_catalog tables. This would cause 'Table not found' errors.
1 parent d84564d commit e1f02fe

File tree

2 files changed

+79
-2
lines changed

2 files changed

+79
-2
lines changed

src/main/java/com/google/cloud/spanner/pgadapter/statements/BackendConnection.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ private final class Execute extends BufferedStatement<StatementResult> {
167167

168168
@Override
169169
void execute() {
170+
Statement updatedStatement = statement;
170171
try {
171172
checkConnectionState();
172173
// TODO(b/235719478): If the statement is a BEGIN statement and there is a COMMIT statement
@@ -212,7 +213,7 @@ void execute() {
212213
result.set(ddlExecutor.execute(parsedStatement, statement));
213214
} else {
214215
// Potentially replace pg_catalog table references with common table expressions.
215-
Statement updatedStatement =
216+
updatedStatement =
216217
sessionState.isReplacePgCatalogTables()
217218
? pgCatalog.replacePgCatalogTables(statement)
218219
: statement;
@@ -229,7 +230,7 @@ void execute() {
229230
spannerConnection
230231
.getDatabaseClient()
231232
.singleUse()
232-
.executeQuery(statement))));
233+
.executeQuery(updatedStatement))));
233234
return;
234235
} catch (Exception exception) {
235236
throw setAndReturn(result, exception);

src/test/java/com/google/cloud/spanner/pgadapter/JdbcMockServerTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2004,6 +2004,82 @@ public void testInformationSchemaQueryInTransactionWithErrorDuringRetry() throws
20042004
assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
20052005
}
20062006

2007+
@Test
2008+
public void testInformationSchemaQueryInTransactionWithReplacedPgCatalogTables()
2009+
throws SQLException {
2010+
String sql = "SELECT 1 FROM pg_namespace";
2011+
String replacedSql =
2012+
"with pg_namespace as (\n"
2013+
+ " select case schema_name when 'pg_catalog' then 11 when 'public' then 2200 else 0 end as oid,\n"
2014+
+ " schema_name as nspname, null as nspowner, null as nspacl\n"
2015+
+ " from information_schema.schemata\n"
2016+
+ ")\n"
2017+
+ "SELECT 1 FROM pg_namespace";
2018+
// Register a result for the query. Note that we don't really care what the result is, just that
2019+
// there is a result.
2020+
mockSpanner.putStatementResult(
2021+
StatementResult.query(Statement.of(replacedSql), SELECT1_RESULTSET));
2022+
2023+
try (Connection connection = DriverManager.getConnection(createUrl())) {
2024+
// Make sure that we start a transaction.
2025+
connection.setAutoCommit(false);
2026+
2027+
// Execute a query to start the transaction.
2028+
try (ResultSet resultSet = connection.createStatement().executeQuery(SELECT1.getSql())) {
2029+
assertTrue(resultSet.next());
2030+
assertEquals(1L, resultSet.getLong(1));
2031+
assertFalse(resultSet.next());
2032+
}
2033+
2034+
// This ensures that the following query returns an error the first time it is executed, and
2035+
// then succeeds the second time. This happens because the exception is 'popped' from the
2036+
// response queue when it is returned. The next time the query is executed, it will return the
2037+
// actual result that we set.
2038+
mockSpanner.setExecuteStreamingSqlExecutionTime(
2039+
SimulatedExecutionTime.ofException(
2040+
Status.INVALID_ARGUMENT
2041+
.withDescription(
2042+
"Unsupported concurrency mode in query using INFORMATION_SCHEMA.")
2043+
.asRuntimeException()));
2044+
try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) {
2045+
assertTrue(resultSet.next());
2046+
assertEquals(1L, resultSet.getLong(1));
2047+
assertFalse(resultSet.next());
2048+
}
2049+
2050+
// Make sure that the connection is still usable.
2051+
try (ResultSet resultSet = connection.createStatement().executeQuery(SELECT2.getSql())) {
2052+
assertTrue(resultSet.next());
2053+
assertEquals(2L, resultSet.getLong(1));
2054+
assertFalse(resultSet.next());
2055+
}
2056+
connection.commit();
2057+
}
2058+
2059+
// We should receive the INFORMATION_SCHEMA statement twice on Cloud Spanner:
2060+
// 1. The first time it returns an error because it is using the wrong concurrency mode.
2061+
// 2. The specific error will cause the connection to retry the statement using a single-use
2062+
// read-only transaction.
2063+
assertEquals(4, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
2064+
List<ExecuteSqlRequest> requests = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class);
2065+
// The first statement should start a transaction
2066+
assertTrue(requests.get(0).getTransaction().hasBegin());
2067+
// The second statement (the initial attempt of the INFORMATION_SCHEMA query) should try to use
2068+
// the transaction.
2069+
assertTrue(requests.get(1).getTransaction().hasId());
2070+
assertEquals(replacedSql, requests.get(1).getSql());
2071+
// The INFORMATION_SCHEMA query is then retried using a single-use read-only transaction.
2072+
assertFalse(requests.get(2).hasTransaction());
2073+
assertEquals(replacedSql, requests.get(2).getSql());
2074+
// The last statement should use the transaction.
2075+
assertTrue(requests.get(3).getTransaction().hasId());
2076+
2077+
assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class));
2078+
CommitRequest commitRequest = mockSpanner.getRequestsOfType(CommitRequest.class).get(0);
2079+
assertEquals(commitRequest.getTransactionId(), requests.get(1).getTransaction().getId());
2080+
assertEquals(commitRequest.getTransactionId(), requests.get(3).getTransaction().getId());
2081+
}
2082+
20072083
@Test
20082084
public void testShowGuessTypes() throws SQLException {
20092085
try (Connection connection = DriverManager.getConnection(createUrl())) {

0 commit comments

Comments
 (0)