Skip to main content

google_cloud_spanner/
read_write_transaction.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::Error;
16use crate::RequestOptions;
17use crate::batch::BatchDml;
18use crate::client::amend_request_options_for_lar;
19use crate::database_client::DatabaseClient;
20use crate::error::internal_error;
21use crate::model::CommitRequest;
22use crate::model::CommitResponse;
23use crate::model::ExecuteBatchDmlRequest;
24use crate::model::ExecuteBatchDmlResponse;
25use crate::model::Mutation as ProtoMutation;
26use crate::model::ResultSet as ProtoResultSet;
27use crate::model::RollbackRequest;
28use crate::model::TransactionOptions;
29use crate::model::TransactionSelector;
30use crate::model::execute_batch_dml_request::Statement as ExecuteBatchDmlStatement;
31use crate::model::request_options::Priority;
32use crate::model::result_set_stats::RowCount;
33use crate::model::transaction_options::IsolationLevel;
34use crate::model::transaction_options::Mode;
35use crate::model::transaction_options::ReadWrite;
36use crate::model::transaction_options::read_write::ReadLockMode;
37use crate::model::transaction_selector::Selector;
38use crate::mutation::Mutation;
39use crate::precommit::PrecommitTokenTracker;
40use crate::read_only_transaction::{
41    BeginTransactionOption, ReadContext, ReadContextTransactionSelector, TransactionState,
42};
43use crate::result_set::ResultSet;
44use crate::retry_policy::SpannerRetryPolicy;
45use crate::statement::Statement;
46#[cfg(test)]
47use crate::transaction_retry_policy::is_aborted;
48use crate::write_only_transaction::create_commit_request;
49use google_cloud_gax::error::Error as GaxError;
50use google_cloud_gax::error::rpc::{Code, Status};
51use google_cloud_gax::options::RequestOptions as GaxRequestOptions;
52use google_cloud_gax::retry_policy::RetryPolicy;
53use google_cloud_gax::retry_result::RetryResult;
54use google_cloud_gax::retry_state::RetryState;
55use google_cloud_gax::throttle_result::ThrottleResult;
56use std::cmp::min;
57use std::mem::take;
58use std::sync::Arc;
59use std::sync::Mutex;
60use std::sync::atomic::{AtomicI64, Ordering};
61use std::time::Duration as StdDuration;
62use tokio::time::Instant;
63use wkt::Duration;
64
65/// A builder for [ReadWriteTransaction].
66#[derive(Clone, Debug)]
67pub(crate) struct ReadWriteTransactionBuilder {
68    pub(crate) client: DatabaseClient,
69    options: TransactionOptions,
70    transaction_tag: Option<String>,
71    max_commit_delay: Option<Duration>,
72    pub(crate) session_name: String,
73    return_commit_stats: bool,
74    commit_priority: Priority,
75    begin_transaction_option: BeginTransactionOption,
76    begin_gax_options: Option<crate::RequestOptions>,
77    commit_gax_options: Option<crate::RequestOptions>,
78}
79
80impl ReadWriteTransactionBuilder {
81    pub(crate) fn new(client: DatabaseClient) -> Self {
82        let session_name = client.session_name();
83        Self {
84            client,
85            options: TransactionOptions::default().set_read_write(ReadWrite::default()),
86            transaction_tag: None,
87            max_commit_delay: None,
88            session_name,
89            return_commit_stats: false,
90            commit_priority: Priority::Unspecified,
91            begin_transaction_option: BeginTransactionOption::InlineBegin,
92            begin_gax_options: None,
93            commit_gax_options: None,
94        }
95    }
96
97    pub(crate) fn set_isolation_level(mut self, isolation_level: IsolationLevel) -> Self {
98        self.options = self.options.set_isolation_level(isolation_level);
99        self
100    }
101
102    pub(crate) fn set_read_lock_mode(mut self, read_lock_mode: ReadLockMode) -> Self {
103        if let Some(Mode::ReadWrite(rw)) = self.options.mode.take() {
104            self.options = self
105                .options
106                .set_read_write(rw.set_read_lock_mode(read_lock_mode));
107        }
108        self
109    }
110
111    pub(crate) fn set_previous_transaction_id(mut self, id: Option<bytes::Bytes>) -> Self {
112        if let Some(id) = id
113            && let Some(Mode::ReadWrite(rw)) = self.options.mode.take()
114        {
115            self.options = self
116                .options
117                .set_read_write(rw.set_multiplexed_session_previous_transaction_id(id));
118        }
119        self
120    }
121
122    pub(crate) fn set_transaction_tag(mut self, tag: impl Into<String>) -> Self {
123        self.transaction_tag = Some(tag.into());
124        self
125    }
126
127    pub(crate) fn set_commit_priority(mut self, priority: Priority) -> Self {
128        self.commit_priority = priority;
129        self
130    }
131
132    pub(crate) fn set_max_commit_delay(mut self, delay: Duration) -> Self {
133        self.max_commit_delay = Some(delay);
134        self
135    }
136
137    pub(crate) fn set_exclude_txn_from_change_streams(mut self, exclude: bool) -> Self {
138        self.options = self.options.set_exclude_txn_from_change_streams(exclude);
139        self
140    }
141
142    pub(crate) fn set_return_commit_stats(mut self, return_stats: bool) -> Self {
143        self.return_commit_stats = return_stats;
144        self
145    }
146
147    pub fn with_begin_transaction_option(mut self, option: BeginTransactionOption) -> Self {
148        self.begin_transaction_option = option;
149        self
150    }
151
152    pub(crate) fn with_begin_transaction_request_options(
153        mut self,
154        options: Option<crate::RequestOptions>,
155    ) -> Self {
156        self.begin_gax_options = options;
157        self
158    }
159
160    pub(crate) fn with_commit_request_options(
161        mut self,
162        options: Option<crate::RequestOptions>,
163    ) -> Self {
164        self.commit_gax_options = options;
165        self
166    }
167
168    async fn begin(
169        &self,
170        session_name: String,
171        channel_hint: usize,
172        request_options: crate::RequestOptions,
173    ) -> crate::Result<ReadContextTransactionSelector> {
174        let response = crate::read_only_transaction::execute_begin_transaction(
175            &self.client,
176            session_name,
177            self.options.clone(),
178            self.transaction_tag.clone(),
179            channel_hint,
180            request_options,
181            None,
182        )
183        .await?;
184
185        Ok(ReadContextTransactionSelector::Fixed(
186            TransactionSelector::default().set_id(response.id),
187            None,
188        ))
189    }
190
191    pub(crate) async fn build(
192        &self,
193        deadline: Option<Instant>,
194    ) -> crate::Result<ReadWriteTransaction> {
195        let session_name = self.session_name.clone();
196        let channel_hint = self.client.spanner.next_channel_hint();
197        let transaction_selector = match self.begin_transaction_option {
198            BeginTransactionOption::ExplicitBegin => {
199                let mut options = self.begin_gax_options.clone().unwrap_or_default();
200                amend_gax_options(
201                    self.client.leader_aware_routing_enabled,
202                    deadline,
203                    &mut options,
204                );
205
206                self.begin(session_name.clone(), channel_hint, options)
207                    .await?
208            }
209            BeginTransactionOption::InlineBegin => ReadContextTransactionSelector::Lazy(Arc::new(
210                Mutex::new(TransactionState::NotStarted(self.options.clone())),
211            )),
212        };
213
214        Ok(ReadWriteTransaction {
215            context: ReadContext {
216                session_name,
217                client: self.client.clone(),
218                transaction_selector,
219                precommit_token_tracker: PrecommitTokenTracker::new(),
220                transaction_tag: self.transaction_tag.clone(),
221                channel_hint,
222                begin_transaction_request_options: None,
223            },
224            seqno: Arc::new(AtomicI64::new(1)),
225            max_commit_delay: self.max_commit_delay,
226            return_commit_stats: self.return_commit_stats,
227            deadline,
228            commit_priority: self.commit_priority.clone(),
229            mutations: Arc::new(Mutex::new(Vec::new())),
230            begin_gax_options: self.begin_gax_options.clone(),
231            commit_gax_options: self.commit_gax_options.clone(),
232        })
233    }
234}
235
236trait CheckServiceError {
237    fn check_service_error(&self) -> Option<Error>;
238}
239
240impl CheckServiceError for ProtoResultSet {
241    fn check_service_error(&self) -> Option<Error> {
242        None
243    }
244}
245
246/// Normalizes responses from `ExecuteBatchDml`.
247/// If Spanner encounters an error during inline transaction initialization (such as a missing table),
248/// it returns an `Ok(ExecuteBatchDmlResponse)` containing the error status but with empty `result_sets`.
249/// This implementation evaluates that payload so fallback handlers can recover.
250impl CheckServiceError for ExecuteBatchDmlResponse {
251    fn check_service_error(&self) -> Option<Error> {
252        if self.result_sets.is_empty()
253            && let Some(status) = &self.status
254            && status.code != Code::Ok as i32
255        {
256            let rpc_status = Status::default()
257                .set_code(status.code)
258                .set_message(status.message.clone());
259            return Some(Error::service(rpc_status));
260        }
261        None
262    }
263}
264
265/// A scope-bound guard that manages the state of a lazy transaction start attempt.
266///
267/// If the first statement in a transaction is executed using an inline `BeginTransaction` option,
268/// the transaction selector is transitioned to the `Starting` state.
269/// If that initial statement execution fails, or if the transaction ID is not successfully returned,
270/// we must reset the starting state back to `NotStarted` and unlock any concurrent threads waiting
271/// for this transaction to start.
272///
273/// This struct implements the RAII pattern:
274/// - It is initialized with `active = true` when the statement is starting the transaction.
275/// - If the transaction successfully starts and yields a valid ID, the guard is `disarm()`ed.
276/// - If the scope exits early due to an error (e.g., aborted error, protocol error, etc.), the guard
277///   is dropped, and its `Drop` implementation automatically calls `maybe_reset_starting()` to
278///   restore the selector state and notify waiters.
279struct LazyTransactionStartGuard {
280    selector: ReadContextTransactionSelector,
281    active: bool,
282}
283
284impl LazyTransactionStartGuard {
285    fn new(selector: ReadContextTransactionSelector, active: bool) -> Self {
286        Self { selector, active }
287    }
288
289    fn disarm(&mut self) {
290        self.active = false;
291    }
292}
293
294impl Drop for LazyTransactionStartGuard {
295    fn drop(&mut self) {
296        if self.active {
297            self.selector.maybe_reset_starting();
298        }
299    }
300}
301
302/// Helper macro to execute a DML or BatchDML RPC with retry logic if the
303/// request included a BeginTransaction option.
304macro_rules! execute_with_retry {
305    ($self:expr, $request:ident, $gax_options:expr, $rpc_method:ident, $extract_id:expr) => {{
306        let is_starting = matches!(
307            $request
308                .transaction
309                .as_ref()
310                .and_then(|t| t.selector.as_ref()),
311            Some(Selector::Begin(_))
312        );
313
314        let mut guard =
315            LazyTransactionStartGuard::new($self.context.transaction_selector.clone(), is_starting);
316
317        let response_result = $self
318            .context
319            .client
320            .spanner
321            .$rpc_method(
322                $request.clone(),
323                $gax_options.clone(),
324                $self.context.channel_hint,
325            )
326            .await;
327
328        let service_error = response_result
329            .as_ref()
330            .ok()
331            .and_then(|res| res.check_service_error());
332        let err_ref = response_result.as_ref().err().or(service_error.as_ref());
333
334        let response = match err_ref {
335            None => {
336                let response = response_result?;
337                if is_starting {
338                    let id = $extract_id(&response).ok_or_else(|| {
339                        internal_error("Transaction ID was not returned by Spanner")
340                    })?;
341                    $self.context.transaction_selector.update(id, None)?;
342                    guard.disarm();
343                }
344                response
345            }
346            Some(error) => {
347                if is_starting {
348                    $self.context.transaction_selector.set_failed(error);
349                }
350                response_result?
351            }
352        };
353
354        response
355    }};
356}
357
358/// A read-write transaction.
359#[derive(Clone, Debug)]
360pub struct ReadWriteTransaction {
361    pub(crate) context: ReadContext,
362    pub(crate) deadline: Option<Instant>,
363    seqno: Arc<AtomicI64>,
364    max_commit_delay: Option<Duration>,
365    return_commit_stats: bool,
366    commit_priority: Priority,
367    mutations: Arc<Mutex<Vec<ProtoMutation>>>,
368    begin_gax_options: Option<crate::RequestOptions>,
369    commit_gax_options: Option<crate::RequestOptions>,
370}
371
372impl ReadWriteTransaction {
373    /// Buffers one or more mutations to be applied when the transaction commits.
374    ///
375    /// # Example
376    /// ```
377    /// # use google_cloud_spanner::client::Spanner;
378    /// # use google_cloud_spanner::mutation::Mutation;
379    /// # async fn sample(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
380    /// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
381    /// let runner = db_client.read_write_transaction().build().await?;
382    /// runner.run(async |tx| {
383    ///     let mutation = Mutation::new_insert_builder("users")
384    ///         .set("id").to(&1)
385    ///         .build();
386    ///     tx.buffer([mutation])?;
387    ///     Ok(())
388    /// }).await?;
389    /// # Ok(())
390    /// # }
391    /// ```
392    pub fn buffer<I>(&self, mutations: I) -> crate::Result<()>
393    where
394        I: IntoIterator<Item = Mutation>,
395    {
396        let mut guard = self
397            .mutations
398            .lock()
399            .map_err(|_| crate::error::internal_error("mutations mutex poisoned"))?;
400        for mutation in mutations {
401            guard.push(mutation.build_proto());
402        }
403        Ok(())
404    }
405
406    /// Executes a query using this transaction.
407    pub async fn execute_query<T: Into<Statement>>(
408        &self,
409        statement: T,
410    ) -> crate::Result<ResultSet> {
411        let stmt = statement.into();
412        let mut gax_options = stmt.gax_options().clone();
413        self.amend_gax_options(&mut gax_options);
414        let stmt = stmt.with_gax_options(gax_options);
415        let seqno = self.seqno.fetch_add(1, Ordering::SeqCst);
416        self.context.execute_query(stmt, Some(seqno)).await
417    }
418
419    /// Reads rows from the database using key lookups and scans, as a simple key/value style alternative to execute_query.
420    pub async fn execute_read<T: Into<crate::read::ReadRequest>>(
421        &self,
422        read: T,
423    ) -> crate::Result<ResultSet> {
424        let mut req = read.into();
425        self.amend_gax_options(&mut req.gax_options);
426        self.context.execute_read(req).await
427    }
428
429    /// Executes an update using this transaction.
430    pub async fn execute_update<T: Into<Statement>>(&self, statement: T) -> crate::Result<i64> {
431        let statement = statement.into();
432        let mut gax_options = statement.gax_options().clone();
433        self.amend_gax_options(&mut gax_options);
434        let seqno = self.seqno.fetch_add(1, Ordering::SeqCst);
435        let mut request = statement
436            .into_request()
437            .set_session(self.context.session_name.clone())
438            .set_transaction(self.context.transaction_selector.selector().await?)
439            .set_seqno(seqno);
440        request.request_options = self.context.amend_request_options(request.request_options);
441
442        let response = execute_with_retry!(
443            self,
444            request,
445            gax_options,
446            execute_sql,
447            |response: &crate::model::ResultSet| {
448                response
449                    .metadata
450                    .as_ref()
451                    .and_then(|md| md.transaction.as_ref())
452                    .map(|t| t.id.clone())
453            }
454        );
455
456        self.context
457            .precommit_token_tracker
458            .update(response.precommit_token);
459
460        let stats = response
461            .stats
462            .ok_or_else(|| internal_error("No stats returned"))?;
463        match stats.row_count {
464            Some(RowCount::RowCountExact(c)) => Ok(c),
465            _ => Err(internal_error(
466                "ExecuteSql returned an invalid or missing row count type for a read/write transaction",
467            )),
468        }
469    }
470
471    /// Executes a batch of DML statements using this transaction.
472    ///
473    /// # Example
474    /// ```
475    /// # use google_cloud_spanner::client::Spanner;
476    /// # use google_cloud_spanner::statement::Statement;
477    /// # use google_cloud_spanner::batch::BatchDml;
478    /// # async fn build(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
479    /// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
480    /// let runner = db_client.read_write_transaction().build().await?;
481    /// let result = runner.run(async |transaction| {
482    ///     let statement1 = Statement::builder("UPDATE users SET active = true WHERE id = @id")
483    ///         .add_param("id", &1)
484    ///         .build();
485    ///     let statement2 = Statement::builder("UPDATE users SET active = true WHERE id = @id")
486    ///         .add_param("id", &2)
487    ///         .build();
488    ///     let batch = BatchDml::builder()
489    ///         .add_statement(statement1)
490    ///         .add_statement(statement2)
491    ///         .build();
492    ///     let update_counts = transaction.execute_batch_update(batch).await?;
493    ///     Ok(())
494    /// }).await?;
495    /// # Ok(())
496    /// # }
497    /// ```
498    ///
499    /// If a `BatchDml` request fails halfway through execution, `execute_batch_update` will return a
500    /// `BatchUpdateError` indicating exactly which statements succeeded (and their respective update counts)
501    /// before the batch execution failed.
502    ///
503    /// # Error Handling Example
504    /// ```
505    /// # use google_cloud_spanner::client::Spanner;
506    /// # use google_cloud_spanner::statement::Statement;
507    /// # use google_cloud_spanner::batch::BatchDml;
508    /// # use google_cloud_spanner::error::BatchUpdateError;
509    /// # async fn build(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
510    /// # let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
511    /// # let runner = db_client.read_write_transaction().build().await?;
512    /// # let result = runner.run(async |transaction| {
513    /// let statement1 = Statement::builder("UPDATE users SET active = true WHERE id = 1").build();
514    /// let statement2 = Statement::builder("UPDATE non_existent_table SET active = true WHERE id = 2").build();
515    ///
516    /// let batch = BatchDml::builder()
517    ///     .add_statement(statement1)
518    ///     .add_statement(statement2)
519    ///     .build();
520    ///
521    /// match transaction.execute_batch_update(batch).await {
522    ///     Ok(update_counts) => {
523    ///         println!("All statements succeeded. Update counts: {:?}", update_counts);
524    ///     }
525    ///     Err(e) => {
526    ///         if let Some(batch_error) = BatchUpdateError::extract(&e) {
527    ///             println!("Batch execution failed. Successful update counts: {:?}", batch_error.update_counts);
528    ///         } else {
529    ///             println!("RPC failed or internal error occurred: {}", e);
530    ///         }
531    ///     }
532    /// }
533    /// # Ok(())
534    /// # }).await?;
535    /// # Ok(())
536    /// # }
537    /// ```
538    pub async fn execute_batch_update<T: Into<BatchDml>>(
539        &self,
540        batch: T,
541    ) -> crate::Result<Vec<i64>> {
542        let mut batch = batch.into();
543        self.amend_gax_options(&mut batch.gax_options);
544        let seqno = self.seqno.fetch_add(1, Ordering::SeqCst);
545
546        let statements: Vec<ExecuteBatchDmlStatement> = batch
547            .statements
548            .into_iter()
549            .map(|stmt: crate::statement::Statement| stmt.into_batch_statement())
550            .collect();
551
552        let request = ExecuteBatchDmlRequest::default()
553            .set_session(self.context.session_name.clone())
554            .set_transaction(self.context.transaction_selector.selector().await?)
555            .set_seqno(seqno)
556            .set_statements(statements)
557            .set_or_clear_request_options(self.context.amend_request_options(batch.request_options))
558            .set_last_statements(batch.last_statements);
559
560        let response = execute_with_retry!(
561            self,
562            request,
563            batch.gax_options,
564            execute_batch_dml,
565            |response: &crate::model::ExecuteBatchDmlResponse| {
566                response
567                    .result_sets
568                    .first()
569                    .and_then(|rs| rs.metadata.as_ref())
570                    .and_then(|md| md.transaction.as_ref())
571                    .map(|t| t.id.clone())
572            }
573        );
574
575        self.context
576            .precommit_token_tracker
577            .update(response.precommit_token.clone());
578        crate::batch_dml::process_response(response)
579    }
580
581    pub(crate) async fn begin_explicitly_if_not_started(
582        &self,
583        is_stream_fallback: bool,
584        mutation_key: Option<crate::model::Mutation>,
585    ) -> crate::Result<bool> {
586        let mut begin_options = self.begin_gax_options.clone().unwrap_or_default();
587        self.amend_gax_options(&mut begin_options);
588        self.context
589            .begin_explicitly_if_not_started(begin_options, is_stream_fallback, mutation_key)
590            .await
591    }
592
593    pub(crate) fn is_starting(&self) -> crate::Result<bool> {
594        self.context.transaction_selector.is_starting()
595    }
596
597    fn commit_request_options(&self) -> Option<crate::model::RequestOptions> {
598        let mut options = self.context.amend_request_options(None);
599        if self.commit_priority != Priority::Unspecified {
600            options
601                .get_or_insert_with(crate::model::RequestOptions::default)
602                .priority = self.commit_priority.clone();
603        }
604        options
605    }
606
607    fn build_commit_request(
608        &self,
609        transaction_id: bytes::Bytes,
610        mutations: Vec<ProtoMutation>,
611        precommit_token: Option<crate::model::MultiplexedSessionPrecommitToken>,
612    ) -> CommitRequest {
613        create_commit_request(
614            self.context.session_name.clone(),
615            transaction_id,
616            mutations,
617            precommit_token,
618            self.commit_request_options(),
619            self.max_commit_delay,
620            self.return_commit_stats,
621        )
622    }
623
624    /// Commits the transaction.
625    pub(crate) async fn commit(self) -> crate::Result<CommitResponse> {
626        self.context.transaction_selector.check_failed()?;
627        let mutations = take(&mut *self.mutations.lock().unwrap());
628        let mut id = self.context.transaction_selector.get_id_no_wait()?;
629        if id.is_none() {
630            if self.is_starting()? {
631                return Err(crate::error::internal_error(
632                    "Commit called while an asynchronous statement is still starting the transaction",
633                ));
634            }
635            let mutation_key = Mutation::select_mutation_key(&mutations);
636            if self
637                .begin_explicitly_if_not_started(false, mutation_key)
638                .await?
639            {
640                self.context.transaction_selector.check_failed()?;
641                id = self.context.transaction_selector.get_id_no_wait()?;
642            }
643        }
644        let transaction_id = id.ok_or_else(|| internal_error("Transaction ID is missing"))?;
645        let precommit_token = self.context.precommit_token_tracker.get();
646
647        let request = self.build_commit_request(transaction_id.clone(), mutations, precommit_token);
648
649        let mut gax_options = self.commit_gax_options.clone().unwrap_or_default();
650        self.amend_gax_options(&mut gax_options);
651
652        let response = self
653            .context
654            .client
655            .spanner
656            .commit(request, gax_options, self.context.channel_hint)
657            .await?;
658
659        let response =
660            if let Some(new_precommit_token) = response.precommit_token().map(|b| (*b).clone()) {
661                let retry_commit_req = self.build_commit_request(
662                    transaction_id,
663                    Vec::new(), // mutations are never re-sent in retry requests
664                    Some(*new_precommit_token),
665                );
666
667                let mut gax_options = self.commit_gax_options.clone().unwrap_or_default();
668                self.amend_gax_options(&mut gax_options);
669
670                self.context
671                    .client
672                    .spanner
673                    .commit(retry_commit_req, gax_options, self.context.channel_hint)
674                    .await?
675            } else {
676                response
677            };
678
679        Ok(response)
680    }
681
682    /// Rolls back the transaction.
683    pub(crate) async fn rollback(self) -> crate::Result<()> {
684        let Some(transaction_id) = self.context.transaction_selector.get_id_no_wait()? else {
685            return Ok(());
686        };
687
688        let request = RollbackRequest::default()
689            .set_session(self.context.session_name.clone())
690            .set_transaction_id(transaction_id);
691
692        let mut gax_options = RequestOptions::default();
693        self.amend_gax_options(&mut gax_options);
694
695        self.context
696            .client
697            .spanner
698            .rollback(request, gax_options, self.context.channel_hint)
699            .await?;
700
701        Ok(())
702    }
703
704    fn amend_gax_options(&self, options: &mut GaxRequestOptions) {
705        amend_gax_options(
706            self.context.client.leader_aware_routing_enabled,
707            self.deadline,
708            options,
709        );
710    }
711}
712
713pub(crate) fn amend_gax_options(
714    leader_aware_routing_enabled: bool,
715    deadline: Option<Instant>,
716    options: &mut GaxRequestOptions,
717) {
718    if let Some(deadline) = deadline {
719        let remaining = deadline.saturating_duration_since(Instant::now());
720        let attempt_timeout = match options.attempt_timeout() {
721            Some(custom_timeout) => std::cmp::min(*custom_timeout, remaining),
722            None => remaining,
723        };
724        options.set_attempt_timeout(attempt_timeout);
725
726        let inner_policy = options
727            .retry_policy()
728            .clone()
729            .unwrap_or_else(|| Arc::new(SpannerRetryPolicy::new()));
730        let bounded_policy = TransactionBoundedRetryPolicy {
731            inner: inner_policy,
732            deadline,
733        };
734        options.set_retry_policy(bounded_policy);
735    }
736    *options = amend_request_options_for_lar(leader_aware_routing_enabled, take(options));
737}
738
739/// A retry policy that wraps another policy and bounds the total execution time
740/// by a specific transaction deadline.
741///
742/// This policy delegates `on_error` to the inner policy but overrides `remaining_time`
743/// to ensure that it never exceeds the time left until the transaction deadline.
744#[derive(Debug)]
745struct TransactionBoundedRetryPolicy {
746    inner: Arc<dyn RetryPolicy>,
747    deadline: Instant,
748}
749
750impl RetryPolicy for TransactionBoundedRetryPolicy {
751    fn on_error(&self, state: &RetryState, error: GaxError) -> RetryResult {
752        self.inner.on_error(state, error)
753    }
754
755    fn on_throttle(&self, state: &RetryState, error: GaxError) -> ThrottleResult {
756        self.inner.on_throttle(state, error)
757    }
758
759    fn remaining_time(&self, state: &RetryState) -> Option<StdDuration> {
760        let remaining = self.deadline.saturating_duration_since(Instant::now());
761        let attempt_timeout = self
762            .inner
763            .remaining_time(state)
764            .map(|inner| min(remaining, inner))
765            .unwrap_or(remaining);
766        Some(attempt_timeout)
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773    use crate::error::BatchUpdateError;
774    use crate::read_only_transaction::tests::{create_session_mock, setup_db_client};
775    use crate::result_set::tests::{adapt, string_val};
776    use crate::transaction_retry_policy::BasicTransactionRetryPolicy;
777    use gaxi::grpc::tonic;
778    use gaxi::grpc::tonic::MetadataMap;
779    use google_cloud_gax::options::internal::RequestOptionsExt as _;
780    use google_cloud_gax::retry_policy::NeverRetry;
781    use google_cloud_gax::retry_result::RetryResult;
782    use google_cloud_gax::retry_state::RetryState;
783    use google_cloud_test_macros::tokio_test_no_panics;
784    use http::HeaderMap;
785    use prost_types::Timestamp;
786    use spanner_grpc_mock::google::spanner::v1;
787    use std::fmt::Debug;
788    use std::sync::Mutex;
789    use std::sync::atomic::{AtomicU32, Ordering};
790    use std::time::Duration as StdDuration;
791
792    #[test]
793    fn auto_traits() {
794        static_assertions::assert_impl_all!(ReadWriteTransactionBuilder: Send, Sync, Clone, Debug);
795        static_assertions::assert_impl_all!(ReadWriteTransaction: Send, Sync, Debug);
796    }
797
798    #[tokio_test_no_panics]
799    async fn read_write_transaction_commit_retry_explicit() -> anyhow::Result<()> {
800        run_read_write_transaction_commit_retry(BeginTransactionOption::ExplicitBegin).await
801    }
802
803    #[tokio_test_no_panics]
804    async fn read_write_transaction_commit_retry_inline() -> anyhow::Result<()> {
805        run_read_write_transaction_commit_retry(BeginTransactionOption::InlineBegin).await
806    }
807
808    async fn run_read_write_transaction_commit_retry(
809        begin_transaction_option: BeginTransactionOption,
810    ) -> anyhow::Result<()> {
811        let mut mock = create_session_mock();
812        let remotes = Arc::new(Mutex::new(Vec::new()));
813
814        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
815            let remotes_clone = remotes.clone();
816            mock.expect_begin_transaction()
817                .once()
818                .returning(move |req| {
819                    remotes_clone
820                        .lock()
821                        .unwrap()
822                        .push(req.remote_addr().expect("remote_addr should be available"));
823                    let req = req.into_inner();
824                    assert_eq!(
825                        req.session,
826                        "projects/p/instances/i/databases/d/sessions/123"
827                    );
828                    Ok(tonic::Response::new(v1::Transaction {
829                        id: vec![0, 0, 7],
830                        ..Default::default()
831                    }))
832                });
833        }
834
835        // execute_update returns a precommit token.
836        let remotes_clone = remotes.clone();
837        mock.expect_execute_sql().once().returning(move |req| {
838            remotes_clone
839                .lock()
840                .unwrap()
841                .push(req.remote_addr().expect("remote_addr should be available"));
842            let req = req.into_inner();
843            assert_eq!(req.sql, "UPDATE Users SET Name = 'Bob' WHERE Id = 1");
844
845            if begin_transaction_option == BeginTransactionOption::InlineBegin {
846                let transaction = req
847                    .transaction
848                    .as_ref()
849                    .expect("transaction options required for inline begin");
850                let selector = transaction.selector.as_ref().expect("selector required");
851                assert!(matches!(
852                    selector,
853                    v1::transaction_selector::Selector::Begin(_)
854                ));
855            }
856
857            let mut metadata = v1::ResultSetMetadata {
858                row_type: Some(v1::StructType { fields: vec![] }),
859                ..Default::default()
860            };
861            if begin_transaction_option == BeginTransactionOption::InlineBegin {
862                metadata.transaction = Some(v1::Transaction {
863                    id: vec![0, 0, 7],
864                    ..Default::default()
865                });
866            }
867
868            Ok(tonic::Response::new(v1::ResultSet {
869                metadata: Some(metadata),
870                stats: Some(v1::ResultSetStats {
871                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
872                    ..Default::default()
873                }),
874                precommit_token: Some(v1::MultiplexedSessionPrecommitToken {
875                    precommit_token: vec![101],
876                    seq_num: 1,
877                }),
878                ..Default::default()
879            }))
880        });
881
882        // Simulate that commit returns a precommit token in the response.
883        // This would normally not happen, but we test it here to verify
884        // that the commit is retried.
885        let remotes_clone = remotes.clone();
886        mock.expect_commit().once().returning(move |req| {
887            remotes_clone
888                .lock()
889                .unwrap()
890                .push(req.remote_addr().expect("remote_addr should be available"));
891            let req = req.into_inner();
892            assert_eq!(
893                req.precommit_token,
894                Some(v1::MultiplexedSessionPrecommitToken {
895                    precommit_token: vec![101],
896                    seq_num: 1,
897                })
898            );
899            Ok(tonic::Response::new(v1::CommitResponse {
900                commit_timestamp: Some(prost_types::Timestamp {
901                    seconds: 1000,
902                    nanos: 0,
903                }),
904                multiplexed_session_retry: Some(
905                    v1::commit_response::MultiplexedSessionRetry::PrecommitToken(
906                        v1::MultiplexedSessionPrecommitToken {
907                            precommit_token: vec![202],
908                            seq_num: 2,
909                        },
910                    ),
911                ),
912                ..Default::default()
913            }))
914        });
915
916        // Second commit retry is automatically issued with the new token
917        let remotes_clone = remotes.clone();
918        mock.expect_commit().once().returning(move |req| {
919            remotes_clone
920                .lock()
921                .unwrap()
922                .push(req.remote_addr().expect("remote_addr should be available"));
923            let req = req.into_inner();
924            assert_eq!(
925                req.precommit_token,
926                Some(v1::MultiplexedSessionPrecommitToken {
927                    precommit_token: vec![202],
928                    seq_num: 2,
929                })
930            );
931            assert!(
932                req.mutations.is_empty(),
933                "Expected mutations to be empty in retried CommitRequest"
934            );
935            Ok(tonic::Response::new(v1::CommitResponse {
936                commit_timestamp: Some(prost_types::Timestamp {
937                    seconds: 1001,
938                    nanos: 0,
939                }),
940                ..Default::default()
941            }))
942        });
943
944        let (db_client, _server) = setup_db_client(mock).await;
945
946        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
947            .with_begin_transaction_option(begin_transaction_option)
948            .build(None)
949            .await
950            .expect("Failed to build transaction");
951
952        let count = tx
953            .execute_update("UPDATE Users SET Name = 'Bob' WHERE Id = 1")
954            .await?;
955        assert_eq!(count, 1);
956
957        let timestamp = tx.commit().await?;
958        assert_eq!(
959            timestamp
960                .commit_timestamp
961                .as_ref()
962                .expect("timestamp should be present")
963                .seconds(),
964            1001
965        );
966
967        // Verify that all RPCs used the same channel (same remote address)
968        let remotes = remotes.lock().unwrap();
969        let expected_rpcs = if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
970            4
971        } else {
972            3
973        };
974        assert_eq!(
975            remotes.len(),
976            expected_rpcs,
977            "Expected exactly {} RPCs",
978            expected_rpcs
979        );
980        let first = remotes[0];
981        for addr in remotes.iter() {
982            assert_eq!(*addr, first, "All RPCs should use the same gRPC channel");
983        }
984
985        Ok(())
986    }
987
988    #[tokio_test_no_panics]
989    async fn read_write_transaction_commit_retry_preserves_options() -> anyhow::Result<()> {
990        let mut mock = create_session_mock();
991
992        // execute_update returns a precommit token.
993        mock.expect_execute_sql().once().returning(move |req| {
994            let req = req.into_inner();
995            assert_eq!(req.sql, "UPDATE Users SET Name = 'Bob' WHERE Id = 1");
996
997            let mut metadata = v1::ResultSetMetadata {
998                row_type: Some(v1::StructType { fields: vec![] }),
999                ..Default::default()
1000            };
1001            metadata.transaction = Some(v1::Transaction {
1002                id: vec![0, 0, 7],
1003                ..Default::default()
1004            });
1005
1006            Ok(tonic::Response::new(v1::ResultSet {
1007                metadata: Some(metadata),
1008                stats: Some(v1::ResultSetStats {
1009                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
1010                    ..Default::default()
1011                }),
1012                precommit_token: Some(v1::MultiplexedSessionPrecommitToken {
1013                    precommit_token: vec![101],
1014                    seq_num: 1,
1015                }),
1016                ..Default::default()
1017            }))
1018        });
1019
1020        // Simulate that commit returns a precommit token in the response.
1021        // This would normally not happen, but we test it here to verify
1022        // that the commit is retried and the options are preserved.
1023        let expected_delay = prost_types::Duration {
1024            seconds: 0,
1025            nanos: 200_000_000,
1026        };
1027
1028        let expected_delay_clone = expected_delay;
1029        mock.expect_commit().once().returning(move |req| {
1030            let req = req.into_inner();
1031            assert_eq!(
1032                req.precommit_token,
1033                Some(v1::MultiplexedSessionPrecommitToken {
1034                    precommit_token: vec![101],
1035                    seq_num: 1,
1036                })
1037            );
1038            // Assert original options are present
1039            assert!(
1040                req.return_commit_stats,
1041                "Expected return_commit_stats to be true in first commit"
1042            );
1043            assert_eq!(
1044                req.max_commit_delay.as_ref(),
1045                Some(&expected_delay_clone),
1046                "Expected max_commit_delay to be set in first commit"
1047            );
1048
1049            Ok(tonic::Response::new(v1::CommitResponse {
1050                commit_timestamp: Some(prost_types::Timestamp {
1051                    seconds: 1000,
1052                    nanos: 0,
1053                }),
1054                multiplexed_session_retry: Some(
1055                    v1::commit_response::MultiplexedSessionRetry::PrecommitToken(
1056                        v1::MultiplexedSessionPrecommitToken {
1057                            precommit_token: vec![202],
1058                            seq_num: 2,
1059                        },
1060                    ),
1061                ),
1062                ..Default::default()
1063            }))
1064        });
1065
1066        // Second commit retry is automatically issued with the new token and MUST preserve original options
1067        mock.expect_commit().once().returning(move |req| {
1068            let req = req.into_inner();
1069            assert_eq!(
1070                req.precommit_token,
1071                Some(v1::MultiplexedSessionPrecommitToken {
1072                    precommit_token: vec![202],
1073                    seq_num: 2,
1074                })
1075            );
1076            assert!(
1077                req.return_commit_stats,
1078                "Expected return_commit_stats to be preserved in retried commit request"
1079            );
1080            assert_eq!(
1081                req.max_commit_delay.as_ref(),
1082                Some(&expected_delay),
1083                "Expected max_commit_delay to be preserved in retried commit request"
1084            );
1085            assert!(
1086                req.mutations.is_empty(),
1087                "Expected mutations to be empty in retried CommitRequest"
1088            );
1089
1090            Ok(tonic::Response::new(v1::CommitResponse {
1091                commit_timestamp: Some(prost_types::Timestamp {
1092                    seconds: 1001,
1093                    nanos: 0,
1094                }),
1095                ..Default::default()
1096            }))
1097        });
1098
1099        let (db_client, _server) = setup_db_client(mock).await;
1100
1101        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
1102            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
1103            .set_return_commit_stats(true)
1104            .set_max_commit_delay(Duration::new(0, 200_000_000).expect("valid duration"))
1105            .build(None)
1106            .await
1107            .expect("Failed to build transaction");
1108
1109        let count = tx
1110            .execute_update("UPDATE Users SET Name = 'Bob' WHERE Id = 1")
1111            .await?;
1112        assert_eq!(count, 1);
1113
1114        let timestamp = tx.commit().await?;
1115        assert_eq!(
1116            timestamp
1117                .commit_timestamp
1118                .as_ref()
1119                .expect("timestamp should be present")
1120                .seconds(),
1121            1001
1122        );
1123
1124        Ok(())
1125    }
1126
1127    #[tokio_test_no_panics]
1128    async fn read_write_transaction_commit_carries_commit_priority() -> anyhow::Result<()> {
1129        let mut mock = create_session_mock();
1130
1131        mock.expect_execute_sql().once().returning(move |_req| {
1132            let mut metadata = v1::ResultSetMetadata {
1133                row_type: Some(v1::StructType { fields: vec![] }),
1134                ..Default::default()
1135            };
1136            metadata.transaction = Some(v1::Transaction {
1137                id: vec![1, 2, 3],
1138                ..Default::default()
1139            });
1140
1141            Ok(tonic::Response::new(v1::ResultSet {
1142                metadata: Some(metadata),
1143                stats: Some(v1::ResultSetStats {
1144                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
1145                    ..Default::default()
1146                }),
1147                ..Default::default()
1148            }))
1149        });
1150
1151        mock.expect_commit().once().returning(|req| {
1152            let req = req.into_inner();
1153            let request_options = req
1154                .request_options
1155                .expect("Expected request_options in CommitRequest");
1156            assert_eq!(
1157                request_options.priority,
1158                v1::request_options::Priority::Low as i32,
1159                "Expected priority to be Priority::Low in CommitRequest"
1160            );
1161
1162            Ok(tonic::Response::new(v1::CommitResponse {
1163                commit_timestamp: Some(prost_types::Timestamp {
1164                    seconds: 123456789,
1165                    nanos: 0,
1166                }),
1167                ..Default::default()
1168            }))
1169        });
1170
1171        let (db_client, _server) = setup_db_client(mock).await;
1172
1173        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
1174            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
1175            .set_commit_priority(Priority::Low)
1176            .build(None)
1177            .await
1178            .expect("Failed to build transaction");
1179
1180        let count = tx
1181            .execute_update("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
1182            .await?;
1183        assert_eq!(count, 1);
1184
1185        let _ = tx.commit().await?;
1186
1187        Ok(())
1188    }
1189
1190    #[tokio_test_no_panics]
1191    async fn read_write_transaction_execute_update_last_statement() {
1192        let stmt = Statement::builder("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
1193            .set_last_statement(true)
1194            .build();
1195        run_read_write_transaction_execute_update(BeginTransactionOption::ExplicitBegin, stmt)
1196            .await;
1197    }
1198
1199    #[tokio_test_no_panics]
1200    async fn read_write_transaction_execute_update_explicit() {
1201        run_read_write_transaction_execute_update(
1202            BeginTransactionOption::ExplicitBegin,
1203            "UPDATE Users SET Name = 'Alice' WHERE Id = 1",
1204        )
1205        .await;
1206    }
1207
1208    #[tokio_test_no_panics]
1209    async fn read_write_transaction_execute_update_inline() {
1210        run_read_write_transaction_execute_update(
1211            BeginTransactionOption::InlineBegin,
1212            "UPDATE Users SET Name = 'Alice' WHERE Id = 1",
1213        )
1214        .await;
1215    }
1216
1217    async fn run_read_write_transaction_execute_update(
1218        begin_transaction_option: BeginTransactionOption,
1219        statement: impl Into<Statement>,
1220    ) {
1221        let statement = statement.into();
1222        let expected_last_statement = statement.last_statement;
1223        let mut mock = create_session_mock();
1224
1225        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1226            mock.expect_begin_transaction().once().returning(|req| {
1227                let req = req.into_inner();
1228                assert_eq!(
1229                    req.session,
1230                    "projects/p/instances/i/databases/d/sessions/123"
1231                );
1232                Ok(tonic::Response::new(v1::Transaction {
1233                    id: vec![1, 2, 3],
1234                    ..Default::default()
1235                }))
1236            });
1237        }
1238
1239        mock.expect_execute_sql().once().returning(move |req| {
1240            let req = req.into_inner();
1241            assert_eq!(req.sql, "UPDATE Users SET Name = 'Alice' WHERE Id = 1");
1242            assert_eq!(req.seqno, 1);
1243            assert_eq!(req.last_statement, expected_last_statement);
1244
1245            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1246                let transaction = req
1247                    .transaction
1248                    .as_ref()
1249                    .expect("transaction options required for inline begin");
1250                let selector = transaction.selector.as_ref().expect("selector required");
1251                assert!(matches!(
1252                    selector,
1253                    v1::transaction_selector::Selector::Begin(_)
1254                ));
1255            }
1256
1257            let mut metadata = v1::ResultSetMetadata {
1258                row_type: Some(v1::StructType { fields: vec![] }),
1259                ..Default::default()
1260            };
1261            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1262                metadata.transaction = Some(v1::Transaction {
1263                    id: vec![1, 2, 3],
1264                    ..Default::default()
1265                });
1266            }
1267
1268            Ok(tonic::Response::new(v1::ResultSet {
1269                metadata: Some(metadata),
1270                stats: Some(v1::ResultSetStats {
1271                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
1272                    ..Default::default()
1273                }),
1274                ..Default::default()
1275            }))
1276        });
1277
1278        mock.expect_commit().once().returning(|req| {
1279            let req = req.into_inner();
1280            assert_eq!(
1281                req.session,
1282                "projects/p/instances/i/databases/d/sessions/123"
1283            );
1284            assert_eq!(
1285                req.transaction,
1286                Some(v1::commit_request::Transaction::TransactionId(vec![
1287                    1, 2, 3
1288                ]))
1289            );
1290            Ok(tonic::Response::new(v1::CommitResponse {
1291                commit_timestamp: Some(prost_types::Timestamp {
1292                    seconds: 123456789,
1293                    nanos: 0,
1294                }),
1295                ..Default::default()
1296            }))
1297        });
1298
1299        let (db_client, _server) = setup_db_client(mock).await;
1300
1301        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
1302            .with_begin_transaction_option(begin_transaction_option)
1303            .build(None)
1304            .await
1305            .expect("Failed to build transaction");
1306        let count = tx
1307            .execute_update(statement)
1308            .await
1309            .expect("Failed to execute update");
1310        assert_eq!(count, 1);
1311
1312        let ts = tx.commit().await.expect("Failed to commit");
1313        assert_eq!(
1314            ts.commit_timestamp
1315                .expect("Commit timestamp should be present")
1316                .seconds(),
1317            123456789
1318        );
1319    }
1320
1321    #[tokio_test_no_panics]
1322    async fn read_write_transaction_execute_update_invalid_stats_explicit() -> anyhow::Result<()> {
1323        run_read_write_transaction_execute_update_invalid_stats(
1324            BeginTransactionOption::ExplicitBegin,
1325        )
1326        .await
1327    }
1328
1329    #[tokio_test_no_panics]
1330    async fn read_write_transaction_execute_update_invalid_stats_inline() -> anyhow::Result<()> {
1331        run_read_write_transaction_execute_update_invalid_stats(BeginTransactionOption::InlineBegin)
1332            .await
1333    }
1334
1335    async fn run_read_write_transaction_execute_update_invalid_stats(
1336        begin_transaction_option: BeginTransactionOption,
1337    ) -> anyhow::Result<()> {
1338        let mut mock = create_session_mock();
1339
1340        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1341            mock.expect_begin_transaction().once().returning(|_| {
1342                Ok(tonic::Response::new(v1::Transaction {
1343                    id: vec![1, 2, 3],
1344                    ..Default::default()
1345                }))
1346            });
1347        }
1348
1349        mock.expect_execute_sql().once().returning(move |req| {
1350            let req = req.into_inner();
1351            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1352                let transaction = req
1353                    .transaction
1354                    .as_ref()
1355                    .expect("transaction options required for inline begin");
1356                let selector = transaction.selector.as_ref().expect("selector required");
1357                assert!(matches!(
1358                    selector,
1359                    v1::transaction_selector::Selector::Begin(_)
1360                ));
1361            }
1362
1363            let mut metadata = v1::ResultSetMetadata {
1364                row_type: Some(v1::StructType { fields: vec![] }),
1365                ..Default::default()
1366            };
1367            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1368                metadata.transaction = Some(v1::Transaction {
1369                    id: vec![1, 2, 3],
1370                    ..Default::default()
1371                });
1372            }
1373
1374            Ok(tonic::Response::new(v1::ResultSet {
1375                metadata: Some(metadata),
1376                stats: Some(v1::ResultSetStats {
1377                    row_count: Some(v1::result_set_stats::RowCount::RowCountLowerBound(1)),
1378                    ..Default::default()
1379                }),
1380                ..Default::default()
1381            }))
1382        });
1383
1384        let (db_client, _server) = setup_db_client(mock).await;
1385
1386        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
1387            .with_begin_transaction_option(begin_transaction_option)
1388            .build(None)
1389            .await
1390            .expect("Failed to build transaction");
1391
1392        let result = tx
1393            .execute_update("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
1394            .await;
1395
1396        let err = result.expect_err("Expected an error for invalid row count stats");
1397        assert!(
1398            format!("{:?}", err).contains("invalid or missing row count type"),
1399            "Error did not contain expected message: {:?}",
1400            err
1401        );
1402        Ok(())
1403    }
1404
1405    #[tokio_test_no_panics]
1406    async fn read_write_transaction_rollback_explicit() -> anyhow::Result<()> {
1407        run_read_write_transaction_rollback(BeginTransactionOption::ExplicitBegin).await
1408    }
1409
1410    #[tokio_test_no_panics]
1411    async fn read_write_transaction_rollback_inline() -> anyhow::Result<()> {
1412        run_read_write_transaction_rollback(BeginTransactionOption::InlineBegin).await
1413    }
1414
1415    async fn run_read_write_transaction_rollback(
1416        begin_transaction_option: BeginTransactionOption,
1417    ) -> anyhow::Result<()> {
1418        let mut mock = create_session_mock();
1419        let transaction_id = vec![9, 9, 9];
1420
1421        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1422            let id = transaction_id.clone();
1423            mock.expect_begin_transaction().once().returning(move |_| {
1424                Ok(tonic::Response::new(v1::Transaction {
1425                    id: id.clone(),
1426                    ..Default::default()
1427                }))
1428            });
1429        } else {
1430            let id = transaction_id.clone();
1431            mock.expect_execute_sql().once().returning(move |req| {
1432                let req = req.into_inner();
1433                let transaction = req
1434                    .transaction
1435                    .as_ref()
1436                    .expect("transaction options required for inline begin");
1437                let selector = transaction.selector.as_ref().expect("selector required");
1438                assert!(matches!(
1439                    selector,
1440                    v1::transaction_selector::Selector::Begin(_)
1441                ));
1442
1443                Ok(tonic::Response::new(v1::ResultSet {
1444                    metadata: Some(v1::ResultSetMetadata {
1445                        transaction: Some(v1::Transaction {
1446                            id: id.clone(),
1447                            ..Default::default()
1448                        }),
1449                        ..Default::default()
1450                    }),
1451                    stats: Some(v1::ResultSetStats {
1452                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
1453                        ..Default::default()
1454                    }),
1455                    ..Default::default()
1456                }))
1457            });
1458        }
1459
1460        let id = transaction_id.clone();
1461        mock.expect_rollback().once().returning(move |req| {
1462            let req = req.into_inner();
1463            assert_eq!(
1464                req.session,
1465                "projects/p/instances/i/databases/d/sessions/123"
1466            );
1467            assert_eq!(req.transaction_id, id);
1468            Ok(tonic::Response::new(()))
1469        });
1470
1471        let (db_client, _server) = setup_db_client(mock).await;
1472
1473        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
1474            .with_begin_transaction_option(begin_transaction_option)
1475            .build(None)
1476            .await?;
1477
1478        if begin_transaction_option == BeginTransactionOption::InlineBegin {
1479            tx.execute_update("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
1480                .await
1481                .expect("Failed to execute update");
1482        }
1483
1484        tx.rollback().await?;
1485        Ok(())
1486    }
1487
1488    #[tokio_test_no_panics]
1489    async fn read_write_transaction_execute_batch_update_last_statements() -> anyhow::Result<()> {
1490        let batch = BatchDml::builder()
1491            .add_statement("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
1492            .add_statement("UPDATE Users SET Name = 'Bob' WHERE Id = 2")
1493            .set_last_statements(true)
1494            .build();
1495        run_read_write_transaction_execute_batch_update(
1496            BeginTransactionOption::ExplicitBegin,
1497            batch,
1498        )
1499        .await
1500    }
1501
1502    #[tokio_test_no_panics]
1503    async fn read_write_transaction_execute_batch_update_explicit() -> anyhow::Result<()> {
1504        let batch = BatchDml::builder()
1505            .add_statement("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
1506            .add_statement("UPDATE Users SET Name = 'Bob' WHERE Id = 2")
1507            .build();
1508        run_read_write_transaction_execute_batch_update(
1509            BeginTransactionOption::ExplicitBegin,
1510            batch,
1511        )
1512        .await
1513    }
1514
1515    #[tokio_test_no_panics]
1516    async fn read_write_transaction_execute_batch_update_inline() -> anyhow::Result<()> {
1517        let batch = BatchDml::builder()
1518            .add_statement("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
1519            .add_statement("UPDATE Users SET Name = 'Bob' WHERE Id = 2")
1520            .build();
1521        run_read_write_transaction_execute_batch_update(BeginTransactionOption::InlineBegin, batch)
1522            .await
1523    }
1524
1525    #[tokio_test_no_panics]
1526    async fn read_write_transaction_execute_batch_update_vec() -> anyhow::Result<()> {
1527        let statements = vec![
1528            "UPDATE Users SET Name = 'Alice' WHERE Id = 1",
1529            "UPDATE Users SET Name = 'Bob' WHERE Id = 2",
1530        ];
1531        run_read_write_transaction_execute_batch_update(
1532            BeginTransactionOption::InlineBegin,
1533            statements,
1534        )
1535        .await
1536    }
1537
1538    #[tokio_test_no_panics]
1539    async fn read_write_transaction_execute_batch_update_vec_statement() -> anyhow::Result<()> {
1540        let statement1 = Statement::builder("UPDATE Users SET Name = 'Alice' WHERE Id = 1").build();
1541        let statement2 = Statement::builder("UPDATE Users SET Name = 'Bob' WHERE Id = 2").build();
1542        let statements = vec![statement1, statement2];
1543        run_read_write_transaction_execute_batch_update(
1544            BeginTransactionOption::InlineBegin,
1545            statements,
1546        )
1547        .await
1548    }
1549
1550    async fn run_read_write_transaction_execute_batch_update(
1551        begin_transaction_option: BeginTransactionOption,
1552        batch: impl Into<BatchDml>,
1553    ) -> anyhow::Result<()> {
1554        let batch = batch.into();
1555        let expected_last_statements = batch.last_statements;
1556        let mut mock = create_session_mock();
1557
1558        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1559            mock.expect_begin_transaction().once().returning(|_| {
1560                Ok(tonic::Response::new(v1::Transaction {
1561                    id: vec![4, 5, 6],
1562                    ..Default::default()
1563                }))
1564            });
1565        }
1566
1567        mock.expect_execute_batch_dml()
1568            .once()
1569            .returning(move |req| {
1570                let req = req.into_inner();
1571                assert_eq!(req.last_statements, expected_last_statements);
1572                assert_eq!(req.statements.len(), 2);
1573                assert_eq!(
1574                    req.statements[0].sql,
1575                    "UPDATE Users SET Name = 'Alice' WHERE Id = 1"
1576                );
1577                assert_eq!(
1578                    req.statements[1].sql,
1579                    "UPDATE Users SET Name = 'Bob' WHERE Id = 2"
1580                );
1581
1582                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1583                    let selector = req
1584                        .transaction
1585                        .expect("missing transaction selector")
1586                        .selector
1587                        .expect("missing selector");
1588                    assert!(matches!(
1589                        selector,
1590                        v1::transaction_selector::Selector::Begin(_)
1591                    ));
1592                }
1593
1594                let mut metadata = v1::ResultSetMetadata {
1595                    ..Default::default()
1596                };
1597                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1598                    metadata.transaction = Some(v1::Transaction {
1599                        id: vec![4, 5, 6],
1600                        ..Default::default()
1601                    });
1602                }
1603
1604                Ok(tonic::Response::new(v1::ExecuteBatchDmlResponse {
1605                    result_sets: vec![
1606                        v1::ResultSet {
1607                            metadata: Some(metadata),
1608                            stats: Some(v1::ResultSetStats {
1609                                row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
1610                                ..Default::default()
1611                            }),
1612                            ..Default::default()
1613                        },
1614                        v1::ResultSet {
1615                            stats: Some(v1::ResultSetStats {
1616                                row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
1617                                ..Default::default()
1618                            }),
1619                            ..Default::default()
1620                        },
1621                    ],
1622                    status: Some(spanner_grpc_mock::google::rpc::Status {
1623                        code: 0,
1624                        message: "OK".into(),
1625                        details: vec![],
1626                    }),
1627                    ..Default::default()
1628                }))
1629            });
1630
1631        let (db_client, _server) = setup_db_client(mock).await;
1632
1633        let transaction = ReadWriteTransactionBuilder::new(db_client)
1634            .with_begin_transaction_option(begin_transaction_option)
1635            .build(None)
1636            .await?;
1637
1638        let counts = transaction.execute_batch_update(batch).await?;
1639
1640        assert_eq!(counts, vec![1, 1]);
1641        Ok(())
1642    }
1643
1644    #[tokio_test_no_panics]
1645    async fn read_write_transaction_execute_batch_update_partial_failure_explicit()
1646    -> anyhow::Result<()> {
1647        run_read_write_transaction_execute_batch_update_partial_failure(
1648            BeginTransactionOption::ExplicitBegin,
1649        )
1650        .await
1651    }
1652
1653    #[tokio_test_no_panics]
1654    async fn read_write_transaction_execute_batch_update_partial_failure_inline()
1655    -> anyhow::Result<()> {
1656        run_read_write_transaction_execute_batch_update_partial_failure(
1657            BeginTransactionOption::InlineBegin,
1658        )
1659        .await
1660    }
1661
1662    async fn run_read_write_transaction_execute_batch_update_partial_failure(
1663        begin_transaction_option: BeginTransactionOption,
1664    ) -> anyhow::Result<()> {
1665        let mut mock = create_session_mock();
1666
1667        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1668            mock.expect_begin_transaction().once().returning(|_| {
1669                Ok(tonic::Response::new(v1::Transaction {
1670                    id: vec![7, 8, 9],
1671                    ..Default::default()
1672                }))
1673            });
1674        }
1675
1676        mock.expect_execute_batch_dml()
1677            .once()
1678            .returning(move |req| {
1679                let req = req.into_inner();
1680                if begin_transaction_option == BeginTransactionOption::InlineBegin {
1681                    let selector = req
1682                        .transaction
1683                        .expect("missing transaction selector")
1684                        .selector
1685                        .expect("missing selector");
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![7, 8, 9],
1698                        ..Default::default()
1699                    });
1700                }
1701
1702                Ok(tonic::Response::new(v1::ExecuteBatchDmlResponse {
1703                    result_sets: vec![v1::ResultSet {
1704                        metadata: Some(metadata),
1705                        stats: Some(v1::ResultSetStats {
1706                            row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
1707                            ..Default::default()
1708                        }),
1709                        ..Default::default()
1710                    }],
1711                    status: Some(spanner_grpc_mock::google::rpc::Status {
1712                        code: gaxi::grpc::tonic::Code::AlreadyExists as i32,
1713                        message: "row already exists".into(),
1714                        details: vec![],
1715                    }),
1716                    ..Default::default()
1717                }))
1718            });
1719
1720        let (db_client, _server) = setup_db_client(mock).await;
1721
1722        let tx = ReadWriteTransactionBuilder::new(db_client)
1723            .with_begin_transaction_option(begin_transaction_option)
1724            .build(None)
1725            .await?;
1726
1727        let batch = BatchDml::builder()
1728            .add_statement("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
1729            .add_statement("INSERT INTO Users (Id) VALUES (2)");
1730
1731        let res = tx.execute_batch_update(batch.build()).await;
1732
1733        let err = res.expect_err("expected error");
1734        use std::error::Error;
1735        let batch_err = err
1736            .source()
1737            .and_then(|e| e.downcast_ref::<BatchUpdateError>())
1738            .expect("should be BatchUpdateError");
1739        assert_eq!(batch_err.update_counts, vec![1]);
1740        assert_eq!(
1741            batch_err.status.status().expect("status").code,
1742            (gaxi::grpc::tonic::Code::AlreadyExists as i32).into()
1743        );
1744        Ok(())
1745    }
1746
1747    #[tokio_test_no_panics]
1748    async fn read_write_transaction_execute_multiple_updates_explicit() -> anyhow::Result<()> {
1749        run_read_write_transaction_execute_multiple_updates(BeginTransactionOption::ExplicitBegin)
1750            .await
1751    }
1752
1753    #[tokio_test_no_panics]
1754    async fn read_write_transaction_execute_multiple_updates_inline() -> anyhow::Result<()> {
1755        run_read_write_transaction_execute_multiple_updates(BeginTransactionOption::InlineBegin)
1756            .await
1757    }
1758
1759    async fn run_read_write_transaction_execute_multiple_updates(
1760        begin_transaction_option: BeginTransactionOption,
1761    ) -> anyhow::Result<()> {
1762        let mut mock = create_session_mock();
1763
1764        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
1765            mock.expect_begin_transaction().once().returning(|req| {
1766                let req = req.into_inner();
1767                assert_eq!(
1768                    req.session,
1769                    "projects/p/instances/i/databases/d/sessions/123"
1770                );
1771                Ok(tonic::Response::new(v1::Transaction {
1772                    id: vec![4, 5, 6],
1773                    ..Default::default()
1774                }))
1775            });
1776        }
1777
1778        let counter = Arc::new(AtomicI64::new(1));
1779        mock.expect_execute_sql().times(3).returning(move |req| {
1780            let req = req.into_inner();
1781            assert_eq!(req.sql, "UPDATE Users SET Name = 'Alice' WHERE Id = 1");
1782            let c = counter.fetch_add(1, Ordering::SeqCst);
1783            assert_eq!(req.seqno, c);
1784
1785            let mut metadata = v1::ResultSetMetadata {
1786                ..Default::default()
1787            };
1788
1789            if begin_transaction_option == BeginTransactionOption::InlineBegin {
1790                if c == 1 {
1791                    let selector = req
1792                        .transaction
1793                        .expect("missing transaction selector")
1794                        .selector
1795                        .expect("missing selector");
1796                    assert!(matches!(
1797                        selector,
1798                        v1::transaction_selector::Selector::Begin(_)
1799                    ));
1800                    metadata.transaction = Some(v1::Transaction {
1801                        id: vec![4, 5, 6],
1802                        ..Default::default()
1803                    });
1804                } else {
1805                    let selector = req
1806                        .transaction
1807                        .expect("missing transaction selector")
1808                        .selector
1809                        .expect("missing selector");
1810                    match selector {
1811                        v1::transaction_selector::Selector::Id(id) => {
1812                            assert_eq!(id, vec![4, 5, 6]);
1813                        }
1814                        _ => panic!("Expected Selector::Id"),
1815                    }
1816                }
1817            }
1818
1819            Ok(tonic::Response::new(v1::ResultSet {
1820                metadata: Some(metadata),
1821                stats: Some(v1::ResultSetStats {
1822                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
1823                    ..Default::default()
1824                }),
1825                ..Default::default()
1826            }))
1827        });
1828
1829        let (db_client, _server) = setup_db_client(mock).await;
1830
1831        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
1832            .with_begin_transaction_option(begin_transaction_option)
1833            .build(None)
1834            .await
1835            .expect("Failed to build transaction");
1836
1837        for i in 1..=3 {
1838            let count = tx
1839                .execute_update("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
1840                .await
1841                .unwrap_or_else(|_| panic!("Failed to execute update {}", i));
1842            assert_eq!(count, 1);
1843        }
1844        Ok(())
1845    }
1846
1847    #[tokio_test_no_panics]
1848    async fn read_write_transaction_execute_query() {
1849        use crate::statement::Statement;
1850        let mut mock = create_session_mock();
1851
1852        mock.expect_begin_transaction().once().returning(|req| {
1853            let req = req.into_inner();
1854            assert_eq!(
1855                req.session,
1856                "projects/p/instances/i/databases/d/sessions/123"
1857            );
1858            Ok(tonic::Response::new(v1::Transaction {
1859                id: vec![7, 8, 9],
1860                ..Default::default()
1861            }))
1862        });
1863
1864        mock.expect_execute_streaming_sql().once().returning(|req| {
1865            let req = req.into_inner();
1866            assert_eq!(req.sql, "SELECT 1", "SQL statement mismatch");
1867            // Queries in read/write transactions should always include a sequence number.
1868            assert_eq!(req.seqno, 1, "ExecuteSqlRequest seqno mismatch");
1869
1870            assert_eq!(
1871                req.transaction,
1872                Some(v1::TransactionSelector {
1873                    selector: Some(v1::transaction_selector::Selector::Id(vec![7, 8, 9]))
1874                })
1875            );
1876
1877            let prs = v1::PartialResultSet {
1878                metadata: Some(v1::ResultSetMetadata {
1879                    row_type: Some(v1::StructType { fields: vec![] }),
1880                    ..Default::default()
1881                }),
1882                ..Default::default()
1883            };
1884            Ok(tonic::Response::from(crate::result_set::tests::adapt([
1885                Ok(prs),
1886            ])))
1887        });
1888
1889        let (db_client, _server) = setup_db_client(mock).await;
1890
1891        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
1892            .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
1893            .build(None)
1894            .await
1895            .expect("Failed to build transaction");
1896
1897        let mut rs = tx
1898            .execute_query(Statement::builder("SELECT 1").build())
1899            .await
1900            .expect("Failed to execute query");
1901
1902        let result = rs.next().await;
1903        assert!(result.is_none(), "expected None, got empty stream");
1904    }
1905
1906    #[tokio_test_no_panics]
1907    async fn read_write_transaction_with_options() {
1908        let mut mock = create_session_mock();
1909
1910        mock.expect_begin_transaction().once().returning(|req| {
1911            let req = req.into_inner();
1912            assert_eq!(
1913                req.session,
1914                "projects/p/instances/i/databases/d/sessions/123"
1915            );
1916
1917            let options = req.options.expect("missing transaction options");
1918            let mode = options.mode.expect("missing mode");
1919            match mode {
1920                v1::transaction_options::Mode::ReadWrite(rw) => {
1921                    assert_eq!(
1922                        rw.read_lock_mode,
1923                        v1::transaction_options::read_write::ReadLockMode::Pessimistic as i32
1924                    );
1925                }
1926                _ => panic!("Expected ReadWrite transaction mode"),
1927            }
1928            // Ensure isolation level is passed through
1929            assert_eq!(
1930                options.isolation_level,
1931                v1::transaction_options::IsolationLevel::Serializable as i32
1932            );
1933
1934            Ok(tonic::Response::new(v1::Transaction {
1935                id: vec![9, 9, 9],
1936                ..Default::default()
1937            }))
1938        });
1939
1940        let (db_client, _server) = setup_db_client(mock).await;
1941
1942        let _tx = ReadWriteTransactionBuilder::new(db_client.clone())
1943            .set_isolation_level(IsolationLevel::Serializable)
1944            .set_read_lock_mode(ReadLockMode::Pessimistic)
1945            .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
1946            .build(None)
1947            .await
1948            .expect("Failed to build transaction");
1949    }
1950
1951    #[tokio_test_no_panics]
1952    async fn read_write_transaction_with_exclude_txn_from_change_streams() {
1953        let mut mock = create_session_mock();
1954
1955        mock.expect_begin_transaction().once().returning(|req| {
1956            let req = req.into_inner();
1957            let options = req.options.expect("missing transaction options");
1958            assert!(options.exclude_txn_from_change_streams);
1959
1960            Ok(tonic::Response::new(v1::Transaction {
1961                id: vec![9, 9, 9],
1962                ..Default::default()
1963            }))
1964        });
1965
1966        let (db_client, _server) = setup_db_client(mock).await;
1967
1968        let _tx = ReadWriteTransactionBuilder::new(db_client.clone())
1969            .set_exclude_txn_from_change_streams(true)
1970            .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
1971            .build(None)
1972            .await
1973            .expect("Failed to build transaction");
1974    }
1975
1976    #[tokio_test_no_panics]
1977    async fn read_write_transaction_tracks_highest_precommit_token() {
1978        let mut mock = create_session_mock();
1979
1980        mock.expect_begin_transaction().once().returning(|_| {
1981            Ok(tonic::Response::new(v1::Transaction {
1982                id: vec![4, 2],
1983                ..Default::default()
1984            }))
1985        });
1986
1987        // 3 sequential updates returning tokens [seq 2, seq 5, seq 3]
1988        let tokens_iter = vec![2, 5, 3].into_iter();
1989        let counter_mutex = Mutex::new(tokens_iter);
1990
1991        mock.expect_execute_sql().times(3).returning(move |_req| {
1992            let seq = counter_mutex
1993                .lock()
1994                .expect("Failed to lock mutex")
1995                .next()
1996                .expect("Failed to get next token");
1997            Ok(tonic::Response::new(v1::ResultSet {
1998                stats: Some(v1::ResultSetStats {
1999                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2000                    ..Default::default()
2001                }),
2002                precommit_token: Some(v1::MultiplexedSessionPrecommitToken {
2003                    precommit_token: vec![seq as u8],
2004                    seq_num: seq,
2005                }),
2006                ..Default::default()
2007            }))
2008        });
2009
2010        // Commit should only use the highest token (seq 5)
2011        mock.expect_commit().once().returning(|req| {
2012            let req = req.into_inner();
2013            assert_eq!(
2014                req.precommit_token,
2015                Some(v1::MultiplexedSessionPrecommitToken {
2016                    precommit_token: vec![5],
2017                    seq_num: 5,
2018                })
2019            );
2020            Ok(tonic::Response::new(v1::CommitResponse {
2021                commit_timestamp: Some(prost_types::Timestamp {
2022                    seconds: 12345,
2023                    nanos: 0,
2024                }),
2025                ..Default::default()
2026            }))
2027        });
2028
2029        let (db_client, _server) = setup_db_client(mock).await;
2030        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
2031            .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
2032            .build(None)
2033            .await
2034            .expect("Failed to build transaction");
2035
2036        for _ in 0..3 {
2037            tx.execute_update("UPDATE Y")
2038                .await
2039                .expect("Failed to execute update");
2040        }
2041        let ts = tx.commit().await.expect("Failed to commit transaction");
2042        assert_eq!(
2043            ts.commit_timestamp
2044                .expect("Commit timestamp should be present")
2045                .seconds(),
2046            12345
2047        );
2048    }
2049
2050    #[tokio_test_no_panics]
2051    async fn read_write_transaction_commit_retry_exactly_once_explicit() -> anyhow::Result<()> {
2052        run_read_write_transaction_commit_retry_exactly_once(BeginTransactionOption::ExplicitBegin)
2053            .await
2054    }
2055
2056    #[tokio_test_no_panics]
2057    async fn read_write_transaction_commit_retry_exactly_once_inline() -> anyhow::Result<()> {
2058        run_read_write_transaction_commit_retry_exactly_once(BeginTransactionOption::InlineBegin)
2059            .await
2060    }
2061
2062    async fn run_read_write_transaction_commit_retry_exactly_once(
2063        begin_transaction_option: BeginTransactionOption,
2064    ) -> anyhow::Result<()> {
2065        let mut mock = create_session_mock();
2066
2067        let transaction_id = vec![7, 7];
2068
2069        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
2070            let id = transaction_id.clone();
2071            mock.expect_begin_transaction().once().returning(move |_| {
2072                Ok(tonic::Response::new(v1::Transaction {
2073                    id: id.clone(),
2074                    ..Default::default()
2075                }))
2076            });
2077        } else {
2078            let id = transaction_id.clone();
2079            mock.expect_execute_sql().once().returning(move |req| {
2080                let req = req.into_inner();
2081                let transaction = req
2082                    .transaction
2083                    .as_ref()
2084                    .expect("transaction options required for inline begin");
2085                let selector = transaction.selector.as_ref().expect("selector required");
2086                assert!(matches!(
2087                    selector,
2088                    v1::transaction_selector::Selector::Begin(_)
2089                ));
2090
2091                Ok(tonic::Response::new(v1::ResultSet {
2092                    metadata: Some(v1::ResultSetMetadata {
2093                        transaction: Some(v1::Transaction {
2094                            id: id.clone(),
2095                            ..Default::default()
2096                        }),
2097                        ..Default::default()
2098                    }),
2099                    stats: Some(v1::ResultSetStats {
2100                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2101                        ..Default::default()
2102                    }),
2103                    ..Default::default()
2104                }))
2105            });
2106        }
2107
2108        let mut seq = mockall::Sequence::new();
2109
2110        // Initial commit returns a retry token (seq 2)
2111        mock.expect_commit()
2112            .once()
2113            .in_sequence(&mut seq)
2114            .returning(|_| {
2115                Ok(tonic::Response::new(v1::CommitResponse {
2116                    commit_timestamp: Some(prost_types::Timestamp {
2117                        seconds: 1000,
2118                        nanos: 0,
2119                    }),
2120                    multiplexed_session_retry: Some(
2121                        v1::commit_response::MultiplexedSessionRetry::PrecommitToken(
2122                            v1::MultiplexedSessionPrecommitToken {
2123                                precommit_token: vec![2],
2124                                seq_num: 2,
2125                            },
2126                        ),
2127                    ),
2128                    ..Default::default()
2129                }))
2130            });
2131
2132        // Retry commit returns another retry token (seq 3).
2133        // The library should not retry multiple times.
2134        mock.expect_commit()
2135            .once()
2136            .in_sequence(&mut seq)
2137            .returning(|req| {
2138                let req = req.into_inner();
2139                assert_eq!(
2140                    req.precommit_token
2141                        .as_ref()
2142                        .expect("Missing precommit token in retry req")
2143                        .seq_num,
2144                    2
2145                );
2146
2147                Ok(tonic::Response::new(v1::CommitResponse {
2148                    commit_timestamp: Some(prost_types::Timestamp {
2149                        seconds: 9999,
2150                        nanos: 0,
2151                    }),
2152                    multiplexed_session_retry: Some(
2153                        v1::commit_response::MultiplexedSessionRetry::PrecommitToken(
2154                            v1::MultiplexedSessionPrecommitToken {
2155                                precommit_token: vec![3],
2156                                seq_num: 3,
2157                            },
2158                        ),
2159                    ),
2160                    ..Default::default()
2161                }))
2162            });
2163
2164        let (db_client, _server) = setup_db_client(mock).await;
2165        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
2166            .with_begin_transaction_option(begin_transaction_option)
2167            .build(None)
2168            .await?;
2169
2170        if begin_transaction_option == BeginTransactionOption::InlineBegin {
2171            tx.execute_update("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
2172                .await?;
2173        }
2174
2175        let ts = tx.commit().await.expect("Failed to commit transaction");
2176        assert_eq!(
2177            ts.commit_timestamp
2178                .as_ref()
2179                .expect("timestamp should be present")
2180                .seconds(),
2181            9999
2182        );
2183        Ok(())
2184    }
2185
2186    #[tokio_test_no_panics]
2187    async fn read_write_transaction_commit_with_max_commit_delay_explicit() -> anyhow::Result<()> {
2188        run_read_write_transaction_commit_with_max_commit_delay(
2189            BeginTransactionOption::ExplicitBegin,
2190        )
2191        .await
2192    }
2193
2194    #[tokio_test_no_panics]
2195    async fn read_write_transaction_commit_with_max_commit_delay_inline() -> anyhow::Result<()> {
2196        run_read_write_transaction_commit_with_max_commit_delay(BeginTransactionOption::InlineBegin)
2197            .await
2198    }
2199
2200    async fn run_read_write_transaction_commit_with_max_commit_delay(
2201        begin_transaction_option: BeginTransactionOption,
2202    ) -> anyhow::Result<()> {
2203        let mut mock = create_session_mock();
2204
2205        if begin_transaction_option == BeginTransactionOption::ExplicitBegin {
2206            mock.expect_begin_transaction().once().returning(|_| {
2207                Ok(tonic::Response::new(v1::Transaction {
2208                    id: vec![1, 2, 3],
2209                    ..Default::default()
2210                }))
2211            });
2212        } else {
2213            mock.expect_execute_sql().once().returning(|req| {
2214                let req = req.into_inner();
2215                let transaction = req
2216                    .transaction
2217                    .as_ref()
2218                    .expect("transaction options required for inline begin");
2219                let selector = transaction.selector.as_ref().expect("selector required");
2220                assert!(matches!(
2221                    selector,
2222                    v1::transaction_selector::Selector::Begin(_)
2223                ));
2224
2225                Ok(tonic::Response::new(v1::ResultSet {
2226                    metadata: Some(v1::ResultSetMetadata {
2227                        transaction: Some(v1::Transaction {
2228                            id: vec![1, 2, 3],
2229                            ..Default::default()
2230                        }),
2231                        ..Default::default()
2232                    }),
2233                    stats: Some(v1::ResultSetStats {
2234                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2235                        ..Default::default()
2236                    }),
2237                    ..Default::default()
2238                }))
2239            });
2240        }
2241
2242        mock.expect_commit().once().returning(|req| {
2243            let req = req.into_inner();
2244            assert_eq!(
2245                req.max_commit_delay,
2246                Some(::prost_types::Duration {
2247                    seconds: 0,
2248                    nanos: 200_000_000, // 200ms
2249                })
2250            );
2251            Ok(tonic::Response::new(v1::CommitResponse {
2252                commit_timestamp: Some(prost_types::Timestamp {
2253                    seconds: 123456789,
2254                    nanos: 0,
2255                }),
2256                ..Default::default()
2257            }))
2258        });
2259
2260        let (db_client, _server) = setup_db_client(mock).await;
2261
2262        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
2263            .set_max_commit_delay(Duration::new(0, 200_000_000).unwrap())
2264            .with_begin_transaction_option(begin_transaction_option)
2265            .build(None)
2266            .await
2267            .expect("Failed to build transaction");
2268
2269        if begin_transaction_option == BeginTransactionOption::InlineBegin {
2270            tx.execute_update("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
2271                .await?;
2272        }
2273
2274        let ts = tx.commit().await.expect("Failed to commit");
2275        assert_eq!(
2276            ts.commit_timestamp
2277                .expect("Commit timestamp should be present")
2278                .seconds(),
2279            123456789
2280        );
2281        Ok(())
2282    }
2283
2284    #[tokio_test_no_panics]
2285    async fn read_write_transaction_execute_update_no_fallback() {
2286        let mut mock = create_session_mock();
2287
2288        // 1. Initial DML attempt fails!
2289        mock.expect_execute_sql().once().returning(|req| {
2290            let req = req.into_inner();
2291            assert_eq!(req.sql, "UPDATE Users SET Name = 'Alice' WHERE Id = 1");
2292
2293            let selector = req
2294                .transaction
2295                .expect("missing transaction selector")
2296                .selector
2297                .expect("missing selector");
2298            match selector {
2299                v1::transaction_selector::Selector::Begin(_) => {}
2300                _ => panic!("Expected Selector::Begin"),
2301            }
2302
2303            Err(tonic::Status::new(tonic::Code::Internal, "internal error"))
2304        });
2305
2306        let (db_client, _server) = setup_db_client(mock).await;
2307
2308        let tx = ReadWriteTransactionBuilder::new(db_client.clone())
2309            .build(None)
2310            .await
2311            .expect("Failed to build transaction");
2312
2313        // First statement fails and propagates the error
2314        let err = tx
2315            .execute_update("UPDATE Users SET Name = 'Alice' WHERE Id = 1")
2316            .await
2317            .expect_err("Expected update statement to fail");
2318        assert_eq!(err.status().unwrap().code, Code::Internal);
2319
2320        // Second statement fails with Aborted because the transaction is marked as failed
2321        let err2 = tx
2322            .execute_update("UPDATE Users SET Name = 'Alice' WHERE Id = 2")
2323            .await
2324            .expect_err("Expected second statement to fail with Aborted");
2325        assert_eq!(err2.status().unwrap().code, Code::Aborted);
2326        assert_eq!(
2327            err2.status().unwrap().message,
2328            "Aborted due to failed initial statement"
2329        );
2330    }
2331
2332    #[tokio_test_no_panics]
2333    async fn read_write_transaction_execute_batch_update_no_fallback() -> anyhow::Result<()> {
2334        let mut mock = create_session_mock();
2335
2336        // 1. First Batch DML attempt fails!
2337        mock.expect_execute_batch_dml().once().returning(|req| {
2338            let req = req.into_inner();
2339            let selector = req
2340                .transaction
2341                .expect("missing transaction selector")
2342                .selector
2343                .expect("missing selector");
2344            match selector {
2345                v1::transaction_selector::Selector::Begin(_) => {}
2346                _ => panic!("Expected Selector::Begin"),
2347            }
2348
2349            Err(tonic::Status::new(tonic::Code::Internal, "internal error"))
2350        });
2351
2352        let (db_client, _server) = setup_db_client(mock).await;
2353
2354        let tx = ReadWriteTransactionBuilder::new(db_client)
2355            .build(None)
2356            .await?;
2357
2358        let batch =
2359            BatchDml::builder().add_statement("UPDATE Users SET Name = 'Alice' WHERE Id = 1");
2360
2361        // First batch fails and propagates error
2362        let err = tx
2363            .execute_batch_update(batch.build())
2364            .await
2365            .expect_err("Expected batch update to fail");
2366        assert_eq!(err.status().unwrap().code, Code::Internal);
2367
2368        // Second statement fails with Aborted
2369        let err2 = tx
2370            .execute_update("UPDATE Users SET Name = 'Alice' WHERE Id = 2")
2371            .await
2372            .expect_err("Expected statement to fail with Aborted");
2373        assert_eq!(err2.status().unwrap().code, Code::Aborted);
2374
2375        Ok(())
2376    }
2377
2378    #[tokio_test_no_panics]
2379    async fn leader_aware_routing_enabled_by_default() -> anyhow::Result<()> {
2380        let mut mock = create_session_mock();
2381        mock.expect_begin_transaction().once().returning(|req| {
2382            assert_eq!(
2383                req.metadata()
2384                    .get("x-goog-spanner-route-to-leader")
2385                    .expect("header required")
2386                    .to_str()
2387                    .unwrap(),
2388                "true"
2389            );
2390            Ok(tonic::Response::new(v1::Transaction {
2391                id: vec![1, 2, 3],
2392                ..Default::default()
2393            }))
2394        });
2395        mock.expect_execute_sql().once().returning(|req| {
2396            assert_eq!(
2397                req.metadata()
2398                    .get("x-goog-spanner-route-to-leader")
2399                    .expect("header required")
2400                    .to_str()
2401                    .unwrap(),
2402                "true"
2403            );
2404            Ok(tonic::Response::new(v1::ResultSet {
2405                stats: Some(v1::ResultSetStats {
2406                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2407                    ..Default::default()
2408                }),
2409                ..Default::default()
2410            }))
2411        });
2412        mock.expect_commit().once().returning(|req| {
2413            assert_eq!(
2414                req.metadata()
2415                    .get("x-goog-spanner-route-to-leader")
2416                    .expect("header required")
2417                    .to_str()
2418                    .unwrap(),
2419                "true"
2420            );
2421            Ok(tonic::Response::new(v1::CommitResponse {
2422                commit_timestamp: Some(prost_types::Timestamp {
2423                    seconds: 1,
2424                    nanos: 0,
2425                }),
2426                ..Default::default()
2427            }))
2428        });
2429
2430        let (db_client, _server) = setup_db_client(mock).await;
2431        let tx = ReadWriteTransactionBuilder::new(db_client)
2432            .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
2433            .build(None)
2434            .await?;
2435        let count = tx.execute_update("UPDATE Users SET active = true").await?;
2436        assert_eq!(count, 1);
2437        let _ = tx.commit().await?;
2438        Ok(())
2439    }
2440
2441    #[tokio_test_no_panics]
2442    async fn leader_aware_routing_disabled() -> anyhow::Result<()> {
2443        use crate::client::Spanner;
2444        use google_cloud_auth::credentials::anonymous::Builder as Anonymous;
2445
2446        let mut mock = create_session_mock();
2447        mock.expect_begin_transaction().once().returning(|req| {
2448            assert!(
2449                req.metadata()
2450                    .get("x-goog-spanner-route-to-leader")
2451                    .is_none()
2452            );
2453            Ok(tonic::Response::new(v1::Transaction {
2454                id: vec![1, 2, 3],
2455                ..Default::default()
2456            }))
2457        });
2458        mock.expect_execute_sql().once().returning(|req| {
2459            assert!(
2460                req.metadata()
2461                    .get("x-goog-spanner-route-to-leader")
2462                    .is_none()
2463            );
2464            Ok(tonic::Response::new(v1::ResultSet {
2465                stats: Some(v1::ResultSetStats {
2466                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2467                    ..Default::default()
2468                }),
2469                ..Default::default()
2470            }))
2471        });
2472        mock.expect_commit().once().returning(|req| {
2473            assert!(
2474                req.metadata()
2475                    .get("x-goog-spanner-route-to-leader")
2476                    .is_none()
2477            );
2478            Ok(tonic::Response::new(v1::CommitResponse {
2479                commit_timestamp: Some(prost_types::Timestamp {
2480                    seconds: 1,
2481                    nanos: 0,
2482                }),
2483                ..Default::default()
2484            }))
2485        });
2486
2487        let (address, _server) = spanner_grpc_mock::start("0.0.0.0:0", mock).await.unwrap();
2488        let spanner = Spanner::builder()
2489            .with_endpoint(address)
2490            .with_credentials(Anonymous::new().build())
2491            .build()
2492            .await?;
2493        let db_client = spanner
2494            .database_client("projects/p/instances/i/databases/d")
2495            .with_leader_aware_routing(false)
2496            .build()
2497            .await?;
2498
2499        let tx = ReadWriteTransactionBuilder::new(db_client)
2500            .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
2501            .build(None)
2502            .await?;
2503        let count = tx.execute_update("UPDATE Users SET active = true").await?;
2504        assert_eq!(count, 1);
2505        let _ = tx.commit().await?;
2506        Ok(())
2507    }
2508
2509    #[tokio_test_no_panics]
2510    async fn leader_aware_routing_query_in_read_write() -> anyhow::Result<()> {
2511        let mut mock = create_session_mock();
2512        mock.expect_begin_transaction().once().returning(|req| {
2513            assert_eq!(
2514                req.metadata()
2515                    .get("x-goog-spanner-route-to-leader")
2516                    .expect("header required")
2517                    .to_str()
2518                    .unwrap(),
2519                "true"
2520            );
2521            Ok(tonic::Response::new(v1::Transaction {
2522                id: vec![1, 2, 3],
2523                ..Default::default()
2524            }))
2525        });
2526        mock.expect_execute_streaming_sql().once().returning(|req| {
2527            assert_eq!(
2528                req.metadata()
2529                    .get("x-goog-spanner-route-to-leader")
2530                    .expect("header required")
2531                    .to_str()
2532                    .unwrap(),
2533                "true"
2534            );
2535            let stream = adapt([Ok(v1::PartialResultSet {
2536                metadata: Some(v1::ResultSetMetadata {
2537                    row_type: Some(v1::StructType { fields: vec![] }),
2538                    ..Default::default()
2539                }),
2540                ..Default::default()
2541            })]);
2542            Ok(tonic::Response::from(stream))
2543        });
2544
2545        let (db_client, _server) = setup_db_client(mock).await;
2546        let tx = ReadWriteTransactionBuilder::new(db_client)
2547            .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
2548            .build(None)
2549            .await?;
2550        let _rs = tx
2551            .execute_query(Statement::builder("SELECT 1").build())
2552            .await?;
2553        Ok(())
2554    }
2555
2556    #[tokio_test_no_panics]
2557    async fn leader_aware_routing_merges_custom_headers() -> anyhow::Result<()> {
2558        let mut mock = create_session_mock();
2559        mock.expect_begin_transaction().once().returning(|req| {
2560            assert_eq!(
2561                req.metadata()
2562                    .get("x-goog-spanner-route-to-leader")
2563                    .expect("header required")
2564                    .to_str()
2565                    .unwrap(),
2566                "true"
2567            );
2568            Ok(tonic::Response::new(v1::Transaction {
2569                id: vec![1, 2, 3],
2570                ..Default::default()
2571            }))
2572        });
2573        mock.expect_execute_sql().once().returning(|req| {
2574            assert_eq!(
2575                req.metadata()
2576                    .get("x-goog-spanner-route-to-leader")
2577                    .expect("header required")
2578                    .to_str()
2579                    .unwrap(),
2580                "true"
2581            );
2582            assert_eq!(
2583                req.metadata()
2584                    .get("x-custom-user-header")
2585                    .expect("custom header required")
2586                    .to_str()
2587                    .unwrap(),
2588                "custom-value"
2589            );
2590            Ok(tonic::Response::new(v1::ResultSet {
2591                stats: Some(v1::ResultSetStats {
2592                    row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2593                    ..Default::default()
2594                }),
2595                ..Default::default()
2596            }))
2597        });
2598
2599        let (db_client, _server) = setup_db_client(mock).await;
2600        let tx = ReadWriteTransactionBuilder::new(db_client)
2601            .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
2602            .build(None)
2603            .await?;
2604
2605        let mut custom_headers = http::HeaderMap::new();
2606        custom_headers.insert(
2607            "x-custom-user-header",
2608            http::HeaderValue::from_static("custom-value"),
2609        );
2610
2611        let mut stmt = Statement::builder("UPDATE Users SET active = true").build();
2612        let opts = stmt.gax_options().clone().insert_extension(custom_headers);
2613        stmt = stmt.with_gax_options(opts);
2614
2615        let count = tx.execute_update(stmt).await?;
2616        assert_eq!(count, 1);
2617        Ok(())
2618    }
2619
2620    #[tokio_test_no_panics]
2621    async fn leader_aware_routing_implicit_begin_runner_retry() -> anyhow::Result<()> {
2622        let mut mock = create_session_mock();
2623        let mut seq = mockall::Sequence::new();
2624
2625        // 1. Initial execute_sql attempts implicit begin and fails with Internal.
2626        // It must include the LAR header because it is a modifying operation.
2627        mock.expect_execute_sql()
2628            .once()
2629            .in_sequence(&mut seq)
2630            .returning(|req| {
2631                assert_eq!(
2632                    req.metadata()
2633                        .get("x-goog-spanner-route-to-leader")
2634                        .expect("header required on initial execute")
2635                        .to_str()
2636                        .unwrap(),
2637                    "true"
2638                );
2639                let req = req.into_inner();
2640                let transaction = req.transaction.unwrap();
2641                assert!(matches!(
2642                    transaction.selector.unwrap(),
2643                    v1::transaction_selector::Selector::Begin(_)
2644                ));
2645                Err(tonic::Status::new(
2646                    tonic::Code::Internal,
2647                    "transient internal error",
2648                ))
2649            });
2650
2651        // 2. Commit of the first attempt will fail with AbortedDueToFailedFirstStatement (client-side aborted).
2652        // The runner retries. Since first statement failed on starting, it forces explicit BeginTransaction.
2653        // This BeginTransaction must include the LAR header.
2654        mock.expect_begin_transaction()
2655            .once()
2656            .in_sequence(&mut seq)
2657            .returning(|req| {
2658                assert_eq!(
2659                    req.metadata()
2660                        .get("x-goog-spanner-route-to-leader")
2661                        .expect("header required on explicit begin fallback")
2662                        .to_str()
2663                        .unwrap(),
2664                    "true"
2665                );
2666                Ok(tonic::Response::new(v1::Transaction {
2667                    id: vec![42],
2668                    ..Default::default()
2669                }))
2670            });
2671
2672        // 3. Statement execution on the retry attempt must succeed and use the transaction ID.
2673        // It must also include the LAR header.
2674        mock.expect_execute_sql()
2675            .once()
2676            .in_sequence(&mut seq)
2677            .returning(|req| {
2678                assert_eq!(
2679                    req.metadata()
2680                        .get("x-goog-spanner-route-to-leader")
2681                        .expect("header required on retry execute")
2682                        .to_str()
2683                        .unwrap(),
2684                    "true"
2685                );
2686                let req = req.into_inner();
2687                let transaction = req.transaction.unwrap();
2688                assert!(matches!(
2689                    transaction.selector.unwrap(),
2690                    v1::transaction_selector::Selector::Id(id) if id == vec![42]
2691                ));
2692                Ok(tonic::Response::new(v1::ResultSet {
2693                    metadata: Some(v1::ResultSetMetadata {
2694                        transaction: Some(v1::Transaction {
2695                            id: vec![42],
2696                            ..Default::default()
2697                        }),
2698                        ..Default::default()
2699                    }),
2700                    stats: Some(v1::ResultSetStats {
2701                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2702                        ..Default::default()
2703                    }),
2704                    ..Default::default()
2705                }))
2706            });
2707
2708        mock.expect_commit().once().returning(|_req| {
2709            Ok(tonic::Response::new(v1::CommitResponse {
2710                commit_timestamp: Some(prost_types::Timestamp {
2711                    seconds: 123,
2712                    nanos: 0,
2713                }),
2714                ..Default::default()
2715            }))
2716        });
2717
2718        let (db_client, _server) = setup_db_client(mock).await;
2719
2720        let runner = db_client
2721            .read_write_transaction()
2722            .with_retry_policy(BasicTransactionRetryPolicy::new().with_max_attempts(2))
2723            .build()
2724            .await?;
2725
2726        let attempt = Arc::new(AtomicU32::new(0));
2727        let att_clone = attempt.clone();
2728        let res = runner
2729            .run(async |tx| {
2730                let current_attempt = att_clone.fetch_add(1, Ordering::SeqCst) + 1;
2731                let res = tx.execute_update("UPDATE Users SET active = true").await;
2732                if current_attempt == 1 {
2733                    assert!(res.is_err());
2734                    let err = res.unwrap_err();
2735                    assert_eq!(err.status().map(|s| s.code), Some(Code::Internal));
2736                    // Swallow the internal error and let the transaction attempt to commit.
2737                    // The commit will fail with AbortedDueToFailedFirstStatement because the first statement failed.
2738                    Ok(())
2739                } else {
2740                    assert_eq!(res.unwrap(), 1);
2741                    Ok(())
2742                }
2743            })
2744            .await?;
2745
2746        assert!(res.commit_response.commit_timestamp.is_some());
2747        Ok(())
2748    }
2749
2750    #[tokio_test_no_panics]
2751    async fn read_write_transaction_tag_forwarded_on_runner_retry() -> anyhow::Result<()> {
2752        let mut mock = create_session_mock();
2753        let mut seq = mockall::Sequence::new();
2754
2755        // 1. Initial execute_sql attempts inline begin and fails with Internal.
2756        // It must include the transaction tag.
2757        mock.expect_execute_sql()
2758            .once()
2759            .in_sequence(&mut seq)
2760            .returning(|req| {
2761                let req = req.into_inner();
2762                assert_eq!(
2763                    req.request_options
2764                        .as_ref()
2765                        .expect("Missing request_options on initial RPC")
2766                        .transaction_tag,
2767                    "fallback-test-tag"
2768                );
2769                assert!(matches!(
2770                    req.transaction.unwrap().selector.unwrap(),
2771                    v1::transaction_selector::Selector::Begin(_)
2772                ));
2773                Err(tonic::Status::new(
2774                    tonic::Code::Internal,
2775                    "transient internal error",
2776                ))
2777            });
2778
2779        // 2. Commit fails with AbortedDueToFailedFirstStatement, runner retries with explicit BeginTransaction.
2780        // The BeginTransaction must include the transaction tag.
2781        mock.expect_begin_transaction()
2782            .once()
2783            .in_sequence(&mut seq)
2784            .returning(|req| {
2785                let req = req.into_inner();
2786                assert_eq!(
2787                    req.request_options
2788                        .as_ref()
2789                        .expect("Missing request_options on explicit begin")
2790                        .transaction_tag,
2791                    "fallback-test-tag"
2792                );
2793                Ok(tonic::Response::new(v1::Transaction {
2794                    id: vec![7, 7, 7],
2795                    ..Default::default()
2796                }))
2797            });
2798
2799        // 3. Retried statement execution must succeed and use transaction ID.
2800        // It must include the transaction tag.
2801        mock.expect_execute_sql()
2802            .once()
2803            .in_sequence(&mut seq)
2804            .returning(|req| {
2805                let req = req.into_inner();
2806                assert_eq!(
2807                    req.request_options
2808                        .as_ref()
2809                        .expect("Missing request_options on retried RPC")
2810                        .transaction_tag,
2811                    "fallback-test-tag"
2812                );
2813                assert!(matches!(
2814                    req.transaction.unwrap().selector.unwrap(),
2815                    v1::transaction_selector::Selector::Id(id) if id == vec![7, 7, 7]
2816                ));
2817                Ok(tonic::Response::new(v1::ResultSet {
2818                    metadata: Some(v1::ResultSetMetadata {
2819                        transaction: Some(v1::Transaction {
2820                            id: vec![7, 7, 7],
2821                            ..Default::default()
2822                        }),
2823                        ..Default::default()
2824                    }),
2825                    stats: Some(v1::ResultSetStats {
2826                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
2827                        ..Default::default()
2828                    }),
2829                    ..Default::default()
2830                }))
2831            });
2832
2833        mock.expect_commit().once().returning(|req| {
2834            let req = req.into_inner();
2835            assert_eq!(
2836                req.request_options
2837                    .as_ref()
2838                    .expect("Missing request_options on commit")
2839                    .transaction_tag,
2840                "fallback-test-tag"
2841            );
2842            Ok(tonic::Response::new(v1::CommitResponse {
2843                commit_timestamp: Some(prost_types::Timestamp {
2844                    seconds: 123,
2845                    nanos: 0,
2846                }),
2847                ..Default::default()
2848            }))
2849        });
2850
2851        let (db_client, _server) = setup_db_client(mock).await;
2852        let runner = db_client
2853            .read_write_transaction()
2854            .set_transaction_tag("fallback-test-tag")
2855            .with_retry_policy(BasicTransactionRetryPolicy::new().with_max_attempts(2))
2856            .build()
2857            .await?;
2858
2859        let attempt = Arc::new(AtomicU32::new(0));
2860        let att_clone = attempt.clone();
2861        let res = runner
2862            .run(async |tx| {
2863                let current_attempt = att_clone.fetch_add(1, Ordering::SeqCst) + 1;
2864                let res = tx.execute_update("UPDATE Users SET active = true").await;
2865                if current_attempt == 1 {
2866                    assert!(res.is_err());
2867                    let err = res.unwrap_err();
2868                    assert_eq!(err.status().map(|s| s.code), Some(Code::Internal));
2869                    // Swallow the internal error and let it commit to trigger AbortedDueToFailedFirstStatement retry
2870                    Ok(())
2871                } else {
2872                    assert_eq!(res.unwrap(), 1);
2873                    Ok(())
2874                }
2875            })
2876            .await?;
2877
2878        assert!(res.commit_response.commit_timestamp.is_some());
2879        Ok(())
2880    }
2881
2882    #[tokio_test_no_panics]
2883    async fn read_write_transaction_mutation_only_inline_begin_commit() -> anyhow::Result<()> {
2884        let mut mock = create_session_mock();
2885
2886        // Since no statement was executed, commit will detect NotStarted and call begin_explicitly
2887        mock.expect_begin_transaction().once().returning(|req| {
2888            let req = req.into_inner();
2889            assert_eq!(
2890                req.session,
2891                "projects/p/instances/i/databases/d/sessions/123"
2892            );
2893            assert!(
2894                req.mutation_key.is_some(),
2895                "mutation_key should be populated when starting transaction at commit time"
2896            );
2897            let key = req
2898                .mutation_key
2899                .as_ref()
2900                .expect("mutation_key is populated");
2901            assert!(
2902                key.operation.is_some(),
2903                "mutation_key should have an operation"
2904            );
2905            Ok(tonic::Response::new(v1::Transaction {
2906                id: vec![7, 7, 7],
2907                ..Default::default()
2908            }))
2909        });
2910
2911        mock.expect_commit().once().returning(|req| {
2912            let req = req.into_inner();
2913            assert_eq!(
2914                req.session,
2915                "projects/p/instances/i/databases/d/sessions/123"
2916            );
2917            assert_eq!(
2918                req.transaction,
2919                Some(v1::commit_request::Transaction::TransactionId(vec![
2920                    7, 7, 7
2921                ]))
2922            );
2923            assert_eq!(req.mutations.len(), 1);
2924            Ok(tonic::Response::new(v1::CommitResponse {
2925                commit_timestamp: Some(prost_types::Timestamp {
2926                    seconds: 5000,
2927                    nanos: 0,
2928                }),
2929                ..Default::default()
2930            }))
2931        });
2932
2933        let (db_client, _server) = setup_db_client(mock).await;
2934        let tx = ReadWriteTransactionBuilder::new(db_client)
2935            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
2936            .build(None)
2937            .await
2938            .expect("Transaction build should succeed");
2939
2940        let mutation = Mutation::new_insert_builder("Users")
2941            .set("UserId")
2942            .to(&1)
2943            .build();
2944        tx.buffer([mutation]).expect("Buffer should succeed");
2945
2946        let response = tx.commit().await.expect("Commit should succeed");
2947        assert_eq!(
2948            response
2949                .commit_timestamp
2950                .expect("timestamp present")
2951                .seconds(),
2952            5000
2953        );
2954        Ok(())
2955    }
2956
2957    #[tokio_test_no_panics]
2958    async fn transaction_runner_batch_dml_aborted_retry() -> anyhow::Result<()> {
2959        let mut mock = create_session_mock();
2960        let mut sequence = mockall::Sequence::new();
2961
2962        // 1. First attempt: Inline begin, execute_batch_dml returns OK with status Aborted.
2963        mock.expect_execute_batch_dml()
2964            .times(1)
2965            .in_sequence(&mut sequence)
2966            .returning(|req| {
2967                let req = req.into_inner();
2968                assert!(matches!(
2969                    req.transaction.unwrap().selector.unwrap(),
2970                    v1::transaction_selector::Selector::Begin(_)
2971                ));
2972                Ok(tonic::Response::new(v1::ExecuteBatchDmlResponse {
2973                    result_sets: vec![],
2974                    status: Some(spanner_grpc_mock::google::rpc::Status {
2975                        code: tonic::Code::Aborted as i32,
2976                        message: "concurrent lock abort".into(),
2977                        details: vec![],
2978                    }),
2979                    ..Default::default()
2980                }))
2981            });
2982
2983        // 2. TransactionRunner catches Aborted error and initiates attempt 2.
2984        mock.expect_execute_batch_dml()
2985            .times(1)
2986            .in_sequence(&mut sequence)
2987            .returning(|req| {
2988                let req = req.into_inner();
2989                assert!(matches!(
2990                    req.transaction.unwrap().selector.unwrap(),
2991                    v1::transaction_selector::Selector::Begin(_)
2992                ));
2993                Ok(tonic::Response::new(v1::ExecuteBatchDmlResponse {
2994                    result_sets: vec![v1::ResultSet {
2995                        metadata: Some(v1::ResultSetMetadata {
2996                            transaction: Some(v1::Transaction {
2997                                id: vec![9, 9, 9],
2998                                ..Default::default()
2999                            }),
3000                            ..Default::default()
3001                        }),
3002                        stats: Some(v1::ResultSetStats {
3003                            row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
3004                            ..Default::default()
3005                        }),
3006                        ..Default::default()
3007                    }],
3008                    status: Some(spanner_grpc_mock::google::rpc::Status {
3009                        code: 0,
3010                        message: "OK".into(),
3011                        details: vec![],
3012                    }),
3013                    ..Default::default()
3014                }))
3015            });
3016
3017        mock.expect_commit().once().returning(|req| {
3018            let req = req.into_inner();
3019            assert_eq!(
3020                req.transaction,
3021                Some(v1::commit_request::Transaction::TransactionId(vec![
3022                    9, 9, 9
3023                ]))
3024            );
3025            Ok(tonic::Response::new(v1::CommitResponse {
3026                commit_timestamp: Some(prost_types::Timestamp {
3027                    seconds: 999,
3028                    nanos: 0,
3029                }),
3030                ..Default::default()
3031            }))
3032        });
3033
3034        let (db_client, _server) = setup_db_client(mock).await;
3035
3036        let runner = db_client
3037            .read_write_transaction()
3038            .with_retry_policy(
3039                BasicTransactionRetryPolicy::new()
3040                    .with_max_attempts(3)
3041                    .with_total_timeout(std::time::Duration::from_secs(5)),
3042            )
3043            .build()
3044            .await?;
3045
3046        runner
3047            .run(async |tx| {
3048                let batch = BatchDml::builder()
3049                    .add_statement("UPDATE Users SET active = true WHERE id = 1");
3050                tx.execute_batch_update(batch.build()).await?;
3051                Ok(())
3052            })
3053            .await?;
3054
3055        Ok(())
3056    }
3057
3058    #[tokio_test_no_panics]
3059    async fn read_write_transaction_first_dml_aborted_and_continue_success() -> anyhow::Result<()> {
3060        let mut mock = create_session_mock();
3061        let mut sequence = mockall::Sequence::new();
3062
3063        // 1. First statement (execute_sql) attempts inline begin and is aborted by Spanner
3064        mock.expect_execute_sql()
3065            .times(1)
3066            .in_sequence(&mut sequence)
3067            .returning(|req| {
3068                let req = req.into_inner();
3069                assert!(matches!(
3070                    req.transaction.unwrap().selector.unwrap(),
3071                    v1::transaction_selector::Selector::Begin(_)
3072                ));
3073                Err(tonic::Status::new(
3074                    tonic::Code::Aborted,
3075                    "concurrent lock abort",
3076                ))
3077            });
3078
3079        // 2. Second statement (execute_sql) sees NotStarted and attempts inline begin again
3080        mock.expect_execute_sql()
3081            .times(1)
3082            .in_sequence(&mut sequence)
3083            .returning(|req| {
3084                let req = req.into_inner();
3085                assert!(matches!(
3086                    req.transaction.unwrap().selector.unwrap(),
3087                    v1::transaction_selector::Selector::Begin(_)
3088                ));
3089                Ok(tonic::Response::new(v1::ResultSet {
3090                    metadata: Some(v1::ResultSetMetadata {
3091                        transaction: Some(v1::Transaction {
3092                            id: vec![9, 9, 9],
3093                            ..Default::default()
3094                        }),
3095                        ..Default::default()
3096                    }),
3097                    stats: Some(v1::ResultSetStats {
3098                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
3099                        ..Default::default()
3100                    }),
3101                    ..Default::default()
3102                }))
3103            });
3104
3105        // 3. Commit called with the transaction ID returned in step 2
3106        mock.expect_commit().once().returning(|req| {
3107            let req = req.into_inner();
3108            assert_eq!(
3109                req.transaction,
3110                Some(v1::commit_request::Transaction::TransactionId(vec![
3111                    9, 9, 9
3112                ]))
3113            );
3114            Ok(tonic::Response::new(v1::CommitResponse {
3115                commit_timestamp: Some(prost_types::Timestamp {
3116                    seconds: 999,
3117                    nanos: 0,
3118                }),
3119                ..Default::default()
3120            }))
3121        });
3122
3123        let (db_client, _server) = setup_db_client(mock).await;
3124
3125        let runner = db_client
3126            .read_write_transaction()
3127            .with_retry_policy(
3128                BasicTransactionRetryPolicy::new()
3129                    .with_max_attempts(1)
3130                    .with_total_timeout(std::time::Duration::from_secs(5)),
3131            )
3132            .build()
3133            .await?;
3134
3135        runner
3136            .run(async |tx| {
3137                // 1. First statement fails with Aborted. We catch it and continue.
3138                let res = tx
3139                    .execute_update("UPDATE Users SET active = true WHERE id = 1")
3140                    .await;
3141                assert!(res.is_err(), "First statement must return error");
3142                assert!(is_aborted(&res.unwrap_err()), "Error must be Aborted");
3143
3144                // 2. Second statement continues. Without the fix, this would block/deadlock forever.
3145                let count = tx
3146                    .execute_update("UPDATE Users SET active = true WHERE id = 2")
3147                    .await?;
3148                assert_eq!(count, 1);
3149                Ok(())
3150            })
3151            .await?;
3152
3153        Ok(())
3154    }
3155
3156    #[tokio_test_no_panics]
3157    async fn read_write_transaction_first_batch_dml_aborted_and_continue_success()
3158    -> anyhow::Result<()> {
3159        let mut mock = create_session_mock();
3160        let mut sequence = mockall::Sequence::new();
3161
3162        // 1. First statement (execute_batch_dml) attempts inline begin and is aborted by Spanner
3163        mock.expect_execute_batch_dml()
3164            .times(1)
3165            .in_sequence(&mut sequence)
3166            .returning(|req| {
3167                let req = req.into_inner();
3168                assert!(matches!(
3169                    req.transaction.unwrap().selector.unwrap(),
3170                    v1::transaction_selector::Selector::Begin(_)
3171                ));
3172                Err(tonic::Status::new(
3173                    tonic::Code::Aborted,
3174                    "concurrent lock abort",
3175                ))
3176            });
3177
3178        // 2. Second statement (execute_sql) sees NotStarted and attempts inline begin again
3179        mock.expect_execute_sql()
3180            .times(1)
3181            .in_sequence(&mut sequence)
3182            .returning(|req| {
3183                let req = req.into_inner();
3184                assert!(matches!(
3185                    req.transaction.unwrap().selector.unwrap(),
3186                    v1::transaction_selector::Selector::Begin(_)
3187                ));
3188                Ok(tonic::Response::new(v1::ResultSet {
3189                    metadata: Some(v1::ResultSetMetadata {
3190                        transaction: Some(v1::Transaction {
3191                            id: vec![9, 9, 9],
3192                            ..Default::default()
3193                        }),
3194                        ..Default::default()
3195                    }),
3196                    stats: Some(v1::ResultSetStats {
3197                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
3198                        ..Default::default()
3199                    }),
3200                    ..Default::default()
3201                }))
3202            });
3203
3204        // 3. Commit called with the transaction ID returned in step 2
3205        mock.expect_commit().once().returning(|req| {
3206            let req = req.into_inner();
3207            assert_eq!(
3208                req.transaction,
3209                Some(v1::commit_request::Transaction::TransactionId(vec![
3210                    9, 9, 9
3211                ]))
3212            );
3213            Ok(tonic::Response::new(v1::CommitResponse {
3214                commit_timestamp: Some(prost_types::Timestamp {
3215                    seconds: 999,
3216                    nanos: 0,
3217                }),
3218                ..Default::default()
3219            }))
3220        });
3221
3222        let (db_client, _server) = setup_db_client(mock).await;
3223
3224        let runner = db_client
3225            .read_write_transaction()
3226            .with_retry_policy(
3227                BasicTransactionRetryPolicy::new()
3228                    .with_max_attempts(1)
3229                    .with_total_timeout(std::time::Duration::from_secs(5)),
3230            )
3231            .build()
3232            .await?;
3233
3234        runner
3235            .run(async |tx| {
3236                // 1. First statement (Batch DML) fails with Aborted. We catch it and continue.
3237                let batch = BatchDml::builder()
3238                    .add_statement("UPDATE Users SET active = true WHERE id = 1");
3239                let res = tx.execute_batch_update(batch.build()).await;
3240                assert!(res.is_err(), "First statement must return error");
3241                assert!(is_aborted(&res.unwrap_err()), "Error must be Aborted");
3242
3243                // 2. Second statement continues. Without the fix, this would block/deadlock forever.
3244                let count = tx
3245                    .execute_update("UPDATE Users SET active = true WHERE id = 2")
3246                    .await?;
3247                assert_eq!(count, 1);
3248                Ok(())
3249            })
3250            .await?;
3251
3252        Ok(())
3253    }
3254
3255    #[tokio_test_no_panics]
3256    async fn read_write_transaction_first_query_failed_and_continue_retry() -> anyhow::Result<()> {
3257        let mut mock = create_session_mock();
3258        let mut sequence = mockall::Sequence::new();
3259
3260        // 1. First attempt: execute_streaming_sql fails directly with Internal error
3261        mock.expect_execute_streaming_sql()
3262            .once()
3263            .in_sequence(&mut sequence)
3264            .returning(|_| {
3265                Err(tonic::Status::new(
3266                    tonic::Code::Internal,
3267                    "transient internal error",
3268                ))
3269            });
3270
3271        // 2. Commit fails client-side with Aborted due to failed initial statement.
3272        // Runner retries with explicit BeginTransaction
3273        mock.expect_begin_transaction()
3274            .once()
3275            .in_sequence(&mut sequence)
3276            .returning(|_| {
3277                Ok(tonic::Response::new(v1::Transaction {
3278                    id: vec![42],
3279                    ..Default::default()
3280                }))
3281            });
3282
3283        // 3. Second attempt query succeeds
3284        mock.expect_execute_streaming_sql()
3285            .once()
3286            .in_sequence(&mut sequence)
3287            .returning(|_| {
3288                let rx = adapt([Ok(v1::PartialResultSet {
3289                    metadata: Some(v1::ResultSetMetadata {
3290                        row_type: Some(v1::StructType {
3291                            fields: vec![v1::struct_type::Field {
3292                                name: "Name".to_string(),
3293                                r#type: Some(v1::Type {
3294                                    code: v1::TypeCode::String as i32,
3295                                    ..Default::default()
3296                                }),
3297                            }],
3298                        }),
3299                        transaction: Some(v1::Transaction {
3300                            id: vec![42],
3301                            ..Default::default()
3302                        }),
3303                        ..Default::default()
3304                    }),
3305                    values: vec![string_val("alice")],
3306                    ..Default::default()
3307                })]);
3308                Ok(tonic::Response::from(rx))
3309            });
3310
3311        mock.expect_commit().once().returning(|_| {
3312            Ok(tonic::Response::new(v1::CommitResponse {
3313                commit_timestamp: Some(prost_types::Timestamp {
3314                    seconds: 1234,
3315                    nanos: 0,
3316                }),
3317                ..Default::default()
3318            }))
3319        });
3320
3321        let (db_client, _server) = setup_db_client(mock).await;
3322        let runner = db_client
3323            .read_write_transaction()
3324            .with_retry_policy(BasicTransactionRetryPolicy::new().with_max_attempts(2))
3325            .build()
3326            .await?;
3327
3328        let attempt = Arc::new(AtomicU32::new(0));
3329        let att_clone = attempt.clone();
3330        let res = runner
3331            .run(async |tx| {
3332                let current_attempt = att_clone.fetch_add(1, Ordering::SeqCst) + 1;
3333                let res = tx
3334                    .execute_query("SELECT Name FROM Users WHERE Id = 1")
3335                    .await;
3336                if current_attempt == 1 {
3337                    assert!(res.is_err());
3338                    let err = res.unwrap_err();
3339                    assert_eq!(err.status().map(|s| s.code), Some(Code::Internal));
3340                    // Swallow error and continue
3341                    Ok(())
3342                } else {
3343                    let mut rs = res.unwrap();
3344                    let row = rs.next().await.unwrap().unwrap();
3345                    assert_eq!(row.get::<String, _>(0), "alice");
3346                    Ok(())
3347                }
3348            })
3349            .await?;
3350
3351        assert!(res.commit_response.commit_timestamp.is_some());
3352        Ok(())
3353    }
3354
3355    #[tokio_test_no_panics]
3356    async fn read_write_transaction_first_query_failed_propagate_no_retry() -> anyhow::Result<()> {
3357        let mut mock = create_session_mock();
3358
3359        // 1. Initial query fails directly
3360        mock.expect_execute_streaming_sql().once().returning(|_| {
3361            Err(tonic::Status::new(
3362                tonic::Code::Internal,
3363                "transient internal error",
3364            ))
3365        });
3366
3367        let (db_client, _server) = setup_db_client(mock).await;
3368        let runner = db_client
3369            .read_write_transaction()
3370            .with_retry_policy(BasicTransactionRetryPolicy::new().with_max_attempts(2))
3371            .build()
3372            .await?;
3373
3374        let res = runner
3375            .run(async |tx| {
3376                let _ = tx
3377                    .execute_query("SELECT Name FROM Users WHERE Id = 1")
3378                    .await?;
3379                Ok(())
3380            })
3381            .await;
3382
3383        assert!(res.is_err());
3384        let err = res.unwrap_err();
3385        assert_eq!(err.status().map(|s| s.code), Some(Code::Internal));
3386        Ok(())
3387    }
3388
3389    #[tokio_test_no_panics]
3390    async fn read_write_transaction_query_failed_after_id_received_no_retry() -> anyhow::Result<()>
3391    {
3392        let mut mock = create_session_mock();
3393        let mut sequence = mockall::Sequence::new();
3394
3395        // 1. Query execution starts and yields a stream
3396        mock.expect_execute_streaming_sql()
3397            .once()
3398            .in_sequence(&mut sequence)
3399            .returning(|_| {
3400                let rx = adapt([
3401                    Ok(v1::PartialResultSet {
3402                        metadata: Some(v1::ResultSetMetadata {
3403                            row_type: Some(v1::StructType {
3404                                fields: vec![v1::struct_type::Field {
3405                                    name: "Name".to_string(),
3406                                    r#type: Some(v1::Type {
3407                                        code: v1::TypeCode::String as i32,
3408                                        ..Default::default()
3409                                    }),
3410                                }],
3411                            }),
3412                            transaction: Some(v1::Transaction {
3413                                id: vec![42],
3414                                ..Default::default()
3415                            }),
3416                            ..Default::default()
3417                        }),
3418                        values: vec![string_val("alice")],
3419                        resume_token: b"resume-1".to_vec(),
3420                        ..Default::default()
3421                    }),
3422                    Err(tonic::Status::new(tonic::Code::Internal, "stream broken")),
3423                ]);
3424                Ok(tonic::Response::from(rx))
3425            });
3426
3427        // 2. Commit fails because the transaction was already started, but we returned an Internal error
3428        mock.expect_commit()
3429            .once()
3430            .in_sequence(&mut sequence)
3431            .returning(|req| {
3432                let req = req.into_inner();
3433                assert_eq!(
3434                    req.transaction,
3435                    Some(v1::commit_request::Transaction::TransactionId(vec![42]))
3436                );
3437                Err(tonic::Status::new(tonic::Code::Internal, "commit failed"))
3438            });
3439
3440        let (db_client, _server) = setup_db_client(mock).await;
3441        let runner = db_client
3442            .read_write_transaction()
3443            .with_retry_policy(BasicTransactionRetryPolicy::new().with_max_attempts(2))
3444            .build()
3445            .await?;
3446
3447        let attempt = Arc::new(AtomicU32::new(0));
3448        let att_clone = attempt.clone();
3449        let res = runner
3450            .run(async |tx| {
3451                let _attempt = att_clone.fetch_add(1, Ordering::SeqCst) + 1;
3452                let mut rs = tx
3453                    .execute_query("SELECT Name FROM Users WHERE Id = 1")
3454                    .await?;
3455                // First next() succeeds
3456                let row = rs.next().await.unwrap().unwrap();
3457                assert_eq!(row.get::<String, _>(0), "alice");
3458                // Second next() fails
3459                let next_res = rs.next().await.unwrap();
3460                assert!(next_res.is_err());
3461                assert_eq!(
3462                    next_res.unwrap_err().status().map(|s| s.code),
3463                    Some(Code::Internal)
3464                );
3465                // Swallow error and return Ok
3466                Ok(())
3467            })
3468            .await;
3469
3470        assert!(res.is_err());
3471        let err = res.unwrap_err();
3472        assert_eq!(err.status().map(|s| s.code), Some(Code::Internal));
3473        assert_eq!(
3474            attempt.load(Ordering::SeqCst),
3475            1,
3476            "Should not retry since transaction ID was received"
3477        );
3478        Ok(())
3479    }
3480
3481    #[tokio_test_no_panics]
3482    async fn read_write_transaction_batch_dml_failed_after_id_received_no_retry()
3483    -> anyhow::Result<()> {
3484        let mut mock = create_session_mock();
3485        let mut sequence = mockall::Sequence::new();
3486
3487        // 1. First Batch DML execution returns N succeeded statements and one failed status,
3488        // and includes the transaction ID in the first result set's metadata.
3489        mock.expect_execute_batch_dml()
3490            .once()
3491            .in_sequence(&mut sequence)
3492            .returning(|_| {
3493                Ok(tonic::Response::new(v1::ExecuteBatchDmlResponse {
3494                    result_sets: vec![v1::ResultSet {
3495                        metadata: Some(v1::ResultSetMetadata {
3496                            transaction: Some(v1::Transaction {
3497                                id: vec![42],
3498                                ..Default::default()
3499                            }),
3500                            ..Default::default()
3501                        }),
3502                        stats: Some(v1::ResultSetStats {
3503                            row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
3504                            ..Default::default()
3505                        }),
3506                        ..Default::default()
3507                    }],
3508                    status: Some(spanner_grpc_mock::google::rpc::Status {
3509                        code: tonic::Code::InvalidArgument as i32,
3510                        message: "invalid argument on statement 2".into(),
3511                        details: vec![],
3512                    }),
3513                    ..Default::default()
3514                }))
3515            });
3516
3517        // Commit fails because of some other reason
3518        mock.expect_commit()
3519            .once()
3520            .in_sequence(&mut sequence)
3521            .returning(|req| {
3522                let req = req.into_inner();
3523                assert_eq!(
3524                    req.transaction,
3525                    Some(v1::commit_request::Transaction::TransactionId(vec![42]))
3526                );
3527                Err(tonic::Status::new(tonic::Code::Internal, "commit failed"))
3528            });
3529
3530        let (db_client, _server) = setup_db_client(mock).await;
3531        let runner = db_client
3532            .read_write_transaction()
3533            .with_retry_policy(BasicTransactionRetryPolicy::new().with_max_attempts(2))
3534            .build()
3535            .await?;
3536
3537        let attempt = Arc::new(AtomicU32::new(0));
3538        let att_clone = attempt.clone();
3539        let res = runner
3540            .run(async |tx| {
3541                let _attempt = att_clone.fetch_add(1, Ordering::SeqCst) + 1;
3542                let batch = BatchDml::builder()
3543                    .add_statement("UPDATE Users SET active = true WHERE id = 1")
3544                    .add_statement("UPDATE Users SET active = true WHERE id = invalid_val");
3545                let res = tx.execute_batch_update(batch.build()).await;
3546                assert!(res.is_err());
3547                let err = res.unwrap_err();
3548                assert!(crate::error::BatchUpdateError::extract(&err).is_some());
3549                // Swallow error
3550                Ok(())
3551            })
3552            .await;
3553
3554        assert!(res.is_err());
3555        let err = res.unwrap_err();
3556        assert_eq!(err.status().map(|s| s.code), Some(Code::Internal));
3557        assert_eq!(
3558            attempt.load(Ordering::SeqCst),
3559            1,
3560            "Should not retry since transaction ID was received"
3561        );
3562        Ok(())
3563    }
3564
3565    fn parse_grpc_timeout(metadata: &MetadataMap) -> Option<StdDuration> {
3566        let timeout_header = metadata.get("grpc-timeout")?.to_str().ok()?;
3567        let numeric_part: String = timeout_header
3568            .chars()
3569            .take_while(|c| c.is_ascii_digit())
3570            .collect();
3571        let value = numeric_part.parse::<u64>().ok()?;
3572        let unit = timeout_header.trim_start_matches(&numeric_part);
3573        let duration = match unit {
3574            "u" => StdDuration::from_micros(value),
3575            "m" => StdDuration::from_millis(value),
3576            "S" => StdDuration::from_secs(value),
3577            "M" => StdDuration::from_secs(value * 60),
3578            "H" => StdDuration::from_secs(value * 3600),
3579            _ => return None,
3580        };
3581        Some(duration)
3582    }
3583
3584    #[tokio_test_no_panics]
3585    async fn read_write_transaction_lazy_begin_never_retry() -> anyhow::Result<()> {
3586        let mut mock = create_session_mock();
3587        let mut sequence = mockall::Sequence::new();
3588
3589        // 1. First statement execution uses inline-begin and fails with Unavailable (transient error)
3590        mock.expect_execute_sql()
3591            .once()
3592            .in_sequence(&mut sequence)
3593            .withf(|req| {
3594                matches!(
3595                    req.get_ref()
3596                        .transaction
3597                        .as_ref()
3598                        .and_then(|t| t.selector.as_ref()),
3599                    Some(v1::transaction_selector::Selector::Begin(_))
3600                )
3601            })
3602            .returning(move |_req| Err(tonic::Status::unavailable("transient error")));
3603
3604        // 2. Commit of the first attempt will fail client-side with AbortedDueToFailedFirstStatement.
3605        // Runner retries and calls begin_transaction explicitly, which fails with Unavailable.
3606        // Since we set NeverRetry for begin_retry_policy, it should not retry the BeginTransaction RPC.
3607        mock.expect_begin_transaction()
3608            .once()
3609            .in_sequence(&mut sequence)
3610            .returning(move |_req| Err(tonic::Status::unavailable("transient error")));
3611
3612        let (db_client, _server) = setup_db_client(mock).await;
3613
3614        let runner = db_client
3615            .read_write_transaction()
3616            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
3617            .with_begin_retry_policy(NeverRetry)
3618            .build()
3619            .await?;
3620
3621        let res = runner
3622            .run(async |tx| {
3623                let mut stmt_opts = crate::RequestOptions::default();
3624                stmt_opts.set_retry_policy(NeverRetry);
3625                let stmt = Statement::builder("UPDATE Users SET active = true WHERE id = 1")
3626                    .build()
3627                    .with_gax_options(stmt_opts);
3628                let res = tx.execute_update(stmt).await;
3629                assert!(res.is_err());
3630                let err = res.unwrap_err();
3631                assert_eq!(err.status().map(|s| s.code), Some(Code::Unavailable));
3632                // Swallow the transient error to trigger commit and retry
3633                Ok(())
3634            })
3635            .await;
3636
3637        assert!(
3638            res.is_err(),
3639            "Should fail because explicit BeginTransaction on retry failed with Unavailable and NeverRetry was configured"
3640        );
3641        let err = res.unwrap_err();
3642        assert_eq!(err.status().map(|s| s.code), Some(Code::Unavailable));
3643
3644        Ok(())
3645    }
3646
3647    #[tokio_test_no_panics]
3648    async fn read_write_transaction_commit_under_deadline_delegates_to_custom_retry_policy()
3649    -> anyhow::Result<()> {
3650        let mut mock = create_session_mock();
3651        mock.expect_begin_transaction().once().returning(|_| {
3652            Ok(tonic::Response::new(v1::Transaction {
3653                id: vec![8, 8, 8],
3654                ..Default::default()
3655            }))
3656        });
3657
3658        // Commit fails with Unavailable. Since we use NeverRetry, it must fail immediately without retry.
3659        mock.expect_commit()
3660            .once()
3661            .returning(|_| Err(tonic::Status::unavailable("transient error")));
3662
3663        let (db_client, _server) = setup_db_client(mock).await;
3664
3665        let runner = db_client
3666            .read_write_transaction()
3667            .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
3668            .with_commit_retry_policy(NeverRetry)
3669            .with_transaction_timeout(StdDuration::from_secs(5))
3670            .build()
3671            .await?;
3672
3673        let res = runner.run(async |_tx| Ok(())).await;
3674
3675        assert!(
3676            res.is_err(),
3677            "Should fail because NeverRetry aborted retries"
3678        );
3679        let err = res.unwrap_err();
3680        assert_eq!(
3681            err.status().map(|s| s.code),
3682            Some(Code::Unavailable),
3683            "Error code should be Unavailable"
3684        );
3685        Ok(())
3686    }
3687
3688    #[tokio_test_no_panics]
3689    async fn read_write_transaction_commit_timeout_combination() -> anyhow::Result<()> {
3690        let mut mock = create_session_mock();
3691        mock.expect_begin_transaction().once().returning(|_| {
3692            Ok(tonic::Response::new(v1::Transaction {
3693                id: vec![8, 8, 8],
3694                ..Default::default()
3695            }))
3696        });
3697
3698        // Assert that the commit attempt timeout of 2 seconds propagates as the gRPC timeout header metadata (approx 2000m/2000000u).
3699        mock.expect_commit()
3700            .once()
3701            .withf(|req| {
3702                let duration =
3703                    parse_grpc_timeout(req.metadata()).expect("valid grpc-timeout header");
3704                assert_eq!(
3705                    duration,
3706                    StdDuration::from_secs(2),
3707                    "Timeout duration should be exactly 2 seconds"
3708                );
3709                true
3710            })
3711            .returning(|_| {
3712                Ok(tonic::Response::new(v1::CommitResponse {
3713                    commit_timestamp: Some(Timestamp {
3714                        seconds: 999,
3715                        nanos: 0,
3716                    }),
3717                    ..Default::default()
3718                }))
3719            });
3720
3721        let (db_client, _server) = setup_db_client(mock).await;
3722
3723        let runner = db_client
3724            .read_write_transaction()
3725            .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin)
3726            .with_commit_attempt_timeout(StdDuration::from_secs(2))
3727            .with_transaction_timeout(StdDuration::from_secs(10))
3728            .build()
3729            .await?;
3730
3731        let res = runner.run(async |_tx| Ok(())).await?;
3732
3733        assert!(res.commit_response.commit_timestamp.is_some());
3734        Ok(())
3735    }
3736
3737    #[tokio_test_no_panics]
3738    async fn read_write_transaction_fallback_begin_under_deadline() -> anyhow::Result<()> {
3739        let mut mock = create_session_mock();
3740        let mut sequence = mockall::Sequence::new();
3741
3742        // 1. Initial Attempt: First statement execution fails with Unavailable (transient error)
3743        mock.expect_execute_sql()
3744            .once()
3745            .in_sequence(&mut sequence)
3746            .withf(|req| {
3747                matches!(
3748                    req.get_ref()
3749                        .transaction
3750                        .as_ref()
3751                        .and_then(|t| t.selector.as_ref()),
3752                    Some(v1::transaction_selector::Selector::Begin(_))
3753                )
3754            })
3755            .returning(move |_req| Err(tonic::Status::unavailable("transient error")));
3756
3757        // 2. Commit of the first attempt will fail client-side with AbortedDueToFailedFirstStatement.
3758        // Runner retries and calls begin_transaction explicitly.
3759        // The attempt timeout must be based on the remaining transaction deadline (approx 5 seconds).
3760        mock.expect_begin_transaction()
3761            .once()
3762            .in_sequence(&mut sequence)
3763            .withf(|req| {
3764                let duration =
3765                    parse_grpc_timeout(req.metadata()).expect("valid grpc-timeout header");
3766                assert!(
3767                    duration >= StdDuration::from_millis(4000)
3768                        && duration <= StdDuration::from_millis(6000),
3769                    "Retry begin timeout is wrong: {:?}",
3770                    duration
3771                );
3772                true
3773            })
3774            .returning(move |_req| {
3775                Ok(tonic::Response::new(v1::Transaction {
3776                    id: vec![42],
3777                    ..Default::default()
3778                }))
3779            });
3780
3781        // 3. Statement retry succeeds on the started transaction
3782        mock.expect_execute_sql()
3783            .once()
3784            .in_sequence(&mut sequence)
3785            .withf(|req| {
3786                matches!(
3787                    req.get_ref()
3788                        .transaction
3789                        .as_ref()
3790                        .and_then(|t| t.selector.as_ref()),
3791                    Some(v1::transaction_selector::Selector::Id(id)) if id == &vec![42]
3792                )
3793            })
3794            .returning(move |_req| {
3795                Ok(tonic::Response::new(v1::ResultSet {
3796                    metadata: Some(v1::ResultSetMetadata {
3797                        transaction: Some(v1::Transaction {
3798                            id: vec![42],
3799                            ..Default::default()
3800                        }),
3801                        ..Default::default()
3802                    }),
3803                    stats: Some(v1::ResultSetStats {
3804                        row_count: Some(v1::result_set_stats::RowCount::RowCountExact(1)),
3805                        ..Default::default()
3806                    }),
3807                    ..Default::default()
3808                }))
3809            });
3810
3811        // 4. Commit succeeds
3812        mock.expect_commit()
3813            .once()
3814            .in_sequence(&mut sequence)
3815            .returning(move |_req| {
3816                Ok(tonic::Response::new(v1::CommitResponse {
3817                    commit_timestamp: Some(Timestamp {
3818                        seconds: 1234,
3819                        nanos: 0,
3820                    }),
3821                    ..Default::default()
3822                }))
3823            });
3824
3825        let (db_client, _server) = setup_db_client(mock).await;
3826
3827        let runner = db_client
3828            .read_write_transaction()
3829            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
3830            .with_retry_policy(BasicTransactionRetryPolicy::new().with_max_attempts(2))
3831            .with_transaction_timeout(StdDuration::from_secs(5))
3832            .build()
3833            .await?;
3834
3835        let attempt = Arc::new(AtomicU32::new(0));
3836        let att_clone = attempt.clone();
3837        let res = runner
3838            .run(async |tx| {
3839                let current_attempt = att_clone.fetch_add(1, Ordering::SeqCst) + 1;
3840                let mut query_opts = crate::RequestOptions::default();
3841                query_opts.set_retry_policy(NeverRetry);
3842                let stmt = Statement::builder("UPDATE Users SET active = true WHERE id = 1")
3843                    .build()
3844                    .with_gax_options(query_opts);
3845                let res = tx.execute_update(stmt).await;
3846                if current_attempt == 1 {
3847                    assert!(res.is_err());
3848                    let err = res.unwrap_err();
3849                    assert_eq!(err.status().map(|s| s.code), Some(Code::Unavailable));
3850                    // Swallow the transient error and attempt commit to trigger AbortedDueToFailedFirstStatement retry
3851                    Ok(())
3852                } else {
3853                    assert_eq!(res.unwrap(), 1);
3854                    Ok(())
3855                }
3856            })
3857            .await?;
3858
3859        assert!(res.commit_response.commit_timestamp.is_some());
3860        Ok(())
3861    }
3862
3863    #[tokio_test_no_panics]
3864    async fn read_write_transaction_commit_fallback_begin_under_deadline() -> anyhow::Result<()> {
3865        let mut mock = create_session_mock();
3866        let mut sequence = mockall::Sequence::new();
3867
3868        // 1. Transaction was never started (empty runner block), so commit falls back to explicit BeginTransaction.
3869        // Assert fallback explicit BeginTransaction sets timeout based on remaining transaction deadline (approx 5 seconds).
3870        mock.expect_begin_transaction()
3871            .once()
3872            .in_sequence(&mut sequence)
3873            .withf(|req| {
3874                let duration =
3875                    parse_grpc_timeout(req.metadata()).expect("valid grpc-timeout header");
3876                assert!(
3877                    duration >= StdDuration::from_millis(4000)
3878                        && duration <= StdDuration::from_millis(6000),
3879                    "Fallback begin timeout inside commit is wrong: {:?}",
3880                    duration
3881                );
3882                true
3883            })
3884            .returning(move |_req| {
3885                Ok(tonic::Response::new(v1::Transaction {
3886                    id: vec![42],
3887                    ..Default::default()
3888                }))
3889            });
3890
3891        // 2. Commit succeeds
3892        mock.expect_commit()
3893            .once()
3894            .in_sequence(&mut sequence)
3895            .returning(move |_req| {
3896                Ok(tonic::Response::new(v1::CommitResponse {
3897                    commit_timestamp: Some(Timestamp {
3898                        seconds: 5678,
3899                        nanos: 0,
3900                    }),
3901                    ..Default::default()
3902                }))
3903            });
3904
3905        let (db_client, _server) = setup_db_client(mock).await;
3906
3907        let runner = db_client
3908            .read_write_transaction()
3909            .with_begin_transaction_option(BeginTransactionOption::InlineBegin)
3910            .with_transaction_timeout(StdDuration::from_secs(5))
3911            .build()
3912            .await?;
3913
3914        let res = runner.run(async |_tx| Ok(())).await?;
3915
3916        assert!(res.commit_response.commit_timestamp.is_some());
3917        Ok(())
3918    }
3919
3920    #[test]
3921    fn test_amend_gax_options() {
3922        // Case 1: No Deadline, LAR disabled
3923        let mut options = RequestOptions::default();
3924        options.set_attempt_timeout(StdDuration::from_secs(4));
3925        amend_gax_options(false, None, &mut options);
3926        assert_eq!(*options.attempt_timeout(), Some(StdDuration::from_secs(4)));
3927        assert!(options.retry_policy().is_none());
3928
3929        // Case 2: No Deadline, LAR enabled
3930        let mut options = RequestOptions::default();
3931        amend_gax_options(true, None, &mut options);
3932        // Verify LAR extension is added
3933        let headers = options
3934            .get_extension::<HeaderMap>()
3935            .expect("HeaderMap extension missing");
3936        assert_eq!(
3937            headers
3938                .get("x-goog-spanner-route-to-leader")
3939                .unwrap()
3940                .to_str()
3941                .unwrap(),
3942            "true"
3943        );
3944
3945        // Case 3: Deadline present, no custom timeout.
3946        // Since Instant::now() is called inside amend_gax_options slightly after the test's
3947        // Instant::now() call, the remaining time will be slightly less than 5 seconds.
3948        // Therefore, we assert that it falls within a very close range.
3949        let mut options = RequestOptions::default();
3950        let deadline = Instant::now() + StdDuration::from_secs(5);
3951        amend_gax_options(false, Some(deadline), &mut options);
3952        let timeout = options.attempt_timeout().expect("attempt timeout missing");
3953        assert!(
3954            timeout >= StdDuration::from_millis(4500) && timeout <= StdDuration::from_millis(5500)
3955        );
3956        assert!(
3957            options.retry_policy().is_some(),
3958            "retry policy should be wrapped"
3959        );
3960
3961        // Case 4: Deadline present, custom timeout shorter than deadline.
3962        // Since custom timeout is 2s and remaining deadline is 10s, it does not depend
3963        // on Time/Instant and must be exactly 2s.
3964        let mut options = RequestOptions::default();
3965        options.set_attempt_timeout(StdDuration::from_secs(2));
3966        let deadline = Instant::now() + StdDuration::from_secs(10);
3967        amend_gax_options(false, Some(deadline), &mut options);
3968        assert_eq!(*options.attempt_timeout(), Some(StdDuration::from_secs(2)));
3969
3970        // Case 5: Deadline present, custom timeout longer than deadline.
3971        // The remaining deadline (approx 2 seconds) is shorter than custom timeout (10s).
3972        // Due to slight time passing, remaining will be slightly less than 2 seconds.
3973        let mut options = RequestOptions::default();
3974        options.set_attempt_timeout(StdDuration::from_secs(10));
3975        let deadline = Instant::now() + StdDuration::from_secs(2);
3976        amend_gax_options(false, Some(deadline), &mut options);
3977        let timeout = options.attempt_timeout().expect("attempt timeout missing");
3978        assert!(
3979            timeout >= StdDuration::from_millis(1500) && timeout <= StdDuration::from_millis(2500)
3980        );
3981    }
3982
3983    #[test]
3984    fn test_transaction_bounded_retry_policy_throttle_delegation() {
3985        #[derive(Debug)]
3986        struct ThrottleTestPolicy;
3987        impl RetryPolicy for ThrottleTestPolicy {
3988            fn on_error(&self, _state: &RetryState, error: GaxError) -> RetryResult {
3989                RetryResult::Continue(error)
3990            }
3991            fn on_throttle(&self, _state: &RetryState, error: GaxError) -> ThrottleResult {
3992                ThrottleResult::Exhausted(error)
3993            }
3994        }
3995
3996        let inner = Arc::new(ThrottleTestPolicy);
3997        let deadline = Instant::now() + StdDuration::from_secs(10);
3998        let bounded = TransactionBoundedRetryPolicy { inner, deadline };
3999
4000        let state = RetryState::new(true);
4001        let status = Status::default()
4002            .set_code(Code::Unavailable)
4003            .set_message("error");
4004        let error = GaxError::service(status);
4005
4006        let res = bounded.on_throttle(&state, error);
4007        assert!(matches!(res, ThrottleResult::Exhausted(_)));
4008    }
4009
4010    #[test]
4011    fn test_transaction_bounded_retry_policy_remaining_time_capping() {
4012        #[derive(Debug)]
4013        struct RemainingTimeTestPolicy {
4014            timeout: Option<StdDuration>,
4015        }
4016        impl RetryPolicy for RemainingTimeTestPolicy {
4017            fn on_error(&self, _state: &RetryState, error: GaxError) -> RetryResult {
4018                RetryResult::Continue(error)
4019            }
4020            fn remaining_time(&self, _state: &RetryState) -> Option<StdDuration> {
4021                self.timeout
4022            }
4023        }
4024
4025        let state = RetryState::new(true);
4026
4027        // Case A: Inner policy timeout (3s) is shorter than remaining transaction deadline (approx 10s)
4028        let inner = Arc::new(RemainingTimeTestPolicy {
4029            timeout: Some(StdDuration::from_secs(3)),
4030        });
4031        let deadline = Instant::now() + StdDuration::from_secs(10);
4032        let bounded = TransactionBoundedRetryPolicy { inner, deadline };
4033        let remaining = bounded.remaining_time(&state).expect("remaining time");
4034        assert!(
4035            remaining >= StdDuration::from_millis(2500)
4036                && remaining <= StdDuration::from_millis(3500)
4037        );
4038
4039        // Case B: Transaction deadline (approx 2s) is shorter than inner policy timeout (10s)
4040        let inner = Arc::new(RemainingTimeTestPolicy {
4041            timeout: Some(StdDuration::from_secs(10)),
4042        });
4043        let deadline = Instant::now() + StdDuration::from_secs(2);
4044        let bounded = TransactionBoundedRetryPolicy { inner, deadline };
4045        let remaining = bounded.remaining_time(&state).expect("remaining time");
4046        assert!(
4047            remaining >= StdDuration::from_millis(1500)
4048                && remaining <= StdDuration::from_millis(2500)
4049        );
4050
4051        // Case C: Inner policy timeout is None (returns transaction remaining approx 10s)
4052        let inner = Arc::new(RemainingTimeTestPolicy { timeout: None });
4053        let deadline = Instant::now() + StdDuration::from_secs(10);
4054        let bounded = TransactionBoundedRetryPolicy { inner, deadline };
4055        let remaining = bounded.remaining_time(&state).expect("remaining time");
4056        assert!(
4057            remaining >= StdDuration::from_millis(9500)
4058                && remaining <= StdDuration::from_millis(10500)
4059        );
4060    }
4061}