google_cloud_spanner/
partitioned_dml_transaction.rs1use crate::client::amend_request_options_for_lar;
16use crate::database_client::DatabaseClient;
17use crate::google::spanner::v1::result_set_stats::RowCount::RowCountLowerBound;
18use crate::model::transaction_options::PartitionedDml;
19use crate::model::{
20 BeginTransactionRequest, TransactionOptions, TransactionSelector, transaction_selector,
21};
22use crate::server_streaming::stream::PartialResultSetStream;
23use crate::statement::Statement;
24use crate::transaction_retry_policy::{
25 BasicTransactionRetryPolicy, TransactionRetryPolicy, retry_aborted,
26};
27use google_cloud_gax::options::RequestOptions as GaxRequestOptions;
28
29pub struct PartitionedDmlTransactionBuilder {
44 client: DatabaseClient,
45 retry_policy: Box<dyn TransactionRetryPolicy>,
46 exclude_txn_from_change_streams: bool,
47}
48
49impl PartitionedDmlTransactionBuilder {
50 pub(crate) fn new(client: DatabaseClient) -> Self {
51 Self {
52 client,
53 retry_policy: Box::new(BasicTransactionRetryPolicy::default()),
54 exclude_txn_from_change_streams: false,
55 }
56 }
57
58 pub fn with_exclude_txn_from_change_streams(mut self, exclude: bool) -> Self {
82 self.exclude_txn_from_change_streams = exclude;
83 self
84 }
85
86 pub fn with_retry_policy<P: TransactionRetryPolicy + 'static>(mut self, policy: P) -> Self {
113 self.retry_policy = Box::new(policy);
114 self
115 }
116
117 pub async fn build(self) -> crate::Result<PartitionedDmlTransaction> {
119 Ok(PartitionedDmlTransaction {
120 client: self.client,
121 retry_policy: self.retry_policy,
122 exclude_txn_from_change_streams: self.exclude_txn_from_change_streams,
123 })
124 }
125}
126
127pub struct PartitionedDmlTransaction {
137 client: DatabaseClient,
138 retry_policy: Box<dyn TransactionRetryPolicy>,
139 exclude_txn_from_change_streams: bool,
140}
141
142impl PartitionedDmlTransaction {
143 pub async fn execute_update<T: Into<Statement>>(self, statement: T) -> crate::Result<i64> {
167 let statement = statement.into();
168 let mut gax_options = statement.gax_options().clone();
169 self.amend_gax_options(&mut gax_options);
170
171 let session_name = self.client.session_name();
172 let transaction_options = TransactionOptions::default()
173 .set_partitioned_dml(PartitionedDml::default())
174 .set_exclude_txn_from_change_streams(self.exclude_txn_from_change_streams);
175 let begin_request = BeginTransactionRequest {
176 session: session_name.clone(),
177 options: Some(transaction_options),
178 ..Default::default()
179 };
180 let base_request = statement.into_request();
181 let channel_hint = self.client.spanner.next_channel_hint();
182 let client = self.client;
183 let is_emulator = client.is_emulator();
184
185 let action = || {
186 let begin_request = begin_request.clone();
187 let base_request = base_request.clone();
188 let session_name = session_name.clone();
189 let gax_options = gax_options.clone();
190 let client = client.clone();
191
192 async move {
193 let transaction = client
194 .spanner
195 .begin_transaction(
196 begin_request,
197 gax_options.clone(),
198 channel_hint,
199 &client.o11y,
200 )
201 .await?;
202
203 let execute_request =
204 base_request
205 .set_session(session_name)
206 .set_transaction(TransactionSelector {
207 selector: Some(transaction_selector::Selector::Id(
208 transaction.id.clone(),
209 )),
210 ..Default::default()
211 });
212
213 let stream_builder = client.spanner.execute_streaming_sql(
214 execute_request,
215 gax_options,
216 channel_hint,
217 );
218 let stream = stream_builder.send().await?;
219
220 extract_lower_bound_update_count_from_stream(stream).await
221 }
222 };
223
224 retry_aborted(&*self.retry_policy, action, is_emulator).await
225 }
226
227 fn amend_gax_options(&self, options: &mut GaxRequestOptions) {
228 *options = amend_request_options_for_lar(
229 self.client.leader_aware_routing_enabled,
230 options.clone(),
231 );
232 }
233}
234
235async fn extract_lower_bound_update_count_from_stream(
240 mut stream: PartialResultSetStream,
241) -> crate::Result<i64> {
242 let mut lower_bound: Option<i64> = None;
243 while let Some(prs) = stream.next_message().await.transpose()? {
244 if let Some(RowCountLowerBound(val)) = prs.stats.and_then(|s| s.row_count) {
245 lower_bound = Some(val);
246 }
247 }
248 lower_bound.ok_or_else(|| {
249 crate::Error::deser(crate::error::SpannerInternalError::new(
250 "ExecuteStreamingSql completed successfully but no row_count_lower_bound was returned",
251 ))
252 })
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::read_only_transaction::tests::{create_session_mock, setup_db_client};
259 use crate::result_set::tests::adapt;
260 use crate::transaction_retry_policy::tests::create_aborted_status;
261 use gaxi::grpc::tonic;
262 use google_cloud_test_macros::tokio_test_no_panics;
263 use spanner_grpc_mock::google::spanner::v1;
264
265 #[test]
266 fn auto_traits() {
267 static_assertions::assert_impl_all!(PartitionedDmlTransactionBuilder: Send, Sync);
268 static_assertions::assert_impl_all!(PartitionedDmlTransaction: Send, Sync);
269 }
270
271 #[tokio_test_no_panics]
272 async fn execute_update_success() {
273 let mut mock = create_session_mock();
274
275 mock.expect_begin_transaction().once().returning(|req| {
276 let req = req.into_inner();
277 assert_eq!(
278 req.session,
279 "projects/p/instances/i/databases/d/sessions/123"
280 );
281 Ok(tonic::Response::new(v1::Transaction {
282 id: vec![0, 1, 2],
283 ..Default::default()
284 }))
285 });
286
287 mock.expect_execute_streaming_sql().once().returning(|req| {
288 let req = req.into_inner();
289 assert_eq!(req.sql, "UPDATE Users SET active = true");
290
291 let stream = adapt([Ok(v1::PartialResultSet {
292 stats: Some(v1::ResultSetStats {
293 row_count: Some(v1::result_set_stats::RowCount::RowCountLowerBound(500)),
294 ..Default::default()
295 }),
296 ..Default::default()
297 })]);
298 Ok(tonic::Response::from(stream))
299 });
300
301 let (db_client, _server) = setup_db_client(mock).await;
302 let transaction = db_client
303 .partitioned_dml_transaction()
304 .build()
305 .await
306 .unwrap();
307 let statement = Statement::builder("UPDATE Users SET active = true").build();
308 let res: i64 = transaction.execute_update(statement).await.unwrap();
309 assert_eq!(res, 500);
310 }
311
312 #[tokio_test_no_panics]
313 async fn execute_update_with_exclude_txn_from_change_streams() {
314 let mut mock = create_session_mock();
315
316 mock.expect_begin_transaction().once().returning(|req| {
317 let req = req.into_inner();
318 let options = req.options.expect("missing transaction options");
319 assert!(options.exclude_txn_from_change_streams);
320
321 Ok(tonic::Response::new(v1::Transaction {
322 id: vec![0, 1, 2],
323 ..Default::default()
324 }))
325 });
326
327 mock.expect_execute_streaming_sql()
328 .once()
329 .returning(|_req| {
330 let stream = adapt([Ok(v1::PartialResultSet {
331 stats: Some(v1::ResultSetStats {
332 row_count: Some(v1::result_set_stats::RowCount::RowCountLowerBound(500)),
333 ..Default::default()
334 }),
335 ..Default::default()
336 })]);
337 Ok(tonic::Response::from(stream))
338 });
339
340 let (db_client, _server) = setup_db_client(mock).await;
341 let transaction = db_client
342 .partitioned_dml_transaction()
343 .with_exclude_txn_from_change_streams(true)
344 .build()
345 .await
346 .unwrap();
347 let statement = Statement::builder("UPDATE Users SET active = true").build();
348 let res: i64 = transaction.execute_update(statement).await.unwrap();
349 assert_eq!(res, 500);
350 }
351
352 #[tokio_test_no_panics]
353 async fn execute_update_with_aborted_retry() {
354 let mut mock = create_session_mock();
355
356 mock.expect_begin_transaction().times(2).returning(|_req| {
357 Ok(tonic::Response::new(v1::Transaction {
358 id: vec![0, 1, 2],
359 ..Default::default()
360 }))
361 });
362
363 let mut seq = mockall::Sequence::new();
364 mock.expect_execute_streaming_sql()
365 .times(1)
366 .in_sequence(&mut seq)
367 .returning(move |_req| {
368 let stream = adapt([Err(create_aborted_status(std::time::Duration::from_nanos(
370 1,
371 )))]);
372 Ok(tonic::Response::from(stream))
373 });
374 mock.expect_execute_streaming_sql()
375 .times(1)
376 .in_sequence(&mut seq)
377 .returning(move |_req| {
378 let stream = adapt([Ok(v1::PartialResultSet {
379 stats: Some(v1::ResultSetStats {
380 row_count: Some(v1::result_set_stats::RowCount::RowCountLowerBound(100)),
381 ..Default::default()
382 }),
383 ..Default::default()
384 })]);
385 Ok(tonic::Response::from(stream))
386 });
387
388 let (db_client, _server) = setup_db_client(mock).await;
389 let transaction = db_client
390 .partitioned_dml_transaction()
391 .build()
392 .await
393 .unwrap();
394 let res: i64 = transaction
395 .execute_update(Statement::builder("UPDATE Users SET active = true").build())
396 .await
397 .unwrap();
398 assert_eq!(res, 100);
399 }
400
401 #[tokio_test_no_panics]
402 async fn builder_with_retry_settings() {
403 let mock = create_session_mock();
404 let (db_client, _server) = setup_db_client(mock).await;
405
406 let policy = BasicTransactionRetryPolicy::new()
407 .with_max_attempts(10)
408 .with_total_timeout(std::time::Duration::from_secs(42));
409
410 let _transaction = db_client
411 .partitioned_dml_transaction()
412 .with_retry_policy(policy)
413 .build()
414 .await
415 .unwrap();
416 }
417
418 #[tokio_test_no_panics]
419 async fn execute_update_missing_lower_bound() {
420 let mut mock = create_session_mock();
421
422 mock.expect_begin_transaction().once().returning(|_req| {
423 Ok(tonic::Response::new(v1::Transaction {
424 id: vec![0, 1, 2],
425 ..Default::default()
426 }))
427 });
428
429 mock.expect_execute_streaming_sql()
430 .once()
431 .returning(|_req| {
432 let stream = adapt([Ok(v1::PartialResultSet {
433 stats: Some(v1::ResultSetStats {
434 row_count: Some(v1::result_set_stats::RowCount::RowCountExact(100)),
436 ..Default::default()
437 }),
438 ..Default::default()
439 })]);
440 Ok(tonic::Response::from(stream))
441 });
442
443 let (db_client, _server) = setup_db_client(mock).await;
444 let transaction = db_client
445 .partitioned_dml_transaction()
446 .build()
447 .await
448 .unwrap();
449
450 let statement = Statement::builder("UPDATE Users SET active = true").build();
451 let res = transaction.execute_update(statement).await;
452
453 assert!(res.is_err());
454 let err = res.unwrap_err();
455 assert!(err.is_deserialization());
456 assert!(
457 err.to_string()
458 .contains("no row_count_lower_bound was returned")
459 );
460 }
461
462 #[tokio_test_no_panics]
463 async fn leader_aware_routing_enabled_by_default() {
464 let mut mock = create_session_mock();
465 mock.expect_begin_transaction().once().returning(|req| {
466 assert_eq!(
467 req.metadata()
468 .get("x-goog-spanner-route-to-leader")
469 .expect("header required")
470 .to_str()
471 .unwrap(),
472 "true"
473 );
474 Ok(tonic::Response::new(v1::Transaction {
475 id: vec![0, 1, 2],
476 ..Default::default()
477 }))
478 });
479
480 mock.expect_execute_streaming_sql().once().returning(|req| {
481 assert_eq!(
482 req.metadata()
483 .get("x-goog-spanner-route-to-leader")
484 .expect("header required")
485 .to_str()
486 .unwrap(),
487 "true"
488 );
489 let stream = adapt([Ok(v1::PartialResultSet {
490 stats: Some(v1::ResultSetStats {
491 row_count: Some(v1::result_set_stats::RowCount::RowCountLowerBound(500)),
492 ..Default::default()
493 }),
494 ..Default::default()
495 })]);
496 Ok(tonic::Response::from(stream))
497 });
498
499 let (db_client, _server) = setup_db_client(mock).await;
500 let transaction = db_client
501 .partitioned_dml_transaction()
502 .build()
503 .await
504 .unwrap();
505 let statement = Statement::builder("UPDATE Users SET active = true").build();
506 let res: i64 = transaction.execute_update(statement).await.unwrap();
507 assert_eq!(res, 500);
508 }
509}