1use 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
36pub trait FromSql: Sized {
74 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 #[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}