Skip to main content

google_cloud_spanner/
transaction_runner.rs

1// Copyright 2026 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::database_client::DatabaseClient;
16use crate::model::CommitResponse;
17use crate::model::request_options::Priority;
18use crate::model::transaction_options::IsolationLevel;
19use crate::model::transaction_options::read_write::ReadLockMode;
20use crate::read_only_transaction::{BeginTransactionOption, ReadContextTransactionSelector};
21use crate::read_write_transaction::{ReadWriteTransaction, ReadWriteTransactionBuilder};
22use crate::transaction_retry_policy::{
23    BasicTransactionRetryPolicy, TransactionRetryPolicy, backoff_if_aborted, is_aborted,
24};
25use google_cloud_gax::backoff_policy::BackoffPolicyArg;
26use google_cloud_gax::retry_policy::RetryPolicyArg;
27
28use std::time::Duration as StdDuration;
29use tokio::time::Instant;
30use wkt::Duration;
31
32/// A builder for a [TransactionRunner] for a read/write transaction.
33///
34/// # Example
35/// ```
36/// # use google_cloud_spanner::client::Spanner;
37/// # use google_cloud_spanner::statement::Statement;
38/// # async fn run(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
39/// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
40/// let runner = db_client.read_write_transaction().build().await?;
41///
42/// let result = runner.run(async |transaction| {
43///     let statement = Statement::builder("UPDATE MyTable SET MyColumn = 'MyValue' WHERE Id = 1").build();
44///     transaction.execute_update(statement).await?;
45///     Ok(42)
46/// }).await?;
47/// # Ok(())
48/// # }
49/// ```
50///
51/// Spanner can abort any read/write transaction at any time. A [TransactionRunner]
52/// automatically retries aborted transactions according to the configured retry policy.
53pub struct TransactionRunnerBuilder {
54    builder: ReadWriteTransactionBuilder,
55    retry_policy: Box<dyn TransactionRetryPolicy>,
56    timeout: Option<StdDuration>,
57    begin_gax_options: Option<crate::RequestOptions>,
58    commit_gax_options: Option<crate::RequestOptions>,
59}
60
61impl TransactionRunnerBuilder {
62    pub(crate) fn new(client: DatabaseClient) -> Self {
63        Self {
64            builder: ReadWriteTransactionBuilder::new(client),
65            retry_policy: Box::new(BasicTransactionRetryPolicy::default()),
66            timeout: None,
67            begin_gax_options: None,
68            commit_gax_options: None,
69        }
70    }
71
72    /// Sets the timeout for the entire transaction.
73    ///
74    /// # Example
75    /// ```
76    /// # use google_cloud_spanner::client::Spanner;
77    /// # use std::time::Duration;
78    /// # async fn run(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
79    /// # let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
80    /// let runner = db_client.read_write_transaction()
81    ///     .with_transaction_timeout(Duration::from_secs(5))
82    ///     .build()
83    ///     .await?;
84    /// # Ok(())
85    /// # }
86    /// ```
87    ///
88    /// This timeout applies to the total time spent executing the transaction, including
89    /// all statements and automatic retries. Each individual RPC within the transaction
90    /// is automatically assigned a deadline derived from the remaining time of this
91    /// overall timeout.
92    pub fn with_transaction_timeout(mut self, timeout: StdDuration) -> Self {
93        self.timeout = Some(timeout);
94        self
95    }
96
97    /// Sets the per-attempt timeout for the BeginTransaction RPC.
98    ///
99    /// # Example
100    /// ```
101    /// # use google_cloud_spanner::client::Spanner;
102    /// # use std::time::Duration;
103    /// # async fn sample(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
104    /// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
105    /// let runner = db_client.read_write_transaction()
106    ///     .with_begin_attempt_timeout(Duration::from_secs(5))
107    ///     .build()
108    ///     .await?;
109    /// # Ok(())
110    /// # }
111    /// ```
112    ///
113    /// Note: This timeout is only used if the transaction uses the `ExplicitBegin` transaction option.
114    pub fn with_begin_attempt_timeout(mut self, timeout: StdDuration) -> Self {
115        self.begin_gax_options
116            .get_or_insert_with(crate::RequestOptions::default)
117            .set_attempt_timeout(timeout);
118        self
119    }
120
121    /// Sets the retry policy for the BeginTransaction RPC.
122    ///
123    /// # Example
124    /// ```
125    /// # use google_cloud_spanner::client::Spanner;
126    /// # use google_cloud_gax::retry_policy::NeverRetry;
127    /// # async fn sample(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
128    /// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
129    /// let runner = db_client.read_write_transaction()
130    ///     .with_begin_retry_policy(NeverRetry)
131    ///     .build()
132    ///     .await?;
133    /// # Ok(())
134    /// # }
135    /// ```
136    ///
137    /// Note: This policy is only used if the transaction uses the `ExplicitBegin` transaction option.
138    pub fn with_begin_retry_policy(mut self, policy: impl Into<RetryPolicyArg>) -> Self {
139        self.begin_gax_options
140            .get_or_insert_with(crate::RequestOptions::default)
141            .set_retry_policy(policy);
142        self
143    }
144
145    /// Sets the backoff policy for the BeginTransaction RPC.
146    ///
147    /// # Example
148    /// ```
149    /// # use google_cloud_spanner::client::Spanner;
150    /// # use google_cloud_gax::exponential_backoff::ExponentialBackoff;
151    /// # async fn sample(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
152    /// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
153    /// let runner = db_client.read_write_transaction()
154    ///     .with_begin_backoff_policy(ExponentialBackoff::default())
155    ///     .build()
156    ///     .await?;
157    /// # Ok(())
158    /// # }
159    /// ```
160    ///
161    /// Note: This policy is only used if the transaction uses the `ExplicitBegin` transaction option.
162    pub fn with_begin_backoff_policy(mut self, policy: impl Into<BackoffPolicyArg>) -> Self {
163        self.begin_gax_options
164            .get_or_insert_with(crate::RequestOptions::default)
165            .set_backoff_policy(policy);
166        self
167    }
168
169    /// Sets the per-attempt timeout for the Commit RPC.
170    ///
171    /// # Example
172    /// ```
173    /// # use google_cloud_spanner::client::Spanner;
174    /// # use std::time::Duration;
175    /// # async fn sample(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
176    /// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
177    /// let runner = db_client.read_write_transaction()
178    ///     .with_commit_attempt_timeout(Duration::from_secs(5))
179    ///     .build()
180    ///     .await?;
181    /// # Ok(())
182    /// # }
183    /// ```
184    pub fn with_commit_attempt_timeout(mut self, timeout: StdDuration) -> Self {
185        self.commit_gax_options
186            .get_or_insert_with(crate::RequestOptions::default)
187            .set_attempt_timeout(timeout);
188        self
189    }
190
191    /// Sets the retry policy for the Commit RPC.
192    ///
193    /// # Example
194    /// ```
195    /// # use google_cloud_spanner::client::Spanner;
196    /// # use google_cloud_gax::retry_policy::NeverRetry;
197    /// # async fn sample(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
198    /// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
199    /// let runner = db_client.read_write_transaction()
200    ///     .with_commit_retry_policy(NeverRetry)
201    ///     .build()
202    ///     .await?;
203    /// # Ok(())
204    /// # }
205    /// ```
206    pub fn with_commit_retry_policy(mut self, policy: impl Into<RetryPolicyArg>) -> Self {
207        self.commit_gax_options
208            .get_or_insert_with(crate::RequestOptions::default)
209            .set_retry_policy(policy);
210        self
211    }
212
213    /// Sets the backoff policy for the Commit RPC.
214    ///
215    /// # Example
216    /// ```
217    /// # use google_cloud_spanner::client::Spanner;
218    /// # use google_cloud_gax::exponential_backoff::ExponentialBackoff;
219    /// # async fn sample(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
220    /// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
221    /// let runner = db_client.read_write_transaction()
222    ///     .with_commit_backoff_policy(ExponentialBackoff::default())
223    ///     .build()
224    ///     .await?;
225    /// # Ok(())
226    /// # }
227    /// ```
228    pub fn with_commit_backoff_policy(mut self, policy: impl Into<BackoffPolicyArg>) -> Self {
229        self.commit_gax_options
230            .get_or_insert_with(crate::RequestOptions::default)
231            .set_backoff_policy(policy);
232        self
233    }
234
235    /// Sets the isolation level for the transaction.
236    ///
237    /// # Example
238    /// ```
239    /// # use google_cloud_spanner::client::Spanner;
240    /// # use google_cloud_spanner::model::transaction_options::IsolationLevel;
241    /// # async fn run(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
242    /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
243    /// let runner = db_client
244    ///     .read_write_transaction()
245    ///     .set_isolation_level(IsolationLevel::Serializable)
246    ///     .build()
247    ///     .await?;
248    /// # Ok(())
249    /// # }
250    /// ```
251    ///
252    /// See also: <https://docs.cloud.google.com/spanner/docs/isolation-levels>
253    pub fn set_isolation_level(mut self, isolation_level: IsolationLevel) -> Self {
254        self.builder = self.builder.set_isolation_level(isolation_level);
255        self
256    }
257
258    /// Sets the read lock mode for the transaction.
259    ///
260    /// # Example
261    /// ```
262    /// # use google_cloud_spanner::client::Spanner;
263    /// # use google_cloud_spanner::model::transaction_options::read_write::ReadLockMode;
264    /// # async fn run(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
265    /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
266    /// let runner = db_client
267    ///     .read_write_transaction()
268    ///     .set_read_lock_mode(ReadLockMode::Pessimistic)
269    ///     .build()
270    ///     .await?;
271    /// # Ok(())
272    /// # }
273    /// ```
274    ///
275    /// See also: <https://docs.cloud.google.com/spanner/docs/concurrency-control>
276    pub fn set_read_lock_mode(mut self, read_lock_mode: ReadLockMode) -> Self {
277        self.builder = self.builder.set_read_lock_mode(read_lock_mode);
278        self
279    }
280
281    /// Sets the transaction tag for the transaction.
282    ///
283    /// # Example
284    /// ```
285    /// # use google_cloud_spanner::client::Spanner;
286    /// # async fn build_tx(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
287    /// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
288    /// let runner = db_client.read_write_transaction()
289    ///     .set_transaction_tag("my-tag")
290    ///     .build()
291    ///     .await?;
292    /// # Ok(())
293    /// # }
294    /// ```
295    ///
296    /// The tag is applied to all statements executed within the transaction.
297    ///
298    /// See also: [Troubleshooting with tags](https://docs.cloud.google.com/spanner/docs/introspection/troubleshooting-with-tags)
299    pub fn set_transaction_tag(mut self, tag: impl Into<String>) -> Self {
300        self.builder = self.builder.set_transaction_tag(tag);
301        self
302    }
303
304    /// Sets the option for how to start a transaction.
305    ///
306    /// # Example
307    /// ```
308    /// # use google_cloud_spanner::client::Spanner;
309    /// # use google_cloud_spanner::transaction::BeginTransactionOption;
310    /// # async fn run(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
311    /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
312    /// let runner = db_client
313    ///     .read_write_transaction()
314    ///     .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
315    ///     .build()
316    ///     .await?;
317    /// # Ok(())
318    /// # }
319    /// ```
320    ///
321    /// By default, the Spanner client will inline the `BeginTransaction` call with the first query
322    /// or DML statement in the transaction. This reduces the number of round-trips to Spanner that
323    /// are needed for a transaction. Setting this option to `ExplicitBegin` can be beneficial for
324    /// specific transaction shapes:
325    ///
326    /// 1. When the transaction executes multiple parallel queries at the start of the transaction.
327    ///    Only one query can include a `BeginTransaction` option, and all other queries must wait for
328    ///    the first query to return the first result before they can proceed to execute. A
329    ///    `BeginTransaction` RPC will quickly return a transaction ID and allow all queries to start
330    ///    execution in parallel once the transaction ID has been returned.
331    /// 2. When the first statement in the transaction could fail. If the statement fails, then it
332    ///    will also not start a transaction and return a transaction ID. The transaction will then
333    ///    fall back to executing a `BeginTransaction` RPC and retry the first statement.
334    ///
335    /// Default is `BeginTransactionOption::InlineBegin`.
336    pub fn with_begin_transaction_option(mut self, option: BeginTransactionOption) -> Self {
337        self.builder = self.builder.with_begin_transaction_option(option);
338        self
339    }
340
341    /// Sets the RPC priority to use for the commit of this transaction.
342    ///
343    /// # Example
344    /// ```
345    /// # use google_cloud_spanner::client::Spanner;
346    /// # use google_cloud_spanner::model::request_options::Priority;
347    /// # async fn run(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
348    /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
349    /// let runner = db_client
350    ///     .read_write_transaction()
351    ///     .set_commit_priority(Priority::Low)
352    ///     .build()
353    ///     .await?;
354    /// # Ok(())
355    /// # }
356    /// ```
357    pub fn set_commit_priority(mut self, priority: Priority) -> Self {
358        self.builder = self.builder.set_commit_priority(priority);
359        self
360    }
361
362    /// Sets the maximum commit delay for the transaction.
363    ///
364    /// # Example
365    /// ```
366    /// # use google_cloud_spanner::client::Spanner;
367    /// # use wkt::Duration;
368    /// # async fn run(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
369    /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
370    /// let runner = db_client
371    ///     .read_write_transaction()
372    ///     .set_max_commit_delay(Duration::try_from("0.2s").unwrap())
373    ///     .build()
374    ///     .await?;
375    /// # Ok(())
376    /// # }
377    /// ```
378    ///
379    /// This option allows you to specify the maximum amount of time Spanner can
380    /// adjust the commit timestamp of the transaction to allow for commit batching.
381    /// Increasing this value can increase throughput at the expense of latency.
382    /// The value must be between 0 and 500 milliseconds. If not set, or set to 0,
383    /// Spanner does not delay the commit.
384    pub fn set_max_commit_delay(mut self, delay: Duration) -> Self {
385        self.builder = self.builder.set_max_commit_delay(delay);
386        self
387    }
388
389    /// Sets whether to exclude the transaction from change streams.
390    ///
391    /// # Example
392    /// ```
393    /// # use google_cloud_spanner::client::Spanner;
394    /// # async fn build_tx(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
395    /// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
396    /// let runner = db_client.read_write_transaction()
397    ///     .set_exclude_txn_from_change_streams(true)
398    ///     .build()
399    ///     .await?;
400    /// # Ok(())
401    /// # }
402    /// ```
403    ///
404    /// When set to `true`, it prevents modifications from this transaction from being tracked in change streams.
405    /// Note that this only affects change streams that have been created with the DDL option `allow_txn_exclusion = true`.
406    /// If `allow_txn_exclusion` is not set or set to `false` for a change stream, updates made within this transaction
407    /// are recorded in that change stream regardless of this setting.
408    ///
409    /// When set to `false` or not specified, modifications from this transaction are recorded in all change streams
410    /// tracking columns modified by this transaction.
411    pub fn set_exclude_txn_from_change_streams(mut self, exclude: bool) -> Self {
412        self.builder = self.builder.set_exclude_txn_from_change_streams(exclude);
413        self
414    }
415
416    /// Sets whether to return commit stats for the transaction.
417    ///
418    /// # Example
419    /// ```
420    /// # use google_cloud_spanner::client::Spanner;
421    /// # use google_cloud_spanner::statement::Statement;
422    /// # async fn run_tx(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
423    /// # let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
424    /// let runner = db_client.read_write_transaction()
425    ///     .set_return_commit_stats(true)
426    ///     .build()
427    ///     .await?;
428    ///
429    /// let result = runner.run(async |transaction| {
430    ///     let statement = Statement::builder("UPDATE MyTable SET MyColumn = 'MyValue' WHERE Id = 1").build();
431    ///     transaction.execute_update(statement).await?;
432    ///     Ok(42)
433    /// }).await?;
434    ///
435    /// if let Some(stats) = result.commit_response.commit_stats {
436    ///     println!("Mutation count: {}", stats.mutation_count);
437    /// }
438    /// # Ok(())
439    /// # }
440    /// ```
441    ///
442    /// See also: <https://docs.cloud.google.com/spanner/docs/commit-statistics>
443    pub fn set_return_commit_stats(mut self, return_stats: bool) -> Self {
444        self.builder = self.builder.set_return_commit_stats(return_stats);
445        self
446    }
447
448    /// Sets the retry policy for the transaction.
449    ///
450    /// # Example
451    /// ```
452    /// # use std::time::Duration;
453    /// # use google_cloud_spanner::client::Spanner;
454    /// # use google_cloud_spanner::transaction::BasicTransactionRetryPolicy;
455    /// # async fn run(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
456    /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
457    ///
458    /// let retry_policy = BasicTransactionRetryPolicy::new()
459    ///     .with_max_attempts(5)
460    ///     .with_total_timeout(Duration::from_secs(60));
461    ///
462    /// let runner = db_client
463    ///     .read_write_transaction()
464    ///     .with_retry_policy(retry_policy)
465    ///     .build()
466    ///     .await?;
467    /// # Ok(())
468    /// # }
469    /// ```
470    pub fn with_retry_policy<P: TransactionRetryPolicy + 'static>(mut self, policy: P) -> Self {
471        self.retry_policy = Box::new(policy);
472        self
473    }
474
475    /// Builds a [TransactionRunner] for a read/write transaction.
476    ///
477    /// # Example
478    /// ```
479    /// # use google_cloud_spanner::client::Spanner;
480    /// # use google_cloud_spanner::statement::Statement;
481    /// # async fn run(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
482    /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
483    /// let runner = db_client.read_write_transaction().build().await?;
484    ///
485    /// let result = runner.run(async |transaction| {
486    ///     let statement = Statement::builder("UPDATE MyTable SET MyColumn = 'MyValue' WHERE Id = 1").build();
487    ///     transaction.execute_update(statement).await?;
488    ///     Ok(42)
489    /// }).await?;
490    /// # Ok(())
491    /// # }
492    /// ```
493    pub async fn build(self) -> crate::Result<TransactionRunner> {
494        Ok(TransactionRunner {
495            builder: self
496                .builder
497                .with_begin_transaction_request_options(self.begin_gax_options)
498                .with_commit_request_options(self.commit_gax_options),
499            retry_policy: self.retry_policy,
500            timeout: self.timeout,
501        })
502    }
503}
504
505/// Result of a read/write transaction executed by a [TransactionRunner].
506#[derive(Debug)]
507#[non_exhaustive]
508pub struct TransactionResult<T> {
509    /// The result returned by the closure executed within the transaction.
510    pub result: T,
511    /// The response from the commit RPC.
512    pub commit_response: CommitResponse,
513}
514
515/// A runner for read/write transactions. Aborted transactions are automatically retried.
516pub struct TransactionRunner {
517    builder: ReadWriteTransactionBuilder,
518    retry_policy: Box<dyn TransactionRetryPolicy>,
519    timeout: Option<StdDuration>,
520}
521
522impl TransactionRunner {
523    /// Runs the provided closure within the context of a read/write transaction.
524    ///
525    /// # Example
526    /// ```
527    /// # use google_cloud_spanner::client::Spanner;
528    /// # use google_cloud_spanner::statement::Statement;
529    /// # async fn run_tx(client: Spanner) -> Result<(), google_cloud_spanner::Error> {
530    /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
531    /// let runner = db_client.read_write_transaction().build().await?;
532    ///
533    /// let result = runner.run(async |transaction| {
534    ///     let statement = Statement::builder("UPDATE MyTable SET MyColumn = 'MyValue' WHERE Id = 1").build();
535    ///     transaction.execute_update(statement).await?;
536    ///     Ok(42)
537    /// }).await?;
538    /// # Ok(())
539    /// # }
540    /// ```
541    ///
542    /// If the transaction is aborted by Spanner, the closure will be retried
543    /// automatically according to the configured `TransactionRetryPolicy`.
544    ///
545    /// The transaction is automatically committed if the closure returns `Ok`.
546    /// If the closure returns `Err`, the transaction will be rolled back and
547    /// the error will be propagated.
548    pub async fn run<T, F>(mut self, mut work: F) -> crate::Result<TransactionResult<T>>
549    where
550        F: std::ops::AsyncFnMut(ReadWriteTransaction) -> crate::Result<T>,
551    {
552        let start_time = Instant::now();
553        let mut attempts: u32 = 0;
554        let backoff = crate::transaction_retry_policy::default_retry_backoff();
555        let deadline = self.timeout.map(|t| start_time + t);
556
557        let mut force_explicit_begin = false;
558        loop {
559            attempts += 1;
560
561            let mut current_tx_id = None;
562            let attempt_result = async {
563                let mut builder = self.builder.clone();
564                if force_explicit_begin {
565                    builder = builder
566                        .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin);
567                }
568                let transaction = builder.build(deadline).await.map_err(|e| (e, None))?;
569                let selector = transaction.context.transaction_selector.clone();
570
571                let result = match work(transaction.clone()).await {
572                    Ok(res) => res,
573                    Err(e) => {
574                        // We call `get_id_no_wait` here to retrieve the transaction ID without waiting.
575                        // We do not require the transaction ID to be unconditionally available here;
576                        // we only wish to capture it if the transaction successfully started prior to
577                        // failing, so it can be used as the previous transaction ID if the transaction
578                        // was aborted.
579                        let id = selector.get_id_no_wait().ok().flatten();
580                        // Rollback if the closure failed and it was not an Aborted error.
581                        if !is_aborted(&e) {
582                            let _ = transaction.rollback().await;
583                        }
584                        current_tx_id = id;
585                        return Err((e, Some(selector)));
586                    }
587                };
588
589                // `commit()` consumes `transaction`. If the commit RPC fails with an Aborted error,
590                // we still need access to the transaction ID so we can provide it as `previous_transaction_id`
591                // on retry. Cloning only `transaction_selector` preserves access to the internal state efficiently.
592                let commit_result = transaction.commit().await;
593                current_tx_id = selector.get_id_no_wait().ok().flatten();
594                let commit_response = match commit_result {
595                    Ok(r) => r,
596                    Err(e) => return Err((e, Some(selector))),
597                };
598                Ok::<TransactionResult<T>, (crate::Error, Option<ReadContextTransactionSelector>)>(
599                    TransactionResult {
600                        result,
601                        commit_response,
602                    },
603                )
604            }
605            .await;
606
607            match attempt_result {
608                Ok(res) => return Ok(res),
609                Err((e, selector_opt)) => {
610                    if is_aborted(&e) {
611                        let current_tx_id = current_tx_id.clone();
612                        self.builder = self.builder.set_previous_transaction_id(current_tx_id);
613                        force_explicit_begin = selector_opt
614                            .as_ref()
615                            .is_some_and(|s| s.is_first_statement_failed());
616                    }
617
618                    backoff_if_aborted(
619                        e,
620                        attempts,
621                        start_time.elapsed(),
622                        self.retry_policy.as_ref(),
623                        &backoff,
624                        self.builder.client.is_emulator(),
625                    )
626                    .await?;
627                }
628            }
629        }
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636    use crate::mutation::Mutation;
637    use crate::read_only_transaction::tests::{create_session_mock, setup_db_client};
638    use crate::transaction_retry_policy::tests::create_aborted_status;
639    use gaxi::grpc::tonic;
640    use google_cloud_gax::exponential_backoff::ExponentialBackoff;
641    use google_cloud_gax::retry_policy::NeverRetry;
642    use google_cloud_test_macros::tokio_test_no_panics;
643    use prost_types::value::Kind;
644    use spanner_grpc_mock::google::spanner::v1;
645    use spanner_grpc_mock::google::spanner::v1::CommitResponse;
646    use spanner_grpc_mock::google::spanner::v1::commit_request::Transaction as CommitTransaction;
647    use spanner_grpc_mock::google::spanner::v1::commit_response::CommitStats;
648    use spanner_grpc_mock::google::spanner::v1::mutation::Operation;
649    use spanner_grpc_mock::google::spanner::v1::transaction_options::Mode;
650    use spanner_grpc_mock::google::spanner::v1::transaction_selector::Selector as ProtoSelector;
651    use std::sync::Mutex;
652    use std::sync::mpsc::channel as std_channel;
653    use std::time::Duration as StdDuration;
654    use std::time::Duration as StdTimeDuration;
655    use tokio::sync::oneshot::channel as oneshot_channel;
656
657    fn expect_begin_transaction(
658        mock: &mut spanner_grpc_mock::MockSpanner,
659        times: usize,
660        transaction_id: Vec<u8>,
661    ) {
662        mock.expect_begin_transaction()
663            .times(times)
664            .returning(move |req| {
665                let req = req.into_inner();
666                assert_eq!(
667                    req.session,
668                    "projects/p/instances/i/databases/d/sessions/123"
669                );
670                Ok(tonic::Response::new(v1::Transaction {
671                    id: transaction_id.clone(),
672                    ..Default::default()
673                }))
674            });
675    }
676
677    async fn execute_test_runner(
678        mock: spanner_grpc_mock::MockSpanner,
679        begin_transaction_option: BeginTransactionOption,
680    ) -> Result<i64, crate::Error> {
681        let (db_client, server) = setup_db_client(mock).await;
682        let runner = TransactionRunnerBuilder::new(db_client)
683            .with_begin_transaction_option(begin_transaction_option)
684            .build()
685            .await
686            .unwrap();
687        tokio::select! {
688            res = runner.run(async |tx| {
689                let count = tx.execute_update("UPDATE Users SET active = true").await?;
690                Ok(count)
691            }) => res.map(|r| r.result),
692            err = server => panic!("Mock server panicked or terminated unexpectedly: {:?}", err),
693        }
694    }
695
696    fn commit_response() -> Result<tonic::Response<v1::CommitResponse>, tonic::Status> {
697        Ok(tonic::Response::new(v1::CommitResponse {
698            commit_timestamp: Some(prost_types::Timestamp {
699                seconds: 123456789,
700                nanos: 0,
701            }),
702            ..Default::default()
703        }))
704    }
705
706    fn row_count_exact_response(
707        count: i64,
708    ) -> Result<tonic::Response<v1::ResultSet>, tonic::Status> {
709        Ok(tonic::Response::new(v1::ResultSet {
710            stats: Some(v1::ResultSetStats {
711                row_count: Some(v1::result_set_stats::RowCount::RowCountExact(count)),
712                ..Default::default()
713            }),
714            ..Default::default()
715        }))
716    }
717
718    #[test]
719    fn auto_traits() {
720        static_assertions::assert_impl_all!(TransactionRunnerBuilder: Send, Sync);
721        static_assertions::assert_impl_all!(TransactionRunner: Send, Sync);
722    }
723
724    #[tokio_test_no_panics]
725    async fn execute_run_success_explicit() {
726        run_success(BeginTransactionOption::ExplicitBegin).await;
727    }
728
729    #[tokio_test_no_panics]
730    async fn execute_run_success_inline() {
731        run_success(BeginTransactionOption::InlineBegin).await;
732    }
733
734    async fn run_success(begin_transaction_option: BeginTransactionOption) {
735        let mut mock = create_session_mock();
736
737        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
738            expect_begin_transaction(&mut mock, 1, vec![1, 2, 3]);
739        }
740
741        mock.expect_execute_sql().once().returning(move |req| {
742            let req = req.into_inner();
743            assert_eq!(req.sql, "UPDATE Users SET active = true");
744            assert_eq!(req.seqno, 1);
745
746            if begin_transaction_option == BeginTransactionOption::InlineBegin {
747                let transaction = req
748                    .transaction
749                    .as_ref()
750                    .expect("transaction options required for inline begin");
751                let selector = transaction.selector.as_ref().expect("selector required");
752                assert!(matches!(
753                    selector,
754                    v1::transaction_selector::Selector::Begin(_)
755                ));
756            }
757
758            let mut metadata = v1::ResultSetMetadata {
759                ..Default::default()
760            };
761            if begin_transaction_option == BeginTransactionOption::InlineBegin {
762                metadata.transaction = Some(v1::Transaction {
763                    id: vec![1, 2, 3],
764                    ..Default::default()
765                });
766            }
767
768            Ok(tonic::Response::new(v1::ResultSet {
769                metadata: Some(metadata),
770                stats: Some(v1::ResultSetStats {
771                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
772                    ..Default::default()
773                }),
774                ..Default::default()
775            }))
776        });
777
778        mock.expect_commit().once().returning(|req| {
779            let req = req.into_inner();
780            assert_eq!(
781                req.transaction,
782                Some(v1::commit_request::Transaction::TransactionId(vec![
783                    1, 2, 3
784                ]))
785            );
786            commit_response()
787        });
788
789        let res = execute_test_runner(mock, begin_transaction_option)
790            .await
791            .unwrap();
792        assert_eq!(res, 1);
793    }
794
795    #[tokio_test_no_panics]
796    async fn execute_run_success_with_commit_stats_explicit() {
797        run_success_with_commit_stats(BeginTransactionOption::ExplicitBegin).await;
798    }
799
800    #[tokio_test_no_panics]
801    async fn execute_run_success_with_commit_stats_inline() {
802        run_success_with_commit_stats(BeginTransactionOption::InlineBegin).await;
803    }
804
805    async fn run_success_with_commit_stats(begin_transaction_option: BeginTransactionOption) {
806        let mut mock = create_session_mock();
807
808        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
809            expect_begin_transaction(&mut mock, 1, vec![1, 2, 3]);
810        }
811
812        mock.expect_execute_sql().once().returning(move |req| {
813            let req = req.into_inner();
814            assert_eq!(req.sql, "UPDATE Users SET active = true");
815
816            if begin_transaction_option == BeginTransactionOption::InlineBegin {
817                let transaction = req
818                    .transaction
819                    .as_ref()
820                    .expect("transaction options required for inline begin");
821                let selector = transaction.selector.as_ref().expect("selector required");
822                assert!(matches!(
823                    selector,
824                    v1::transaction_selector::Selector::Begin(_)
825                ));
826            }
827
828            let mut metadata = v1::ResultSetMetadata {
829                ..Default::default()
830            };
831            if begin_transaction_option == BeginTransactionOption::InlineBegin {
832                metadata.transaction = Some(v1::Transaction {
833                    id: vec![1, 2, 3],
834                    ..Default::default()
835                });
836            }
837
838            Ok(tonic::Response::new(v1::ResultSet {
839                metadata: Some(metadata),
840                stats: Some(v1::ResultSetStats {
841                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
842                    ..Default::default()
843                }),
844                ..Default::default()
845            }))
846        });
847
848        mock.expect_commit().once().returning(|req| {
849            let req = req.into_inner();
850            assert!(req.return_commit_stats);
851            Ok(tonic::Response::new(CommitResponse {
852                commit_timestamp: Some(prost_types::Timestamp {
853                    seconds: 123456789,
854                    nanos: 0,
855                }),
856                commit_stats: Some(CommitStats { mutation_count: 5 }),
857                ..Default::default()
858            }))
859        });
860
861        let (db_client, _server) = setup_db_client(mock).await;
862        let runner = TransactionRunnerBuilder::new(db_client)
863            .set_return_commit_stats(true)
864            .with_begin_transaction_option(begin_transaction_option)
865            .build()
866            .await
867            .unwrap();
868
869        let res = runner
870            .run(async |tx| {
871                let count = tx.execute_update("UPDATE Users SET active = true").await?;
872                Ok(count)
873            })
874            .await
875            .unwrap();
876
877        assert_eq!(res.result, 1);
878        assert!(res.commit_response.commit_stats.is_some());
879        assert_eq!(
880            res.commit_response
881                .commit_stats
882                .expect("Commit stats should be present")
883                .mutation_count,
884            5
885        );
886    }
887
888    #[tokio_test_no_panics]
889    async fn execute_run_with_aborted_retry_explicit() -> anyhow::Result<()> {
890        run_with_aborted_retry(BeginTransactionOption::ExplicitBegin).await
891    }
892
893    #[tokio_test_no_panics]
894    async fn execute_run_with_aborted_retry_inline() -> anyhow::Result<()> {
895        run_with_aborted_retry(BeginTransactionOption::InlineBegin).await
896    }
897
898    async fn run_with_aborted_retry(
899        begin_transaction_option: BeginTransactionOption,
900    ) -> anyhow::Result<()> {
901        let mut mock = create_session_mock();
902        let mut seq = mockall::Sequence::new();
903
904        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
905            mock.expect_begin_transaction()
906                .once()
907                .in_sequence(&mut seq)
908                .returning(move |req| {
909                    let req = req.into_inner();
910                    assert_eq!(
911                        req.session,
912                        "projects/p/instances/i/databases/d/sessions/123"
913                    );
914                    Ok(tonic::Response::new(v1::Transaction {
915                        id: vec![9, 9, 9],
916                        ..Default::default()
917                    }))
918                });
919        }
920
921        if begin_transaction_option == BeginTransactionOption::InlineBegin {
922            // Attempt 1: execute_sql fails with Aborted
923            mock.expect_execute_sql()
924                .once()
925                .in_sequence(&mut seq)
926                .returning(move |req| {
927                    let req = req.into_inner();
928                    let transaction = req
929                        .transaction
930                        .as_ref()
931                        .expect("transaction options required for inline begin");
932                    let selector = transaction.selector.as_ref().expect("selector required");
933                    assert!(matches!(
934                        selector,
935                        v1::transaction_selector::Selector::Begin(_)
936                    ));
937
938                    Err(create_aborted_status(std::time::Duration::from_nanos(1)))
939                });
940        } else {
941            mock.expect_execute_sql()
942                .once()
943                .in_sequence(&mut seq)
944                .returning(move |_req| {
945                    Err(create_aborted_status(std::time::Duration::from_nanos(1)))
946                });
947        }
948
949        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
950            mock.expect_begin_transaction()
951                .once()
952                .in_sequence(&mut seq)
953                .returning(move |req| {
954                    let req = req.into_inner();
955                    assert_eq!(req.session, "projects/p/instances/i/databases/d/sessions/123");
956
957                    let options = req.options.as_ref().expect("options required on retry");
958                    let read_write = options.mode.as_ref().expect("mode required on retry");
959                    match read_write {
960                        Mode::ReadWrite(rw) => {
961                            assert_eq!(rw.multiplexed_session_previous_transaction_id, vec![9, 9, 9], "previous_transaction_id should be set to the ID of the aborted transaction");
962                        }
963                        _ => panic!("Expected ReadWrite mode"),
964                    }
965
966                    Ok(tonic::Response::new(v1::Transaction {
967                        id: vec![8, 8, 8],
968                        ..Default::default()
969                    }))
970                });
971        }
972
973        // Attempt 2 (retry of closure)
974        mock.expect_execute_sql()
975            .once()
976            .in_sequence(&mut seq)
977            .returning(move |req| {
978                if begin_transaction_option == BeginTransactionOption::InlineBegin {
979                    let req = req.into_inner();
980                    let transaction = req
981                        .transaction
982                        .as_ref()
983                        .expect("transaction options required for inline begin");
984                    let selector = transaction.selector.as_ref().expect("selector required");
985                    assert!(matches!(
986                        selector,
987                        v1::transaction_selector::Selector::Begin(_)
988                    ));
989
990                    let options = match selector {
991                        v1::transaction_selector::Selector::Begin(o) => o,
992                        _ => panic!("Expected Begin"),
993                    };
994                    let read_write = options.mode.as_ref().expect("mode required");
995                    match read_write {
996                        Mode::ReadWrite(rw) => {
997                            assert!(rw.multiplexed_session_previous_transaction_id.is_empty());
998                        }
999                        _ => panic!("Expected ReadWrite"),
1000                    }
1001                }
1002
1003                let mut metadata = v1::ResultSetMetadata {
1004                    ..Default::default()
1005                };
1006                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1007                    metadata.transaction = Some(v1::Transaction {
1008                        id: vec![8, 8, 8],
1009                        ..Default::default()
1010                    });
1011                }
1012
1013                Ok(tonic::Response::new(v1::ResultSet {
1014                    metadata: Some(metadata),
1015                    stats: Some(v1::ResultSetStats {
1016                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(5)),
1017                        ..Default::default()
1018                    }),
1019                    ..Default::default()
1020                }))
1021            });
1022
1023        mock.expect_commit()
1024            .once()
1025            .returning(|_req| commit_response());
1026
1027        let res = execute_test_runner(mock, begin_transaction_option)
1028            .await
1029            .expect("runner should succeed");
1030        assert_eq!(res, 5);
1031        Ok(())
1032    }
1033
1034    #[tokio_test_no_panics]
1035    async fn execute_run_query_stream_with_aborted_retry_explicit() -> anyhow::Result<()> {
1036        run_query_stream_with_aborted_retry(BeginTransactionOption::ExplicitBegin).await
1037    }
1038
1039    #[tokio_test_no_panics]
1040    async fn execute_run_query_stream_with_aborted_retry_inline() -> anyhow::Result<()> {
1041        run_query_stream_with_aborted_retry(BeginTransactionOption::InlineBegin).await
1042    }
1043
1044    async fn run_query_stream_with_aborted_retry(
1045        begin_transaction_option: BeginTransactionOption,
1046    ) -> anyhow::Result<()> {
1047        let mut mock = create_session_mock();
1048        let mut seq = mockall::Sequence::new();
1049
1050        let tx_id_1 = vec![9, 9, 9];
1051        let tx_id_2 = vec![8, 8, 8];
1052
1053        let tx_id_1_c1 = tx_id_1.clone();
1054        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1055            mock.expect_begin_transaction()
1056                .once()
1057                .in_sequence(&mut seq)
1058                .returning(move |_| {
1059                    Ok(tonic::Response::new(v1::Transaction {
1060                        id: tx_id_1_c1.clone(),
1061                        ..Default::default()
1062                    }))
1063                });
1064        }
1065
1066        let tx_id_1_c2 = tx_id_1.clone();
1067        mock.expect_execute_streaming_sql()
1068            .once()
1069            .in_sequence(&mut seq)
1070            .returning(move |req| {
1071                let req = req.into_inner();
1072                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1073                    let transaction = req
1074                        .transaction
1075                        .as_ref()
1076                        .expect("transaction options required for inline begin");
1077                    let selector = transaction.selector.as_ref().expect("selector required");
1078                    assert!(matches!(
1079                        selector,
1080                        v1::transaction_selector::Selector::Begin(_)
1081                    ));
1082                }
1083
1084                let mut rs = v1::PartialResultSet {
1085                    metadata: Some(v1::ResultSetMetadata {
1086                        row_type: Some(v1::StructType {
1087                            fields: vec![Default::default()],
1088                        }),
1089                        ..Default::default()
1090                    }),
1091                    values: vec![prost_types::Value {
1092                        kind: Some(prost_types::value::Kind::StringValue("1".to_string())),
1093                    }],
1094                    resume_token: b"token1".to_vec(),
1095                    ..Default::default()
1096                };
1097
1098                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1099                    rs.metadata.as_mut().unwrap().transaction = Some(v1::Transaction {
1100                        id: tx_id_1_c2.clone(),
1101                        ..Default::default()
1102                    });
1103                }
1104
1105                let (tx, rx) = tokio::sync::mpsc::channel(2);
1106                tx.try_send(Ok(rs)).unwrap();
1107                tx.try_send(Err(tonic::Status::new(tonic::Code::Aborted, "aborted")))
1108                    .unwrap();
1109                Ok(tonic::Response::from(rx))
1110            });
1111
1112        let tx_id_2_c1 = tx_id_2.clone();
1113        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1114            mock.expect_begin_transaction()
1115                .once()
1116                .in_sequence(&mut seq)
1117                .returning(move |req| {
1118                    let req = req.into_inner();
1119                    let options = req.options.as_ref().expect("options required on retry");
1120                    let read_write = options.mode.as_ref().expect("mode required on retry");
1121                    match read_write {
1122                        Mode::ReadWrite(rw) => {
1123                            assert_eq!(
1124                                rw.multiplexed_session_previous_transaction_id,
1125                                vec![9, 9, 9]
1126                            );
1127                        }
1128                        _ => panic!("Expected ReadWrite mode"),
1129                    }
1130
1131                    Ok(tonic::Response::new(v1::Transaction {
1132                        id: tx_id_2_c1.clone(),
1133                        ..Default::default()
1134                    }))
1135                });
1136        }
1137
1138        let tx_id_2_c2 = tx_id_2.clone();
1139        mock.expect_execute_streaming_sql()
1140            .once()
1141            .in_sequence(&mut seq)
1142            .returning(move |req| {
1143                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1144                    let req = req.into_inner();
1145                    let transaction = req
1146                        .transaction
1147                        .as_ref()
1148                        .expect("transaction options required for inline begin");
1149                    let selector = transaction.selector.as_ref().expect("selector required");
1150                    assert!(matches!(
1151                        selector,
1152                        v1::transaction_selector::Selector::Begin(_)
1153                    ));
1154
1155                    let options = match selector {
1156                        v1::transaction_selector::Selector::Begin(o) => o,
1157                        _ => panic!("Expected Begin"),
1158                    };
1159                    let read_write = options.mode.as_ref().expect("mode required");
1160                    match read_write {
1161                        Mode::ReadWrite(rw) => {
1162                            assert_eq!(
1163                                rw.multiplexed_session_previous_transaction_id,
1164                                vec![9, 9, 9]
1165                            );
1166                        }
1167                        _ => panic!("Expected ReadWrite"),
1168                    }
1169                }
1170
1171                let mut rs = v1::PartialResultSet {
1172                    metadata: Some(v1::ResultSetMetadata {
1173                        row_type: Some(v1::StructType {
1174                            fields: vec![Default::default()],
1175                        }),
1176                        ..Default::default()
1177                    }),
1178                    values: vec![prost_types::Value {
1179                        kind: Some(prost_types::value::Kind::StringValue("1".to_string())),
1180                    }],
1181                    last: true,
1182                    ..Default::default()
1183                };
1184
1185                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1186                    rs.metadata.as_mut().unwrap().transaction = Some(v1::Transaction {
1187                        id: tx_id_2_c2.clone(),
1188                        ..Default::default()
1189                    });
1190                }
1191
1192                let (tx, rx) = tokio::sync::mpsc::channel(2);
1193                tx.try_send(Ok(rs)).unwrap();
1194                Ok(tonic::Response::from(rx))
1195            });
1196
1197        mock.expect_commit()
1198            .once()
1199            .returning(|_req| commit_response());
1200
1201        let (db_client, _server) = setup_db_client(mock).await;
1202        let runner = TransactionRunnerBuilder::new(db_client)
1203            .with_begin_transaction_option(begin_transaction_option)
1204            .build()
1205            .await?;
1206
1207        let mut attempt_counter = 0;
1208        let res = runner
1209            .run(async |tx| {
1210                attempt_counter += 1;
1211                let mut rs = tx.execute_query("SELECT 1").await?;
1212                let mut last_val = None;
1213                while let Some(row_res) = rs.next().await {
1214                    let row = row_res?;
1215                    last_val = Some(row.raw_values()[0].as_string().to_string());
1216                }
1217                Ok(last_val.unwrap())
1218            })
1219            .await?;
1220
1221        assert_eq!(res.result, "1");
1222        assert_eq!(attempt_counter, 2);
1223        Ok(())
1224    }
1225
1226    #[tokio_test_no_panics]
1227    async fn execute_run_with_non_aborted_error_explicit() {
1228        run_with_non_aborted_error(BeginTransactionOption::ExplicitBegin).await;
1229    }
1230
1231    #[tokio_test_no_panics]
1232    async fn execute_run_with_non_aborted_error_inline() {
1233        run_with_non_aborted_error(BeginTransactionOption::InlineBegin).await;
1234    }
1235
1236    async fn run_with_non_aborted_error(begin_transaction_option: BeginTransactionOption) {
1237        let mut mock = create_session_mock();
1238
1239        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1240            expect_begin_transaction(&mut mock, 1, vec![9, 9, 9]);
1241        }
1242
1243        // Let execute_sql return an error to trigger a rollback.
1244        mock.expect_execute_sql().once().returning(move |_req| {
1245            Err(tonic::Status::new(
1246                tonic::Code::PermissionDenied,
1247                "permission denied",
1248            ))
1249        });
1250
1251        if begin_transaction_option == BeginTransactionOption::InlineBegin {
1252            expect_begin_transaction(&mut mock, 1, vec![9, 9, 9]);
1253            mock.expect_execute_sql().once().returning(move |_req| {
1254                Err(tonic::Status::new(
1255                    tonic::Code::PermissionDenied,
1256                    "permission denied",
1257                ))
1258            });
1259        }
1260
1261        // Must explicitly trigger rollback
1262        mock.expect_rollback()
1263            .once()
1264            .returning(|_req| Ok(tonic::Response::new(())));
1265
1266        let res = execute_test_runner(mock, begin_transaction_option).await;
1267
1268        assert!(res.is_err());
1269        let err = res.unwrap_err();
1270        if let Some(status) = err.status() {
1271            assert_eq!(
1272                status.code,
1273                google_cloud_gax::error::rpc::Code::PermissionDenied
1274            );
1275        } else {
1276            panic!("Expected GRPC error");
1277        }
1278    }
1279
1280    #[tokio_test_no_panics]
1281    async fn execute_run_with_non_aborted_error_and_rollback_fails_explicit() {
1282        run_with_non_aborted_error_and_rollback_fails(BeginTransactionOption::ExplicitBegin).await;
1283    }
1284
1285    #[tokio_test_no_panics]
1286    async fn execute_run_with_non_aborted_error_and_rollback_fails_inline() {
1287        run_with_non_aborted_error_and_rollback_fails(BeginTransactionOption::InlineBegin).await;
1288    }
1289
1290    async fn run_with_non_aborted_error_and_rollback_fails(
1291        begin_transaction_option: BeginTransactionOption,
1292    ) {
1293        let mut mock = create_session_mock();
1294
1295        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1296            expect_begin_transaction(&mut mock, 1, vec![9, 9, 9]);
1297        }
1298
1299        // Let execute_sql return an error to trigger a rollback.
1300        mock.expect_execute_sql().once().returning(move |_req| {
1301            Err(tonic::Status::new(
1302                tonic::Code::PermissionDenied,
1303                "permission denied",
1304            ))
1305        });
1306
1307        if begin_transaction_option == BeginTransactionOption::InlineBegin {
1308            expect_begin_transaction(&mut mock, 1, vec![9, 9, 9]);
1309            mock.expect_execute_sql().once().returning(move |_req| {
1310                Err(tonic::Status::new(
1311                    tonic::Code::PermissionDenied,
1312                    "permission denied",
1313                ))
1314            });
1315        }
1316
1317        // Force the rollback itself to fail as well
1318        mock.expect_rollback()
1319            .once()
1320            .returning(|_req| Err(tonic::Status::new(tonic::Code::Internal, "rollback failed")));
1321
1322        let res = execute_test_runner(mock, begin_transaction_option).await;
1323
1324        // Verify the user unequivocally receives the PRIMARY original error
1325        assert!(res.is_err());
1326        let err = res.unwrap_err();
1327        if let Some(status) = err.status() {
1328            assert_eq!(
1329                status.code,
1330                google_cloud_gax::error::rpc::Code::PermissionDenied
1331            );
1332        } else {
1333            panic!("Expected GRPC error");
1334        }
1335    }
1336
1337    #[tokio_test_no_panics]
1338    async fn execute_run_commit_aborted_retry_explicit() {
1339        run_commit_aborted_retry(BeginTransactionOption::ExplicitBegin).await;
1340    }
1341
1342    #[tokio_test_no_panics]
1343    async fn execute_run_commit_aborted_retry_inline() {
1344        run_commit_aborted_retry(BeginTransactionOption::InlineBegin).await;
1345    }
1346
1347    async fn run_commit_aborted_retry(begin_transaction_option: BeginTransactionOption) {
1348        let mut mock = create_session_mock();
1349
1350        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1351            expect_begin_transaction(&mut mock, 2, vec![9, 9, 9]);
1352        }
1353
1354        let mut attempt = 0;
1355        mock.expect_execute_sql().times(2).returning(move |req| {
1356            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1357                let req = req.into_inner();
1358                let transaction = req
1359                    .transaction
1360                    .as_ref()
1361                    .expect("transaction options required for inline begin");
1362                let selector = transaction.selector.as_ref().expect("selector required");
1363                assert!(matches!(
1364                    selector,
1365                    v1::transaction_selector::Selector::Begin(_)
1366                ));
1367
1368                attempt += 1;
1369                if attempt == 2 {
1370                    let options = match selector {
1371                        v1::transaction_selector::Selector::Begin(o) => o,
1372                        _ => panic!("Expected Begin"),
1373                    };
1374                    let read_write = options.mode.as_ref().expect("mode required");
1375                    match read_write {
1376                        Mode::ReadWrite(rw) => {
1377                            assert_eq!(
1378                                rw.multiplexed_session_previous_transaction_id,
1379                                vec![9, 9, 9]
1380                            );
1381                        }
1382                        _ => panic!("Expected ReadWrite"),
1383                    }
1384                }
1385
1386                let mut metadata = v1::ResultSetMetadata {
1387                    ..Default::default()
1388                };
1389                metadata.transaction = Some(v1::Transaction {
1390                    id: vec![9, 9, 9],
1391                    ..Default::default()
1392                });
1393
1394                return Ok(tonic::Response::new(v1::ResultSet {
1395                    metadata: Some(metadata),
1396                    stats: Some(v1::ResultSetStats {
1397                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(5)),
1398                        ..Default::default()
1399                    }),
1400                    ..Default::default()
1401                }));
1402            }
1403            row_count_exact_response(5)
1404        });
1405
1406        let mut commit_attempt = 0;
1407        mock.expect_commit().times(2).returning(move |_req| {
1408            commit_attempt += 1;
1409            if commit_attempt == 1 {
1410                Err(create_aborted_status(std::time::Duration::from_nanos(1)))
1411            } else {
1412                commit_response()
1413            }
1414        });
1415
1416        let res = execute_test_runner(mock, begin_transaction_option)
1417            .await
1418            .unwrap();
1419        assert_eq!(res, 5);
1420    }
1421
1422    #[tokio_test_no_panics]
1423    async fn execute_run_begin_transaction_fails_explicit() {
1424        run_begin_transaction_fails(BeginTransactionOption::ExplicitBegin).await;
1425    }
1426
1427    #[tokio_test_no_panics]
1428    async fn execute_run_begin_transaction_fails_inline() {
1429        run_begin_transaction_fails(BeginTransactionOption::InlineBegin).await;
1430    }
1431
1432    async fn run_begin_transaction_fails(begin_transaction_option: BeginTransactionOption) {
1433        let mut mock = create_session_mock();
1434        let mut seq = mockall::Sequence::new();
1435
1436        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1437            mock.expect_begin_transaction()
1438                .once()
1439                .returning(|_req| Err(tonic::Status::new(tonic::Code::Internal, "internal error")));
1440        } else {
1441            mock.expect_execute_sql()
1442                .once()
1443                .in_sequence(&mut seq)
1444                .returning(move |req| {
1445                    let req = req.into_inner();
1446                    let transaction = req
1447                        .transaction
1448                        .as_ref()
1449                        .expect("transaction options required for inline begin");
1450                    let selector = transaction.selector.as_ref().expect("selector required");
1451                    assert!(matches!(
1452                        selector,
1453                        v1::transaction_selector::Selector::Begin(_)
1454                    ));
1455
1456                    Err(tonic::Status::new(tonic::Code::Internal, "internal error"))
1457                });
1458
1459            mock.expect_begin_transaction()
1460                .once()
1461                .in_sequence(&mut seq)
1462                .returning(|_req| Err(tonic::Status::new(tonic::Code::Internal, "internal error")));
1463        }
1464
1465        let res = execute_test_runner(mock, begin_transaction_option).await;
1466
1467        assert!(res.is_err());
1468        let err = res.unwrap_err();
1469        if let Some(status) = err.status() {
1470            assert_eq!(status.code, google_cloud_gax::error::rpc::Code::Internal);
1471        } else {
1472            panic!("Expected GRPC error");
1473        }
1474    }
1475
1476    #[tokio_test_no_panics]
1477    async fn builder_options() {
1478        use crate::transaction_retry_policy::BasicTransactionRetryPolicy;
1479
1480        let mock = create_session_mock();
1481        let (db_client, _server) = setup_db_client(mock).await;
1482
1483        let retry_policy = BasicTransactionRetryPolicy::new()
1484            .with_max_attempts(1)
1485            .with_total_timeout(std::time::Duration::from_secs(10));
1486
1487        // Validate builder chaining safely accepts and compiles options dynamically
1488        let _runner = TransactionRunnerBuilder::new(db_client)
1489            .set_isolation_level(IsolationLevel::Serializable)
1490            .set_read_lock_mode(ReadLockMode::Pessimistic)
1491            .with_retry_policy(retry_policy)
1492            .build()
1493            .await
1494            .unwrap();
1495    }
1496
1497    #[tokio_test_no_panics]
1498    async fn execute_run_batch_dml_aborted_retry_explicit() {
1499        run_batch_dml_aborted_retry(BeginTransactionOption::ExplicitBegin).await;
1500    }
1501
1502    #[tokio_test_no_panics]
1503    async fn execute_run_batch_dml_aborted_retry_inline() {
1504        run_batch_dml_aborted_retry(BeginTransactionOption::InlineBegin).await;
1505    }
1506
1507    async fn run_batch_dml_aborted_retry(begin_transaction_option: BeginTransactionOption) {
1508        use crate::batch_dml::BatchDml;
1509        use crate::statement::Statement;
1510        use gaxi::grpc::tonic::Code;
1511        use spanner_grpc_mock::google::rpc::Status;
1512        use spanner_grpc_mock::google::spanner::v1::result_set_stats::RowCount;
1513
1514        let mut mock = create_session_mock();
1515
1516        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1517            expect_begin_transaction(&mut mock, 2, vec![9, 9, 9]);
1518        }
1519
1520        let mut seq = mockall::Sequence::new();
1521        mock.expect_execute_batch_dml()
1522            .once()
1523            .in_sequence(&mut seq)
1524            .returning(move |req| {
1525                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1526                    let req = req.into_inner();
1527                    let selector = req
1528                        .transaction
1529                        .expect("missing transaction selector")
1530                        .selector
1531                        .expect("missing selector");
1532                    assert!(matches!(
1533                        selector,
1534                        v1::transaction_selector::Selector::Begin(_)
1535                    ));
1536                }
1537
1538                // Return a successful response but with an embedded aborted status.
1539                let status = Status {
1540                    code: Code::Aborted as i32,
1541                    message: "transaction aborted".to_string(),
1542                    ..Default::default()
1543                };
1544
1545                let mut metadata = v1::ResultSetMetadata {
1546                    ..Default::default()
1547                };
1548                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1549                    metadata.transaction = Some(v1::Transaction {
1550                        id: vec![9, 9, 9],
1551                        ..Default::default()
1552                    });
1553                }
1554
1555                Ok(tonic::Response::new(v1::ExecuteBatchDmlResponse {
1556                    result_sets: vec![v1::ResultSet {
1557                        metadata: Some(metadata),
1558                        stats: Some(v1::ResultSetStats {
1559                            row_count: Some(RowCount::RowCountExact(1)),
1560                            ..Default::default()
1561                        }),
1562                        ..Default::default()
1563                    }],
1564                    status: Some(status),
1565                    ..Default::default()
1566                }))
1567            });
1568        mock.expect_execute_batch_dml()
1569            .once()
1570            .in_sequence(&mut seq)
1571            .returning(move |req| {
1572                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1573                    let req = req.into_inner();
1574                    let selector = req
1575                        .transaction
1576                        .expect("missing transaction selector")
1577                        .selector
1578                        .expect("missing selector");
1579                    assert!(matches!(
1580                        selector,
1581                        v1::transaction_selector::Selector::Begin(_)
1582                    ));
1583                }
1584
1585                let mut metadata = v1::ResultSetMetadata {
1586                    ..Default::default()
1587                };
1588                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1589                    metadata.transaction = Some(v1::Transaction {
1590                        id: vec![9, 9, 9],
1591                        ..Default::default()
1592                    });
1593                }
1594
1595                // Return success after the retry.
1596                Ok(tonic::Response::new(v1::ExecuteBatchDmlResponse {
1597                    result_sets: vec![v1::ResultSet {
1598                        metadata: Some(metadata),
1599                        stats: Some(v1::ResultSetStats {
1600                            row_count: Some(RowCount::RowCountExact(5)),
1601                            ..Default::default()
1602                        }),
1603                        ..Default::default()
1604                    }],
1605                    ..Default::default()
1606                }))
1607            });
1608
1609        mock.expect_commit()
1610            .once()
1611            .returning(move |_| commit_response());
1612
1613        let (db_client, _) = setup_db_client(mock).await;
1614        let runner = TransactionRunnerBuilder::new(db_client)
1615            .with_begin_transaction_option(begin_transaction_option)
1616            .build()
1617            .await
1618            .expect("failed to build TransactionRunner");
1619
1620        let mut attempt_counter = 0;
1621
1622        // TransactionRunner retries the closure on transaction aborts
1623        let res = runner
1624            .run(async |tx| {
1625                attempt_counter += 1;
1626                let stmt = Statement::builder("UPDATE t SET c = 1").build();
1627                let batch = BatchDml::builder().add_statement(stmt).build();
1628                let counts = tx.execute_batch_update(batch).await?;
1629                Ok(counts)
1630            })
1631            .await
1632            .expect("transaction failed");
1633
1634        assert_eq!(res.result, vec![5]);
1635        assert_eq!(attempt_counter, 2);
1636    }
1637
1638    #[tokio_test_no_panics]
1639    async fn execute_run_with_transaction_tag_explicit() -> anyhow::Result<()> {
1640        run_with_transaction_tag(BeginTransactionOption::ExplicitBegin).await
1641    }
1642
1643    #[tokio_test_no_panics]
1644    async fn execute_run_with_transaction_tag_inline() -> anyhow::Result<()> {
1645        run_with_transaction_tag(BeginTransactionOption::InlineBegin).await
1646    }
1647
1648    async fn run_with_transaction_tag(
1649        begin_transaction_option: BeginTransactionOption,
1650    ) -> anyhow::Result<()> {
1651        let mut mock = create_session_mock();
1652
1653        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1654            mock.expect_begin_transaction().once().returning(|req| {
1655                let req = req.into_inner();
1656                // Check if the transaction tag is correctly propagated.
1657                assert_eq!(
1658                    req.request_options
1659                        .expect("Missing request_options")
1660                        .transaction_tag,
1661                    "my-test-tag"
1662                );
1663
1664                Ok(tonic::Response::new(v1::Transaction {
1665                    id: vec![9, 9, 9],
1666                    ..Default::default()
1667                }))
1668            });
1669        }
1670
1671        mock.expect_execute_sql().once().returning(move |req| {
1672            let req = req.into_inner();
1673            assert_eq!(
1674                req.request_options
1675                    .expect("Missing request_options")
1676                    .transaction_tag,
1677                "my-test-tag"
1678            );
1679
1680            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1681                let transaction = req
1682                    .transaction
1683                    .as_ref()
1684                    .expect("transaction options required for inline begin");
1685                let selector = transaction.selector.as_ref().expect("selector required");
1686                assert!(matches!(
1687                    selector,
1688                    v1::transaction_selector::Selector::Begin(_)
1689                ));
1690            }
1691
1692            let mut metadata = v1::ResultSetMetadata {
1693                ..Default::default()
1694            };
1695            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1696                metadata.transaction = Some(v1::Transaction {
1697                    id: vec![9, 9, 9],
1698                    ..Default::default()
1699                });
1700            }
1701
1702            Ok(tonic::Response::new(v1::ResultSet {
1703                metadata: Some(metadata),
1704                stats: Some(v1::ResultSetStats {
1705                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(5)),
1706                    ..Default::default()
1707                }),
1708                ..Default::default()
1709            }))
1710        });
1711
1712        mock.expect_commit().once().returning(|req| {
1713            let req = req.into_inner();
1714            assert_eq!(
1715                req.request_options
1716                    .expect("Missing request_options")
1717                    .transaction_tag,
1718                "my-test-tag"
1719            );
1720            commit_response()
1721        });
1722
1723        let (db_client, _server) = setup_db_client(mock).await;
1724
1725        let runner = TransactionRunnerBuilder::new(db_client)
1726            .with_begin_transaction_option(begin_transaction_option)
1727            .set_transaction_tag("my-test-tag")
1728            .build()
1729            .await?;
1730
1731        let res = runner
1732            .run(async |tx| {
1733                let count = tx.execute_update("UPDATE Users SET active = true").await?;
1734                Ok(count)
1735            })
1736            .await?;
1737
1738        assert_eq!(res.result, 5);
1739
1740        Ok(())
1741    }
1742
1743    #[tokio_test_no_panics]
1744    async fn execute_run_with_exclude_txn_from_change_streams_explicit() -> anyhow::Result<()> {
1745        run_with_exclude_txn_from_change_streams(BeginTransactionOption::ExplicitBegin).await
1746    }
1747
1748    #[tokio_test_no_panics]
1749    async fn execute_run_with_exclude_txn_from_change_streams_inline() -> anyhow::Result<()> {
1750        run_with_exclude_txn_from_change_streams(BeginTransactionOption::InlineBegin).await
1751    }
1752
1753    async fn run_with_exclude_txn_from_change_streams(
1754        begin_transaction_option: BeginTransactionOption,
1755    ) -> anyhow::Result<()> {
1756        let mut mock = create_session_mock();
1757
1758        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1759            mock.expect_begin_transaction().once().returning(|req| {
1760                let req = req.into_inner();
1761                let options = req.options.expect("Missing transaction options");
1762                assert!(options.exclude_txn_from_change_streams);
1763
1764                Ok(tonic::Response::new(v1::Transaction {
1765                    id: vec![9, 9, 9],
1766                    ..Default::default()
1767                }))
1768            });
1769        }
1770
1771        mock.expect_execute_sql().once().returning(move |req| {
1772            let req = req.into_inner();
1773            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1774                let transaction = req
1775                    .transaction
1776                    .as_ref()
1777                    .expect("transaction options required for inline begin");
1778                let selector = transaction.selector.as_ref().expect("selector required");
1779                assert!(matches!(
1780                    selector,
1781                    v1::transaction_selector::Selector::Begin(_)
1782                ));
1783            }
1784
1785            let mut metadata = v1::ResultSetMetadata {
1786                ..Default::default()
1787            };
1788            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1789                metadata.transaction = Some(v1::Transaction {
1790                    id: vec![9, 9, 9],
1791                    ..Default::default()
1792                });
1793            }
1794
1795            Ok(tonic::Response::new(v1::ResultSet {
1796                metadata: Some(metadata),
1797                stats: Some(v1::ResultSetStats {
1798                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(5)),
1799                    ..Default::default()
1800                }),
1801                ..Default::default()
1802            }))
1803        });
1804
1805        mock.expect_commit()
1806            .once()
1807            .returning(|_req| commit_response());
1808
1809        let (db_client, _server) = setup_db_client(mock).await;
1810
1811        let runner = TransactionRunnerBuilder::new(db_client)
1812            .set_exclude_txn_from_change_streams(true)
1813            .with_begin_transaction_option(begin_transaction_option)
1814            .build()
1815            .await?;
1816
1817        let res = runner
1818            .run(async |tx| {
1819                let count = tx.execute_update("UPDATE Users SET active = true").await?;
1820                Ok(count)
1821            })
1822            .await?;
1823
1824        assert_eq!(res.result, 5);
1825
1826        Ok(())
1827    }
1828
1829    #[tokio_test_no_panics]
1830    async fn execute_run_with_max_commit_delay_explicit() -> anyhow::Result<()> {
1831        run_with_max_commit_delay(BeginTransactionOption::ExplicitBegin).await
1832    }
1833
1834    #[tokio_test_no_panics]
1835    async fn execute_run_with_max_commit_delay_inline() -> anyhow::Result<()> {
1836        run_with_max_commit_delay(BeginTransactionOption::InlineBegin).await
1837    }
1838
1839    async fn run_with_max_commit_delay(
1840        begin_transaction_option: BeginTransactionOption,
1841    ) -> anyhow::Result<()> {
1842        let mut mock = create_session_mock();
1843
1844        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1845            expect_begin_transaction(&mut mock, 1, vec![1, 2, 3]);
1846        }
1847
1848        mock.expect_execute_sql().once().returning(move |req| {
1849            let req = req.into_inner();
1850            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1851                let transaction = req
1852                    .transaction
1853                    .as_ref()
1854                    .expect("transaction options required for inline begin");
1855                let selector = transaction.selector.as_ref().expect("selector required");
1856                assert!(matches!(
1857                    selector,
1858                    v1::transaction_selector::Selector::Begin(_)
1859                ));
1860            }
1861
1862            let mut metadata = v1::ResultSetMetadata {
1863                ..Default::default()
1864            };
1865            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1866                metadata.transaction = Some(v1::Transaction {
1867                    id: vec![1, 2, 3],
1868                    ..Default::default()
1869                });
1870            }
1871
1872            Ok(tonic::Response::new(v1::ResultSet {
1873                metadata: Some(metadata),
1874                stats: Some(v1::ResultSetStats {
1875                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
1876                    ..Default::default()
1877                }),
1878                ..Default::default()
1879            }))
1880        });
1881
1882        mock.expect_commit().once().returning(|req| {
1883            let req = req.into_inner();
1884            assert_eq!(
1885                req.max_commit_delay,
1886                Some(::prost_types::Duration {
1887                    seconds: 0,
1888                    nanos: 200_000_000, // 200ms
1889                })
1890            );
1891            commit_response()
1892        });
1893
1894        let (db_client, _server) = setup_db_client(mock).await;
1895        let runner = TransactionRunnerBuilder::new(db_client)
1896            .set_max_commit_delay(Duration::try_from("0.2s").unwrap())
1897            .with_begin_transaction_option(begin_transaction_option)
1898            .build()
1899            .await?;
1900
1901        let res = runner
1902            .run(async |tx| {
1903                let count = tx.execute_update("UPDATE Users SET active = true").await?;
1904                Ok(count)
1905            })
1906            .await?;
1907        assert_eq!(res.result, 1);
1908        Ok(())
1909    }
1910
1911    #[tokio_test_no_panics]
1912    async fn execute_run_empty_closure_inline() {
1913        let mut mock = create_session_mock();
1914        expect_begin_transaction(&mut mock, 1, vec![1, 2, 3]);
1915        mock.expect_commit().once().returning(|req| {
1916            let req = req.into_inner();
1917            assert_eq!(
1918                req.transaction,
1919                Some(v1::commit_request::Transaction::TransactionId(vec![
1920                    1, 2, 3
1921                ]))
1922            );
1923            commit_response()
1924        });
1925
1926        let (db_client, _server) = setup_db_client(mock).await;
1927
1928        let runner = TransactionRunnerBuilder::new(db_client)
1929            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
1930            .build()
1931            .await
1932            .unwrap();
1933
1934        let res = runner.run(async |_tx| Ok(42)).await.unwrap();
1935        assert_eq!(res.result, 42);
1936    }
1937
1938    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1939    async fn execute_run_async_statement_still_starting() {
1940        let (tx_rpc, rx_rpc) = std_channel();
1941        let (tx_started, rx_started) = oneshot_channel();
1942        let tx_started_mutex = Mutex::new(Some(tx_started));
1943
1944        let mut mock = create_session_mock();
1945
1946        mock.expect_execute_sql().once().returning(move |_req| {
1947            if let Some(tx) = tx_started_mutex.lock().unwrap().take() {
1948                let _ = tx.send(());
1949            }
1950            rx_rpc.recv().unwrap();
1951            row_count_exact_response(1)
1952        });
1953
1954        let (db_client, _server) = setup_db_client(mock).await;
1955
1956        let runner = TransactionRunnerBuilder::new(db_client)
1957            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
1958            .build()
1959            .await
1960            .unwrap();
1961
1962        let mut rx_started_opt = Some(rx_started);
1963        let res = runner
1964            .run(async |tx| {
1965                tokio::spawn(async move {
1966                    let _ = tx.execute_update("UPDATE Users SET active = true").await;
1967                });
1968                if let Some(rx) = rx_started_opt.take() {
1969                    rx.await.unwrap();
1970                }
1971                Ok(42)
1972            })
1973            .await;
1974
1975        tx_rpc.send(()).unwrap();
1976
1977        assert!(res.is_err());
1978        assert!(
1979            format!("{:?}", res.unwrap_err())
1980                .contains("asynchronous statement is still starting the transaction")
1981        );
1982    }
1983
1984    #[tokio_test_no_panics]
1985    async fn execute_run_with_mutations_happy_flow() {
1986        let mut mock = create_session_mock();
1987
1988        mock.expect_execute_sql().once().returning(move |req| {
1989            let req = req.into_inner();
1990            assert_eq!(req.sql, "UPDATE Users SET active = true");
1991            let transaction = req
1992                .transaction
1993                .as_ref()
1994                .expect("transaction options required for inline begin");
1995            let selector = transaction.selector.as_ref().expect("selector required");
1996            assert!(matches!(selector, ProtoSelector::Begin(_)));
1997
1998            Ok(tonic::Response::new(v1::ResultSet {
1999                metadata: Some(v1::ResultSetMetadata {
2000                    transaction: Some(v1::Transaction {
2001                        id: vec![1, 1, 1],
2002                        ..Default::default()
2003                    }),
2004                    ..Default::default()
2005                }),
2006                stats: Some(v1::ResultSetStats {
2007                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2008                    ..Default::default()
2009                }),
2010                ..Default::default()
2011            }))
2012        });
2013
2014        mock.expect_commit().once().returning(|req| {
2015            let req = req.into_inner();
2016            assert_eq!(
2017                req.transaction,
2018                Some(CommitTransaction::TransactionId(vec![1, 1, 1]))
2019            );
2020            assert_eq!(req.mutations.len(), 1);
2021            commit_response()
2022        });
2023
2024        let (db_client, _server) = setup_db_client(mock).await;
2025        let runner = TransactionRunnerBuilder::new(db_client)
2026            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
2027            .build()
2028            .await
2029            .expect("Failed to build transaction runner");
2030
2031        let res = runner
2032            .run(async |tx| {
2033                let count = tx.execute_update("UPDATE Users SET active = true").await?;
2034                let mutation = Mutation::new_insert_builder("Audits")
2035                    .set("AuditId")
2036                    .to(&1)
2037                    .build();
2038                tx.buffer([mutation])?;
2039                Ok(count)
2040            })
2041            .await
2042            .expect("Transaction runner failed");
2043
2044        assert_eq!(res.result, 1);
2045    }
2046
2047    #[tokio_test_no_panics]
2048    async fn execute_run_with_mutations_aborted_retry() {
2049        let mut mock = create_session_mock();
2050        let mut sequence = mockall::Sequence::new();
2051
2052        // Initial attempt: statement succeeds, returns tx id [10, 20, 30]
2053        mock.expect_execute_sql()
2054            .once()
2055            .in_sequence(&mut sequence)
2056            .returning(move |_req| {
2057                Ok(tonic::Response::new(v1::ResultSet {
2058                    metadata: Some(v1::ResultSetMetadata {
2059                        transaction: Some(v1::Transaction {
2060                            id: vec![10, 20, 30],
2061                            ..Default::default()
2062                        }),
2063                        ..Default::default()
2064                    }),
2065                    stats: Some(v1::ResultSetStats {
2066                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2067                        ..Default::default()
2068                    }),
2069                    ..Default::default()
2070                }))
2071            });
2072
2073        // Initial commit fails with Aborted
2074        mock.expect_commit()
2075            .once()
2076            .in_sequence(&mut sequence)
2077            .returning(|req| {
2078                let req = req.into_inner();
2079                assert_eq!(req.mutations.len(), 1);
2080                // Verify initial attempt added mutation for UserId 100
2081                let write = req.mutations[0]
2082                    .operation
2083                    .as_ref()
2084                    .expect("Operation required");
2085                match write {
2086                    Operation::Insert(w) => {
2087                        assert_eq!(
2088                            w.values[0].values[0].kind,
2089                            Some(Kind::StringValue("100".to_string()))
2090                        );
2091                    }
2092                    _ => panic!("Expected insert mutation"),
2093                }
2094                Err(create_aborted_status(StdTimeDuration::from_nanos(1)))
2095            });
2096
2097        // Retry attempt: execute_sql sends inline BeginTransaction with previous_transaction_id
2098        mock.expect_execute_sql()
2099            .once()
2100            .in_sequence(&mut sequence)
2101            .returning(move |req| {
2102                let req = req.into_inner();
2103                let transaction = req
2104                    .transaction
2105                    .as_ref()
2106                    .expect("transaction options required for inline begin");
2107                let selector = transaction.selector.as_ref().expect("selector required");
2108                let options = match selector {
2109                    ProtoSelector::Begin(o) => o,
2110                    _ => panic!("Expected Begin"),
2111                };
2112                let read_write = options.mode.as_ref().expect("mode required");
2113                match read_write {
2114                    Mode::ReadWrite(rw) => {
2115                        assert_eq!(
2116                            rw.multiplexed_session_previous_transaction_id,
2117                            vec![10, 20, 30]
2118                        );
2119                    }
2120                    _ => panic!("Expected ReadWrite"),
2121                }
2122
2123                Ok(tonic::Response::new(v1::ResultSet {
2124                    metadata: Some(v1::ResultSetMetadata {
2125                        transaction: Some(v1::Transaction {
2126                            id: vec![99, 99, 99],
2127                            ..Default::default()
2128                        }),
2129                        ..Default::default()
2130                    }),
2131                    stats: Some(v1::ResultSetStats {
2132                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2133                        ..Default::default()
2134                    }),
2135                    ..Default::default()
2136                }))
2137            });
2138
2139        // Second commit succeeds with the new mutation
2140        mock.expect_commit()
2141            .once()
2142            .in_sequence(&mut sequence)
2143            .returning(|req| {
2144                let req = req.into_inner();
2145                assert_eq!(
2146                    req.transaction,
2147                    Some(CommitTransaction::TransactionId(vec![99, 99, 99]))
2148                );
2149                assert_eq!(req.mutations.len(), 1);
2150                // Verify retry attempt added mutation for UserId 200
2151                let write = req.mutations[0]
2152                    .operation
2153                    .as_ref()
2154                    .expect("Operation required");
2155                match write {
2156                    Operation::Insert(w) => {
2157                        assert_eq!(
2158                            w.values[0].values[0].kind,
2159                            Some(Kind::StringValue("200".to_string()))
2160                        );
2161                    }
2162                    _ => panic!("Expected insert mutation"),
2163                }
2164                commit_response()
2165            });
2166
2167        let (db_client, _server) = setup_db_client(mock).await;
2168        let runner = TransactionRunnerBuilder::new(db_client)
2169            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
2170            .build()
2171            .await
2172            .expect("Failed to build transaction runner");
2173
2174        let mut attempt = 0;
2175        let res = runner
2176            .run(async |tx| {
2177                attempt += 1;
2178                let count = tx.execute_update("UPDATE Users SET active = true").await?;
2179                let mutation_value = if attempt == 1 { 100 } else { 200 };
2180                let mutation = Mutation::new_insert_builder("Users")
2181                    .set("UserId")
2182                    .to(&mutation_value)
2183                    .build();
2184                tx.buffer([mutation])?;
2185                Ok(count)
2186            })
2187            .await
2188            .expect("Transaction runner failed");
2189
2190        assert_eq!(res.result, 1);
2191    }
2192
2193    #[tokio_test_no_panics]
2194    async fn execute_run_mutation_only_explicit_begin_fallback() {
2195        let mut mock = create_session_mock();
2196
2197        // Since the user closure executes no statements, commit() calls explicit BeginTransaction
2198        mock.expect_begin_transaction().once().returning(|req| {
2199            let req = req.into_inner();
2200            assert_eq!(
2201                req.session,
2202                "projects/p/instances/i/databases/d/sessions/123"
2203            );
2204            Ok(tonic::Response::new(v1::Transaction {
2205                id: vec![77, 88, 99],
2206                ..Default::default()
2207            }))
2208        });
2209
2210        mock.expect_commit().once().returning(|req| {
2211            let req = req.into_inner();
2212            assert_eq!(
2213                req.transaction,
2214                Some(CommitTransaction::TransactionId(vec![77, 88, 99]))
2215            );
2216            assert_eq!(req.mutations.len(), 2);
2217            commit_response()
2218        });
2219
2220        let (db_client, _server) = setup_db_client(mock).await;
2221        let runner = TransactionRunnerBuilder::new(db_client)
2222            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
2223            .build()
2224            .await
2225            .expect("Failed to build transaction runner");
2226
2227        let res = runner
2228            .run(async |tx| {
2229                let m1 = Mutation::new_insert_builder("Orders")
2230                    .set("OrderId")
2231                    .to(&1)
2232                    .build();
2233                let m2 = Mutation::new_insert_builder("Orders")
2234                    .set("OrderId")
2235                    .to(&2)
2236                    .build();
2237                tx.buffer([m1, m2])?;
2238                Ok(())
2239            })
2240            .await
2241            .expect("Transaction runner failed");
2242
2243        assert_eq!(
2244            res.commit_response
2245                .commit_timestamp
2246                .expect("Timestamp required")
2247                .seconds(),
2248            123456789
2249        );
2250    }
2251
2252    #[tokio_test_no_panics]
2253    async fn read_write_transaction_builder_sets_gax_options() -> anyhow::Result<()> {
2254        let mock = create_session_mock();
2255        let (db_client, _server) = setup_db_client(mock).await;
2256
2257        let runner = TransactionRunnerBuilder::new(db_client)
2258            .with_begin_attempt_timeout(StdDuration::from_secs(5))
2259            .with_begin_retry_policy(NeverRetry)
2260            .with_begin_backoff_policy(ExponentialBackoff::default())
2261            .with_commit_attempt_timeout(StdDuration::from_secs(10))
2262            .with_commit_retry_policy(NeverRetry)
2263            .with_commit_backoff_policy(ExponentialBackoff::default());
2264
2265        let begin_gax = runner
2266            .begin_gax_options
2267            .as_ref()
2268            .expect("begin_gax_options missing");
2269        assert_eq!(
2270            *begin_gax.attempt_timeout(),
2271            Some(StdDuration::from_secs(5))
2272        );
2273        assert!(begin_gax.retry_policy().is_some());
2274        assert!(begin_gax.backoff_policy().is_some());
2275
2276        let commit_gax = runner
2277            .commit_gax_options
2278            .as_ref()
2279            .expect("commit_gax_options missing");
2280        assert_eq!(
2281            *commit_gax.attempt_timeout(),
2282            Some(StdDuration::from_secs(10))
2283        );
2284        assert!(commit_gax.retry_policy().is_some());
2285        assert!(commit_gax.backoff_policy().is_some());
2286
2287        Ok(())
2288    }
2289}