From 664295cabce5ed577f383a224858bcdb39e8afa6 Mon Sep 17 00:00:00 2001 From: Charlotte Yun Date: Wed, 29 Jul 2026 17:39:46 -0700 Subject: [PATCH 1/2] fix(spanner): ensure active transaction is rolled back on exception --- Spanner/src/Database.php | 13 ++++++++++- Spanner/tests/Unit/DatabaseTest.php | 36 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Spanner/src/Database.php b/Spanner/src/Database.php index 14d16d777c3..da631bd32bf 100644 --- a/Spanner/src/Database.php +++ b/Spanner/src/Database.php @@ -964,6 +964,17 @@ public function runTransaction(callable $operation, array $options = []): mixed $this->isRunningTransaction = true; try { $res = call_user_func($operation, $transaction); + } catch (\Throwable $e) { + $active = $transaction->state() === Transaction::STATE_ACTIVE; + $singleUse = $transaction->type() === Transaction::TYPE_SINGLE_USE; + if ($active && !$singleUse) { + try { + $transaction->rollback(); + } catch (\Throwable $rollbackException) { + // ignore rollback failure and bubble up the original exception + } + } + throw $e; } finally { $this->isRunningTransaction = false; } @@ -971,7 +982,7 @@ public function runTransaction(callable $operation, array $options = []): mixed $active = $transaction->state() === Transaction::STATE_ACTIVE; $singleUse = $transaction->type() === Transaction::TYPE_SINGLE_USE; if ($active && !$singleUse) { - $transaction->rollback($options); + $transaction->rollback(); throw new \RuntimeException('Transactions must be rolled back or committed.'); } diff --git a/Spanner/tests/Unit/DatabaseTest.php b/Spanner/tests/Unit/DatabaseTest.php index 730ab9ed5d6..e3b0013b6d1 100644 --- a/Spanner/tests/Unit/DatabaseTest.php +++ b/Spanner/tests/Unit/DatabaseTest.php @@ -792,6 +792,20 @@ public function testRunTransactionNoCommit() $this->database->runTransaction($this->noop()); } + public function testRunTransactionNoCommitWithTag() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Transactions must be rolled back or committed.'); + + $sql = $this->createStreamingAPIArgs()['sql']; + $this->stubExecuteStreamingSql(); + $this->spannerClient->rollback(Argument::cetera())->shouldBeCalled(); + + $this->database->runTransaction(function (Transaction $t) use ($sql) { + $t->execute($sql); + }, ['tag' => self::TRANSACTION_TAG]); + } + public function testRunTransactionNestedTransaction() { $this->expectException(BadMethodCallException::class); @@ -2288,6 +2302,28 @@ public function testRunTransactionWithRollback() }, ['tag' => self::TRANSACTION_TAG]); } + public function testRunTransactionRollsBackOnException() + { + $sql = $this->createStreamingAPIArgs()['sql']; + + $this->stubExecuteStreamingSql(); + $this->spannerClient->rollback( + Argument::that(function ($request) use ($sql) { + return $request->getTransactionId() == self::TRANSACTION; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Callback exception'); + + $this->database->runTransaction(function (Transaction $t) use ($sql) { + $t->execute($sql); + throw new \RuntimeException('Callback exception'); + }, ['tag' => self::TRANSACTION_TAG]); + } + public function testRunTransactionWithExcludeTxnFromChangeStreams() { $sql = 'SELECT example FROM sql_query'; From 9ed14ef72a8c71c8583784cf4fe1f14d248aacc8 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 5 Aug 2026 18:54:59 -0700 Subject: [PATCH 2/2] Update Spanner/tests/Unit/DatabaseTest.php --- Spanner/tests/Unit/DatabaseTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Spanner/tests/Unit/DatabaseTest.php b/Spanner/tests/Unit/DatabaseTest.php index e3b0013b6d1..72b37daef98 100644 --- a/Spanner/tests/Unit/DatabaseTest.php +++ b/Spanner/tests/Unit/DatabaseTest.php @@ -2304,6 +2304,9 @@ public function testRunTransactionWithRollback() public function testRunTransactionRollsBackOnException() { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Callback exception'); + $sql = $this->createStreamingAPIArgs()['sql']; $this->stubExecuteStreamingSql(); @@ -2315,9 +2318,6 @@ public function testRunTransactionRollsBackOnException() ) ->shouldBeCalledOnce(); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Callback exception'); - $this->database->runTransaction(function (Transaction $t) use ($sql) { $t->execute($sql); throw new \RuntimeException('Callback exception');