Skip to content

Commit 6611262

Browse files
authored
fix: avoid implicit transaction when query is pipelined with client-side statements (#4976)
* fix: avoid implicit transaction when query is pipelined with client-side statements Previously, when a query was sent in the same pipeline (before a Sync message) along with client-side or session statements (such as SHOW spanner.read_timestamp or SET timezone = 'UTC'), PGAdapter treated the batch as multiple statements and began an implicit read-only transaction. This caused two issues: 1. An unnecessary BeginTransaction RPC was executed. 2. Queries failed when spanner.read_only_staleness was set to bounded staleness (e.g., MAX_STALENESS or MIN_READ_TIMESTAMP), because Spanner only allows bounded staleness on single-use reads outside of a multi-statement transaction. This change: - Updates BackendConnection.maybeBeginImplicitTransaction() to check if all subsequent statements in the pipeline are client-side or session-management statements. If so, it keeps auto-commit mode and executes the query as a single-use read. - Unifies the detection of client-side statements with getSessionManagementStatement() to recognize SET, SHOW, and RESET ALL commands. - Generalizes the Describe + Execute statement check to be index-relative, preventing pipelines that start with a client-side statement from triggering an unintended transaction. * fix: transaction handling for mixed client-side + DML
1 parent 5e791f7 commit 6611262

3 files changed

Lines changed: 1757 additions & 37 deletions

File tree

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

Lines changed: 125 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,10 @@ boolean isBatchingPossible() {
242242

243243
abstract boolean isUpdate();
244244

245+
boolean isClientSide() {
246+
return false;
247+
}
248+
245249
void execute() {
246250
Span span = createSpan(toString(), statement);
247251
try (Scope ignore = span.makeCurrent()) {
@@ -272,6 +276,10 @@ private final class Execute extends BufferedStatement<StatementResult> {
272276
private final String command;
273277
private final Function<Statement, Statement> statementBinder;
274278
private final boolean analyze;
279+
private final LocalStatement localStatement;
280+
private final SessionStatement sessionStatement;
281+
private final Exception parseException;
282+
private final boolean clientSide;
275283

276284
Execute(
277285
String command,
@@ -291,6 +299,29 @@ private final class Execute extends BufferedStatement<StatementResult> {
291299
this.command = command;
292300
this.statementBinder = statementBinder;
293301
this.analyze = analyze;
302+
LocalStatement localStatement = getLocalStatement(this.statement);
303+
if (localStatement != null && !localStatement.hasReplacementStatement()) {
304+
this.localStatement = localStatement;
305+
this.sessionStatement = null;
306+
this.parseException = null;
307+
this.clientSide = true;
308+
} else {
309+
this.localStatement = null;
310+
SessionStatement sessionStatement = null;
311+
Exception parseException = null;
312+
try {
313+
sessionStatement = getSessionManagementStatement(this.statement, this.parsedStatement);
314+
} catch (Exception exception) {
315+
parseException = exception;
316+
}
317+
this.sessionStatement = sessionStatement;
318+
this.parseException = parseException;
319+
this.clientSide =
320+
!isTransactionStatement(this.parsedStatement)
321+
&& (this.parsedStatement.getType() == StatementType.CLIENT_SIDE
322+
|| this.sessionStatement != null
323+
|| this.parseException != null);
324+
}
294325
}
295326

296327
@Override
@@ -308,23 +339,28 @@ boolean isUpdate() {
308339
return this.parsedStatement.isUpdate();
309340
}
310341

342+
@Override
343+
boolean isClientSide() {
344+
return this.clientSide;
345+
}
346+
311347
@Override
312348
void doExecute() {
313349
Statement updatedStatement = statement;
314350
try {
315351
checkConnectionState();
352+
if (this.parseException != null) {
353+
throw SpannerExceptionFactory.asSpannerException(this.parseException);
354+
}
316355
// TODO(b/235719478): If the statement is a BEGIN statement and there is a COMMIT statement
317356
// at a later point in the batch, and all the statements in the transaction block are
318357
// SELECT statements, then we should create a read-only transaction. Also, if a transaction
319358
// block always ends with a ROLLBACK, PGAdapter should skip the entire execution of that
320359
// block.
321-
SessionStatement sessionStatement =
322-
getSessionManagementStatement(updatedStatement, parsedStatement);
323-
LocalStatement localStatement = getLocalStatement(statement);
324-
if (localStatement != null && !localStatement.hasReplacementStatement()) {
325-
result.set(localStatement.execute(BackendConnection.this, statement));
326-
} else if (sessionStatement != null) {
327-
result.set(sessionStatement.execute(sessionState, spannerConnection));
360+
if (this.localStatement != null) {
361+
result.set(this.localStatement.execute(BackendConnection.this, statement));
362+
} else if (this.sessionStatement != null) {
363+
result.set(this.sessionStatement.execute(sessionState, spannerConnection));
328364
} else if (connectionState == ConnectionState.ABORTED
329365
&& !spannerConnection.isInTransaction()
330366
&& (isRollback(parsedStatement) || isCommit(parsedStatement))) {
@@ -539,20 +575,6 @@ boolean isUnsupportedConcurrencyModeException(SpannerException spannerException)
539575
.startsWith(
540576
"INVALID_ARGUMENT: io.grpc.StatusRuntimeException: INVALID_ARGUMENT: Unsupported concurrency mode in query using INFORMATION_SCHEMA.");
541577
}
542-
543-
@Nullable
544-
SessionStatement getSessionManagementStatement(
545-
Statement statement, ParsedStatement parsedStatement) {
546-
if (parsedStatement.getType() == StatementType.UNKNOWN
547-
|| (parsedStatement.getType() == StatementType.QUERY
548-
&& isCommand(SHOW, statement.getSql()))
549-
|| (parsedStatement.getType() == StatementType.CLIENT_SIDE
550-
&& parsedStatement.getClientSideStatementType()
551-
== ClientSideStatementType.RESET_ALL)) {
552-
return SessionStatementParser.parse(parsedStatement, statement.getSql());
553-
}
554-
return null;
555-
}
556578
}
557579

558580
public static final int MAX_PARTITIONS =
@@ -1299,9 +1321,10 @@ private void maybeBeginImplicitTransaction(int index, boolean isSync) {
12991321
return;
13001322
}
13011323

1302-
// Only start an implicit transaction if we have more than one statement left. Otherwise, just
1303-
// let the Spanner connection execute the statement in auto-commit mode.
1304-
if (isSync && index == bufferedStatements.size() - 1) {
1324+
// Only start an implicit transaction if we have more than one statement left that can be
1325+
// executed in a transaction. Otherwise, just let the Spanner connection execute the statement
1326+
// in auto-commit mode.
1327+
if (isSync && hasOnlyClientSideStatementsAfter(index)) {
13051328
return;
13061329
}
13071330
// Don't start an implicit transaction if this is already a transaction statement.
@@ -1310,7 +1333,7 @@ private void maybeBeginImplicitTransaction(int index, boolean isSync) {
13101333
}
13111334
// No need to start a transaction for DDL or client side statements.
13121335
if (bufferedStatements.get(index).parsedStatement.getType() == StatementType.DDL
1313-
|| bufferedStatements.get(index).parsedStatement.getType() == StatementType.CLIENT_SIDE) {
1336+
|| bufferedStatements.get(index).isClientSide()) {
13141337
return;
13151338
}
13161339
// If there are only DML statements left, those can be executed as an auto-commit dml batch.
@@ -1322,18 +1345,20 @@ private void maybeBeginImplicitTransaction(int index, boolean isSync) {
13221345
return;
13231346
}
13241347
// Do not start an implicit transaction if all that is in the buffer is a DESCRIBE and an
1325-
// EXECUTE message for the same statement.
1348+
// EXECUTE message for the same statement (optionally surrounded only by client-side
1349+
// statements).
13261350
if (isSync
1327-
&& bufferedStatements.size() == 2
1328-
&& bufferedStatements.get(0) instanceof Execute
1329-
&& bufferedStatements.get(1) instanceof Execute
1330-
&& ((Execute) bufferedStatements.get(0)).analyze
1331-
&& !((Execute) bufferedStatements.get(1)).analyze
1351+
&& bufferedStatements.size() > index + 1
1352+
&& bufferedStatements.get(index) instanceof Execute
1353+
&& bufferedStatements.get(index + 1) instanceof Execute
1354+
&& ((Execute) bufferedStatements.get(index)).analyze
1355+
&& !((Execute) bufferedStatements.get(index + 1)).analyze
13321356
&& bufferedStatements
1333-
.get(0)
1357+
.get(index)
13341358
.statement
13351359
.getSql()
1336-
.equals(bufferedStatements.get(1).statement.getSql())) {
1360+
.equals(bufferedStatements.get(index + 1).statement.getSql())
1361+
&& hasOnlyClientSideStatementsAfter(index + 1)) {
13371362
return;
13381363
}
13391364

@@ -1505,9 +1530,36 @@ private boolean isTransactionStatement(int index) {
15051530
return isBegin(index) || isCommit(index) || isRollback(index);
15061531
}
15071532

1508-
private boolean hasOnlyDmlStatementsAfter(int index) {
1509-
return bufferedStatements.subList(index, bufferedStatements.size()).stream()
1510-
.allMatch(statement -> statement.parsedStatement.getType() == StatementType.UPDATE);
1533+
@VisibleForTesting
1534+
boolean hasOnlyDmlStatementsAfter(int index) {
1535+
boolean seenExecute = false;
1536+
boolean seenClientSide = false;
1537+
for (int i = index; i < bufferedStatements.size(); i++) {
1538+
BufferedStatement<?> statement = bufferedStatements.get(i);
1539+
if (statement.isClientSide()) {
1540+
seenClientSide = true;
1541+
} else if (statement.isUpdate() && !statement.parsedStatement.hasReturningClause()) {
1542+
if (seenClientSide) {
1543+
// A DML statement appeared after a client-side statement.
1544+
// These cannot be batched together in a single auto-commit DML batch.
1545+
return false;
1546+
}
1547+
boolean isAnalyze = statement instanceof Execute && ((Execute) statement).analyze;
1548+
if (isAnalyze) {
1549+
if (seenExecute || i > index) {
1550+
// An analyze/describe statement appeared after an execute statement,
1551+
// or there are multiple analyze statements. These cannot be executed
1552+
// in a single auto-commit DML batch.
1553+
return false;
1554+
}
1555+
} else {
1556+
seenExecute = true;
1557+
}
1558+
} else {
1559+
return false;
1560+
}
1561+
}
1562+
return true;
15111563
}
15121564

15131565
@VisibleForTesting
@@ -1516,7 +1568,43 @@ boolean hasUpdateStatementsAfter(int index) {
15161568
.anyMatch(BufferedStatement::isUpdate);
15171569
}
15181570

1519-
private int getStatementCount() {
1571+
@Nullable
1572+
SessionStatement getSessionManagementStatement(
1573+
Statement statement, ParsedStatement parsedStatement) {
1574+
if (parsedStatement.getType() == StatementType.UNKNOWN
1575+
|| (parsedStatement.getType() == StatementType.QUERY && isCommand(SHOW, statement.getSql()))
1576+
|| (parsedStatement.getType() == StatementType.CLIENT_SIDE
1577+
&& parsedStatement.getClientSideStatementType() == ClientSideStatementType.RESET_ALL)) {
1578+
return SessionStatementParser.parse(parsedStatement, statement.getSql());
1579+
}
1580+
return null;
1581+
}
1582+
1583+
@VisibleForTesting
1584+
boolean hasOnlyClientSideStatementsAfter(int index) {
1585+
if (index >= bufferedStatements.size() - 1) {
1586+
return true;
1587+
}
1588+
for (int i = index + 1; i < bufferedStatements.size(); i++) {
1589+
if (!bufferedStatements.get(i).isClientSide()) {
1590+
return false;
1591+
}
1592+
}
1593+
return true;
1594+
}
1595+
1596+
@VisibleForTesting
1597+
boolean isClientSide(BufferedStatement<?> statement) {
1598+
return statement.isClientSide();
1599+
}
1600+
1601+
@VisibleForTesting
1602+
BufferedStatement<?> getBufferedStatement(int index) {
1603+
return bufferedStatements.get(index);
1604+
}
1605+
1606+
@VisibleForTesting
1607+
int getStatementCount() {
15201608
return bufferedStatements.size();
15211609
}
15221610

0 commit comments

Comments
 (0)