Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 193 additions & 12 deletions src/bigquery/src/query/query_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,33 @@ use google_cloud_gax::polling_state::PollingState;
use std::collections::VecDeque;
use std::sync::Arc;

/// A handle representing a running query.
/// A handle representing a running or completed SQL query execution.
///
/// An instance of `Query` is returned by [`RunQuery::run()`](crate::query::RunQuery::run).
/// Depending on how the query was routed and executed, this handle may represent an asynchronous
/// background job currently executing on BigQuery, or a fast-path query that has already completed.
///
/// To obtain the final result set, call [`until_done()`](Query::until_done), which will check the
/// execution status and automatically poll the service if the job is still running in the background.
///
/// # Example
///
/// ```
/// # async fn sample() -> anyhow::Result<()> {
/// use google_cloud_bigquery::client::BigQuery;
///
/// let client = BigQuery::builder().build().await?;
/// let query_handle = client
/// .query("SELECT 42 AS answer")
/// .with_project_id("my-project-id")
/// .run()
/// .await?;
///
/// // Poll until execution completes.
/// let completed = query_handle.until_done().await?;
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Debug)]
pub struct Query {
pub(crate) job_service: Arc<JobService>,
Expand All @@ -39,13 +65,37 @@ pub struct Query {
}

impl Query {
/// Returns the [`QueryReference`] for this query.
/// Returns the [`QueryReference`](crate::model::QueryReference) identifying this query execution.
///
/// The reference will be [`QueryReference::Job`] with a query [job reference],
/// or [`QueryReference::Stateless`] with an opaque query ID if job creation
/// was skipped.
/// The reference will be [`QueryReference::Job`](crate::model::QueryReference::Job) containing a BigQuery Query [job reference]
/// if a job was created, or [`QueryReference::Stateless`](crate::model::QueryReference::Stateless) with an opaque
/// query ID if the execution ran statelessly via [jobs.query].
Comment on lines +68 to +72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The QueryReference type is imported from crate::query::QueryReference (defined in src/bigquery/src/query/query_reference.rs), not crate::model::QueryReference. Using the incorrect path will result in broken intra-doc links and rustdoc warnings, which can fail the CI build.

Suggested change
/// Returns the [`QueryReference`](crate::model::QueryReference) identifying this query execution.
///
/// The reference will be [`QueryReference::Job`] with a query [job reference],
/// or [`QueryReference::Stateless`] with an opaque query ID if job creation
/// was skipped.
/// The reference will be [`QueryReference::Job`](crate::model::QueryReference::Job) containing a BigQuery Query [job reference]
/// if a job was created, or [`QueryReference::Stateless`](crate::model::QueryReference::Stateless) with an opaque
/// query ID if the execution ran statelessly via [jobs.query].
/// Returns the [`QueryReference`](crate::query::QueryReference) identifying this query execution.
///
/// The reference will be [`QueryReference::Job`](crate::query::QueryReference::Job) containing a BigQuery Query [job reference]
/// if a job was created, or [`QueryReference::Stateless`](crate::query::QueryReference::Stateless) with an opaque
/// query ID if the execution ran statelessly via [jobs.query].
References
  1. Always verify that Rust documentation compiles without warnings (e.g., by running cargo doc) before merging, because rustdoc warnings (such as bare URLs or unresolved links) will fail CI builds that enforce -D warnings.

///
/// [job reference]: https://docs.cloud.google.com/bigquery/docs/reference/rest/v2/JobReference
/// [jobs.query]: https://docs.cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query
///
/// # Example
///
/// ```
/// # async fn sample() -> anyhow::Result<()> {
/// use google_cloud_bigquery::client::BigQuery;
/// use google_cloud_bigquery::model::QueryReference;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the import path in the example to use query instead of model to match where QueryReference is exported.

Suggested change
/// use google_cloud_bigquery::model::QueryReference;
/// use google_cloud_bigquery::query::QueryReference;

///
/// let client = BigQuery::builder().build().await?;
/// let query_handle = client
/// .query("SELECT 'hello' AS msg")
/// .with_project_id("my-project-id")
/// .run()
/// .await?;
///
/// match query_handle.query_reference() {
/// QueryReference::Job(job_ref) => println!("Running job ID: {}", job_ref.job_id),
/// QueryReference::Stateless { query_id } => println!("Stateless query ID: {query_id}"),
/// _ => println!("Other query reference"),
/// }
/// # Ok(())
/// # }
/// ```
pub fn query_reference(&self) -> QueryReference {
let from_query_id = self
.initial_response
Expand All @@ -60,8 +110,34 @@ impl Query {
.expect("query must have either a job reference or query id")
}

/// Periodically checks the status of the background job until it finishes.
/// Returns an error if a remote service or connection failure happens during polling.
/// Periodically polls the background job status until query execution finishes.
///
/// If the query was executed via the fast path and already completed during the initial request,
/// this method immediately returns a [`CompleteQuery`](crate::query::CompleteQuery) without making additional network calls.
/// Otherwise, it implements a backoff loop querying the job status until it succeeds or fails.
///
/// # Errors
///
/// Returns an error if a remote service or network failure happens during polling, or if the BigQuery job
/// fails due to runtime execution errors.
///
/// # Example
///
/// ```
/// # async fn sample() -> anyhow::Result<()> {
/// use google_cloud_bigquery::client::BigQuery;
///
/// let client = BigQuery::builder().build().await?;
/// let complete = client
/// .query("SELECT 1 + 1 AS result")
/// .with_project_id("my-project-id")
/// .run()
/// .await?
/// .until_done()
/// .await?;
/// # Ok(())
/// # }
/// ```
pub async fn until_done(self) -> Result<CompleteQuery> {
let Query {
job_service,
Expand Down Expand Up @@ -100,7 +176,32 @@ impl Query {
}
}

/// A handle representing a successfully completed query ready for reading.
/// A handle representing a successfully completed query ready for reading results.
///
/// An instance of `CompleteQuery` is returned by [`Query::until_done()`](crate::query::Query::until_done).
///
/// This handle provides access to cached execution metadata, schema definitions, and a
/// row iterator via [`read()`](CompleteQuery::read).
///
/// # Example
///
/// ```
/// # async fn sample() -> anyhow::Result<()> {
/// use google_cloud_bigquery::client::BigQuery;
///
/// let client = BigQuery::builder().build().await?;
/// let complete = client
/// .query("SELECT 'done' AS status")
/// .with_project_id("my-project-id")
/// .run()
/// .await?
/// .until_done()
/// .await?;
///
/// println!("Cache hit: {:?}", complete.metadata().cache_hit);
/// # Ok(())
/// # }
/// ```
#[derive(Clone)]
pub struct CompleteQuery {
pub(crate) job_service: Arc<JobService>,
Expand Down Expand Up @@ -178,19 +279,99 @@ impl CompleteQuery {
}
}

/// Returns a row iterator for the query result.
/// Returns a row iterator for reading the query result set.
///
/// Consumes the `CompleteQuery` handle and initializes a [`RowIterator`](crate::query::RowIterator) that
/// iterates over initial cached rows in memory before automatically fetching subsequent pages from the API.
///
/// # Example
///
/// ```
/// # async fn sample() -> anyhow::Result<()> {
/// use google_cloud_bigquery::client::BigQuery;
///
/// let client = BigQuery::builder().build().await?;
/// let mut rows = client
/// .query("SELECT 100 AS score")
/// .with_project_id("my-project-id")
/// .run()
/// .await?
/// .until_done()
/// .await?
/// .read();
///
/// while let Some(row) = rows.next().await.transpose()? {
/// let score: i64 = row.get("score");
/// println!("Score: {score}");
/// }
/// # Ok(())
/// # }
/// ```
pub fn read(self) -> RowIterator {
RowIterator::new(self)
}

/// Returns the cached metadata for this query.
/// Returns a reference to the cached summary metadata for this query.
///
/// The returned [`QueryMetadata`](crate::model::QueryMetadata) contains useful summary statistics such as
/// total rows, schema details, cache hit indicators, and estimated bytes processed without making additional RPC calls.
///
/// # Example
///
/// ```
/// # async fn sample() -> anyhow::Result<()> {
/// use google_cloud_bigquery::client::BigQuery;
///
/// let client = BigQuery::builder().build().await?;
/// let completed = client
/// .query("SELECT 'metadata_check'")
/// .with_project_id("my-project-id")
/// .run()
/// .await?
/// .until_done()
/// .await?;
///
/// let meta = completed.metadata();
/// println!("Total rows: {:?}", meta.total_rows);
/// # Ok(())
/// # }
/// ```
pub fn metadata(&self) -> &QueryMetadata {
&self.metadata
}

/// Fetches the full `Job` information for the given query.
/// Fetches full [Job] execution metadata from the service for this query.
///
/// Unlike [`metadata()`](CompleteQuery::metadata), this method executes an RPC request (`jobs.get`) to
/// retrieve complete job details, including user email, execution timelines, billing tier estimates, and detailed error summaries.
///
/// > [!IMPORTANT]
/// > Calling this method on a stateless query will return [`QueryError::StatelessQuery`](crate::error::QueryError::StatelessQuery).
///
/// [Job]: https://docs.cloud.google.com/bigquery/docs/reference/rest/v2/Job
///
/// # Example
///
/// ```
/// # async fn sample() -> anyhow::Result<()> {
/// use google_cloud_bigquery::client::BigQuery;
/// use google_cloud_bigquery::model::query_request::JobCreationMode;
///
/// let client = BigQuery::builder().build().await?;
/// let completed = client
/// .query("SELECT 1")
/// .with_project_id("my-project-id")
/// .set_job_creation_mode(JobCreationMode::JobCreationRequired) // Forces a job to be created
/// .run()
/// .await?
/// .until_done()
/// .await?;
///
/// Stateless queries will return `QueryError::StatelessQuery`.
/// let job_info = completed.job_metadata().await?;
/// println!("Executed by user: {}", job_info.user_email);
/// # Ok(())
/// # }
/// ```
pub async fn job_metadata(&self) -> Result<Job> {
let job_ref = self.job_ref.as_ref().ok_or(QueryError::StatelessQuery)?;

Expand Down
119 changes: 108 additions & 11 deletions src/bigquery/src/query/run_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,60 @@ use google_cloud_bigquery_v2::model::{
};
use std::sync::Arc;

/// A unified request builder for configuring and running a SQL query.
/// It automatically routes to either `jobs.query` (fast path) or `jobs.insert` (job path)
/// depending on the configured fields.
/// A unified request builder for configuring and executing a SQL query.
///
/// Instances of this struct are returned by [`BigQuery::query()`](crate::client::BigQuery::query).
///
/// This builder allows you to chain configuration methods to define query parameters, set dataset defaults,
/// specify locations, and configure result limitations before initiating execution with [`run()`](RunQuery::run).
///
/// # Automatic Path Routing
///
/// The builder automatically decides whether to execute via [`jobs.query`](https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query)
/// (the low-latency fast path) or [`jobs.insert`](https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/insert)
/// (the async job creation path) depending on which configuration options are enabled:
///
/// - **Fast path (`jobs.query`)**: Taken by default when executing queries with standard parameters and limits.
/// - **Job path (`jobs.insert`)**: Chosen if options require job creation (such as setting a destination table, enabling large result allowances, or customizing job labels).
///
/// # Common Configuration Methods
///
/// In addition to setting the target GCP project via [`with_project_id()`](RunQuery::with_project_id),
/// this builder inherits generated configuration setter methods including:
/// - `set_job_creation_mode(JobCreationMode::JobCreationRequired)`: Explicitly forces job creation. By default, the SDK sets this as JobCreationOptional.
/// - `set_location("US")`: Sets the geographic routing location where the job should run.
/// - `set_max_results(100)`: Limits the number of rows buffered per result page from the API.
/// - `set_use_cache(true)`: Enables or disables query result caching (enabled by default).
/// - `set_dry_run(true)`: Validates the SQL syntax and calculates bytes processed without executing the query or incurring billing.
/// - `set_parameter_mode("NAMED")` and `set_query_parameters(...)`: Configures parameterized queries to prevent SQL injection and reuse execution plans.
///
/// # Example
///
/// ```
/// # async fn sample() -> anyhow::Result<()> {
/// use google_cloud_bigquery::client::BigQuery;
///
/// let client = BigQuery::builder().build().await?;
///
/// // Configure and run a simple query with a custom geographic location and result limit.
/// let mut rows = client
/// .query("SELECT name FROM `bigquery-public-data.usa_names.usa_1910_2013` WHERE state = 'TX' LIMIT 100")
/// .with_project_id("my-project-id")
/// .set_location("US")
/// .set_max_results(50_u32)
/// .run()
/// .await?
/// .until_done()
/// .await?
/// .read();
///
/// while let Some(row) = rows.next().await.transpose()? {
/// let name: String = row.get("name");
/// println!("Name: {name}");
/// }
/// # Ok(())
/// # }
/// ```
#[derive(Clone)]
pub struct RunQuery {
pub(crate) job_service: Arc<JobService>,
Expand All @@ -46,21 +97,67 @@ impl RunQuery {
}
}

/// Sets the project ID to override the default client project ID.
/// Sets the target Google Cloud Project ID for query execution and billing.
///
/// This parameter is required before initiating execution with [`run()`](RunQuery::run).
/// If omitted, calling `run()` will return [`QueryError::MissingProjectId`](crate::error::QueryError::MissingProjectId).
///
/// # Example
///
/// ```
/// # async fn sample() -> anyhow::Result<()> {
/// use google_cloud_bigquery::client::BigQuery;
///
/// let client = BigQuery::builder().build().await?;
/// let query_handle = client
/// .query("SELECT 1 AS count")
/// .with_project_id("my-project-id")
/// .run()
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn with_project_id<S: Into<String>>(mut self, project_id: S) -> Self {
self.project_id = Some(project_id.into());
self
}

/// Executes the SQL query
/// Submits the configured SQL query for execution.
///
/// This is the terminal method of the [`RunQuery`] builder. Upon success, it returns a [`Query`](crate::query::Query)
/// handle representing either a running background job or a fast-path execution that has already finished.
/// You can call [`until_done()`](crate::query::Query::until_done) on the returned handle to wait for the final results.
///
/// # Errors
///
/// Returns [`QueryError::MissingProjectId`](crate::error::QueryError::MissingProjectId) if [`with_project_id()`](RunQuery::with_project_id)
/// was not called prior to running. Returns an RPC error if the initial service communication fails or if syntax errors occur during immediate fast-path validation.
///
/// # Example
///
/// ```
/// # async fn sample() -> anyhow::Result<()> {
/// use google_cloud_bigquery::client::BigQuery;
///
/// let client = BigQuery::builder().build().await?;
///
/// The implementation routes internally to [jobs.query] (fast path)
/// or [jobs.insert] (job path) depending on configured fields.
/// If the fast path is available, the client library takes it.
/// If not, it falls back to creating a job, which is typically slower.
/// // Execute the query and poll until complete.
/// let completed_query = client
/// .query("SELECT CURRENT_TIMESTAMP() AS now")
/// .with_project_id("my-project-id")
/// .run()
/// .await?
/// .until_done()
/// .await?;
///
/// [jobs.query]: https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/query
/// [jobs.insert]: https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/insert
/// let mut rows = completed_query.read();
/// if let Some(row) = rows.next().await.transpose()? {
/// let now: String = row.get("now");
/// println!("Current time: {now}");
/// }
/// # Ok(())
/// # }
/// ```
pub async fn run(self) -> Result<Query> {
let project_id = self.project_id.ok_or(QueryError::MissingProjectId)?;
let max_results = self.request.max_results;
Expand Down
Loading