Skip to main content

google_cloud_bigquery/
error.rs

1// Copyright 2026 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use google_cloud_bigquery_v2::model::ErrorProto;
16use google_cloud_gax::error::Error;
17
18/// Errors that can occur during query configuration, execution, or polling.
19#[derive(thiserror::Error, Debug)]
20#[non_exhaustive]
21pub enum QueryError {
22    /// Only query jobs are supported by this client.
23    #[error("only query jobs are supported")]
24    UnsupportedJobType,
25
26    /// Dry run queries cannot be converted to a complete query.
27    #[error("cannot convert dry run query to complete query")]
28    DryRun,
29
30    /// The query job failed on the BigQuery service side.
31    /// Includes the list of error protocols returned by the service.
32    #[non_exhaustive]
33    #[error("query job failed: {errors:?}")]
34    JobFailed {
35        /// The list of all errors associated with the job.
36        errors: Vec<ErrorProto>,
37    },
38
39    /// The underlying RPC failed.
40    #[non_exhaustive]
41    #[error("the operation failed. RPC error: {source}")]
42    Rpc {
43        /// The error returned by the service for the request.
44        #[from]
45        #[source]
46        source: Error,
47    },
48}
49
50/// Errors that can occur when retrieving value cells from a Row or iterating over query results.
51#[derive(thiserror::Error, Debug)]
52#[non_exhaustive]
53pub enum RowError {
54    /// The requested column name or index was not found in the row.
55    #[error("could not find column: {0}")]
56    ColumnNotFound(String),
57
58    /// The requested column index was out of range.
59    #[non_exhaustive]
60    #[error("column index out of range: {index} (expected < {len})")]
61    IndexOutOfRange {
62        /// The index that was requested.
63        index: usize,
64        /// The total number of columns in the row.
65        len: usize,
66    },
67
68    /// Failed to convert/parse the cell value to the target type.
69    #[error("type conversion error for column '{column}' (SQL type {sql_type}): {source}")]
70    #[non_exhaustive]
71    TypeConversion {
72        /// The column identifier (name or index).
73        column: String,
74        /// The BigQuery SQL type of the column.
75        sql_type: String,
76        /// The underlying parsing error.
77        #[source]
78        source: ConvertError,
79    },
80
81    /// The JSON format returned by the service did not match expectations.
82    #[error("internal service JSON layout is invalid: {0}")]
83    InvalidRowFormat(String),
84
85    /// The underlying RPC failed.
86    #[non_exhaustive]
87    #[error("the operation failed. RPC error: {source}")]
88    Rpc {
89        /// The error returned by the service for the request.
90        #[from]
91        #[source]
92        source: Error,
93    },
94}
95
96/// Represents failures when converting a BigQuery cell value to a Rust type.
97#[derive(thiserror::Error, Debug)]
98#[non_exhaustive]
99pub enum ConvertError {
100    /// The value type did not match the expected type.
101    #[error("type mismatch, expected {expected}, got {got}")]
102    #[non_exhaustive]
103    TypeMismatch {
104        /// The expected type name.
105        expected: String,
106        /// The actual type received.
107        got: String,
108    },
109
110    /// The value was null, but the target type does not support nulls (non-Option).
111    #[error("expected non-null value, got null")]
112    NotNull,
113
114    /// A required field or element was missing during SQL type conversion.
115    #[error("missing field: {0}")]
116    MissingField(String),
117
118    /// An error occurred during custom conversion (e.g. parsing date/time strings).
119    #[error("cannot convert value: {0}")]
120    Convert(
121        #[from]
122        #[source]
123        Box<dyn std::error::Error + Send + Sync + 'static>,
124    ),
125}
126
127impl ConvertError {
128    pub(crate) fn type_mismatch(
129        expected: impl Into<String>,
130        got: &crate::query::from_sql::SqlValueInner,
131    ) -> Self {
132        Self::TypeMismatch {
133            expected: expected.into(),
134            got: got.type_name().to_string(),
135        }
136    }
137}
138
139pub use crate::write::error::AppendError;
140pub use crate::write::error::CommitError;
141pub use crate::write::error::WriterBuilderError;
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use google_cloud_gax::error::rpc::{Code, Status};
147
148    #[test]
149    fn test_dry_run_display() {
150        let err = QueryError::DryRun;
151        assert_eq!(
152            err.to_string(),
153            "cannot convert dry run query to complete query"
154        );
155    }
156
157    #[test]
158    fn test_job_failed_display() {
159        let err = QueryError::JobFailed {
160            errors: vec![
161                ErrorProto::new()
162                    .set_reason("invalidQuery")
163                    .set_message("Syntax error: Unexpected end of input"),
164            ],
165        };
166        assert!(err.to_string().contains("query job failed:"));
167        assert!(err.to_string().contains("invalidQuery"));
168        assert!(
169            err.to_string()
170                .contains("Syntax error: Unexpected end of input")
171        );
172    }
173
174    #[test]
175    fn test_rpc_display() {
176        let status = Status::default()
177            .set_code(Code::InvalidArgument)
178            .set_message("simulated bad request");
179        let err = QueryError::Rpc {
180            source: Error::service(status),
181        };
182        assert_eq!(
183            err.to_string(),
184            "the operation failed. RPC error: the service reports an error with code INVALID_ARGUMENT described as: simulated bad request"
185        );
186    }
187
188    #[test]
189    fn test_row_error_display() {
190        let err = RowError::ColumnNotFound("name".to_string());
191        assert_eq!(err.to_string(), "could not find column: name");
192
193        let err = RowError::IndexOutOfRange { index: 5, len: 3 };
194        assert_eq!(
195            err.to_string(),
196            "column index out of range: 5 (expected < 3)"
197        );
198
199        let err = RowError::TypeConversion {
200            column: "age".to_string(),
201            sql_type: "INTEGER".to_string(),
202            source: ConvertError::NotNull,
203        };
204        assert_eq!(
205            err.to_string(),
206            "type conversion error for column 'age' (SQL type INTEGER): expected non-null value, got null"
207        );
208
209        let err = RowError::InvalidRowFormat("missing f field".to_string());
210        assert_eq!(
211            err.to_string(),
212            "internal service JSON layout is invalid: missing f field"
213        );
214
215        let status = Status::default()
216            .set_code(Code::Internal)
217            .set_message("internal error");
218        let err = RowError::Rpc {
219            source: Error::service(status),
220        };
221        assert!(err.to_string().contains("the operation failed. RPC error:"));
222    }
223
224    #[test]
225    fn test_convert_error_display() {
226        let val = crate::query::SqlValue::new(wkt::Value::String("hello".to_string()));
227        let err = ConvertError::type_mismatch("i64", &val.inner);
228        assert_eq!(err.to_string(), "type mismatch, expected i64, got string");
229
230        let err = ConvertError::NotNull;
231        assert_eq!(err.to_string(), "expected non-null value, got null");
232
233        let err = ConvertError::MissingField("custom_col".to_string());
234        assert_eq!(err.to_string(), "missing field: custom_col");
235
236        let inner_err: Box<dyn std::error::Error + Send + Sync> = "invalid integer".into();
237        let err = ConvertError::Convert(inner_err);
238        assert_eq!(err.to_string(), "cannot convert value: invalid integer");
239    }
240}