1use crate::database_client::DatabaseClient;
16use crate::model::PartitionOptions;
17use crate::precommit::PrecommitTokenTracker;
18use crate::read_only_transaction::{
19 BeginTransactionOption, MultiUseReadOnlyTransaction, MultiUseReadOnlyTransactionBuilder,
20 ReadContextTransactionSelector,
21};
22use crate::result_set::{ResultSet, ResultSetParams, StreamOperation};
23use crate::statement::Statement;
24use crate::timestamp_bound::TimestampBound;
25use google_cloud_gax::backoff_policy::BackoffPolicyArg;
26use google_cloud_gax::options::RequestOptions as GaxRequestOptions;
27use google_cloud_gax::retry_policy::RetryPolicyArg;
28use serde::{Deserialize, Serialize};
29use std::time::Duration;
30
31pub struct BatchReadOnlyTransactionBuilder {
47 inner: MultiUseReadOnlyTransactionBuilder,
48}
49
50impl BatchReadOnlyTransactionBuilder {
51 pub(crate) fn new(client: DatabaseClient) -> Self {
52 Self {
53 inner: MultiUseReadOnlyTransactionBuilder::new(client)
54 .with_begin_transaction_option(BeginTransactionOption::ExplicitBegin),
55 }
56 }
57
58 pub fn set_timestamp_bound(self, bound: TimestampBound) -> Self {
71 Self {
72 inner: self.inner.set_timestamp_bound(bound),
73 }
74 }
75
76 pub async fn build(self) -> crate::Result<BatchReadOnlyTransaction> {
89 let inner = self.inner.build().await?;
90 Ok(BatchReadOnlyTransaction { inner })
91 }
92}
93
94#[derive(Debug)]
118pub struct BatchReadOnlyTransaction {
119 inner: MultiUseReadOnlyTransaction,
120}
121
122impl BatchReadOnlyTransaction {
123 pub fn read_timestamp(&self) -> Option<wkt::Timestamp> {
125 self.inner.read_timestamp()
126 }
127
128 pub async fn partition_query<T: Into<Statement>>(
149 &self,
150 statement: T,
151 options: PartitionOptions,
152 ) -> crate::Result<Vec<Partition>> {
153 let selector = self.inner.context.transaction_selector.selector().await?;
154 let statement = statement.into();
155 let request = statement
156 .clone()
157 .into_partition_query_request()
158 .set_session(self.inner.context.session_name.clone())
159 .set_transaction(selector.clone())
160 .set_partition_options(options);
161
162 let response = self
163 .inner
164 .context
165 .client
166 .spanner
167 .partition_query(
168 request,
169 crate::RequestOptions::default(),
170 self.inner.context.channel_hint,
171 &self.inner.context.client.o11y,
172 )
173 .await?;
174
175 Ok(response
176 .partitions
177 .into_iter()
178 .map(|p| {
179 let mut req = statement.clone().into_request();
180 req.session = self.inner.context.session_name.clone();
181 req.transaction = Some(selector.clone());
182 req.partition_token = p.partition_token;
183
184 Partition {
185 inner: PartitionedOperation::Query(req),
186 gax_options: GaxRequestOptions::default(),
187 }
188 })
189 .collect())
190 }
191
192 pub async fn partition_read<T: Into<crate::read::ReadRequest>>(
214 &self,
215 read: T,
216 options: PartitionOptions,
217 ) -> crate::Result<Vec<Partition>> {
218 let selector = self.inner.context.transaction_selector.selector().await?;
219 let read = read.into();
220 let request = read
221 .clone()
222 .into_partition_read_request()
223 .set_session(self.inner.context.session_name.clone())
224 .set_transaction(selector.clone())
225 .set_partition_options(options);
226
227 let response = self
228 .inner
229 .context
230 .client
231 .spanner
232 .partition_read(
233 request,
234 crate::RequestOptions::default(),
235 self.inner.context.channel_hint,
236 &self.inner.context.client.o11y,
237 )
238 .await?;
239
240 Ok(response
241 .partitions
242 .into_iter()
243 .map(|p| {
244 let mut req = read.clone().into_request();
245 req.session = self.inner.context.session_name.clone();
246 req.transaction = Some(selector.clone());
247 req.partition_token = p.partition_token;
248
249 Partition {
250 inner: PartitionedOperation::Read(req),
251 gax_options: GaxRequestOptions::default(),
252 }
253 })
254 .collect())
255 }
256}
257
258#[derive(Clone, Debug, Serialize, Deserialize)]
262pub struct Partition {
263 pub(crate) inner: PartitionedOperation,
264 #[serde(skip)]
265 pub(crate) gax_options: GaxRequestOptions,
266}
267
268impl Partition {
269 pub fn set_data_boost(mut self, enabled: bool) -> Self {
289 match &mut self.inner {
290 PartitionedOperation::Query(req) => req.data_boost_enabled = enabled,
291 PartitionedOperation::Read(req) => req.data_boost_enabled = enabled,
292 }
293 self
294 }
295
296 pub fn with_attempt_timeout(mut self, timeout: Duration) -> Self {
300 self.gax_options.set_attempt_timeout(timeout);
301 self
302 }
303
304 pub fn with_retry_policy(mut self, policy: impl Into<RetryPolicyArg>) -> Self {
308 self.gax_options.set_retry_policy(policy);
309 self
310 }
311
312 pub fn with_backoff_policy(mut self, policy: impl Into<BackoffPolicyArg>) -> Self {
316 self.gax_options.set_backoff_policy(policy);
317 self
318 }
319
320 pub async fn execute(&self, client: &DatabaseClient) -> crate::Result<ResultSet> {
372 match &self.inner {
373 PartitionedOperation::Query(req) => {
374 Self::execute_query(client, req, self.gax_options.clone()).await
375 }
376 PartitionedOperation::Read(req) => {
377 Self::execute_read(client, req, self.gax_options.clone()).await
378 }
379 }
380 }
381
382 async fn execute_query(
383 client: &DatabaseClient,
384 req: &crate::model::ExecuteSqlRequest,
385 gax_options: GaxRequestOptions,
386 ) -> crate::Result<ResultSet> {
387 let channel_hint = client.spanner.next_channel_hint();
388 let stream = client
389 .spanner
390 .execute_streaming_sql(req.clone(), gax_options.clone(), channel_hint)
391 .send()
392 .await?;
393
394 ResultSet::create(ResultSetParams {
395 stream,
396 transaction_selector: Some(ReadContextTransactionSelector::Fixed(
397 req.transaction
398 .clone()
399 .expect("transaction must be set in partition request"),
400 None,
401 )),
402 precommit_token_tracker: PrecommitTokenTracker::new_noop(),
403 client: client.clone(),
404 session_name: req.session.clone(),
405 transaction_tag: None,
406 operation: StreamOperation::Query(req.clone()),
407 channel_hint,
408 gax_options,
409 })
410 .await
411 }
412
413 async fn execute_read(
414 client: &DatabaseClient,
415 req: &crate::model::ReadRequest,
416 gax_options: GaxRequestOptions,
417 ) -> crate::Result<ResultSet> {
418 let channel_hint = client.spanner.next_channel_hint();
419 let stream = client
420 .spanner
421 .streaming_read(req.clone(), gax_options.clone(), channel_hint)
422 .send()
423 .await?;
424
425 ResultSet::create(ResultSetParams {
426 stream,
427 transaction_selector: Some(ReadContextTransactionSelector::Fixed(
428 req.transaction
429 .clone()
430 .expect("transaction must be set in partition request"),
431 None,
432 )),
433 precommit_token_tracker: PrecommitTokenTracker::new_noop(),
434 client: client.clone(),
435 session_name: req.session.clone(),
436 transaction_tag: None,
437 operation: StreamOperation::Read(req.clone()),
438 channel_hint,
439 gax_options,
440 })
441 .await
442 }
443}
444
445#[derive(Clone, Debug, Serialize, Deserialize)]
446pub(crate) enum PartitionedOperation {
447 Query(crate::model::ExecuteSqlRequest),
448 Read(crate::model::ReadRequest),
449}
450
451#[cfg(test)]
452pub(crate) mod tests {
453 use super::*;
454 use crate::key::KeySet;
455 use crate::model::transaction_selector::Selector;
456 use crate::model::{ExecuteSqlRequest, ReadRequest as GrpcReadRequest, TransactionSelector};
457 use crate::read::ReadRequest as SpannerReadRequest;
458 use crate::read_only_transaction::tests::{create_session_mock, setup_db_client};
459 use crate::statement::Statement;
460 use crate::transaction::TimestampBound;
461 use gaxi::grpc::tonic::Response;
462 use google_cloud_test_macros::tokio_test_no_panics;
463 use prost_types::Timestamp;
464 use spanner_grpc_mock::google::spanner::v1::{
465 PartialResultSet, Partition as MockPartition, PartitionResponse, ResultSetMetadata,
466 StructType, Transaction,
467 };
468 use static_assertions::assert_impl_all;
469 use std::fmt::Debug;
470
471 #[test]
472 fn auto_traits() {
473 assert_impl_all!(BatchReadOnlyTransactionBuilder: Send, Sync);
474 assert_impl_all!(BatchReadOnlyTransaction: Send, Sync, Debug);
475 assert_impl_all!(Partition: Send, Sync, Debug);
476 }
477
478 #[test]
479 fn serialize_partition_skips_gax_options() -> anyhow::Result<()> {
480 use std::time::Duration;
481
482 let req = crate::model::ExecuteSqlRequest::new()
483 .set_sql("SELECT 1")
484 .set_partition_token(b"token".to_vec());
485
486 let mut gax_options = GaxRequestOptions::default();
487 gax_options.set_attempt_timeout(Duration::from_secs(5));
488 let partition = Partition {
489 inner: PartitionedOperation::Query(req),
490 gax_options,
491 };
492
493 let serialized = serde_json::to_string(&partition)?;
494 let deserialized: Partition = serde_json::from_str(&serialized)?;
495
496 assert_eq!(*deserialized.gax_options.attempt_timeout(), None);
498
499 Ok(())
500 }
501
502 fn setup_select1() -> PartialResultSet {
503 PartialResultSet {
504 metadata: Some(ResultSetMetadata {
505 row_type: Some(StructType {
506 fields: vec![Default::default()],
507 }),
508 ..Default::default()
509 }),
510 values: vec![prost_types::Value {
511 kind: Some(prost_types::value::Kind::StringValue("1".to_string())),
512 }],
513 last: true,
514 ..Default::default()
515 }
516 }
517
518 #[tokio_test_no_panics]
519 async fn partition_execute_respects_options() -> anyhow::Result<()> {
520 use gaxi::grpc::tonic::Response;
521 use std::time::Duration;
522
523 let mut mock = create_session_mock();
524
525 mock.expect_execute_streaming_sql().once().returning(|req| {
526 let timeout = req.metadata().get("grpc-timeout");
527 assert!(timeout.is_some(), "Missing grpc-timeout header");
528 assert_eq!(timeout.unwrap(), "5000000u"); Ok(Response::from(crate::result_set::tests::adapt([Ok(
531 setup_select1(),
532 )])))
533 });
534
535 let (db_client, _server) = setup_db_client(mock).await;
536
537 let req = crate::model::ExecuteSqlRequest::new()
538 .set_session("projects/p/instances/i/databases/d/sessions/123")
539 .set_transaction(crate::model::TransactionSelector {
540 selector: Some(Selector::Id(b"tx_id_1".to_vec().into())),
541 ..Default::default()
542 })
543 .set_sql("SELECT 1")
544 .set_partition_token(b"token".to_vec());
545
546 let partition = Partition {
547 inner: PartitionedOperation::Query(req),
548 gax_options: GaxRequestOptions::default(),
549 };
550
551 let partition = partition.with_attempt_timeout(Duration::from_secs(5));
552
553 let _result_set = partition.execute(&db_client).await?;
554
555 Ok(())
556 }
557
558 #[test]
559 fn serialize_partition_query() -> anyhow::Result<()> {
560 let req = crate::model::ExecuteSqlRequest::new()
561 .set_session("projects/p/instances/i/databases/d/sessions/123")
562 .set_transaction(crate::model::TransactionSelector {
563 selector: Some(crate::model::transaction_selector::Selector::Id(
564 b"tx_id_1".to_vec().into(),
565 )),
566 ..Default::default()
567 })
568 .set_sql("SELECT * FROM Users")
569 .set_partition_token(b"partition_token_123".to_vec());
570
571 let partition = Partition {
572 inner: PartitionedOperation::Query(req),
573 gax_options: GaxRequestOptions::default(),
574 };
575
576 let serialized = serde_json::to_string(&partition)?;
577 let deserialized: Partition = serde_json::from_str(&serialized)?;
578
579 match &deserialized.inner {
580 PartitionedOperation::Query(r) => {
581 assert_eq!(r.partition_token.as_ref(), b"partition_token_123");
582 assert_eq!(r.sql, "SELECT * FROM Users");
583 assert_eq!(r.session, "projects/p/instances/i/databases/d/sessions/123");
584 }
585 _ => panic!("Expected Query partition"),
586 }
587 Ok(())
588 }
589
590 #[test]
591 fn serialize_partition_read() -> anyhow::Result<()> {
592 let req = crate::model::ReadRequest::new()
593 .set_session("projects/p/instances/i/databases/d/sessions/456")
594 .set_transaction(crate::model::TransactionSelector {
595 selector: Some(crate::model::transaction_selector::Selector::Id(
596 b"tx_id_2".to_vec().into(),
597 )),
598 ..Default::default()
599 })
600 .set_table("Users")
601 .set_columns(vec!["Id"])
602 .set_partition_token(b"partition_token_456".to_vec());
603
604 let partition = Partition {
605 inner: PartitionedOperation::Read(req),
606 gax_options: GaxRequestOptions::default(),
607 };
608
609 let serialized = serde_json::to_string(&partition)?;
610 let deserialized: Partition = serde_json::from_str(&serialized)?;
611
612 match &deserialized.inner {
613 PartitionedOperation::Read(r) => {
614 assert_eq!(r.partition_token.as_ref(), b"partition_token_456");
615 assert_eq!(r.table, "Users");
616 assert_eq!(r.session, "projects/p/instances/i/databases/d/sessions/456");
617 }
618 _ => panic!("Expected Read partition"),
619 }
620 Ok(())
621 }
622
623 #[tokio_test_no_panics]
624 async fn execute_query() -> anyhow::Result<()> {
625 let mut mock = create_session_mock();
626
627 mock.expect_execute_streaming_sql().once().returning(|req| {
628 let req = req.into_inner();
629 assert_eq!(
631 req.session,
632 "projects/p/instances/i/databases/d/sessions/123"
633 );
634 assert_eq!(req.partition_token, b"partition_token_123".as_slice());
635 assert!(req.transaction.is_some());
636 assert_eq!(req.sql, "SELECT * FROM Users");
637
638 Ok(Response::from(crate::result_set::tests::adapt([Ok(
639 setup_select1(),
640 )])))
641 });
642
643 let (db_client, _server) = setup_db_client(mock).await;
644
645 let req = crate::model::ExecuteSqlRequest::new()
646 .set_session("projects/p/instances/i/databases/d/sessions/123")
647 .set_transaction(crate::model::TransactionSelector {
648 selector: Some(crate::model::transaction_selector::Selector::Id(
649 b"tx_id_1".to_vec().into(),
650 )),
651 ..Default::default()
652 })
653 .set_sql("SELECT * FROM Users")
654 .set_partition_token(b"partition_token_123".to_vec());
655
656 let partition = Partition {
657 inner: PartitionedOperation::Query(req),
658 gax_options: GaxRequestOptions::default(),
659 };
660
661 let _result_set = partition.execute(&db_client).await?;
662
663 Ok(())
664 }
665
666 #[tokio_test_no_panics]
667 async fn execute_read() -> anyhow::Result<()> {
668 let mut mock = create_session_mock();
669
670 mock.expect_streaming_read().once().returning(|req| {
671 let req = req.into_inner();
672 assert_eq!(
674 req.session,
675 "projects/p/instances/i/databases/d/sessions/456"
676 );
677 assert_eq!(req.partition_token, b"partition_token_456".as_slice());
678 assert!(req.transaction.is_some());
679 assert_eq!(req.table, "Users");
680
681 Ok(Response::from(crate::result_set::tests::adapt([Ok(
682 setup_select1(),
683 )])))
684 });
685
686 let (db_client, _server) = setup_db_client(mock).await;
687
688 let req = crate::model::ReadRequest::new()
689 .set_session("projects/p/instances/i/databases/d/sessions/456")
690 .set_transaction(crate::model::TransactionSelector {
691 selector: Some(crate::model::transaction_selector::Selector::Id(
692 b"tx_id_2".to_vec().into(),
693 )),
694 ..Default::default()
695 })
696 .set_table("Users")
697 .set_columns(vec!["Id"])
698 .set_partition_token(b"partition_token_456".to_vec());
699
700 let partition = Partition {
701 inner: PartitionedOperation::Read(req),
702 gax_options: GaxRequestOptions::default(),
703 };
704
705 let _result_set = partition.execute(&db_client).await?;
706
707 Ok(())
708 }
709
710 #[tokio_test_no_panics]
711 async fn partition_query() -> anyhow::Result<()> {
712 let mut mock = create_session_mock();
713
714 mock.expect_begin_transaction().once().returning(|req| {
715 let req = req.into_inner();
716 assert_eq!(
717 req.session,
718 "projects/p/instances/i/databases/d/sessions/123"
719 );
720 Ok(Response::new(Transaction {
721 id: vec![1, 2, 3],
722 read_timestamp: Some(Timestamp {
723 seconds: 123456789,
724 nanos: 0,
725 }),
726 ..Default::default()
727 }))
728 });
729
730 mock.expect_partition_query().once().returning(|req| {
731 let req = req.into_inner();
732 assert_eq!(
733 req.session,
734 "projects/p/instances/i/databases/d/sessions/123"
735 );
736 assert_eq!(req.sql, "SELECT 1");
737 Ok(Response::new(PartitionResponse {
738 partitions: vec![
739 MockPartition {
740 partition_token: vec![10],
741 },
742 MockPartition {
743 partition_token: vec![20],
744 },
745 ],
746 transaction: None,
747 }))
748 });
749
750 let (db_client, _server) = setup_db_client(mock).await;
751
752 let tx = db_client
753 .batch_read_only_transaction()
754 .set_timestamp_bound(TimestampBound::strong())
755 .build()
756 .await?;
757
758 let ts = tx.read_timestamp().expect("Missing read timestamp");
759 assert_eq!(ts.seconds(), 123456789);
760 assert_eq!(ts.nanos(), 0);
761
762 let partitions = tx
763 .partition_query(
764 Statement::builder("SELECT 1").build(),
765 PartitionOptions::default(),
766 )
767 .await?;
768
769 assert_eq!(partitions.len(), 2);
770
771 match &partitions[0].inner {
772 PartitionedOperation::Query(req) => {
773 assert_eq!(req.partition_token.as_ref(), &[10]);
774 assert_eq!(req.sql, "SELECT 1");
775 }
776 _ => panic!("Expected Query partition"),
777 }
778 Ok(())
779 }
780
781 #[tokio_test_no_panics]
782 async fn partition_read() -> anyhow::Result<()> {
783 let mut mock = create_session_mock();
784
785 mock.expect_begin_transaction().once().returning(|req| {
786 let req = req.into_inner();
787 assert_eq!(
788 req.session,
789 "projects/p/instances/i/databases/d/sessions/123"
790 );
791 Ok(Response::new(Transaction {
792 id: vec![1, 2, 3],
793 read_timestamp: Some(Timestamp {
794 seconds: 123456789,
795 nanos: 0,
796 }),
797 ..Default::default()
798 }))
799 });
800
801 mock.expect_partition_read().once().returning(|req| {
802 let req = req.into_inner();
803 assert_eq!(
804 req.session,
805 "projects/p/instances/i/databases/d/sessions/123"
806 );
807 assert_eq!(req.table, "Users");
808 Ok(Response::new(PartitionResponse {
809 partitions: vec![MockPartition {
810 partition_token: vec![30],
811 }],
812 transaction: None,
813 }))
814 });
815
816 let (db_client, _server) = setup_db_client(mock).await;
817
818 let transaction = db_client.batch_read_only_transaction().build().await?;
819
820 let read = SpannerReadRequest::builder("Users", vec!["Id", "Name"])
821 .with_keys(KeySet::all())
822 .build();
823 let partitions = transaction
824 .partition_read(read, PartitionOptions::default())
825 .await?;
826
827 assert_eq!(partitions.len(), 1);
828
829 match &partitions[0].inner {
830 PartitionedOperation::Read(req) => {
831 assert_eq!(req.partition_token.as_ref(), &[30]);
832 assert_eq!(req.table, "Users");
833 }
834 _ => panic!("Expected Read partition"),
835 }
836 Ok(())
837 }
838
839 #[tokio_test_no_panics]
840 async fn execute_query_with_data_boost() -> anyhow::Result<()> {
841 let mut mock = create_session_mock();
842
843 mock.expect_execute_streaming_sql().once().returning(|req| {
844 let req = req.into_inner();
845 assert!(req.data_boost_enabled, "data_boost_enabled should be true");
846 Ok(Response::from(crate::result_set::tests::adapt([Ok(
847 setup_select1(),
848 )])))
849 });
850
851 let (db_client, _server) = setup_db_client(mock).await;
852
853 let req = ExecuteSqlRequest::new()
854 .set_session("projects/p/instances/i/databases/d/sessions/123")
855 .set_transaction(TransactionSelector {
856 selector: Some(Selector::Id(b"tx_id_1".to_vec().into())),
857 ..Default::default()
858 })
859 .set_sql("SELECT * FROM Users")
860 .set_partition_token(b"partition_token_123".to_vec());
861
862 let partition = Partition {
863 inner: PartitionedOperation::Query(req),
864 gax_options: GaxRequestOptions::default(),
865 };
866
867 let _result_set = partition.set_data_boost(true).execute(&db_client).await?;
868
869 Ok(())
870 }
871
872 #[tokio_test_no_panics]
873 async fn execute_read_with_data_boost() -> anyhow::Result<()> {
874 let mut mock = create_session_mock();
875
876 mock.expect_streaming_read().once().returning(|req| {
877 let req = req.into_inner();
878 assert!(req.data_boost_enabled, "data_boost_enabled should be true");
879 Ok(Response::from(crate::result_set::tests::adapt([Ok(
880 setup_select1(),
881 )])))
882 });
883
884 let (db_client, _server) = setup_db_client(mock).await;
885
886 let req = GrpcReadRequest::new()
887 .set_session("projects/p/instances/i/databases/d/sessions/123")
888 .set_transaction(TransactionSelector {
889 selector: Some(Selector::Id(b"tx_id_2".to_vec().into())),
890 ..Default::default()
891 })
892 .set_table("Users")
893 .set_columns(vec!["Id".to_string(), "Name".to_string()])
894 .set_partition_token(b"partition_token_456".to_vec());
895
896 let partition = Partition {
897 inner: PartitionedOperation::Read(req),
898 gax_options: GaxRequestOptions::default(),
899 };
900
901 let _result_set = partition.set_data_boost(true).execute(&db_client).await?;
902
903 Ok(())
904 }
905}