Skip to content

fix: avoid implicit transaction when query is pipelined with client-side statements - #4976

Open
olavloite wants to merge 3 commits into
postgresql-dialectfrom
avoid-implicit-tx
Open

olavloite wants to merge 3 commits into
postgresql-dialectfrom
avoid-implicit-tx

Conversation

@olavloite

Copy link
Copy Markdown
Collaborator

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes transaction handling in PGAdapter by avoiding implicit transactions when only client-side statements (such as SET or SHOW) remain in the buffer. It introduces helper methods to detect client-side statements and adds comprehensive tests. The reviewer identified a performance concern where repeated SQL parsing in isClientSide could lead to O(N^2) complexity, suggesting caching this status on BufferedStatement instead.

…ide 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.
@olavloite

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors how client-side and session-management statements are identified and handled in BackendConnection. It introduces a pre-computed clientSide flag and caches localStatement and sessionStatement on Execute statements. This optimization prevents PGAdapter from starting unnecessary implicit transactions when queries are followed or surrounded only by client-side statements (such as SET or SHOW commands). Extensive unit and mock server tests have been added to verify these transaction-boundary behaviors. There are no review comments to address, and the changes look solid.

@olavloite
olavloite requested a review from rayudu3745 October 7, 2026 07:41
this.clientSide = true;
} else {
this.localStatement = null;
this.sessionStatement = getSessionManagementStatement(this.statement, this.parsedStatement);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling getSessionManagementStatement(this.statement, this.parsedStatement) in the Execute constructor means SessionStatementParser.parse(...) now runs when the statement is buffered (backendConnection.execute(...) / backendConnection.analyze(...)) rather than inside doExecute() during flush() / sync().

If a SET, SHOW, or RESET statement has invalid syntax (e.g., SET foo, SHOW, SHOW foo bar, RESET foo bar), SessionStatementParser.parse(...) throws SpannerException(ErrorCode.INVALID_ARGUMENT, ...). Throwing during buffer() breaks error handling in two ways:

  1. Simple Query Mode (SimpleQueryStatement.execute()): new ExecuteMessage(...).send() calls this.buffer(backendConnection) before handler.buffer(this). When new Execute(...) throws, the message is never added to bufferedStatements or handler.messages, and SimpleQueryStatement.execute() catches (Exception ignore) and sends SyncMessage, resulting in ReadyForQuery with no ErrorResponse (the invalid statement silently succeeds).
  2. Extended Query Mode: Throwing during ExecuteMessage.send() / DescribeMessage.send() sends an immediate out-of-band ErrorResponse + ReadyForQuery before buffered messages are flushed and before Sync is consumed.

Could we catch SpannerException when pre-parsing sessionStatement in the constructor (or defer throwing the parse error until doExecute()) so that invalid session statements still fail the Future<StatementResult> during doExecute()?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

}

@Test
public void testDmlAndClientSideStatement_doesNotStartTransaction() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in this scenario if two INSERT statements are pipelined before show spanner.commit_timestamp (e.g., INSERT 1; INSERT 2; SHOW spanner.commit_timestamp)?

With a single INSERT, hasOnlyClientSideStatementsAfter(0) is true, so no implicit transaction is started and INSERT runs in auto-commit.

However, with two INSERT statements followed by SHOW spanner.commit_timestamp:

  1. hasOnlyClientSideStatementsAfter(0) is false (index 1 is the second INSERT).
  2. hasOnlyDmlStatementsAfter(0) in maybeBeginImplicitTransaction (BackendConnection.java:1325) is also false because index 2 is SHOW, not StatementType.UPDATE.

As a result, maybeBeginImplicitTransaction starts an implicit transaction (beginTransaction()), executes both INSERTs in a batch inside that uncommitted transaction, and then executes show spanner.commit_timestamp before endImplicitTransaction() commits the transaction at the end of flush().

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@olavloite
olavloite requested a review from rayudu3745 October 9, 2026 09:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants