Skip to main content

google_cloud_spanner/
client.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::generated::gapic_dataplane::client::Spanner as GapicSpanner;
16use crate::model::{
17    BeginTransactionRequest, CommitRequest, CommitResponse, CreateSessionRequest,
18    ExecuteBatchDmlRequest, ExecuteBatchDmlResponse, ExecuteSqlRequest, PartitionQueryRequest,
19    PartitionReadRequest, PartitionResponse, RollbackRequest, Session, Transaction,
20};
21use crate::omni::{InstanceType, is_plaintext_endpoint};
22use crate::server_streaming::builder;
23use gaxi::options::{ClientConfig, Credentials};
24use google_cloud_auth::credentials::anonymous;
25use google_cloud_gax::client_builder::ClientBuilder as GaxClientBuilder;
26use google_cloud_gax::client_builder::internal::new_builder;
27use google_cloud_gax::options::{
28    RequestOptions as GaxRequestOptions, internal::RequestOptionsExt as _,
29};
30use google_cloud_spanner_admin_database_v1::builder::database_admin::ClientBuilder as DatabaseAdminBuilder;
31use google_cloud_spanner_admin_instance_v1::builder::instance_admin::ClientBuilder as InstanceAdminBuilder;
32use http::{
33    HeaderMap,
34    header::{HeaderName, HeaderValue},
35};
36use std::sync::{
37    LazyLock,
38    atomic::{AtomicUsize, Ordering},
39};
40
41pub use crate::database_client::DatabaseClient;
42pub use google_cloud_spanner_admin_database_v1::client::DatabaseAdmin;
43pub use google_cloud_spanner_admin_instance_v1::client::InstanceAdmin;
44
45/// A client for the [Spanner] API.
46///
47/// Use this client to interact with the Spanner service.
48///
49/// [Spanner]: https://docs.cloud.google.com/spanner/docs
50#[derive(Clone, Debug)]
51pub struct Spanner {
52    pub(crate) channels: Vec<Channel>,
53    pub(crate) counter: std::sync::Arc<AtomicUsize>,
54    pub(crate) config: ClientConfig,
55    pub(crate) is_emulator: bool,
56    pub(crate) instance_type: InstanceType,
57}
58
59/// A factory for constructing `Spanner` clients.
60pub struct Factory;
61
62impl google_cloud_gax::client_builder::internal::ClientFactory for Factory {
63    type Client = Spanner;
64    type Credentials = Credentials;
65
66    async fn build(self, mut config: ClientConfig) -> crate::ClientBuilderResult<Self::Client> {
67        let mut is_emulator = false;
68        if let Some(endpoint) = std::env::var("SPANNER_EMULATOR_HOST")
69            .ok()
70            .filter(|s| !s.is_empty())
71        {
72            is_emulator = true;
73            if config.endpoint.is_none() {
74                config.endpoint = Some(parse_emulator_endpoint(&endpoint));
75            }
76            if config.cred.is_none() {
77                config.cred = Some(anonymous::Builder::new().build());
78            }
79        }
80
81        if config
82            .endpoint
83            .as_ref()
84            .is_some_and(|ep| is_plaintext_endpoint(ep))
85            && config.cred.is_none()
86        {
87            config.cred = Some(anonymous::Builder::new().build());
88        }
89
90        let num_channels = std::env::var("SPANNER_NUM_CHANNELS")
91            .ok()
92            .and_then(|s| s.parse::<usize>().ok())
93            .unwrap_or(4);
94
95        let mut channels = Vec::with_capacity(num_channels);
96        for _ in 0..num_channels {
97            channels.push(Channel::create(&config).await?);
98        }
99
100        let instance_type = config
101            .extensions
102            .get::<InstanceType>()
103            .copied()
104            .unwrap_or_default();
105
106        Ok(Spanner {
107            channels,
108            counter: std::sync::Arc::new(AtomicUsize::new(0)),
109            config,
110            is_emulator,
111            instance_type,
112        })
113    }
114}
115
116/// A builder for the Spanner client.
117pub type ClientBuilder = google_cloud_gax::client_builder::ClientBuilder<Factory, Credentials>;
118
119/// Extension trait for [`ClientBuilder`] (also exported as `SpannerBuilder`) to configure Spanner-specific options.
120pub trait SpannerBuilderExt {
121    /// Sets the target [`InstanceType`] (`Cloud` vs `Omni`) for the Spanner client.
122    ///
123    /// # Example
124    /// ```
125    /// # use google_cloud_spanner::client::{Spanner, SpannerBuilderExt};
126    /// # use google_cloud_spanner::omni::InstanceType;
127    /// # async fn sample() -> anyhow::Result<()> {
128    /// let client = Spanner::builder()
129    ///     .with_instance_type(InstanceType::Omni)
130    ///     .build()
131    ///     .await?;
132    /// # Ok(()) }
133    /// ```
134    fn with_instance_type(self, instance_type: InstanceType) -> Self;
135}
136
137impl SpannerBuilderExt for ClientBuilder {
138    fn with_instance_type(self, instance_type: InstanceType) -> Self {
139        self.with_extension(instance_type)
140    }
141}
142
143fn parse_emulator_endpoint(endpoint: &str) -> String {
144    match url::Url::parse(endpoint) {
145        Ok(url) if url.has_host() => endpoint.to_string(),
146        _ => format!("http://{}", endpoint),
147    }
148}
149
150macro_rules! define_idempotent_rpc {
151    ($method:ident, $request_type:ty, $response_type:ty, $canonical_name:expr) => {
152        pub(crate) async fn $method(
153            &self,
154            request: $request_type,
155            options: crate::RequestOptions,
156            channel_hint: usize,
157            o11y: &crate::observability::Observability,
158        ) -> crate::Result<$response_type> {
159            o11y.trace_operation($canonical_name, || async move {
160                self.get_channel(channel_hint)
161                    .inner
162                    .$method()
163                    .with_request(request)
164                    .with_options(apply_request_defaults(options))
165                    .send()
166                    .await
167            })
168            .await
169        }
170    };
171}
172
173fn apply_request_defaults(mut options: crate::RequestOptions) -> crate::RequestOptions {
174    if options.idempotent().is_none() {
175        options.set_idempotency(true);
176    }
177    if options.retry_policy().is_none() {
178        options.set_retry_policy(crate::retry_policy::SpannerRetryPolicy::new());
179    }
180    options
181}
182
183pub(crate) static LAR_HEADER_MAP: LazyLock<HeaderMap> = LazyLock::new(|| {
184    let mut map = HeaderMap::new();
185    map.insert(
186        HeaderName::from_static("x-goog-spanner-route-to-leader"),
187        HeaderValue::from_static("true"),
188    );
189    map
190});
191
192pub(crate) fn amend_request_options_for_lar(
193    leader_aware_routing_enabled: bool,
194    mut options: GaxRequestOptions,
195) -> GaxRequestOptions {
196    if leader_aware_routing_enabled {
197        let mut headers = options
198            .get_extension::<HeaderMap>()
199            .cloned()
200            .unwrap_or_default();
201        headers.extend((*LAR_HEADER_MAP).clone());
202        options = options.insert_extension(headers);
203    }
204    options
205}
206
207fn map_emulator_admin_endpoint(endpoint: &str, is_emulator: bool) -> String {
208    let mut ep = endpoint.trim_end_matches('/').to_string();
209    if is_emulator && ep.ends_with(":9010") {
210        ep = ep.replace(":9010", ":9020");
211    }
212    ep
213}
214
215impl Spanner {
216    /// Returns a builder for the `Spanner` client.
217    ///
218    /// # Example
219    /// ```
220    /// # use google_cloud_spanner::client::Spanner;
221    /// # async fn sample() -> anyhow::Result<()> {
222    /// let spanner = Spanner::builder().build().await?;
223    ///
224    /// let db_client = spanner
225    ///     .database_client("projects/my-project/instances/my-instance/databases/my-db")
226    ///     .build()
227    ///     .await?;
228    ///
229    /// let tx = db_client.single_use().build();
230    /// let mut rs = tx.execute_query("SELECT 1").await?;
231    ///
232    /// while let Some(row) = rs.next().await {
233    ///     let row = row?;
234    ///     let val: i64 = row.get(0);
235    ///     assert_eq!(val, 1);
236    /// }
237    /// # Ok(())
238    /// # }
239    /// ```
240    ///
241    /// The returned builder is pre-configured with standard defaults. It automatically
242    /// detects and connects to the Spanner emulator if the `SPANNER_EMULATOR_HOST`
243    /// environment variable is set.
244    pub fn builder() -> ClientBuilder {
245        new_builder(Factory)
246    }
247
248    /// Returns a builder for the [DatabaseAdmin] client.
249    ///
250    /// This builder is automatically pre-configured with the same endpoints, credentials,
251    /// and routing configurations as this `Spanner` instance.
252    /// If configured to use the Emulator (via `SPANNER_EMULATOR_HOST`), it maps the gRPC endpoint port
253    /// (`9010`) to the REST admin port (`9020`).
254    pub fn database_admin_builder(&self) -> DatabaseAdminBuilder {
255        self.configure_admin_builder(DatabaseAdmin::builder())
256    }
257
258    /// Returns a builder for the [InstanceAdmin] client.
259    ///
260    /// This builder is automatically pre-configured with the same endpoints, credentials,
261    /// and routing configurations as this `Spanner` instance.
262    /// If configured to use the Emulator (via `SPANNER_EMULATOR_HOST`), it maps the gRPC endpoint port
263    /// (`9010`) to the REST admin port (`9020`).
264    pub fn instance_admin_builder(&self) -> InstanceAdminBuilder {
265        self.configure_admin_builder(InstanceAdmin::builder())
266    }
267
268    fn configure_admin_builder<F, C>(
269        &self,
270        mut builder: GaxClientBuilder<F, C>,
271    ) -> GaxClientBuilder<F, C>
272    where
273        C: Clone + From<Credentials>,
274    {
275        if let Some(ref endpoint) = self.config.endpoint {
276            let ep = map_emulator_admin_endpoint(endpoint, self.is_emulator);
277            builder = builder.with_endpoint(ep);
278        }
279        if let Some(ref cred) = self.config.cred {
280            builder = builder.with_credentials(cred.clone());
281        }
282        if let Some(ref ud) = self.config.universe_domain {
283            builder = builder.with_universe_domain(ud.clone());
284        }
285        builder
286    }
287
288    /// Returns a new [DatabaseClientBuilder](crate::database_client::DatabaseClientBuilder) for
289    /// interacting with a specific database.
290    ///
291    /// # Example
292    /// ```
293    /// # use google_cloud_spanner::client::Spanner;
294    /// # async fn sample() -> anyhow::Result<()> {
295    ///     let spanner = Spanner::builder().build().await?;
296    ///     let database_client = spanner
297    ///         .database_client("projects/my-project/instances/my-instance/databases/my-db")
298    ///         .build()
299    ///         .await?;
300    ///     # Ok(())
301    /// # }
302    /// ```
303    ///
304    /// The returned `DatabaseClient` is intended to be a long-lived object and should be reused
305    /// for all operations on the database.
306    pub fn database_client(
307        &self,
308        database: impl Into<String>,
309    ) -> crate::builder::DatabaseClientBuilder {
310        crate::builder::DatabaseClientBuilder::new(self.clone(), database.into())
311    }
312
313    /// Creates a new client from the provided stub.
314    ///
315    /// The most common case for calling this function is in tests mocking the
316    /// client's behavior.
317    pub fn from_stub<T>(stub: T) -> Self
318    where
319        T: crate::generated::gapic_dataplane::stub::Spanner + 'static,
320    {
321        // This method is primarily for testing and doesn't fully initialize grpc_client.
322        // For production use, prefer `Spanner::builder().build()`.
323        Self {
324            channels: vec![Channel {
325                inner: GapicSpanner::from_stub(stub),
326                grpc_client: None,
327            }],
328            counter: std::sync::Arc::new(AtomicUsize::new(0)),
329            config: ClientConfig::default(),
330            is_emulator: false,
331            instance_type: InstanceType::Cloud,
332        }
333    }
334
335    pub(crate) fn is_emulator(&self) -> bool {
336        self.is_emulator
337    }
338
339    pub(crate) fn instance_type(&self) -> InstanceType {
340        self.instance_type
341    }
342
343    pub(crate) fn get_channel(&self, hint: usize) -> &Channel {
344        let idx = hint % self.channels.len();
345        &self.channels[idx]
346    }
347
348    pub(crate) fn next_channel_hint(&self) -> usize {
349        self.counter.fetch_add(1, Ordering::Relaxed)
350    }
351
352    define_idempotent_rpc!(
353        create_session,
354        CreateSessionRequest,
355        Session,
356        "google.spanner.v1.Spanner/CreateSession"
357    );
358    define_idempotent_rpc!(
359        execute_sql,
360        ExecuteSqlRequest,
361        crate::model::ResultSet,
362        "google.spanner.v1.Spanner/ExecuteSql"
363    );
364    define_idempotent_rpc!(
365        execute_batch_dml,
366        ExecuteBatchDmlRequest,
367        ExecuteBatchDmlResponse,
368        "google.spanner.v1.Spanner/ExecuteBatchDml"
369    );
370    define_idempotent_rpc!(
371        begin_transaction,
372        BeginTransactionRequest,
373        Transaction,
374        "google.spanner.v1.Spanner/BeginTransaction"
375    );
376    define_idempotent_rpc!(
377        commit,
378        CommitRequest,
379        CommitResponse,
380        "google.spanner.v1.Spanner/Commit"
381    );
382    define_idempotent_rpc!(
383        rollback,
384        RollbackRequest,
385        (),
386        "google.spanner.v1.Spanner/Rollback"
387    );
388    define_idempotent_rpc!(
389        partition_query,
390        PartitionQueryRequest,
391        PartitionResponse,
392        "google.spanner.v1.Spanner/PartitionQuery"
393    );
394    define_idempotent_rpc!(
395        partition_read,
396        PartitionReadRequest,
397        PartitionResponse,
398        "google.spanner.v1.Spanner/PartitionRead"
399    );
400
401    /// Executes an SQL statement, returning a stream of results.
402    ///
403    /// This is a custom streaming implementation over the underlying Spanner gRPC
404    /// transport, since streaming responses are not yet auto-generated here.
405    pub(crate) fn execute_streaming_sql(
406        &self,
407        request: crate::model::ExecuteSqlRequest,
408        options: crate::RequestOptions,
409        channel_hint: usize,
410    ) -> builder::ExecuteStreamingSql {
411        let channel = self.get_channel(channel_hint);
412        let grpc = channel
413            .grpc_client
414            .as_ref()
415            .expect("Streaming RPCs are not supported when using a stub client");
416        builder::ExecuteStreamingSql::new(grpc.clone())
417            .with_request(request)
418            .with_options(options)
419    }
420
421    /// Reads rows from the database, returning a stream of results.
422    ///
423    /// This is a custom streaming implementation over the underlying Spanner gRPC
424    /// transport, since streaming responses are not yet auto-generated here.
425    pub(crate) fn streaming_read(
426        &self,
427        request: crate::model::ReadRequest,
428        options: crate::RequestOptions,
429        channel_hint: usize,
430    ) -> builder::StreamingRead {
431        let channel = self.get_channel(channel_hint);
432        let grpc = channel
433            .grpc_client
434            .as_ref()
435            .expect("Streaming RPCs are not supported when using a stub client");
436        builder::StreamingRead::new(grpc.clone())
437            .with_request(request)
438            .with_options(options)
439    }
440
441    pub(crate) fn batch_write(
442        &self,
443        request: crate::model::BatchWriteRequest,
444        options: crate::RequestOptions,
445        channel_hint: usize,
446    ) -> builder::BatchWrite {
447        let channel = self.get_channel(channel_hint);
448        let grpc = channel
449            .grpc_client
450            .as_ref()
451            .expect("Streaming RPCs are not supported when using a stub client");
452        builder::BatchWrite::new(grpc.clone())
453            .with_request(request)
454            .with_options(options)
455    }
456}
457
458#[derive(Clone, Debug)]
459pub(crate) struct Channel {
460    pub(crate) inner: GapicSpanner,
461    pub(crate) grpc_client: Option<gaxi::grpc::Client>,
462}
463
464impl Channel {
465    pub(crate) async fn create(config: &ClientConfig) -> crate::ClientBuilderResult<Self> {
466        let transport =
467            crate::generated::gapic_dataplane::transport::Spanner::new(config.clone()).await?;
468        let grpc_client = transport.inner.clone();
469
470        let inner = if gaxi::options::tracing_enabled(config) {
471            GapicSpanner::from_stub(crate::generated::gapic_dataplane::tracing::Spanner::new(
472                transport,
473            ))
474        } else {
475            GapicSpanner::from_stub(transport)
476        };
477        Ok(Self {
478            inner,
479            grpc_client: Some(grpc_client),
480        })
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use crate::model::CreateSessionRequest;
488    use crate::read::ReadRequest;
489    use crate::result_set::tests::adapt;
490    use crate::statement::Statement;
491    use gaxi::grpc::tonic::MetadataMap;
492    use gaxi::grpc::tonic::{Code as GrpcCode, Response, Status};
493    use google_cloud_auth::credentials::anonymous::Builder as Anonymous;
494    use google_cloud_gax::backoff_policy::BackoffPolicy;
495    use google_cloud_gax::error::rpc::Code;
496    use google_cloud_gax::retry_state::RetryState;
497    use google_cloud_test_macros::tokio_test_no_panics;
498    use spanner_grpc_mock::google::rpc as mock_rpc;
499    use spanner_grpc_mock::google::spanner::v1 as mock_v1;
500    use spanner_grpc_mock::google::spanner::v1::CommitResponse;
501    use spanner_grpc_mock::google::spanner::v1::ResultSet;
502    use spanner_grpc_mock::google::spanner::v1::ResultSetStats;
503    use spanner_grpc_mock::google::spanner::v1::Session;
504    use spanner_grpc_mock::google::spanner::v1::result_set_stats::RowCount;
505    use spanner_grpc_mock::{MockSpanner, start};
506    use static_assertions::{assert_impl_all, assert_not_impl_any};
507    use std::sync::Arc;
508    use std::sync::atomic::{AtomicU64, Ordering};
509    use std::time::Duration;
510
511    mockall::mock! {
512        #[derive(Debug)]
513        BackoffPolicy {}
514        impl BackoffPolicy for BackoffPolicy {
515            fn on_failure(&self, state: &RetryState) -> Duration;
516        }
517    }
518
519    #[test]
520    fn auto_traits() {
521        assert_impl_all!(Spanner: std::fmt::Debug, Clone, Send, Sync);
522        assert_not_impl_any!(Spanner: std::panic::RefUnwindSafe, std::panic::UnwindSafe);
523    }
524
525    #[tokio_test_no_panics]
526    async fn channel_pool_default_size() {
527        let mock = MockSpanner::new();
528        let (address, _server) = start("0.0.0.0:0", mock)
529            .await
530            .expect("Failed to start mock server");
531
532        let client = Spanner::builder()
533            .with_endpoint(address)
534            .with_credentials(Anonymous::new().build())
535            .build()
536            .await
537            .expect("Failed to build client");
538
539        assert_eq!(client.channels.len(), 4);
540    }
541
542    #[test]
543    fn test_map_emulator_admin_endpoint() {
544        // 1. Test normal endpoint without emulator (should remain unchanged)
545        assert_eq!(
546            map_emulator_admin_endpoint("https://spanner.googleapis.com", false),
547            "https://spanner.googleapis.com"
548        );
549
550        // 2. Test emulator endpoint mapping (9010 -> 9020)
551        assert_eq!(
552            map_emulator_admin_endpoint("http://localhost:9010", true),
553            "http://localhost:9020"
554        );
555
556        // 3. Test emulator endpoint with trailing slash (should be trimmed and mapped)
557        assert_eq!(
558            map_emulator_admin_endpoint("http://127.0.0.1:9010/", true),
559            "http://127.0.0.1:9020"
560        );
561
562        // 4. Test emulator endpoint without is_emulator active (should remain unchanged)
563        assert_eq!(
564            map_emulator_admin_endpoint("http://localhost:9010", false),
565            "http://localhost:9010"
566        );
567    }
568
569    #[tokio_test_no_panics]
570    async fn channel_selection() {
571        let mock = MockSpanner::new();
572        let (address, _server) = start("0.0.0.0:0", mock)
573            .await
574            .expect("Failed to start mock server");
575
576        let client = Spanner::builder()
577            .with_endpoint(address)
578            .with_credentials(Anonymous::new().build())
579            .build()
580            .await
581            .expect("Failed to build client");
582
583        let hint0 = client.next_channel_hint();
584        let hint1 = client.next_channel_hint();
585        let hint2 = client.next_channel_hint();
586        let hint3 = client.next_channel_hint();
587        let hint4 = client.next_channel_hint();
588
589        assert_eq!(hint0 % 4, 0);
590        assert_eq!(hint1 % 4, 1);
591        assert_eq!(hint2 % 4, 2);
592        assert_eq!(hint3 % 4, 3);
593        assert_eq!(hint4 % 4, 0);
594    }
595
596    #[tokio_test_no_panics]
597    async fn test_create_session() {
598        // 1. Setup Mock Server
599        let mut mock = MockSpanner::new();
600        mock.expect_create_session().once().returning(|_| {
601            Ok(gaxi::grpc::tonic::Response::new(mock_v1::Session {
602                name:
603                    "projects/test-project/instances/test-instance/databases/test-db/sessions/123"
604                        .to_string(),
605                ..Default::default()
606            }))
607        });
608
609        // 2. Start mock server
610        let (address, _server) = start("0.0.0.0:0", mock)
611            .await
612            .expect("Failed to start mock server");
613
614        // 3. Configure Client to use mock endpoint
615        let client = Spanner::builder()
616            .with_endpoint(address)
617            .with_credentials(Anonymous::new().build())
618            .build()
619            .await
620            .expect("Failed to build client");
621
622        // 4. Call CreateSession
623        let mut req = CreateSessionRequest::new();
624        req.database =
625            "projects/test-project/instances/test-instance/databases/test-db".to_string();
626
627        let session = client
628            .create_session(
629                req,
630                crate::RequestOptions::default(),
631                client.next_channel_hint(),
632                &crate::observability::Observability::disabled(),
633            )
634            .await
635            .expect("Failed to call create_session");
636
637        // 5. Verify Response
638        assert_eq!(
639            session.name,
640            "projects/test-project/instances/test-instance/databases/test-db/sessions/123"
641        );
642    }
643
644    #[tokio_test_no_panics]
645    async fn test_create_session_retry() {
646        use google_cloud_gax::options::RequestOptionsBuilder;
647        use google_cloud_gax::retry_policy::{Aip194Strict, RetryPolicyExt};
648
649        // 1. Setup Mock Server
650        let mut mock = MockSpanner::new();
651        let mut seq = mockall::Sequence::new();
652        mock.expect_create_session()
653            .once()
654            .in_sequence(&mut seq)
655            .returning(|_| {
656                Err(gaxi::grpc::tonic::Status::unavailable(
657                    "server is unavailable",
658                ))
659            });
660        mock.expect_create_session().once().in_sequence(&mut seq).returning(|_| {
661            Ok(gaxi::grpc::tonic::Response::new(mock_v1::Session {
662                name: "projects/test-project/instances/test-instance/databases/test-db/sessions/456".to_string(),
663                ..Default::default()
664            }))
665        });
666
667        // 2. Start mock server
668        let (address, _server) = start("0.0.0.0:0", mock)
669            .await
670            .expect("Failed to start mock server");
671
672        // 3. Configure Client to use mock endpoint
673        // NOTE: Default retry policy is assigned automatically for GAPIC methods.
674        let client = Spanner::builder()
675            .with_endpoint(address)
676            .with_credentials(Anonymous::new().build())
677            .build()
678            .await
679            .expect("Failed to build client");
680
681        // 4. Call CreateSession with intentional retry configurations
682        let mut req = CreateSessionRequest::new();
683        req.database =
684            "projects/test-project/instances/test-instance/databases/test-db".to_string();
685
686        let session = client
687            .get_channel(client.next_channel_hint())
688            .inner
689            .create_session()
690            .with_request(req)
691            .with_idempotency(true)
692            .with_retry_policy(Aip194Strict.with_attempt_limit(3))
693            .send()
694            .await
695            .expect("Failed to call create_session");
696
697        // 5. Verify Response
698        assert_eq!(
699            session.name,
700            "projects/test-project/instances/test-instance/databases/test-db/sessions/456"
701        );
702    }
703
704    #[tokio_test_no_panics]
705    async fn test_create_session_transport_retry() {
706        // 1. Setup Mock Server
707        let mut mock = MockSpanner::new();
708        let mut seq = mockall::Sequence::new();
709        mock.expect_create_session()
710            .once()
711            .in_sequence(&mut seq)
712            .returning(|_| {
713                let mut status = Status::unavailable("connection reset");
714                let mut headers = std::mem::take(status.metadata_mut()).into_headers();
715                headers.insert("content-type", http::HeaderValue::from_static("text/html"));
716                *status.metadata_mut() = MetadataMap::from_headers(headers);
717                Err(status)
718            });
719        mock.expect_create_session()
720            .once()
721            .in_sequence(&mut seq)
722            .returning(|_| {
723                Ok(gaxi::grpc::tonic::Response::new(mock_v1::Session {
724                    name: "projects/test-project/instances/test-instance/databases/test-db/sessions/789".to_string(),
725                    ..Default::default()
726                }))
727            });
728
729        // 2. Start mock server
730        let (address, _server) = start("0.0.0.0:0", mock)
731            .await
732            .expect("Failed to start mock server");
733
734        // 3. Configure Client to use mock endpoint
735        let client = Spanner::builder()
736            .with_endpoint(address)
737            .with_credentials(Anonymous::new().build())
738            .build()
739            .await
740            .expect("Failed to build client");
741
742        // 4. Call CreateSession
743        let mut req = CreateSessionRequest::new();
744        req.database =
745            "projects/test-project/instances/test-instance/databases/test-db".to_string();
746
747        let session = client
748            .create_session(
749                req,
750                crate::RequestOptions::default(),
751                client.next_channel_hint(),
752                &crate::observability::Observability::disabled(),
753            )
754            .await
755            .expect("Failed to call create_session after transport error retry");
756
757        // 5. Verify Response
758        assert_eq!(
759            session.name,
760            "projects/test-project/instances/test-instance/databases/test-db/sessions/789",
761            "Expected session name to match the second successful response after transport retry"
762        );
763    }
764
765    #[tokio_test_no_panics]
766    async fn test_execute_sql() {
767        use crate::model::ExecuteSqlRequest;
768
769        let mut mock = MockSpanner::new();
770        mock.expect_execute_sql().once().returning(|_| {
771            Ok(gaxi::grpc::tonic::Response::new(mock_v1::ResultSet {
772                metadata: Some(mock_v1::ResultSetMetadata {
773                    row_type: Some(mock_v1::StructType { fields: vec![] }),
774                    transaction: None,
775                    undeclared_parameters: None,
776                }),
777                rows: vec![],
778                stats: None,
779                precommit_token: None,
780                cache_update: None,
781            }))
782        });
783
784        let (address, _server) = start("0.0.0.0:0", mock)
785            .await
786            .expect("Failed to start mock server");
787        let client = Spanner::builder()
788            .with_endpoint(address)
789            .with_credentials(Anonymous::new().build())
790            .build()
791            .await
792            .expect("Failed to build client");
793
794        let mut req = ExecuteSqlRequest::new();
795        req.sql = "SELECT 1".to_string();
796
797        let result_set = client
798            .execute_sql(
799                req,
800                crate::RequestOptions::default(),
801                client.next_channel_hint(),
802                &crate::observability::Observability::disabled(),
803            )
804            .await
805            .expect("Failed to call execute_sql");
806        assert!(result_set.metadata.is_some());
807    }
808
809    #[tokio_test_no_panics]
810    async fn test_execute_batch_dml() {
811        use crate::model::ExecuteBatchDmlRequest;
812
813        let mut mock = MockSpanner::new();
814        mock.expect_execute_batch_dml().once().returning(|_| {
815            Ok(gaxi::grpc::tonic::Response::new(
816                mock_v1::ExecuteBatchDmlResponse {
817                    result_sets: vec![],
818                    status: Some(mock_rpc::Status {
819                        code: 0,
820                        message: "OK".to_string(),
821                        details: vec![],
822                    }),
823                    precommit_token: None,
824                },
825            ))
826        });
827
828        let (address, _server) = start("0.0.0.0:0", mock)
829            .await
830            .expect("Failed to start mock server");
831        let client = Spanner::builder()
832            .with_endpoint(address)
833            .with_credentials(Anonymous::new().build())
834            .build()
835            .await
836            .expect("Failed to build client");
837
838        let mut req = ExecuteBatchDmlRequest::new();
839        req.session = "test_session".to_string();
840
841        let response = client
842            .execute_batch_dml(
843                req,
844                crate::RequestOptions::default(),
845                client.next_channel_hint(),
846                &crate::observability::Observability::disabled(),
847            )
848            .await
849            .expect("Failed to call execute_batch_dml");
850        assert!(response.status.is_some());
851    }
852
853    #[tokio_test_no_panics]
854    async fn test_begin_transaction() {
855        use crate::model::BeginTransactionRequest;
856
857        let mut mock = MockSpanner::new();
858        mock.expect_begin_transaction().once().returning(|_| {
859            Ok(gaxi::grpc::tonic::Response::new(mock_v1::Transaction {
860                id: vec![1, 2, 3],
861                read_timestamp: None,
862                precommit_token: None,
863                ..Default::default()
864            }))
865        });
866
867        let (address, _server) = start("0.0.0.0:0", mock)
868            .await
869            .expect("Failed to start mock server");
870        let client = Spanner::builder()
871            .with_endpoint(address)
872            .with_credentials(Anonymous::new().build())
873            .build()
874            .await
875            .expect("Failed to build client");
876
877        let mut req = BeginTransactionRequest::new();
878        req.session = "test_session".to_string();
879
880        let tx = client
881            .begin_transaction(
882                req,
883                crate::RequestOptions::default(),
884                client.next_channel_hint(),
885                &crate::observability::Observability::disabled(),
886            )
887            .await
888            .expect("Failed to call begin_transaction");
889        assert_eq!(tx.id, vec![1, 2, 3]);
890    }
891
892    #[tokio_test_no_panics]
893    async fn test_commit() {
894        use crate::model::CommitRequest;
895
896        let mut mock = MockSpanner::new();
897        mock.expect_commit().once().returning(|_| {
898            Ok(gaxi::grpc::tonic::Response::new(mock_v1::CommitResponse {
899                commit_timestamp: Some(prost_types::Timestamp {
900                    seconds: 12345,
901                    nanos: 0,
902                }),
903                commit_stats: None,
904                multiplexed_session_retry: None,
905                snapshot_timestamp: None,
906                ..Default::default()
907            }))
908        });
909
910        let (address, _server) = start("0.0.0.0:0", mock)
911            .await
912            .expect("Failed to start mock server");
913        let client = Spanner::builder()
914            .with_endpoint(address)
915            .with_credentials(Anonymous::new().build())
916            .build()
917            .await
918            .expect("Failed to build client");
919
920        let mut req = CommitRequest::new();
921        req.session = "test_session".to_string();
922
923        let response = client
924            .commit(
925                req,
926                crate::RequestOptions::default(),
927                client.next_channel_hint(),
928                &crate::observability::Observability::disabled(),
929            )
930            .await
931            .expect("Failed to call commit");
932        assert!(response.commit_timestamp.is_some());
933    }
934
935    #[tokio_test_no_panics]
936    async fn test_rollback() {
937        use crate::model::RollbackRequest;
938
939        let mut mock = MockSpanner::new();
940        mock.expect_rollback()
941            .once()
942            .returning(|_| Ok(gaxi::grpc::tonic::Response::new(())));
943
944        let (address, _server) = start("0.0.0.0:0", mock)
945            .await
946            .expect("Failed to start mock server");
947        let client = Spanner::builder()
948            .with_endpoint(address)
949            .with_credentials(Anonymous::new().build())
950            .build()
951            .await
952            .expect("Failed to build client");
953
954        let mut req = RollbackRequest::new();
955        req.session = "test_session".to_string();
956
957        client
958            .rollback(
959                req,
960                crate::RequestOptions::default(),
961                client.next_channel_hint(),
962                &crate::observability::Observability::disabled(),
963            )
964            .await
965            .expect("Failed to call rollback");
966    }
967
968    #[tokio_test_no_panics]
969    async fn test_execute_streaming_sql() {
970        use crate::model::ExecuteSqlRequest;
971
972        let mut mock = MockSpanner::new();
973        mock.expect_execute_streaming_sql().once().returning(|_| {
974            let result_set = mock_v1::PartialResultSet {
975                metadata: Some(mock_v1::ResultSetMetadata {
976                    row_type: Some(mock_v1::StructType { fields: vec![] }),
977                    transaction: None,
978                    undeclared_parameters: None,
979                }),
980                values: vec![],
981                chunked_value: false,
982                resume_token: vec![],
983                stats: None,
984                precommit_token: None,
985                cache_update: None,
986                last: false,
987            };
988            Ok(gaxi::grpc::tonic::Response::new(adapt([Ok(result_set)])))
989        });
990
991        let (address, _server) = start("0.0.0.0:0", mock)
992            .await
993            .expect("Failed to start mock server");
994        let client = Spanner::builder()
995            .with_endpoint(address)
996            .with_credentials(Anonymous::new().build())
997            .build()
998            .await
999            .expect("Failed to build client");
1000
1001        let mut req = ExecuteSqlRequest::new();
1002        req.sql = "SELECT 1".to_string();
1003
1004        let mut stream = client
1005            .execute_streaming_sql(
1006                req,
1007                crate::RequestOptions::default(),
1008                client.next_channel_hint(),
1009            )
1010            .send()
1011            .await
1012            .expect("Failed to call execute_streaming_sql");
1013
1014        let result = stream.next_message().await;
1015        assert!(result.is_some());
1016        assert!(result.unwrap().is_ok());
1017    }
1018
1019    #[tokio_test_no_panics]
1020    async fn test_streaming_read() {
1021        use crate::model::ReadRequest;
1022
1023        let mut mock = MockSpanner::new();
1024        mock.expect_streaming_read().once().returning(|_| {
1025            let result_set = mock_v1::PartialResultSet {
1026                metadata: Some(mock_v1::ResultSetMetadata {
1027                    row_type: Some(mock_v1::StructType { fields: vec![] }),
1028                    transaction: None,
1029                    undeclared_parameters: None,
1030                }),
1031                values: vec![],
1032                chunked_value: false,
1033                resume_token: vec![],
1034                stats: None,
1035                precommit_token: None,
1036                cache_update: None,
1037                last: false,
1038            };
1039            Ok(gaxi::grpc::tonic::Response::from(adapt([Ok(result_set)])))
1040        });
1041
1042        let (address, _server) = start("0.0.0.0:0", mock)
1043            .await
1044            .expect("Failed to start mock server");
1045        let client = Spanner::builder()
1046            .with_endpoint(address)
1047            .with_credentials(Anonymous::new().build())
1048            .build()
1049            .await
1050            .expect("Failed to build client");
1051
1052        let mut req = ReadRequest::new();
1053        req.table = "test_table".to_string();
1054        req.columns = vec!["col1".to_string()];
1055
1056        let mut stream = client
1057            .streaming_read(
1058                req,
1059                crate::RequestOptions::default(),
1060                client.next_channel_hint(),
1061            )
1062            .send()
1063            .await
1064            .expect("Failed to call streaming_read");
1065
1066        let result = stream.next_message().await;
1067        assert!(result.is_some());
1068        assert!(result.unwrap().is_ok());
1069    }
1070
1071    #[tokio_test_no_panics]
1072    async fn test_batch_write() {
1073        use crate::model::BatchWriteRequest;
1074
1075        let mut mock = MockSpanner::new();
1076        mock.expect_batch_write().once().returning(|_| {
1077            let response = mock_v1::BatchWriteResponse {
1078                indexes: vec![],
1079                status: None,
1080                commit_timestamp: None,
1081            };
1082            Ok(gaxi::grpc::tonic::Response::from(adapt([Ok(response)])))
1083        });
1084
1085        let (address, _server) = start("0.0.0.0:0", mock)
1086            .await
1087            .expect("Failed to start mock server");
1088        let client = Spanner::builder()
1089            .with_endpoint(address)
1090            .with_credentials(Anonymous::new().build())
1091            .build()
1092            .await
1093            .expect("Failed to build client");
1094
1095        let mut req = BatchWriteRequest::new();
1096        req.session = "test_session".to_string();
1097
1098        let mut stream = client
1099            .batch_write(
1100                req,
1101                crate::RequestOptions::default(),
1102                client.next_channel_hint(),
1103            )
1104            .send()
1105            .await
1106            .expect("Failed to call batch_write");
1107
1108        let result = stream.next_message().await;
1109        assert!(result.is_some());
1110        assert!(result.unwrap().is_ok());
1111    }
1112
1113    #[tokio_test_no_panics]
1114    async fn test_execute_streaming_sql_error() {
1115        use crate::model::ExecuteSqlRequest;
1116
1117        let mut mock = MockSpanner::new();
1118        mock.expect_execute_streaming_sql().once().returning(|_| {
1119            let stream = adapt([Err(gaxi::grpc::tonic::Status::internal(
1120                "unexpected internal error",
1121            ))]);
1122            Ok(gaxi::grpc::tonic::Response::from(stream))
1123        });
1124
1125        let (address, _server) = start("0.0.0.0:0", mock)
1126            .await
1127            .expect("Failed to start mock server");
1128        let client = Spanner::builder()
1129            .with_endpoint(address)
1130            .with_credentials(Anonymous::new().build())
1131            .build()
1132            .await
1133            .expect("Failed to build client");
1134
1135        let mut req = ExecuteSqlRequest::new();
1136        req.sql = "SELECT 1".to_string();
1137
1138        let mut stream = client
1139            .execute_streaming_sql(
1140                req,
1141                crate::RequestOptions::default(),
1142                client.next_channel_hint(),
1143            )
1144            .send()
1145            .await
1146            .expect("Failed to call execute_streaming_sql");
1147
1148        let result = stream.next_message().await;
1149        assert!(result.is_some());
1150        let err = result.unwrap().expect_err("expected error");
1151        assert_eq!(
1152            err.status().unwrap().code,
1153            google_cloud_gax::error::rpc::Code::Internal
1154        );
1155    }
1156
1157    #[tokio_test_no_panics]
1158    async fn default_retry_respected() -> anyhow::Result<()> {
1159        use crate::model::CreateSessionRequest;
1160
1161        // 1. Setup Mock Server
1162        let mut mock = MockSpanner::new();
1163        let mut seq = mockall::Sequence::new();
1164        mock.expect_create_session()
1165            .once()
1166            .in_sequence(&mut seq)
1167            .returning(|_| Err(Status::unavailable("server is unavailable")));
1168        mock.expect_create_session().once().in_sequence(&mut seq).returning(|_| {
1169            Ok(Response::new(Session {
1170                name: "projects/test-project/instances/test-instance/databases/test-db/sessions/456".to_string(),
1171                ..Default::default()
1172            }))
1173        });
1174
1175        // 2. Start mock server
1176        let (address, _server) = start("0.0.0.0:0", mock).await?;
1177
1178        // 3. Configure Client
1179        let client = Spanner::builder()
1180            .with_endpoint(address)
1181            .with_credentials(Anonymous::new().build())
1182            .build()
1183            .await?;
1184
1185        // 4. Call CreateSession using the hand-written wrapper
1186        let mut req = CreateSessionRequest::new();
1187        req.database =
1188            "projects/test-project/instances/test-instance/databases/test-db".to_string();
1189
1190        let session = client
1191            .create_session(
1192                req,
1193                crate::RequestOptions::default(),
1194                client.next_channel_hint(),
1195                &crate::observability::Observability::disabled(),
1196            )
1197            .await
1198            .expect("Failed to call create_session");
1199
1200        // 5. Verify Response
1201        assert_eq!(
1202            session.name,
1203            "projects/test-project/instances/test-instance/databases/test-db/sessions/456"
1204        );
1205
1206        Ok(())
1207    }
1208
1209    #[tokio_test_no_panics]
1210    async fn override_idempotency_to_false() -> anyhow::Result<()> {
1211        use crate::model::CreateSessionRequest;
1212
1213        // 1. Setup Mock Server to fail with UNAVAILABLE
1214        let mut mock = MockSpanner::new();
1215        mock.expect_create_session()
1216            .once()
1217            .returning(|_| Err(Status::unavailable("server is unavailable")));
1218
1219        // 2. Start mock server
1220        let (address, _server) = start("0.0.0.0:0", mock).await?;
1221
1222        // 3. Configure Client
1223        let client = Spanner::builder()
1224            .with_endpoint(address)
1225            .with_credentials(Anonymous::new().build())
1226            .build()
1227            .await?;
1228
1229        // 4. Call CreateSession with explicit idempotency = false
1230        let mut req = CreateSessionRequest::new();
1231        req.database =
1232            "projects/test-project/instances/test-instance/databases/test-db".to_string();
1233
1234        let mut options = crate::RequestOptions::default();
1235        options.set_idempotency(false);
1236
1237        let result = client
1238            .create_session(
1239                req,
1240                options,
1241                client.next_channel_hint(),
1242                &crate::observability::Observability::disabled(),
1243            )
1244            .await;
1245
1246        // 5. Verify that it failed and did not retry
1247        assert!(result.is_err(), "Expected error, got {:?}", result);
1248        let err = result.unwrap_err();
1249        assert_eq!(err.status().map(|s| s.code), Some(Code::Unavailable));
1250
1251        Ok(())
1252    }
1253
1254    #[tokio_test_no_panics]
1255    async fn timeout_respected() -> anyhow::Result<()> {
1256        use crate::batch_dml::BatchDml;
1257        use std::time::Duration;
1258
1259        // 1. Setup Mock Server
1260        let mut mock = MockSpanner::new();
1261
1262        mock.expect_create_session().returning(|_| {
1263            Ok(Response::new(Session {
1264                name: "projects/p/instances/i/databases/d/sessions/123".to_string(),
1265                ..Default::default()
1266            }))
1267        });
1268
1269        mock.expect_begin_transaction().returning(|_| {
1270            Ok(Response::new(mock_v1::Transaction {
1271                id: vec![42],
1272                ..Default::default()
1273            }))
1274        });
1275
1276        mock.expect_execute_streaming_sql().once().returning(|req| {
1277            let metadata = req.metadata();
1278            let timeout = metadata.get("grpc-timeout");
1279            assert!(
1280                timeout.is_some(),
1281                "grpc-timeout header should be present for query"
1282            );
1283
1284            let (tx, rx) = tokio::sync::mpsc::channel(1);
1285            let metadata = mock_v1::ResultSetMetadata {
1286                transaction: Some(mock_v1::Transaction {
1287                    id: vec![42],
1288                    ..Default::default()
1289                }),
1290                ..Default::default()
1291            };
1292            let prs = mock_v1::PartialResultSet {
1293                metadata: Some(metadata),
1294                ..Default::default()
1295            };
1296            tx.try_send(Ok(prs)).unwrap();
1297            Ok(Response::new(rx))
1298        });
1299
1300        mock.expect_streaming_read().once().returning(|req| {
1301            let metadata = req.metadata();
1302            let timeout = metadata.get("grpc-timeout");
1303            assert!(
1304                timeout.is_some(),
1305                "grpc-timeout header should be present for read"
1306            );
1307
1308            let (tx, rx) = tokio::sync::mpsc::channel(1);
1309            let metadata = mock_v1::ResultSetMetadata {
1310                transaction: None,
1311                ..Default::default()
1312            };
1313            let prs = mock_v1::PartialResultSet {
1314                metadata: Some(metadata),
1315                ..Default::default()
1316            };
1317            tx.try_send(Ok(prs)).unwrap();
1318            Ok(Response::new(rx))
1319        });
1320
1321        mock.expect_execute_sql().once().returning(|req| {
1322            let metadata = req.metadata();
1323            let timeout = metadata.get("grpc-timeout");
1324            assert!(
1325                timeout.is_some(),
1326                "grpc-timeout header should be present for single DML"
1327            );
1328
1329            Ok(Response::new(mock_v1::ResultSet {
1330                metadata: Some(mock_v1::ResultSetMetadata {
1331                    transaction: Some(mock_v1::Transaction {
1332                        id: vec![42],
1333                        ..Default::default()
1334                    }),
1335                    ..Default::default()
1336                }),
1337                stats: Some(mock_v1::ResultSetStats {
1338                    row_count: Some(mock_v1::result_set_stats::RowCount::RowCountExact(1)),
1339                    ..Default::default()
1340                }),
1341                ..Default::default()
1342            }))
1343        });
1344
1345        mock.expect_execute_batch_dml().once().returning(|req| {
1346            let metadata = req.metadata();
1347            let timeout = metadata.get("grpc-timeout");
1348            assert!(
1349                timeout.is_some(),
1350                "grpc-timeout header should be present for batch dml"
1351            );
1352
1353            Ok(Response::new(mock_v1::ExecuteBatchDmlResponse {
1354                result_sets: vec![mock_v1::ResultSet {
1355                    stats: Some(mock_v1::ResultSetStats {
1356                        row_count: Some(mock_v1::result_set_stats::RowCount::RowCountExact(1)),
1357                        ..Default::default()
1358                    }),
1359                    ..Default::default()
1360                }],
1361                ..Default::default()
1362            }))
1363        });
1364
1365        mock.expect_commit().returning(|_| {
1366            Ok(Response::new(mock_v1::CommitResponse {
1367                commit_timestamp: Some(prost_types::Timestamp {
1368                    seconds: 1234,
1369                    nanos: 0,
1370                }),
1371                ..Default::default()
1372            }))
1373        });
1374
1375        // 2. Start mock server
1376        let (address, _server) = start("0.0.0.0:0", mock).await?;
1377
1378        // 3. Configure Client
1379        let client = Spanner::builder()
1380            .with_endpoint(address)
1381            .with_credentials(Anonymous::new().build())
1382            .build()
1383            .await?;
1384
1385        let db = client
1386            .database_client("projects/p/instances/i/databases/d")
1387            .build()
1388            .await?;
1389        let runner = db.read_write_transaction().build().await?;
1390
1391        // 4. Run transaction
1392        runner
1393            .run(async |tx| {
1394                // Query
1395                let stmt = Statement::builder("SELECT 1")
1396                    .with_attempt_timeout(Duration::from_secs(10))
1397                    .build();
1398                // TODO(#5673): ensure that transaction ID is processed even if ResultSet is dropped
1399                let _rs = tx.execute_query(stmt).await?;
1400
1401                // Read
1402                let req = ReadRequest::builder("Table", vec!["Col"])
1403                    .with_keys(crate::key::KeySet::all())
1404                    .with_attempt_timeout(Duration::from_secs(5))
1405                    .build();
1406                let _ = tx.execute_read(req).await?;
1407
1408                // Single DML
1409                let dml = Statement::builder("UPDATE t SET c = 1")
1410                    .with_attempt_timeout(Duration::from_secs(7))
1411                    .build();
1412                let _ = tx.execute_update(dml).await?;
1413
1414                // Batch DML
1415                let batch = BatchDml::builder()
1416                    .add_statement("UPDATE t SET c = 2")
1417                    .with_attempt_timeout(Duration::from_secs(8))
1418                    .build();
1419                let _ = tx.execute_batch_update(batch).await?;
1420
1421                Ok(())
1422            })
1423            .await?;
1424
1425        Ok(())
1426    }
1427
1428    #[tokio_test_no_panics]
1429    async fn retry_policy_respected() -> anyhow::Result<()> {
1430        use google_cloud_gax::retry_policy::{Aip194Strict, RetryPolicyExt};
1431
1432        // Extend the default retry policy to also retry on ResourceExhausted.
1433        let retry_policy = Aip194Strict.continue_on_too_many_requests();
1434
1435        // 1. Setup Mock Server
1436        let mut mock = MockSpanner::new();
1437
1438        mock.expect_create_session().returning(|_| {
1439            Ok(Response::new(Session {
1440                name: "projects/p/instances/i/databases/d/sessions/123".to_string(),
1441                ..Default::default()
1442            }))
1443        });
1444
1445        mock.expect_begin_transaction().returning(|_| {
1446            Ok(Response::new(mock_v1::Transaction {
1447                id: vec![42],
1448                ..Default::default()
1449            }))
1450        });
1451
1452        // Mock ExecuteSql to first return RESOURCE_EXHAUSTED and then succeed.
1453        let mut seq = mockall::Sequence::new();
1454
1455        mock.expect_execute_sql()
1456            .once()
1457            .in_sequence(&mut seq)
1458            .returning(|_| Err(Status::new(GrpcCode::ResourceExhausted, "quota exceeded")));
1459
1460        mock.expect_execute_sql()
1461            .once()
1462            .in_sequence(&mut seq)
1463            .returning(|_| {
1464                Ok(Response::new(mock_v1::ResultSet {
1465                    metadata: Some(mock_v1::ResultSetMetadata {
1466                        transaction: Some(mock_v1::Transaction {
1467                            id: vec![42],
1468                            ..Default::default()
1469                        }),
1470                        ..Default::default()
1471                    }),
1472                    stats: Some(mock_v1::ResultSetStats {
1473                        row_count: Some(mock_v1::result_set_stats::RowCount::RowCountExact(1)),
1474                        ..Default::default()
1475                    }),
1476                    ..Default::default()
1477                }))
1478            });
1479
1480        mock.expect_commit().returning(|_| {
1481            Ok(Response::new(mock_v1::CommitResponse {
1482                commit_timestamp: Some(prost_types::Timestamp {
1483                    seconds: 1234,
1484                    nanos: 0,
1485                }),
1486                ..Default::default()
1487            }))
1488        });
1489
1490        // 2. Start mock server
1491        let (address, _server) = start("0.0.0.0:0", mock).await?;
1492
1493        // 3. Configure Client
1494        let client = Spanner::builder()
1495            .with_endpoint(address)
1496            .with_credentials(Anonymous::new().build())
1497            .build()
1498            .await?;
1499
1500        let db = client
1501            .database_client("projects/p/instances/i/databases/d")
1502            .build()
1503            .await?;
1504        let runner = db.read_write_transaction().build().await?;
1505
1506        // 4. Call execute_update with custom retry and backoff
1507        let mut mock_backoff = MockBackoffPolicy::new();
1508        mock_backoff
1509            .expect_on_failure()
1510            .once()
1511            .returning(|_| Duration::from_nanos(1));
1512
1513        let stmt = Statement::builder("UPDATE t SET c = 1")
1514            .with_retry_policy(retry_policy)
1515            .with_backoff_policy(mock_backoff)
1516            .build();
1517
1518        let result = runner
1519            .run(async |tx| {
1520                let count = tx.execute_update(stmt.clone()).await?;
1521                Ok(count)
1522            })
1523            .await?;
1524
1525        // 5. Verify success after retry
1526        assert_eq!(result.result, 1);
1527
1528        Ok(())
1529    }
1530
1531    fn parse_timeout(metadata: &MetadataMap) -> u64 {
1532        let timeout = metadata
1533            .get("grpc-timeout")
1534            .expect("grpc-timeout header should be present");
1535        let timeout_str = timeout
1536            .to_str()
1537            .expect("grpc-timeout should be a valid string");
1538        if timeout_str.ends_with('u') {
1539            timeout_str
1540                .trim_end_matches('u')
1541                .parse()
1542                .expect("valid u64")
1543        } else if timeout_str.ends_with('m') {
1544            timeout_str
1545                .trim_end_matches('m')
1546                .parse::<u64>()
1547                .expect("valid u64")
1548                * 1000
1549        } else if timeout_str.ends_with('n') {
1550            timeout_str
1551                .trim_end_matches('n')
1552                .parse::<u64>()
1553                .expect("valid u64")
1554                / 1000
1555        } else {
1556            panic!("Unknown timeout unit in {}", timeout_str);
1557        }
1558    }
1559
1560    #[tokio_test_no_panics]
1561    async fn transaction_timeout_respected() -> anyhow::Result<()> {
1562        use google_cloud_gax::retry_policy::{Aip194Strict, RetryPolicyExt};
1563        use spanner_grpc_mock::google::spanner::v1::Transaction;
1564
1565        // 1. Setup Mock Server
1566        let mut mock = MockSpanner::new();
1567
1568        mock.expect_create_session().returning(|_| {
1569            Ok(Response::new(Session {
1570                name: "projects/p/instances/i/databases/d/sessions/123".to_string(),
1571                ..Default::default()
1572            }))
1573        });
1574
1575        mock.expect_begin_transaction().returning(|_| {
1576            Ok(Response::new(Transaction {
1577                id: vec![1, 2, 3],
1578                ..Default::default()
1579            }))
1580        });
1581
1582        mock.expect_commit().once().returning(|_| {
1583            Ok(Response::new(CommitResponse {
1584                commit_timestamp: Some(prost_types::Timestamp {
1585                    seconds: 12345,
1586                    nanos: 0,
1587                }),
1588                ..Default::default()
1589            }))
1590        });
1591
1592        // Mock execute_sql to first fail and then succeed, checking timeout header on both
1593        let mut seq = mockall::Sequence::new();
1594
1595        mock.expect_execute_sql()
1596            .once()
1597            .in_sequence(&mut seq)
1598            .returning(|req| {
1599                let timeout_val = parse_timeout(req.metadata());
1600                assert!(
1601                    timeout_val <= 100000,
1602                    "Expected timeout to be <= 100ms, got {}",
1603                    timeout_val
1604                );
1605                Err(Status::new(GrpcCode::ResourceExhausted, "quota exceeded"))
1606            });
1607
1608        mock.expect_execute_sql()
1609            .once()
1610            .in_sequence(&mut seq)
1611            .returning(|req| {
1612                let timeout_val = parse_timeout(req.metadata());
1613                assert!(
1614                    timeout_val <= 100000,
1615                    "Expected timeout to be <= 100ms, got {}",
1616                    timeout_val
1617                );
1618
1619                let res = ResultSet {
1620                    metadata: Some(spanner_grpc_mock::google::spanner::v1::ResultSetMetadata {
1621                        transaction: Some(Transaction {
1622                            id: vec![1, 2, 3],
1623                            ..Default::default()
1624                        }),
1625                        ..Default::default()
1626                    }),
1627                    stats: Some(ResultSetStats {
1628                        row_count: Some(RowCount::RowCountExact(1)),
1629                        ..Default::default()
1630                    }),
1631                    ..Default::default()
1632                };
1633                Ok(Response::new(res))
1634            });
1635
1636        // 2. Initialize Client
1637        let (address, _server) = start("127.0.0.1:0", mock).await?;
1638        let client = Spanner::builder()
1639            .with_endpoint(address)
1640            .with_credentials(Anonymous::new().build())
1641            .build()
1642            .await?;
1643        let db = client
1644            .database_client("projects/p/instances/i/databases/d")
1645            .build()
1646            .await?;
1647
1648        // 3. Setup Transaction Runner with 100ms timeout
1649        let runner = db
1650            .read_write_transaction()
1651            .with_transaction_timeout(Duration::from_millis(100))
1652            .build()
1653            .await?;
1654
1655        // 4. Run transaction and expect success after retry
1656        let result = runner
1657            .run(async |tx| {
1658                let mut mock_backoff = MockBackoffPolicy::new();
1659                mock_backoff
1660                    .expect_on_failure()
1661                    .times(1)
1662                    .returning(|_| Duration::from_nanos(1));
1663
1664                let retry_policy = Aip194Strict.continue_on_too_many_requests();
1665
1666                let stmt = Statement::builder("SELECT 1")
1667                    .with_retry_policy(retry_policy)
1668                    .with_backoff_policy(mock_backoff)
1669                    .build();
1670                tx.execute_update(stmt).await?;
1671                Ok(())
1672            })
1673            .await;
1674
1675        result.expect("Transaction should have succeeded");
1676
1677        Ok(())
1678    }
1679
1680    #[tokio_test_no_panics]
1681    async fn transaction_timeout_ticks_down() -> anyhow::Result<()> {
1682        use spanner_grpc_mock::google::spanner::v1::Transaction;
1683
1684        let mut mock = MockSpanner::new();
1685
1686        mock.expect_create_session().returning(|_| {
1687            Ok(Response::new(Session {
1688                name: "projects/p/instances/i/databases/d/sessions/123".to_string(),
1689                ..Default::default()
1690            }))
1691        });
1692
1693        let mut seq = mockall::Sequence::new();
1694
1695        let previous_timeout = Arc::new(AtomicU64::new(0));
1696        let prev_clone1 = previous_timeout.clone();
1697        mock.expect_execute_sql()
1698            .once()
1699            .in_sequence(&mut seq)
1700            .returning(move |req| {
1701                let timeout_val = parse_timeout(req.metadata());
1702                assert!(
1703                    timeout_val <= 500000,
1704                    "Expected timeout to be <= 500ms, got {}",
1705                    timeout_val
1706                );
1707                prev_clone1.store(timeout_val, Ordering::SeqCst);
1708                Err(Status::new(GrpcCode::Aborted, "Aborted"))
1709            });
1710
1711        // Second attempt: Checks that timeout is <= previous
1712
1713        let prev_clone2 = previous_timeout.clone();
1714        mock.expect_execute_sql()
1715            .once()
1716            .in_sequence(&mut seq)
1717            .returning(move |req| {
1718                let timeout_val = parse_timeout(req.metadata());
1719                let prev = prev_clone2.load(Ordering::SeqCst);
1720                assert!(
1721                    timeout_val <= prev,
1722                    "Timeout should tick down between attempts or be equal, got {} and {}",
1723                    timeout_val,
1724                    prev
1725                );
1726                prev_clone2.store(timeout_val, Ordering::SeqCst); // store for next check
1727
1728                let res = ResultSet {
1729                    metadata: Some(spanner_grpc_mock::google::spanner::v1::ResultSetMetadata {
1730                        transaction: Some(Transaction {
1731                            id: vec![2],
1732                            ..Default::default()
1733                        }),
1734                        ..Default::default()
1735                    }),
1736                    stats: Some(ResultSetStats {
1737                        row_count: Some(RowCount::RowCountExact(1)),
1738                        ..Default::default()
1739                    }),
1740                    ..Default::default()
1741                };
1742                Ok(Response::new(res))
1743            });
1744
1745        let prev_clone3 = previous_timeout.clone();
1746        mock.expect_commit().once().returning(move |req| {
1747            let timeout_val = parse_timeout(req.metadata());
1748            let prev = prev_clone3.load(Ordering::SeqCst);
1749            assert!(
1750                timeout_val < prev,
1751                "Timeout should be smaller for commit, got {} and {}",
1752                timeout_val,
1753                prev
1754            );
1755
1756            Ok(Response::new(CommitResponse {
1757                commit_timestamp: Some(prost_types::Timestamp {
1758                    seconds: 12345,
1759                    nanos: 0,
1760                }),
1761                ..Default::default()
1762            }))
1763        });
1764
1765        let (address, _server) = start("127.0.0.1:0", mock).await?;
1766        let client = Spanner::builder()
1767            .with_endpoint(address)
1768            .with_credentials(Anonymous::new().build())
1769            .build()
1770            .await?;
1771        let db = client
1772            .database_client("projects/p/instances/i/databases/d")
1773            .build()
1774            .await?;
1775
1776        let runner = db
1777            .read_write_transaction()
1778            .with_transaction_timeout(Duration::from_millis(500))
1779            .build()
1780            .await?;
1781
1782        let result = runner
1783            .run(async |tx| {
1784                let stmt = Statement::builder("SELECT 1").build();
1785                tx.execute_update(stmt).await?;
1786                Ok(())
1787            })
1788            .await;
1789
1790        result.expect("Transaction should have succeeded");
1791
1792        Ok(())
1793    }
1794
1795    #[test]
1796    fn test_parse_emulator_endpoint() {
1797        assert_eq!(
1798            super::parse_emulator_endpoint("localhost:9010"),
1799            "http://localhost:9010"
1800        );
1801        assert_eq!(
1802            super::parse_emulator_endpoint("spanner-emulator:9010"),
1803            "http://spanner-emulator:9010"
1804        );
1805        assert_eq!(
1806            super::parse_emulator_endpoint("http://localhost:9010"),
1807            "http://localhost:9010"
1808        );
1809        assert_eq!(
1810            super::parse_emulator_endpoint("https://localhost:9010"),
1811            "https://localhost:9010"
1812        );
1813        assert_eq!(
1814            super::parse_emulator_endpoint("grpc://localhost:9010"),
1815            "grpc://localhost:9010"
1816        );
1817        assert_eq!(
1818            super::parse_emulator_endpoint("http_localhost:9010"),
1819            "http://http_localhost:9010"
1820        );
1821    }
1822}