From baac211d9d0f7740c041e4b9942387428b3b1add Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 28 Jul 2026 18:37:13 +0000 Subject: [PATCH] fix(Spanner): ensure precommit token is set for ILB transactions --- Spanner/src/Operation.php | 2 +- Spanner/src/Transaction.php | 3 ++ Spanner/tests/Unit/OperationTest.php | 24 ++++++++++++ Spanner/tests/Unit/TransactionTest.php | 51 ++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/Spanner/src/Operation.php b/Spanner/src/Operation.php index f24f5e29270..6b7fec90c3a 100644 --- a/Spanner/src/Operation.php +++ b/Spanner/src/Operation.php @@ -736,7 +736,7 @@ public function transaction(SessionCache $session, array $options = []): Transac 'requestOptions' => $beginTransaction->getRequestOptions(), 'transactionOptions' => $txnOptions, ]); - return new Transaction( + $transaction = new Transaction( $this, $session, $id, diff --git a/Spanner/src/Transaction.php b/Spanner/src/Transaction.php index 8eff7d7602d..5a62c33c267 100644 --- a/Spanner/src/Transaction.php +++ b/Spanner/src/Transaction.php @@ -476,6 +476,9 @@ public function commit(array $options = []): Timestamp $transaction = $this->operation->transaction($this->session, $operationTransactionOptions); // Set the transaction ID of the current transaction. $this->transactionId = $transaction->id(); + if (isset($transaction->precommitToken)) { + $this->setPrecommitToken($transaction->precommitToken); + } } if (!$this->singleUseState()) { diff --git a/Spanner/tests/Unit/OperationTest.php b/Spanner/tests/Unit/OperationTest.php index 1a2492476e9..8bd32a4e1ea 100644 --- a/Spanner/tests/Unit/OperationTest.php +++ b/Spanner/tests/Unit/OperationTest.php @@ -490,6 +490,30 @@ public function testTransactionWithExcludeTxnFromChangeStreams() $this->assertEquals('foo', $transaction->id()); } + public function testTransactionWithPrecommitToken() + { + $precommitToken = new MultiplexedSessionPrecommitToken([ + 'precommit_token' => 'my-precommit-token', + ]); + $this->spannerClient->beginTransaction( + Argument::cetera() + ) + ->shouldBeCalled() + ->willReturn(new TransactionProto([ + 'id' => self::TRANSACTION, + 'precommit_token' => $precommitToken, + ])); + + $t = $this->operation->transaction($this->session); + $this->assertInstanceOf(Transaction::class, $t); + $this->assertEquals(self::TRANSACTION, $t->id()); + + $ref = new \ReflectionClass(Transaction::class); + $prop = $ref->getProperty('precommitToken'); + $this->assertNotNull($prop->getValue($t)); + $this->assertEquals($precommitToken, $prop->getValue($t)); + } + public function testExecuteAndExecuteUpdateWithExcludeTxnFromChangeStreams() { $sql = 'SELECT example FROM sql_query'; diff --git a/Spanner/tests/Unit/TransactionTest.php b/Spanner/tests/Unit/TransactionTest.php index de43e1dfb61..5e1d7673b15 100644 --- a/Spanner/tests/Unit/TransactionTest.php +++ b/Spanner/tests/Unit/TransactionTest.php @@ -620,6 +620,57 @@ public function testCommitWithMaxCommitDelay() $this->assertEquals(1, $transaction->getCommitStats()->getMutationCount()); } + public function testCommitSetsPrecommitTokenFromInlineBegin() + { + $precommitToken = new MultiplexedSessionPrecommitToken([ + 'precommit_token' => 'my-precommit-token', + ]); + + $operation = $this->prophesize(Operation::class); + + // Create the transaction returned by Operation::transaction() + $returnedTransaction = new Transaction( + $operation->reveal(), + $this->session->reveal(), + self::TRANSACTION, + [] + ); + $returnedTransaction->setPrecommitToken($precommitToken); + + $operation->transaction($this->session->reveal(), Argument::any()) + ->shouldBeCalled() + ->willReturn($returnedTransaction); + + // Verify that commit() receives the precommit token in options + $operation->commit( + $this->session->reveal(), + Argument::any(), + Argument::that(function ($options) use ($precommitToken) { + $this->assertArrayHasKey('precommitToken', $options); + $this->assertEquals($precommitToken, $options['precommitToken']); + return true; + }) + ) + ->shouldBeCalled() + ->willReturn($this->commitResponseWithCommitStats()); + + $transaction = new Transaction( + $operation->reveal(), + $this->session->reveal(), + null, // Null transaction ID to trigger inline begin + [ + 'begin' => ['readWrite' => []] + ] + ); + + $transaction->insert('Posts', ['foo' => 'bar']); + $transaction->commit(); + + $ref = new \ReflectionClass(Transaction::class); + $prop = $ref->getProperty('precommitToken'); + $this->assertNull($prop->getValue($transaction)); + } + public function testCommitInvalidState() { $this->expectException(\BadMethodCallException::class);