diff --git a/src/bigquery/src/query/query_handle.rs b/src/bigquery/src/query/query_handle.rs index 87445b9d64..fe1f5e2e7e 100644 --- a/src/bigquery/src/query/query_handle.rs +++ b/src/bigquery/src/query/query_handle.rs @@ -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, @@ -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]. /// /// [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; + /// + /// 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 @@ -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 { let Query { job_service, @@ -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, @@ -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 { let job_ref = self.job_ref.as_ref().ok_or(QueryError::StatelessQuery)?; diff --git a/src/bigquery/src/query/run_query.rs b/src/bigquery/src/query/run_query.rs index f2479e06b2..e7b01666f0 100644 --- a/src/bigquery/src/query/run_query.rs +++ b/src/bigquery/src/query/run_query.rs @@ -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, @@ -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>(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 { let project_id = self.project_id.ok_or(QueryError::MissingProjectId)?; let max_results = self.request.max_results;