1use crate::error::{BatchUpdateError, internal_error};
16use crate::model::result_set_stats::RowCount;
17use crate::model::{ExecuteBatchDmlResponse, RequestOptions};
18use crate::statement::Statement;
19use google_cloud_gax::backoff_policy::BackoffPolicyArg;
20use google_cloud_gax::error::rpc::Code;
21use google_cloud_gax::error::rpc::Status as RpcStatus;
22use google_cloud_gax::options::RequestOptions as GaxRequestOptions;
23use google_cloud_gax::retry_policy::RetryPolicyArg;
24use std::time::Duration;
25
26#[derive(Clone, Default, Debug)]
28pub struct BatchDmlBuilder {
29 statements: Vec<Statement>,
30 request_options: Option<RequestOptions>,
31 last_statements: bool,
32 gax_options: GaxRequestOptions,
33}
34
35impl BatchDmlBuilder {
36 pub fn new() -> Self {
38 BatchDmlBuilder::default()
39 }
40
41 pub fn add_statement(mut self, statement: impl Into<Statement>) -> Self {
43 self.statements.push(statement.into());
44 self
45 }
46
47 pub fn set_request_tag(mut self, tag: impl Into<String>) -> Self {
62 self.request_options
63 .get_or_insert_with(RequestOptions::default)
64 .request_tag = tag.into();
65 self
66 }
67
68 pub fn with_attempt_timeout(mut self, timeout: Duration) -> Self {
70 self.gax_options.set_attempt_timeout(timeout);
71 self
72 }
73
74 pub fn with_retry_policy(mut self, policy: impl Into<RetryPolicyArg>) -> Self {
76 self.gax_options.set_retry_policy(policy);
77 self
78 }
79
80 pub fn with_backoff_policy(mut self, policy: impl Into<BackoffPolicyArg>) -> Self {
82 self.gax_options.set_backoff_policy(policy);
83 self
84 }
85
86 pub fn set_last_statements(mut self, last_statements: bool) -> Self {
102 self.last_statements = last_statements;
103 self
104 }
105
106 pub fn build(self) -> BatchDml {
108 BatchDml {
109 statements: self.statements,
110 request_options: self.request_options,
111 last_statements: self.last_statements,
112 gax_options: self.gax_options,
113 }
114 }
115}
116
117#[derive(Clone, Debug)]
119pub struct BatchDml {
120 pub(crate) statements: Vec<Statement>,
121 pub(crate) request_options: Option<RequestOptions>,
122 pub(crate) last_statements: bool,
123 pub(crate) gax_options: GaxRequestOptions,
124}
125
126impl BatchDml {
127 pub fn builder() -> BatchDmlBuilder {
129 BatchDmlBuilder::new()
130 }
131}
132
133impl From<BatchDmlBuilder> for BatchDml {
134 fn from(builder: BatchDmlBuilder) -> Self {
135 builder.build()
136 }
137}
138
139impl<T: Into<Statement>> From<Vec<T>> for BatchDml {
140 fn from(statements: Vec<T>) -> Self {
141 BatchDml {
142 statements: statements.into_iter().map(Into::into).collect(),
143 request_options: None,
144 last_statements: false,
145 gax_options: GaxRequestOptions::default(),
146 }
147 }
148}
149
150pub(crate) fn process_response(response: ExecuteBatchDmlResponse) -> crate::Result<Vec<i64>> {
152 let mut update_counts = Vec::with_capacity(response.result_sets.len());
153 for result_set in response.result_sets {
154 if let Some(stats) = result_set.stats {
155 let exact_count = match stats.row_count {
156 Some(RowCount::RowCountExact(c)) => c,
157 _ => {
158 return Err(internal_error(
159 "ExecuteBatchDml returned an invalid or missing row count type",
160 ));
161 }
162 };
163 update_counts.push(exact_count);
164 }
165 }
166
167 if let Some(status) = response.status.filter(|s| s.code != Code::Ok as i32) {
169 let grpc_status = RpcStatus::default()
170 .set_code(status.code)
171 .set_message(status.message);
172
173 if status.code == Code::Aborted as i32 {
176 return Err(crate::Error::service(grpc_status));
177 }
178 return Err(BatchUpdateError::build_error(update_counts, grpc_status));
179 }
180
181 Ok(update_counts)
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::model::{ResultSet, ResultSetStats};
188 use google_cloud_rpc::model::Status;
189 use static_assertions::assert_impl_all;
190
191 #[test]
192 fn auto_traits() {
193 assert_impl_all!(BatchDml: Send, Sync, Clone, std::fmt::Debug);
194 assert_impl_all!(BatchDmlBuilder: Send, Sync, Clone, std::fmt::Debug);
195 }
196
197 #[test]
198 fn builder() {
199 let stmt1 = Statement::builder("UPDATE t SET c = 1 WHERE id = 1").build();
200 let stmt2 = Statement::builder("UPDATE t SET c = 2 WHERE id = 2").build();
201
202 let batch = BatchDml::builder()
203 .add_statement(stmt1)
204 .add_statement(stmt2)
205 .set_last_statements(true)
206 .build();
207
208 assert_eq!(batch.statements.len(), 2);
209 assert_eq!(batch.statements[0].sql, "UPDATE t SET c = 1 WHERE id = 1");
210 assert_eq!(batch.statements[1].sql, "UPDATE t SET c = 2 WHERE id = 2");
211 assert!(batch.last_statements);
212 assert!(
213 batch.request_options.is_none(),
214 "Unexpected request_options set: {:#?}",
215 batch.request_options
216 );
217 }
218
219 #[test]
220 fn builder_with_gax_options() {
221 use google_cloud_gax::backoff_policy::BackoffPolicy;
222 use google_cloud_gax::retry_policy::Aip194Strict;
223 use google_cloud_gax::retry_state::RetryState;
224 use std::time::Duration;
225
226 #[derive(Debug)]
227 struct DummyBackoff;
228 impl BackoffPolicy for DummyBackoff {
229 fn on_failure(&self, _state: &RetryState) -> Duration {
230 Duration::ZERO
231 }
232 }
233
234 let stmt = Statement::builder("UPDATE t SET c = 1 WHERE id = 1").build();
235
236 let batch = BatchDml::builder()
237 .add_statement(stmt)
238 .with_attempt_timeout(Duration::from_secs(5))
239 .with_retry_policy(Aip194Strict)
240 .with_backoff_policy(DummyBackoff)
241 .build();
242
243 assert_eq!(
244 *batch.gax_options.attempt_timeout(),
245 Some(Duration::from_secs(5))
246 );
247 assert!(batch.gax_options.retry_policy().is_some());
248 assert!(batch.gax_options.backoff_policy().is_some());
249 }
250
251 #[test]
252 fn builder_with_request_tag() {
253 let stmt = Statement::builder("UPDATE t SET c = 1 WHERE id = 1").build();
254
255 let batch = BatchDml::builder()
256 .add_statement(stmt)
257 .set_request_tag("tag1")
258 .build();
259
260 assert_eq!(batch.statements.len(), 1);
261 assert_eq!(
262 batch
263 .request_options
264 .expect("request options missing")
265 .request_tag,
266 "tag1"
267 );
268 }
269
270 #[test]
271 fn process_response_success() -> anyhow::Result<()> {
272 let stats1 = ResultSetStats {
273 row_count: Some(RowCount::RowCountExact(5)),
274 ..Default::default()
275 };
276 let stats2 = ResultSetStats {
277 row_count: Some(RowCount::RowCountExact(10)),
278 ..Default::default()
279 };
280
281 let rs1 = ResultSet {
282 stats: Some(stats1),
283 ..Default::default()
284 };
285 let rs2 = ResultSet {
286 stats: Some(stats2),
287 ..Default::default()
288 };
289
290 let response = ExecuteBatchDmlResponse {
291 result_sets: vec![rs1, rs2],
292 status: None,
293 ..Default::default()
294 };
295
296 let counts = process_response(response)?;
297 assert_eq!(counts, vec![5, 10]);
298 Ok(())
299 }
300
301 #[test]
302 fn process_response_grpc_error() {
303 let stats = ResultSetStats {
304 row_count: Some(RowCount::RowCountExact(3)),
305 ..Default::default()
306 };
307 let rs = ResultSet {
308 stats: Some(stats),
309 ..Default::default()
310 };
311
312 let err_status = Status::default()
314 .set_code(Code::InvalidArgument as i32)
315 .set_message("Bad query");
316
317 let response = ExecuteBatchDmlResponse {
318 result_sets: vec![rs],
319 status: Some(err_status),
320 ..Default::default()
321 };
322
323 let result = process_response(response);
324 let err = result.expect_err("should return error");
325 let batch_err = BatchUpdateError::extract(&err).expect("should extract BatchUpdateError");
326
327 assert_eq!(batch_err.update_counts, vec![3]);
328 assert_eq!(
329 batch_err.status.status().expect("status").code,
330 Code::InvalidArgument
331 );
332 assert_eq!(
333 batch_err.status.status().expect("status").message,
334 "Bad query"
335 );
336 }
337
338 #[test]
339 fn process_response_aborted() {
340 let stats = ResultSetStats {
341 row_count: Some(RowCount::RowCountExact(3)),
342 ..Default::default()
343 };
344 let rs = ResultSet {
345 stats: Some(stats),
346 ..Default::default()
347 };
348
349 let err_status = Status::default()
350 .set_code(Code::Aborted as i32)
351 .set_message("transaction aborted");
352
353 let response = ExecuteBatchDmlResponse {
354 result_sets: vec![rs],
355 status: Some(err_status),
356 ..Default::default()
357 };
358
359 let result = process_response(response);
360 let err = result.expect_err("should return error");
361 let batch_err = BatchUpdateError::extract(&err);
362 assert!(
363 batch_err.is_none(),
364 "Unexpected BatchUpdateError: {batch_err:?}"
365 );
366 assert_eq!(err.status().expect("status").code, Code::Aborted);
367 assert_eq!(err.status().expect("status").message, "transaction aborted");
368 }
369
370 #[test]
371 fn process_response_missing_stats() {
372 let rs = ResultSet {
373 stats: None,
374 ..Default::default()
375 };
376
377 let response = ExecuteBatchDmlResponse {
378 result_sets: vec![rs],
379 ..Default::default()
380 };
381
382 let result = process_response(response).expect("should return empty update counts");
383 assert!(result.is_empty());
384 }
385
386 #[test]
387 fn process_response_missing_row_count_type() {
388 let stats = ResultSetStats {
389 row_count: None,
390 ..Default::default()
391 };
392
393 let rs = ResultSet {
394 stats: Some(stats),
395 ..Default::default()
396 };
397
398 let response = ExecuteBatchDmlResponse {
399 result_sets: vec![rs],
400 ..Default::default()
401 };
402
403 let result = process_response(response);
404 let err = result.expect_err("should fail");
405 assert!(
406 err.to_string()
407 .contains("invalid or missing row count type")
408 );
409 }
410
411 #[test]
412 fn from_vector_of_strings() {
413 let statements = vec!["UPDATE table SET col = 1", "UPDATE table SET col = 2"];
414 let batch: BatchDml = statements.into();
415 assert_eq!(batch.statements.len(), 2);
416 assert_eq!(batch.statements[0].sql, "UPDATE table SET col = 1");
417 assert_eq!(batch.statements[1].sql, "UPDATE table SET col = 2");
418 }
419
420 #[test]
421 fn from_vector_of_statements() {
422 let statement1 = Statement::builder("UPDATE table SET col = 1").build();
423 let statement2 = Statement::builder("UPDATE table SET col = 2").build();
424 let statements = vec![statement1, statement2];
425 let batch: BatchDml = statements.into();
426 assert_eq!(batch.statements.len(), 2);
427 assert_eq!(batch.statements[0].sql, "UPDATE table SET col = 1");
428 assert_eq!(batch.statements[1].sql, "UPDATE table SET col = 2");
429 }
430
431 #[test]
432 fn from_builder() {
433 let builder = BatchDml::builder().add_statement("UPDATE table SET col = 1");
434 let batch: BatchDml = builder.into();
435 assert_eq!(batch.statements.len(), 1);
436 assert_eq!(batch.statements[0].sql, "UPDATE table SET col = 1");
437 }
438
439 #[test]
440 fn process_response_metadata_no_stats_grpc_error() {
441 let rs = ResultSet {
442 metadata: Some(crate::model::ResultSetMetadata {
443 transaction: Some(crate::model::Transaction {
444 id: vec![7, 7, 7].into(),
445 ..Default::default()
446 }),
447 ..Default::default()
448 }),
449 stats: None,
450 ..Default::default()
451 };
452
453 let err_status = Status::default()
454 .set_code(Code::InvalidArgument as i32)
455 .set_message("Table not found or syntax invalid");
456
457 let response = ExecuteBatchDmlResponse {
458 result_sets: vec![rs],
459 status: Some(err_status),
460 ..Default::default()
461 };
462
463 let result = process_response(response);
464 let err = result.expect_err("should return error");
465 let batch_err = BatchUpdateError::extract(&err)
466 .expect("should extract BatchUpdateError cleanly and not return internal error");
467
468 assert_eq!(
469 batch_err.update_counts,
470 Vec::<i64>::new(),
471 "Update counts should be completely empty"
472 );
473 assert_eq!(
474 batch_err.status.status().expect("status").code,
475 Code::InvalidArgument
476 );
477 assert_eq!(
478 batch_err.status.status().expect("status").message,
479 "Table not found or syntax invalid"
480 );
481 }
482}