google_cloud_bigquery/query/
builder.rs1use crate::RequestOptions;
16use crate::error::QueryError;
17use crate::generated::QueryRequest;
18use crate::query::execution::RetryContext;
19use crate::query::retry_policy::{JobRetryPolicy, default_job_retry_policy};
20use crate::query::{CompleteQuery, Query as QueryHandle, Result};
21use google_cloud_bigquery_v2::client::JobService;
22use google_cloud_bigquery_v2::model::JobReference;
23use google_cloud_bigquery_v2::model::query_request::JobCreationMode;
24use std::sync::Arc;
25use uuid::Uuid;
26
27pub(crate) const JOB_ID_PREFIX: &str = "job_";
28pub(crate) const QUERY_REQUEST_ID_PREFIX: &str = "req_";
29
30#[derive(Clone, Debug)]
59pub struct Query {
60 pub(crate) job_service: Arc<JobService>,
61 pub(crate) request: QueryRequest,
62 pub(crate) project_id: Option<String>,
63 pub(crate) job_retry_policy: Arc<dyn JobRetryPolicy>,
64 pub(crate) options: RequestOptions,
65}
66
67impl Query {
68 pub(crate) fn new(job_service: Arc<JobService>, sql: String) -> Self {
70 Self {
71 job_service,
72 request: QueryRequest::default()
73 .set_query(sql)
74 .set_use_legacy_sql(wkt::BoolValue::from(false))
75 .set_job_creation_mode(JobCreationMode::JobCreationOptional),
76 project_id: None,
77 job_retry_policy: default_job_retry_policy(),
78 options: RequestOptions::default(),
79 }
80 }
81
82 pub fn with_project_id<S: Into<String>>(mut self, project_id: S) -> Self {
108 let project_id = project_id.into();
109 self.project_id = Some(project_id.clone());
110 self.options.set_quota_project(project_id);
111 self
112 }
113
114 pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
130 self.options.set_user_agent(user_agent);
131 self
132 }
133
134 pub async fn send(self) -> Result<QueryHandle> {
168 Box::pin(RetryContext::new(self).execute()).await
170 }
171
172 pub async fn until_done(self) -> Result<CompleteQuery> {
203 if self.request.dry_run {
204 return Err(QueryError::DryRun);
205 }
206 Box::pin(async move { self.send().await?.until_done().await }).await
208 }
209}
210
211pub(crate) fn generate_job_reference(project_id: &str, location: &str) -> JobReference {
216 let job_id = generate_prefixed_id(JOB_ID_PREFIX);
217 let mut job_ref = JobReference::new()
218 .set_project_id(project_id.to_string())
219 .set_job_id(job_id);
220
221 if !location.is_empty() {
222 job_ref = job_ref.set_location(location.to_string());
223 }
224 job_ref
225}
226
227pub(crate) fn generate_prefixed_id(prefix: &str) -> String {
236 format!("{prefix}{}", Uuid::new_v4().simple())
237}
238
239include!("../generated/builder.rs");
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::client::BigQuery;
245 use crate::error::QueryError;
246 use crate::query::tests::{MockJobService, create_job_service};
247 use google_cloud_auth::credentials::anonymous::Builder as Anonymous;
248 use google_cloud_bigquery_v2::model::query_request::JobCreationMode;
249 use google_cloud_bigquery_v2::model::{
250 ErrorProto, Job, JobConfiguration, JobReference, JobStatus,
251 QueryRequest as JobsQueryRequest, QueryResponse,
252 };
253 use google_cloud_gax::error::Error as GaxError;
254 use google_cloud_gax::error::rpc::Status;
255 use google_cloud_gax::response::Response;
256
257 const BIGQUERY_REQ_ID_LIMIT: usize = 36;
259
260 type TestResult = anyhow::Result<()>;
261
262 #[test]
263 fn test_new() {
264 let job_service = create_job_service(MockJobService::new());
265 let sql = "SELECT 1".to_string();
266 let query_builder = Query::new(job_service, sql.clone());
267 assert_eq!(query_builder.request.query, sql);
268 assert_eq!(
269 query_builder.request.use_legacy_sql,
270 Some(wkt::BoolValue::from(false))
271 );
272 assert_eq!(
273 query_builder.request.job_creation_mode,
274 JobCreationMode::JobCreationOptional
275 );
276 assert_eq!(query_builder.project_id, None);
277 assert_eq!(query_builder.options.quota_project(), &None);
278 assert_eq!(query_builder.options.user_agent(), &None);
279 }
280
281 #[test]
282 fn test_with_project_id() {
283 let job_service = create_job_service(MockJobService::new());
284 let query_builder =
285 Query::new(job_service, "SELECT 1".to_string()).with_project_id("my-project");
286 assert_eq!(query_builder.project_id.unwrap(), "my-project");
287 assert_eq!(
288 query_builder.options.quota_project().as_deref(),
289 Some("my-project")
290 );
291 }
292
293 #[test]
294 fn test_with_user_agent() {
295 let job_service = create_job_service(MockJobService::new());
296 let query_builder =
297 Query::new(job_service, "SELECT 1".to_string()).with_user_agent("my-app/1.2.3");
298 assert_eq!(
299 query_builder.options.user_agent().as_deref(),
300 Some("my-app/1.2.3")
301 );
302 }
303
304 #[tokio::test]
305 async fn test_run_missing_project_id() -> anyhow::Result<()> {
306 let client = BigQuery::builder()
307 .with_credentials(Anonymous::new().build())
308 .build()
309 .await?;
310 let query_builder = client.query("SELECT 1");
311 let err = query_builder
312 .send()
313 .await
314 .expect_err("should return an error when project_id is missing");
315 assert!(
316 matches!(&err, QueryError::Rpc { source } if source.is_binding()),
317 "expected Binding error for missing project ID, got {err:?}"
318 );
319 Ok(())
320 }
321
322 #[tokio::test]
323 async fn test_run_missing_project_id_force_job_path() -> anyhow::Result<()> {
324 let client = BigQuery::builder()
325 .with_credentials(Anonymous::new().build())
326 .build()
327 .await?;
328 let query_builder = client.query("SELECT 1").set_allow_large_results(true);
329 let err = query_builder
330 .send()
331 .await
332 .expect_err("should return an error when project_id is missing");
333 assert!(
334 matches!(&err, QueryError::Rpc { source } if source.is_binding()),
335 "expected Binding error for missing project ID on job path, got {err:?}"
336 );
337 Ok(())
338 }
339
340 #[tokio::test]
341 async fn test_run_until_done_missing_project_id() -> anyhow::Result<()> {
342 let client = BigQuery::builder()
343 .with_credentials(Anonymous::new().build())
344 .build()
345 .await?;
346 let query_builder = client.query("SELECT 1");
347 let err = query_builder
348 .until_done()
349 .await
350 .expect_err("should return an error when project_id is missing");
351 assert!(
352 matches!(&err, QueryError::Rpc { source } if source.is_binding()),
353 "expected Binding error for missing project ID, got {err:?}"
354 );
355 Ok(())
356 }
357
358 #[test]
359 fn test_generate_prefixed_id() {
360 let job_id = generate_prefixed_id(JOB_ID_PREFIX);
361 assert!(job_id.starts_with(JOB_ID_PREFIX), "{job_id:?}");
362 assert!(
363 Uuid::parse_str(&job_id[JOB_ID_PREFIX.len()..]).is_ok(),
364 "{job_id:?}"
365 );
366
367 let req_id = generate_prefixed_id(QUERY_REQUEST_ID_PREFIX);
368 assert!(req_id.starts_with(QUERY_REQUEST_ID_PREFIX), "{req_id:?}");
369 assert!(req_id.len() <= BIGQUERY_REQ_ID_LIMIT, "{req_id:?}");
370 assert!(
371 Uuid::parse_str(&req_id[QUERY_REQUEST_ID_PREFIX.len()..]).is_ok(),
372 "{req_id:?}"
373 );
374 }
375
376 #[test]
377 fn test_generate_job_reference() {
378 let job_ref = generate_job_reference("my-project", "us-central1");
379 assert_eq!(job_ref.project_id, "my-project");
380 assert!(job_ref.job_id.starts_with(JOB_ID_PREFIX), "{job_ref:?}");
381 assert_eq!(job_ref.location.as_deref(), Some("us-central1"));
382 }
383
384 #[tokio::test]
385 async fn test_run_jobs_insert() -> TestResult {
386 let mut mock = MockJobService::new();
387 mock.expect_insert_job().returning(|req, options| {
388 assert_eq!(options.quota_project().as_deref(), Some("my-project"));
389 let job_ref = req.job.as_ref().unwrap().job_reference.as_ref().unwrap();
390 assert!(job_ref.job_id.starts_with(JOB_ID_PREFIX), "{job_ref:?}");
391 let job_ref = JobReference::new()
392 .set_job_id("test-job")
393 .set_project_id("my-project");
394 let job = Job::new()
395 .set_job_reference(job_ref)
396 .set_status(JobStatus::new().set_state("DONE"));
397 Ok(google_cloud_gax::response::Response::from(job))
398 });
399 mock.expect_query().never();
400
401 let job_service = create_job_service(mock);
402
403 let query_builder = Query::new(job_service, "SELECT 1".to_string())
404 .with_project_id("my-project")
405 .set_allow_large_results(true);
406 let query = query_builder.send().await?;
407 assert!(query.completed, "{query:?}");
408 assert_eq!(query.options.quota_project().as_deref(), Some("my-project"));
409
410 Ok(())
411 }
412
413 #[tokio::test]
414 async fn test_run_jobs_query() -> TestResult {
415 let mut mock = MockJobService::new();
416 mock.expect_query().returning(move |req, options| {
417 assert_eq!(options.quota_project().as_deref(), Some("my-project"));
418 let req_id = &req.query_request.as_ref().unwrap().request_id;
419 assert!(req_id.starts_with(QUERY_REQUEST_ID_PREFIX), "{req_id:?}");
420 assert!(req_id.len() <= BIGQUERY_REQ_ID_LIMIT, "{req_id:?}");
421 Ok(Response::from(
422 QueryResponse::new().set_query_id("some_query_id"),
423 ))
424 });
425 mock.expect_insert_job().never();
426
427 let job_service = create_job_service(mock);
428 let query_builder =
429 Query::new(job_service, "SELECT 1".to_string()).with_project_id("my-project");
430 let query = query_builder.send().await?;
431 assert!(!query.completed, "{query:?}");
432 assert_eq!(query.metadata.query_id, "some_query_id");
433 assert_eq!(query.options.quota_project().as_deref(), Some("my-project"));
434
435 Ok(())
436 }
437
438 #[test]
439 fn test_force_job_path() {
440 let job_service = create_job_service(MockJobService::new());
441 let mut query_builder = Query::new(job_service, "SELECT 1".to_string());
442 assert!(!query_builder.request.force_job_path());
443
444 query_builder = query_builder.set_allow_large_results(true);
446 assert!(query_builder.request.force_job_path());
447 }
448
449 #[test]
450 fn test_request_conversions() {
451 let req = QueryRequest::default()
452 .set_query("SELECT 1".to_string())
453 .set_dry_run(true)
454 .set_use_legacy_sql(true);
455
456 let query_request: JobsQueryRequest = req.clone().into();
457 assert_eq!(query_request.query, "SELECT 1");
458 assert!(query_request.dry_run);
459 assert_eq!(
460 query_request.use_legacy_sql,
461 Some(wkt::BoolValue::from(true))
462 );
463
464 let job_config: JobConfiguration = req.into();
465 let job_query = job_config.query.as_ref().unwrap();
466 assert_eq!(job_query.query, "SELECT 1");
467 assert_eq!(job_query.use_legacy_sql, Some(wkt::BoolValue::from(true)));
468 }
469
470 #[tokio::test(start_paused = true)]
471 async fn test_run_reissue_on_retryable_job_failed() -> TestResult {
472 let mut mock = MockJobService::new();
473 let mut seq = mockall::Sequence::new();
474 let first_request_id = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
475 let first_request_id_clone = first_request_id.clone();
476
477 mock.expect_query()
478 .in_sequence(&mut seq)
479 .times(1)
480 .returning(move |req, _| {
481 let req_id = req.query_request.as_ref().unwrap().request_id.clone();
482 assert!(req_id.starts_with(QUERY_REQUEST_ID_PREFIX));
483 *first_request_id_clone.lock().unwrap() = req_id;
484 let err_proto = ErrorProto::new()
485 .set_reason("backendError")
486 .set_message("temporary server issue");
487 Ok(Response::from(
488 QueryResponse::new().set_errors(vec![err_proto]),
489 ))
490 });
491
492 mock.expect_query()
493 .in_sequence(&mut seq)
494 .times(1)
495 .returning(move |req, _| {
496 let req_id = req.query_request.as_ref().unwrap().request_id.clone();
497 assert!(req_id.starts_with(QUERY_REQUEST_ID_PREFIX));
498 assert_ne!(
499 req_id,
500 *first_request_id.lock().unwrap(),
501 "reissued query must generate a fresh request_id to avoid 409 duplicate ID conflicts"
502 );
503 Ok(Response::from(
504 QueryResponse::new().set_query_id("q_success"),
505 ))
506 });
507
508 let job_service = create_job_service(mock);
509 let query_builder =
510 Query::new(job_service, "SELECT 1".to_string()).with_project_id("my-project");
511 let query = query_builder.send().await?;
512 assert_eq!(query.metadata.query_id, "q_success");
513
514 Ok(())
515 }
516
517 #[tokio::test(start_paused = true)]
518 async fn test_run_reissue_on_retryable_rpc_error() -> TestResult {
519 let mut mock = MockJobService::new();
520 let mut seq = mockall::Sequence::new();
521 let first_request_id = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
522 let first_request_id_clone = first_request_id.clone();
523
524 mock.expect_query()
525 .in_sequence(&mut seq)
526 .times(1)
527 .returning(move |req, _| {
528 let req_id = req.query_request.as_ref().unwrap().request_id.clone();
529 assert!(req_id.starts_with(QUERY_REQUEST_ID_PREFIX));
530 *first_request_id_clone.lock().unwrap() = req_id;
531 const BQ_REST_PAYLOAD: &[u8] = br#"{
532 "error": {
533 "code": 400,
534 "message": "The job encountered an error during execution. Retrying the job may solve the problem.",
535 "errors": [
536 {
537 "message": "The job encountered an error during execution. Retrying the job may solve the problem.",
538 "domain": "global",
539 "reason": "backendError"
540 }
541 ],
542 "status": "INVALID_ARGUMENT"
543 }
544}"#;
545 let status = Status::try_from(&bytes::Bytes::from_static(BQ_REST_PAYLOAD)).unwrap();
546 Err(GaxError::service(status))
547 });
548
549 mock.expect_query()
550 .in_sequence(&mut seq)
551 .times(1)
552 .returning(move |req, _| {
553 let req_id = req.query_request.as_ref().unwrap().request_id.clone();
554 assert!(req_id.starts_with(QUERY_REQUEST_ID_PREFIX));
555 assert_ne!(
556 req_id,
557 *first_request_id.lock().unwrap(),
558 "reissued query must generate a fresh request_id to avoid 409 duplicate ID conflicts"
559 );
560 Ok(Response::from(
561 QueryResponse::new().set_query_id("q_success"),
562 ))
563 });
564
565 let job_service = create_job_service(mock);
566 let query_builder =
567 Query::new(job_service, "SELECT 1".to_string()).with_project_id("my-project");
568 let query = query_builder.send().await?;
569 assert_eq!(query.metadata.query_id, "q_success");
570
571 Ok(())
572 }
573
574 #[tokio::test]
575 async fn test_run_jobs_query_with_page_size() -> TestResult {
576 let mut mock = MockJobService::new();
577 mock.expect_query().returning(move |req, _| {
578 assert_eq!(
579 req.query_request.as_ref().and_then(|r| r.max_results),
580 Some(100)
581 );
582 Ok(Response::from(QueryResponse::new()))
583 });
584 let job_service = create_job_service(mock);
585 let query_builder = Query::new(job_service, "SELECT 1".to_string())
586 .with_project_id("my-project")
587 .set_page_size(100_u32);
588 let query = query_builder.send().await?;
589 assert_eq!(query.page_size, Some(100));
590
591 Ok(())
592 }
593
594 #[tokio::test]
595 async fn test_run_jobs_insert_with_page_size() -> TestResult {
596 let mut mock = MockJobService::new();
597 mock.expect_insert_job().returning(|_, _| {
598 let job_ref = JobReference::new()
599 .set_job_id("test-job")
600 .set_project_id("my-project");
601 let job = Job::new()
602 .set_job_reference(job_ref)
603 .set_status(JobStatus::new().set_state("DONE"));
604 Ok(google_cloud_gax::response::Response::from(job))
605 });
606 let job_service = create_job_service(mock);
607
608 let query_builder = Query::new(job_service, "SELECT 1".to_string())
609 .with_project_id("my-project")
610 .set_allow_large_results(true)
611 .set_page_size(50_u32);
612 let query = query_builder.send().await?;
613 assert_eq!(query.page_size, Some(50));
614
615 Ok(())
616 }
617
618 #[tokio::test]
619 async fn test_until_done_jobs_query() -> TestResult {
620 let mut mock = MockJobService::new();
621 mock.expect_query().returning(move |_, options| {
622 assert_eq!(options.quota_project().as_deref(), Some("my-project"));
623 Ok(Response::from(
624 QueryResponse::new()
625 .set_job_complete(true)
626 .set_query_id("some_query_id"),
627 ))
628 });
629 let job_service = create_job_service(mock);
630 let query = Query::new(job_service, "SELECT 1".to_string()).with_project_id("my-project");
631 let complete = query.until_done().await?;
632 assert_eq!(complete.metadata().query_id, "some_query_id");
633 assert_eq!(
634 complete.options.quota_project().as_deref(),
635 Some("my-project")
636 );
637
638 Ok(())
639 }
640
641 #[tokio::test]
642 async fn test_until_done_dry_run_returns_error() -> TestResult {
643 let mut mock = MockJobService::new();
644 mock.expect_insert_job().never();
645 mock.expect_query().never();
646 let job_service = create_job_service(mock);
647 let query = Query::new(job_service, "SELECT 1".to_string())
648 .with_project_id("my-project")
649 .set_dry_run(true);
650 let err = query.until_done().await.unwrap_err();
651 assert!(
652 matches!(err, QueryError::DryRun),
653 "expected DryRun error, got {err:?}"
654 );
655
656 Ok(())
657 }
658
659 #[tokio::test]
660 async fn test_run_jobs_query_with_user_agent() -> TestResult {
661 let mut mock = MockJobService::new();
662 mock.expect_query().returning(move |_, options| {
663 assert_eq!(options.user_agent().as_deref(), Some("my-app/1.2.3"));
664 Ok(Response::from(QueryResponse::new()))
665 });
666 let job_service = create_job_service(mock);
667 let query_builder = Query::new(job_service, "SELECT 1".to_string())
668 .with_project_id("my-project")
669 .with_user_agent("my-app/1.2.3");
670 let query = query_builder.send().await?;
671 assert_eq!(query.options.user_agent().as_deref(), Some("my-app/1.2.3"));
672
673 Ok(())
674 }
675
676 #[tokio::test]
677 async fn test_run_jobs_insert_with_user_agent() -> TestResult {
678 let mut mock = MockJobService::new();
679 mock.expect_insert_job().returning(|_, options| {
680 assert_eq!(options.user_agent().as_deref(), Some("my-app/1.2.3"));
681 let job_ref = JobReference::new()
682 .set_job_id("test-job")
683 .set_project_id("my-project");
684 let job = Job::new()
685 .set_job_reference(job_ref)
686 .set_status(JobStatus::new().set_state("DONE"));
687 Ok(Response::from(job))
688 });
689 let job_service = create_job_service(mock);
690
691 let query_builder = Query::new(job_service, "SELECT 1".to_string())
692 .with_project_id("my-project")
693 .set_allow_large_results(true)
694 .with_user_agent("my-app/1.2.3");
695 let query = query_builder.send().await?;
696 assert_eq!(query.options.user_agent().as_deref(), Some("my-app/1.2.3"));
697
698 Ok(())
699 }
700}