Skip to main content

google_cloud_bigquery/query/
from_sql.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 base64::Engine;
16use base64::prelude::BASE64_STANDARD;
17#[allow(unused_imports)]
18use wkt::{Struct, Timestamp, Value};
19
20use crate::error::ConvertError;
21
22pub(crate) const BIGQUERY_DATE_FORMAT: &[time::format_description::FormatItem<'static>] =
23    time::macros::format_description!("[year]-[month]-[day]");
24pub(crate) const BIGQUERY_TIME_FORMAT: &[time::format_description::FormatItem<'static>] = time::macros::format_description!(
25    "[hour padding:none]:[minute padding:none]:[second padding:none]"
26);
27pub(crate) const BIGQUERY_TIME_SUBSEC_FORMAT: &[time::format_description::FormatItem<'static>] = time::macros::format_description!(
28    "[hour padding:none]:[minute padding:none]:[second padding:none].[subsecond]"
29);
30pub(crate) const BIGQUERY_DATETIME_FORMAT: &[time::format_description::FormatItem<'static>] =
31    time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]");
32pub(crate) const BIGQUERY_DATETIME_SUBSEC_FORMAT: &[time::format_description::FormatItem<
33    'static,
34>] = time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]");
35
36/// A trait for converting BigQuery [`wkt::Value`] representations into Rust
37/// types.
38///
39/// [`Row::get()`](crate::query::Row::get) and
40/// [`Row::try_get()`](crate::query::Row::try_get) use this trait to convert cell
41/// values, and the [`FromRow`](crate::query::FromRow) derive macro uses it for field
42/// deserialization.
43///
44/// # Supported Types
45///
46/// Built-in implementations include:
47/// - Numbers: `i32`, `i64`, `f32`, `f64`, [`Decimal`](rust_decimal::Decimal)
48/// - Text & Bytes: `String`, `Vec<u8>` (decoded from base64), [`Bytes`](bytes::Bytes)
49/// - Dates & Times: [`Timestamp`](wkt::Timestamp), [`Date`](google_cloud_type::model::Date), [`TimeOfDay`](google_cloud_type::model::TimeOfDay), [`DateTime`](google_cloud_type::model::DateTime)
50/// - Intervals: [`Interval`](crate::datatypes::Interval)
51/// - Collections: `Option<T>` (for `NULL`), `Vec<T>` (for repeated fields), [`Range<T>`](crate::datatypes::Range) (for `RANGE` types)
52/// - Raw JSON: [`Value`](wkt::Value), [`Struct`](wkt::Struct)
53///
54/// # Example
55///
56/// ```
57/// # use google_cloud_bigquery::client::BigQuery;
58/// # async fn sample(client: BigQuery) -> anyhow::Result<()> {
59/// let mut rows = client
60///     .query("SELECT 12345 AS integer_col, 'foo' AS string_col")
61///     .until_done()
62///     .await?
63///     .read();
64///
65/// while let Some(row) = rows.next().await.transpose()? {
66///     let num: i64 = row.get("integer_col");
67///     let txt: String = row.get("string_col");
68///     println!("{txt}: {num}");
69/// }
70/// # Ok(())
71/// # }
72/// ```
73pub trait FromSql: Sized {
74    /// Converts a BigQuery `wkt::Value` into the implementing type.
75    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError>;
76}
77
78impl FromSql for wkt::Value {
79    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
80        Ok(value)
81    }
82}
83
84impl FromSql for String {
85    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
86        match value {
87            wkt::Value::String(s) => Ok(s),
88            wkt::Value::Null => Err(ConvertError::NotNull),
89            other => Err(ConvertError::TypeMismatch {
90                expected: "string",
91                got: other,
92            }),
93        }
94    }
95}
96
97impl FromSql for i32 {
98    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
99        match value {
100            wkt::Value::Number(n) => n
101                .as_i64()
102                .and_then(|v| i32::try_from(v).ok())
103                .ok_or_else(|| ConvertError::Convert("number is not a valid i32".into())),
104            wkt::Value::String(s) => s
105                .parse::<i32>()
106                .map_err(|e| ConvertError::Convert(Box::new(e))),
107            wkt::Value::Null => Err(ConvertError::NotNull),
108            other => Err(ConvertError::TypeMismatch {
109                expected: "number or string",
110                got: other,
111            }),
112        }
113    }
114}
115
116impl FromSql for i64 {
117    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
118        match value {
119            wkt::Value::Number(n) => n
120                .as_i64()
121                .ok_or_else(|| ConvertError::Convert("number is not a valid i64".into())),
122            wkt::Value::String(s) => s
123                .parse::<i64>()
124                .map_err(|e| ConvertError::Convert(Box::new(e))),
125            wkt::Value::Null => Err(ConvertError::NotNull),
126            other => Err(ConvertError::TypeMismatch {
127                expected: "number or string",
128                got: other,
129            }),
130        }
131    }
132}
133
134impl FromSql for f32 {
135    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
136        match value {
137            wkt::Value::Number(n) => n
138                .as_f64()
139                .map(|v| v as f32)
140                .ok_or_else(|| ConvertError::Convert("number is not a valid f32".into())),
141            wkt::Value::String(s) => s
142                .parse::<f32>()
143                .map_err(|e| ConvertError::Convert(Box::new(e))),
144            wkt::Value::Null => Err(ConvertError::NotNull),
145            other => Err(ConvertError::TypeMismatch {
146                expected: "number or string",
147                got: other,
148            }),
149        }
150    }
151}
152
153impl FromSql for f64 {
154    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
155        match value {
156            wkt::Value::Number(n) => n
157                .as_f64()
158                .ok_or_else(|| ConvertError::Convert("number is not a valid f64".into())),
159            wkt::Value::String(s) => s
160                .parse::<f64>()
161                .map_err(|e| ConvertError::Convert(Box::new(e))),
162            wkt::Value::Null => Err(ConvertError::NotNull),
163            other => Err(ConvertError::TypeMismatch {
164                expected: "number or string",
165                got: other,
166            }),
167        }
168    }
169}
170
171impl FromSql for bool {
172    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
173        match value {
174            wkt::Value::Bool(b) => Ok(b),
175            wkt::Value::String(s) => s
176                .parse::<bool>()
177                .map_err(|e| ConvertError::Convert(Box::new(e))),
178            wkt::Value::Null => Err(ConvertError::NotNull),
179            other => Err(ConvertError::TypeMismatch {
180                expected: "bool or string",
181                got: other,
182            }),
183        }
184    }
185}
186
187impl<T: FromSql> FromSql for Option<T> {
188    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
189        match value {
190            wkt::Value::Null => Ok(None),
191            other => T::from_sql(other).map(Some),
192        }
193    }
194}
195
196impl<T: FromSql> FromSql for Vec<T> {
197    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
198        match value {
199            wkt::Value::Array(arr) => arr.into_iter().map(T::from_sql).collect(),
200            wkt::Value::Null => Err(ConvertError::NotNull),
201            other => Err(ConvertError::TypeMismatch {
202                expected: "array",
203                got: other,
204            }),
205        }
206    }
207}
208
209impl FromSql for wkt::Struct {
210    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
211        match value {
212            wkt::Value::Object(obj) => Ok(obj),
213            wkt::Value::Null => Err(ConvertError::NotNull),
214            other => Err(ConvertError::TypeMismatch {
215                expected: "object",
216                got: other,
217            }),
218        }
219    }
220}
221
222impl FromSql for wkt::Timestamp {
223    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
224        match value {
225            wkt::Value::String(s) => {
226                let micros = s
227                    .parse::<i64>()
228                    .map_err(|e| ConvertError::Convert(Box::new(e)))?;
229                timestamp_from_micros(micros)
230            }
231            wkt::Value::Number(n) => {
232                let micros = n.as_i64().ok_or_else(|| {
233                    ConvertError::Convert("timestamp number is not valid i64".into())
234                })?;
235                timestamp_from_micros(micros)
236            }
237            wkt::Value::Null => Err(ConvertError::NotNull),
238            other => Err(ConvertError::TypeMismatch {
239                expected: "string or number",
240                got: other,
241            }),
242        }
243    }
244}
245
246fn timestamp_from_micros(micros: i64) -> Result<wkt::Timestamp, ConvertError> {
247    wkt::Timestamp::new(
248        micros.div_euclid(1_000_000),
249        (micros.rem_euclid(1_000_000) * 1_000) as i32,
250    )
251    .map_err(|e| ConvertError::Convert(Box::new(e)))
252}
253
254impl FromSql for google_cloud_type::model::Date {
255    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
256        match value {
257            wkt::Value::String(s) => {
258                let date = time::Date::parse(s.as_str(), BIGQUERY_DATE_FORMAT)
259                    .map_err(|e| ConvertError::Convert(Box::new(e)))?;
260                Ok(google_cloud_type::model::Date::new()
261                    .set_year(date.year())
262                    .set_month(u8::from(date.month()) as i32)
263                    .set_day(date.day() as i32))
264            }
265            wkt::Value::Null => Err(ConvertError::NotNull),
266            other => Err(ConvertError::TypeMismatch {
267                expected: "string",
268                got: other,
269            }),
270        }
271    }
272}
273
274pub(crate) fn parse_time(s: &str) -> Result<time::Time, ConvertError> {
275    let format = if s.contains('.') {
276        BIGQUERY_TIME_SUBSEC_FORMAT
277    } else {
278        BIGQUERY_TIME_FORMAT
279    };
280    time::Time::parse(s, format).map_err(|e| ConvertError::Convert(Box::new(e)))
281}
282
283impl FromSql for google_cloud_type::model::TimeOfDay {
284    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
285        match value {
286            wkt::Value::String(s) => {
287                let time = parse_time(s.as_str())?;
288                Ok(google_cloud_type::model::TimeOfDay::new()
289                    .set_hours(time.hour() as i32)
290                    .set_minutes(time.minute() as i32)
291                    .set_seconds(time.second() as i32)
292                    .set_nanos(time.nanosecond() as i32))
293            }
294            wkt::Value::Null => Err(ConvertError::NotNull),
295            other => Err(ConvertError::TypeMismatch {
296                expected: "string",
297                got: other,
298            }),
299        }
300    }
301}
302
303impl FromSql for google_cloud_type::model::DateTime {
304    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
305        match value {
306            wkt::Value::String(s) => {
307                let format = if s.contains('.') {
308                    BIGQUERY_DATETIME_SUBSEC_FORMAT
309                } else {
310                    BIGQUERY_DATETIME_FORMAT
311                };
312                let dt = time::PrimitiveDateTime::parse(s.as_str(), format)
313                    .map_err(|e| ConvertError::Convert(Box::new(e)))?;
314                Ok(google_cloud_type::model::DateTime::new()
315                    .set_year(dt.year())
316                    .set_month(u8::from(dt.month()) as i32)
317                    .set_day(dt.day() as i32)
318                    .set_hours(dt.hour() as i32)
319                    .set_minutes(dt.minute() as i32)
320                    .set_seconds(dt.second() as i32)
321                    .set_nanos(dt.nanosecond() as i32))
322            }
323            wkt::Value::Null => Err(ConvertError::NotNull),
324            other => Err(ConvertError::TypeMismatch {
325                expected: "string",
326                got: other,
327            }),
328        }
329    }
330}
331
332impl FromSql for google_cloud_type::model::Decimal {
333    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
334        match value {
335            wkt::Value::String(s) => Ok(google_cloud_type::model::Decimal::new().set_value(s)),
336            wkt::Value::Number(n) => {
337                Ok(google_cloud_type::model::Decimal::new().set_value(n.to_string()))
338            }
339            wkt::Value::Null => Err(ConvertError::NotNull),
340            other => Err(ConvertError::TypeMismatch {
341                expected: "string or number",
342                got: other,
343            }),
344        }
345    }
346}
347
348impl FromSql for rust_decimal::Decimal {
349    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
350        match value {
351            wkt::Value::String(s) => s
352                .trim()
353                .parse::<rust_decimal::Decimal>()
354                .map_err(|e| ConvertError::Convert(Box::new(e))),
355            wkt::Value::Number(n) => {
356                if let Some(i) = n.as_i64() {
357                    Ok(rust_decimal::Decimal::from(i))
358                } else if let Some(u) = n.as_u64() {
359                    Ok(rust_decimal::Decimal::from(u))
360                } else if let Some(f) = n.as_f64() {
361                    rust_decimal::Decimal::try_from(f)
362                        .map_err(|e| ConvertError::Convert(Box::new(e)))
363                } else {
364                    Err(ConvertError::Convert("invalid number".into()))
365                }
366            }
367            wkt::Value::Null => Err(ConvertError::NotNull),
368            other => Err(ConvertError::TypeMismatch {
369                expected: "string or number",
370                got: other,
371            }),
372        }
373    }
374}
375
376impl FromSql for Vec<u8> {
377    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
378        match value {
379            wkt::Value::String(s) => BASE64_STANDARD
380                .decode(s)
381                .map_err(|e| ConvertError::Convert(Box::new(e))),
382            wkt::Value::Null => Err(ConvertError::NotNull),
383            other => Err(ConvertError::TypeMismatch {
384                expected: "string (base64 encoded)",
385                got: other,
386            }),
387        }
388    }
389}
390
391impl FromSql for bytes::Bytes {
392    fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
393        Vec::<u8>::from_sql(value).map(bytes::Bytes::from)
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400    use crate as google_cloud_bigquery;
401    use crate::query::FromSql;
402    use google_cloud_type::model::Decimal;
403    use rust_decimal::Decimal as RustDecimal;
404    use test_case::test_case;
405
406    // Test-only representation of `ConvertError` that implements `PartialEq`.
407    // This allows testing error outcomes using `test_case` assertions without
408    // implementing `PartialEq` on the production `ConvertError`.
409    #[derive(Debug, PartialEq)]
410    enum TestConvertError {
411        NotNull,
412        TypeMismatch(&'static str),
413        Convert(String),
414        MissingField(String),
415    }
416
417    impl From<ConvertError> for TestConvertError {
418        fn from(err: ConvertError) -> Self {
419            match err {
420                ConvertError::NotNull => Self::NotNull,
421                ConvertError::TypeMismatch { expected, .. } => Self::TypeMismatch(expected),
422                ConvertError::Convert(e) => Self::Convert(e.to_string()),
423                ConvertError::MissingField(f) => Self::MissingField(f),
424            }
425        }
426    }
427
428    #[test_case(wkt::Value::String("hello".to_string()) => Ok(wkt::Value::String("hello".to_string())) ; "value string")]
429    fn test_from_sql_value(value: wkt::Value) -> Result<wkt::Value, TestConvertError> {
430        FromSql::from_sql(value).map_err(TestConvertError::from)
431    }
432
433    #[test_case(wkt::Value::String("hello".to_string()) => Ok("hello".to_string()) ; "string")]
434    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null string")]
435    #[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string")) ; "type mismatch string")]
436    fn test_from_sql_string(value: wkt::Value) -> Result<String, TestConvertError> {
437        FromSql::from_sql(value).map_err(TestConvertError::from)
438    }
439
440    #[test_case(wkt::Value::Number(123.into()) => Ok(123) ; "i64 from number")]
441    #[test_case(wkt::Value::String("123".to_string()) => Ok(123) ; "i64 from string")]
442    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null i64")]
443    #[test_case(wkt::Value::Bool(true) => Err(TestConvertError::TypeMismatch("number or string")) ; "try bool as i64")]
444    #[test_case(wkt::Value::String("hello".to_string()) => Err(TestConvertError::Convert("invalid digit found in string".to_string())) ; "invalid string as i64")]
445    fn test_from_sql_i64(value: wkt::Value) -> Result<i64, TestConvertError> {
446        FromSql::from_sql(value).map_err(TestConvertError::from)
447    }
448
449    #[test_case(wkt::Value::Number(serde_json::Number::from_f64(123.45).unwrap()) => Ok(123.45) ; "f64 from number")]
450    #[test_case(wkt::Value::String("123.45".to_string()) => Ok(123.45) ; "f64 from string")]
451    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null f64")]
452    #[test_case(wkt::Value::Bool(true) => Err(TestConvertError::TypeMismatch("number or string")) ; "try bool as f64")]
453    #[test_case(wkt::Value::String("hello".to_string()) => Err(TestConvertError::Convert("invalid float literal".to_string())) ; "invalid string as f64")]
454    fn test_from_sql_f64(value: wkt::Value) -> Result<f64, TestConvertError> {
455        FromSql::from_sql(value).map_err(TestConvertError::from)
456    }
457
458    #[test_case(wkt::Value::Bool(true) => Ok(true) ; "bool true")]
459    #[test_case(wkt::Value::Bool(false) => Ok(false) ; "bool false")]
460    #[test_case(wkt::Value::String("true".to_string()) => Ok(true) ; "bool from string true")]
461    #[test_case(wkt::Value::String("false".to_string()) => Ok(false) ; "bool from string false")]
462    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null bool")]
463    #[test_case(wkt::Value::Number(1.into()) => Err(TestConvertError::TypeMismatch("bool or string")) ; "try number as bool")]
464    #[test_case(wkt::Value::String("hello".to_string()) => Err(TestConvertError::Convert("provided string was not `true` or `false`".to_string())) ; "invalid string as bool")]
465    fn test_from_sql_bool(value: wkt::Value) -> Result<bool, TestConvertError> {
466        FromSql::from_sql(value).map_err(TestConvertError::from)
467    }
468
469    #[test_case(wkt::Value::Null => Ok(None) ; "option null")]
470    #[test_case(wkt::Value::Number(123.into()) => Ok(Some(123)) ; "option some i64")]
471    #[test_case(wkt::Value::String("hello".to_string()) => Err(TestConvertError::Convert("invalid digit found in string".to_string())) ; "option error i64")]
472    fn test_from_sql_option(value: wkt::Value) -> Result<Option<i64>, TestConvertError> {
473        FromSql::from_sql(value).map_err(TestConvertError::from)
474    }
475
476    #[test_case(wkt::Value::Array(vec![wkt::Value::Number(1.into()), wkt::Value::Number(2.into())]) => Ok(vec![1, 2]) ; "vec i64")]
477    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "vec null")]
478    #[test_case(wkt::Value::String("hello".to_string()) => Err(TestConvertError::TypeMismatch("array")) ; "vec type mismatch")]
479    #[test_case(wkt::Value::Array(vec![wkt::Value::String("invalid".to_string())]) => Err(TestConvertError::Convert("invalid digit found in string".to_string())) ; "vec element convert error")]
480    fn test_from_sql_vec(value: wkt::Value) -> Result<Vec<i64>, TestConvertError> {
481        FromSql::from_sql(value).map_err(TestConvertError::from)
482    }
483
484    #[test_case(wkt::Value::Object(wkt::Struct::from_iter([("a".to_string(), wkt::Value::Number(1.into()))])) => Ok(wkt::Struct::from_iter([("a".to_string(), wkt::Value::Number(1.into()))])) ; "struct ok")]
485    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "struct null")]
486    #[test_case(wkt::Value::String("hello".to_string()) => Err(TestConvertError::TypeMismatch("object")) ; "struct type mismatch")]
487    fn test_from_sql_struct(value: wkt::Value) -> Result<wkt::Struct, TestConvertError> {
488        FromSql::from_sql(value).map_err(TestConvertError::from)
489    }
490
491    #[test_case(wkt::Value::String("1779982200000000".to_string()) => Ok(wkt::Timestamp::new(1779982200, 0).unwrap()) ; "timestamp micro integer string")]
492    #[test_case(wkt::Value::Number(1779982200000000i64.into()) => Ok(wkt::Timestamp::new(1779982200, 0).unwrap()) ; "timestamp micro integer number")]
493    #[test_case(wkt::Value::String("2026-05-28T15:30:00Z".to_string()) => Err(TestConvertError::Convert("invalid digit found in string".to_string())) ; "timestamp rfc3339 string fails")]
494    #[test_case(wkt::Value::Number(serde_json::Number::from_f64(1779982200.5).unwrap()) => Err(TestConvertError::Convert("timestamp number is not valid i64".to_string())) ; "timestamp f64 number fails")]
495    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "timestamp null")]
496    #[test_case(wkt::Value::Bool(true) => Err(TestConvertError::TypeMismatch("string or number")) ; "timestamp type mismatch")]
497    fn test_from_sql_timestamp(value: wkt::Value) -> Result<wkt::Timestamp, TestConvertError> {
498        FromSql::from_sql(value).map_err(TestConvertError::from)
499    }
500
501    #[test_case(wkt::Value::String("2026-05-28".to_string()) => Ok(google_cloud_type::model::Date::new().set_year(2026).set_month(5).set_day(28)) ; "date valid")]
502    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "date null")]
503    #[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string")) ; "date type mismatch")]
504    #[test_case(wkt::Value::String("invalid-date".to_string()) => Err(TestConvertError::Convert("the 'year' component could not be parsed".to_string())) ; "date invalid format")]
505    #[test_case(wkt::Value::String("2026-abc-28".to_string()) => Err(TestConvertError::Convert("the 'month' component could not be parsed".to_string())) ; "date invalid digits")]
506    fn test_from_sql_date(
507        value: wkt::Value,
508    ) -> Result<google_cloud_type::model::Date, TestConvertError> {
509        FromSql::from_sql(value).map_err(TestConvertError::from)
510    }
511
512    #[test_case(wkt::Value::String("15:30:00".to_string()) => Ok(google_cloud_type::model::TimeOfDay::new().set_hours(15).set_minutes(30).set_seconds(0).set_nanos(0)) ; "time of day valid")]
513    #[test_case(wkt::Value::String("15:30:00.123456".to_string()) => Ok(google_cloud_type::model::TimeOfDay::new().set_hours(15).set_minutes(30).set_seconds(0).set_nanos(123_456_000)) ; "time of day fractional")]
514    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "time of day null")]
515    #[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string")) ; "time of day type mismatch")]
516    fn test_from_sql_time_of_day(
517        value: wkt::Value,
518    ) -> Result<google_cloud_type::model::TimeOfDay, TestConvertError> {
519        FromSql::from_sql(value).map_err(TestConvertError::from)
520    }
521
522    #[test_case(wkt::Value::String("2026-05-28T15:30:00".to_string()) => Ok(google_cloud_type::model::DateTime::new().set_year(2026).set_month(5).set_day(28).set_hours(15).set_minutes(30).set_seconds(0).set_nanos(0)) ; "datetime without subseconds")]
523    #[test_case(wkt::Value::String("2026-05-28T15:30:00.123456".to_string()) => Ok(google_cloud_type::model::DateTime::new().set_year(2026).set_month(5).set_day(28).set_hours(15).set_minutes(30).set_seconds(0).set_nanos(123_456_000)) ; "datetime with subseconds")]
524    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "datetime null")]
525    #[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string")) ; "datetime type mismatch")]
526    fn test_from_sql_datetime(
527        value: wkt::Value,
528    ) -> Result<google_cloud_type::model::DateTime, TestConvertError> {
529        FromSql::from_sql(value).map_err(TestConvertError::from)
530    }
531
532    #[test_case(wkt::Value::Number(123.into()) => Ok(123) ; "i32 from number")]
533    #[test_case(wkt::Value::String("123".to_string()) => Ok(123) ; "i32 from string")]
534    #[test_case(wkt::Value::Number(3_000_000_000i64.into()) => Err(TestConvertError::Convert("number is not a valid i32".to_string())) ; "i32 overflow from number")]
535    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null i32")]
536    #[test_case(wkt::Value::Bool(true) => Err(TestConvertError::TypeMismatch("number or string")) ; "try bool as i32")]
537    #[test_case(wkt::Value::String("hello".to_string()) => Err(TestConvertError::Convert("invalid digit found in string".to_string())) ; "invalid string as i32")]
538    fn test_from_sql_i32(value: wkt::Value) -> Result<i32, TestConvertError> {
539        FromSql::from_sql(value).map_err(TestConvertError::from)
540    }
541
542    #[test_case(wkt::Value::Number(serde_json::Number::from_f64(123.45).unwrap()) => Ok(123.45) ; "f32 from number")]
543    #[test_case(wkt::Value::String("123.45".to_string()) => Ok(123.45) ; "f32 from string")]
544    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null f32")]
545    #[test_case(wkt::Value::Bool(true) => Err(TestConvertError::TypeMismatch("number or string")) ; "try bool as f32")]
546    #[test_case(wkt::Value::String("hello".to_string()) => Err(TestConvertError::Convert("invalid float literal".to_string())) ; "invalid string as f32")]
547    fn test_from_sql_f32(value: wkt::Value) -> Result<f32, TestConvertError> {
548        FromSql::from_sql(value).map_err(TestConvertError::from)
549    }
550
551    #[test_case(wkt::Value::String("123.456".to_string()) => Ok(Decimal::new().set_value("123.456")) ; "decimal from string")]
552    #[test_case(wkt::Value::Number(serde_json::Number::from_f64(123.456).unwrap()) => Ok(Decimal::new().set_value("123.456")) ; "decimal from number")]
553    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null decimal")]
554    #[test_case(wkt::Value::Bool(true) => Err(TestConvertError::TypeMismatch("string or number")) ; "try bool as decimal")]
555    fn test_from_sql_decimal(value: wkt::Value) -> Result<Decimal, TestConvertError> {
556        FromSql::from_sql(value).map_err(TestConvertError::from)
557    }
558
559    #[test_case(wkt::Value::String("123.456".to_string()) => Ok(RustDecimal::from_str_exact("123.456").unwrap()) ; "rust_decimal from string")]
560    #[test_case(wkt::Value::Number(serde_json::Number::from_f64(123.456).unwrap()) => Ok(RustDecimal::from_str_exact("123.456").unwrap()) ; "rust_decimal from number")]
561    #[test_case(wkt::Value::String("99999999999999999999999999999999.123".to_string()) => Err(TestConvertError::Convert("Invalid decimal: overflow from too many digits".to_string())) ; "rust_decimal overflow")]
562    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null rust_decimal")]
563    #[test_case(wkt::Value::Bool(true) => Err(TestConvertError::TypeMismatch("string or number")) ; "try bool as rust_decimal")]
564    fn test_from_sql_rust_decimal(value: wkt::Value) -> Result<RustDecimal, TestConvertError> {
565        FromSql::from_sql(value).map_err(TestConvertError::from)
566    }
567
568    #[test_case(wkt::Value::String("AQIDBA==".to_string()) => Ok(vec![1, 2, 3, 4]) ; "vec u8 from base64")]
569    #[test_case(wkt::Value::String("".to_string()) => Ok(vec![]) ; "vec u8 from empty base64")]
570    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null vec u8")]
571    #[test_case(wkt::Value::Bool(true) => Err(TestConvertError::TypeMismatch("string (base64 encoded)")) ; "try bool as vec u8")]
572    fn test_from_sql_vec_u8(value: wkt::Value) -> Result<Vec<u8>, TestConvertError> {
573        FromSql::from_sql(value).map_err(TestConvertError::from)
574    }
575
576    #[test_case(wkt::Value::String("AQIDBA==".to_string()) => Ok(bytes::Bytes::from_static(&[1, 2, 3, 4])) ; "bytes from base64")]
577    #[test_case(wkt::Value::String("".to_string()) => Ok(bytes::Bytes::from_static(&[])) ; "bytes from empty base64")]
578    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null bytes")]
579    #[test_case(wkt::Value::Bool(true) => Err(TestConvertError::TypeMismatch("string (base64 encoded)")) ; "try bool as bytes")]
580    fn test_from_sql_bytes(value: wkt::Value) -> Result<bytes::Bytes, TestConvertError> {
581        FromSql::from_sql(value).map_err(TestConvertError::from)
582    }
583
584    #[test_case("AQIDBA" ; "missing padding")]
585    #[test_case("Not a base64 string" ; "words with spaces")]
586    fn test_from_sql_bytes_invalid_base64(input: &str) {
587        let err = bytes::Bytes::from_sql(wkt::Value::String(input.to_string())).unwrap_err();
588        assert!(matches!(err, ConvertError::Convert(_)));
589
590        let err = Vec::<u8>::from_sql(wkt::Value::String(input.to_string())).unwrap_err();
591        assert!(matches!(err, ConvertError::Convert(_)));
592    }
593
594    #[derive(FromSql, Debug, PartialEq)]
595    struct TestSqlStruct {
596        name: String,
597        #[bigquery(rename = "custom_int")]
598        some_int: i64,
599        some_bool: bool,
600    }
601
602    #[test_case(wkt::Value::Array(vec![wkt::Value::String("James".to_string()), wkt::Value::Number(272793.into()), wkt::Value::Bool(true)]) => Ok(TestSqlStruct { name: "James".to_string(), some_int: 272793, some_bool: true }) ; "array success")]
603    #[test_case(wkt::Value::Object(wkt::Struct::from_iter([("name".to_string(), wkt::Value::String("James".to_string())), ("custom_int".to_string(), wkt::Value::Number(272793.into())), ("some_bool".to_string(), wkt::Value::Bool(true))])) => Ok(TestSqlStruct { name: "James".to_string(), some_int: 272793, some_bool: true }) ; "object success")]
604    #[test_case(wkt::Value::Object(wkt::Struct::from_iter([("name".to_string(), wkt::Value::String("James".to_string())), ("some_bool".to_string(), wkt::Value::Bool(true))])) => Err(TestConvertError::MissingField("custom_int".to_string())) ; "missing field")]
605    #[test_case(wkt::Value::String("invalid".to_string()) => Err(TestConvertError::TypeMismatch("array or object")) ; "type mismatch")]
606    fn test_derive_from_sql(value: wkt::Value) -> Result<TestSqlStruct, TestConvertError> {
607        FromSql::from_sql(value).map_err(TestConvertError::from)
608    }
609}