Skip to main content

google_cloud_bigquery/
datatypes.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
15//! Custom data types for BigQuery.
16//!
17//! This module provides Rust representations of BigQuery data types such as
18//! [`Interval`] and [`Range`].
19
20use crate::error::ConvertError;
21use crate::query::FromSql;
22use crate::query::from_sql::SqlValueInner;
23
24/// Represents a BigQuery time [INTERVAL] value.
25///
26/// [INTERVAL]: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#interval_type
27///
28/// # Example
29///
30/// ```
31/// # async fn sample() -> anyhow::Result<()> {
32/// use google_cloud_bigquery::client::BigQuery;
33/// use google_cloud_bigquery::datatypes::Interval;
34///
35/// let client = BigQuery::builder()
36///     .with_project_id("my-project-id")
37///     .build()
38///     .await?;
39/// let mut rows = client
40///     .query("SELECT INTERVAL '1-2 15 5:30:00' YEAR TO SECOND AS duration")
41///     .until_done()
42///     .await?
43///     .read();
44///
45/// if let Some(row) = rows.next().await.transpose()? {
46///     let interval: Interval = row.get("duration")?;
47///     println!("{} years, {} months, {} days", interval.years, interval.months, interval.days);
48/// }
49/// # Ok(())
50/// # }
51/// ```
52#[derive(Clone, Debug, Default, PartialEq)]
53#[non_exhaustive]
54pub struct Interval {
55    /// Years component.
56    pub years: i32,
57    /// Months component.
58    pub months: i32,
59    /// Days component.
60    pub days: i32,
61    /// Hours component.
62    pub hours: i32,
63    /// Minutes component.
64    pub minutes: i32,
65    /// Seconds component.
66    pub seconds: i32,
67    /// Nanoseconds component.
68    pub nanos: i32,
69}
70
71impl Interval {
72    /// Creates a new zero-duration interval (`0-0 0 0:00:00`), with all components set to `0`.
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Sets the value of [years][Self::years].
78    pub fn set_years(mut self, v: i32) -> Self {
79        self.years = v;
80        self
81    }
82
83    /// Sets the value of [months][Self::months].
84    pub fn set_months(mut self, v: i32) -> Self {
85        self.months = v;
86        self
87    }
88
89    /// Sets the value of [days][Self::days].
90    pub fn set_days(mut self, v: i32) -> Self {
91        self.days = v;
92        self
93    }
94
95    /// Sets the value of [hours][Self::hours].
96    pub fn set_hours(mut self, v: i32) -> Self {
97        self.hours = v;
98        self
99    }
100
101    /// Sets the value of [minutes][Self::minutes].
102    pub fn set_minutes(mut self, v: i32) -> Self {
103        self.minutes = v;
104        self
105    }
106
107    /// Sets the value of [seconds][Self::seconds].
108    pub fn set_seconds(mut self, v: i32) -> Self {
109        self.seconds = v;
110        self
111    }
112
113    /// Sets the value of [nanos][Self::nanos].
114    pub fn set_nanos(mut self, v: i32) -> Self {
115        self.nanos = v;
116        self
117    }
118}
119
120impl FromSql for Interval {
121    fn from_value(value: crate::query::SqlValue) -> Result<Self, ConvertError> {
122        match value.inner {
123            SqlValueInner::String(s) => {
124                let mut parts = s.split_whitespace();
125                let ym_str = parts.next();
126                let days_str = parts.next();
127                let time_str = parts.next();
128                let extra = parts.next();
129
130                let (ym_str, days_str, time_str) = match (ym_str, days_str, time_str, extra) {
131                    (Some(ym), Some(d), Some(t), None) => (ym, d, t),
132                    _ => {
133                        return Err(ConvertError::Convert(
134                            format!("invalid interval format: expected 3 parts, got `{s}`").into(),
135                        ));
136                    }
137                };
138
139                // Parse Y-M
140                let ym_neg = ym_str.starts_with('-');
141                let ym_content = if ym_neg { &ym_str[1..] } else { ym_str };
142                let mut ym_parts = ym_content.split('-');
143                let y_str = ym_parts.next();
144                let m_str = ym_parts.next();
145                let ym_extra = ym_parts.next();
146
147                let (y_str, m_str) = match (y_str, m_str, ym_extra) {
148                    (Some(y), Some(m), None) => (y, m),
149                    _ => {
150                        return Err(ConvertError::Convert(
151                            "invalid interval year-month format".into(),
152                        ));
153                    }
154                };
155                let ym_sign = if ym_neg { -1 } else { 1 };
156                let years = y_str
157                    .parse::<i32>()
158                    .map_err(|e| ConvertError::Convert(Box::new(e)))?
159                    * ym_sign;
160                let months = m_str
161                    .parse::<i32>()
162                    .map_err(|e| ConvertError::Convert(Box::new(e)))?
163                    * ym_sign;
164
165                // Parse Days
166                let days = days_str
167                    .parse::<i32>()
168                    .map_err(|e| ConvertError::Convert(Box::new(e)))?;
169
170                // Parse H:M:S.F. Hours are unbounded: BigQuery never spills
171                // hours into days, so a timestamp difference of a month is
172                // `0-0 0 744:0:0`.
173                let time_neg = time_str.starts_with('-');
174                let time_content = if time_neg { &time_str[1..] } else { time_str };
175                let (hms, frac) = time_content.split_once('.').unwrap_or((time_content, "0"));
176                let mut hms_parts = hms.split(':');
177                let (h_str, m_str, s_str) = match (
178                    hms_parts.next(),
179                    hms_parts.next(),
180                    hms_parts.next(),
181                    hms_parts.next(),
182                ) {
183                    (Some(h), Some(m), Some(s), None) => (h, m, s),
184                    _ => {
185                        return Err(ConvertError::Convert("invalid interval time format".into()));
186                    }
187                };
188                let time_sign = if time_neg { -1 } else { 1 };
189                let hours = h_str
190                    .parse::<i32>()
191                    .map_err(|e| ConvertError::Convert(Box::new(e)))?
192                    * time_sign;
193                let minutes = m_str
194                    .parse::<i32>()
195                    .map_err(|e| ConvertError::Convert(Box::new(e)))?
196                    * time_sign;
197                let seconds = s_str
198                    .parse::<i32>()
199                    .map_err(|e| ConvertError::Convert(Box::new(e)))?
200                    * time_sign;
201                // The fraction has any number of digits; nanoseconds need exactly nine.
202                let nanos = format!("{frac:0<9.9}")
203                    .parse::<i32>()
204                    .map_err(|e| ConvertError::Convert(Box::new(e)))?
205                    * time_sign;
206
207                Ok(Interval {
208                    years,
209                    months,
210                    days,
211                    hours,
212                    minutes,
213                    seconds,
214                    nanos,
215                })
216            }
217            SqlValueInner::Null => Err(ConvertError::NotNull),
218            #[cfg(any(
219                google_cloud_unstable_bigquery_arrow,
220                google_cloud_unstable_bigquery_storage_read
221            ))]
222            SqlValueInner::Arrow(cell) => {
223                let v = cell.as_interval()?;
224                let ym_sign = if v.months < 0 { -1 } else { 1 };
225                let total_months = v.months.unsigned_abs();
226                let years = (total_months / 12) as i32 * ym_sign;
227                let months = (total_months % 12) as i32 * ym_sign;
228
229                let time_sign = if v.nanoseconds < 0 { -1 } else { 1 };
230                let total_nanos = v.nanoseconds.unsigned_abs();
231                let nanos = (total_nanos % 1_000_000_000) as i32 * time_sign;
232                let total_secs = total_nanos / 1_000_000_000;
233                let seconds = (total_secs % 60) as i32 * time_sign;
234                let total_mins = total_secs / 60;
235                let minutes = (total_mins % 60) as i32 * time_sign;
236                let hours = (total_mins / 60) as i32 * time_sign;
237
238                Ok(Interval {
239                    years,
240                    months,
241                    days: v.days,
242                    hours,
243                    minutes,
244                    seconds,
245                    nanos,
246                })
247            }
248            other => Err(ConvertError::type_mismatch("string", &other)),
249        }
250    }
251}
252
253mod sealed {
254    /// A sealed trait to prevent external implementation of `RangeElement`.
255    pub trait RangeElement {}
256
257    impl RangeElement for google_cloud_type::model::Date {}
258    impl RangeElement for google_cloud_type::model::DateTime {}
259    impl RangeElement for wkt::Timestamp {}
260}
261
262/// A marker trait for types that can be elements of a BigQuery [`Range`].
263///
264/// BigQuery `RANGE<T>` values support [`Date`](google_cloud_type::model::Date),
265/// [`DateTime`](google_cloud_type::model::DateTime), and [`Timestamp`](wkt::Timestamp)
266/// element types.
267///
268/// This trait is sealed and cannot be implemented for types outside of this crate.
269pub trait RangeElement: FromSql + sealed::RangeElement {}
270
271impl RangeElement for google_cloud_type::model::Date {}
272impl RangeElement for google_cloud_type::model::DateTime {}
273impl RangeElement for wkt::Timestamp {}
274
275/// Represents a BigQuery [RANGE] value.
276///
277/// [RANGE]: https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/data-types#range_type
278///
279/// # Example
280///
281/// ```
282/// # async fn sample() -> anyhow::Result<()> {
283/// use google_cloud_bigquery::client::BigQuery;
284/// use google_cloud_bigquery::datatypes::Range;
285/// use google_cloud_type::model::Date;
286///
287/// let client = BigQuery::builder()
288///     .with_project_id("my-project-id")
289///     .build()
290///     .await?;
291/// let mut rows = client
292///     .query("SELECT RANGE(DATE '2024-01-01', DATE '2024-12-31') AS date_range")
293///     .until_done()
294///     .await?
295///     .read();
296///
297/// if let Some(row) = rows.next().await.transpose()? {
298///     let date_range: Range<Date> = row.get("date_range")?;
299///     println!("Start: {:?}, End: {:?}", date_range.start, date_range.end);
300/// }
301/// # Ok(())
302/// # }
303/// ```
304#[derive(Clone, Debug, PartialEq)]
305#[non_exhaustive]
306pub struct Range<T: RangeElement> {
307    /// The inclusive start of the range (or None if unbounded).
308    pub start: Option<T>,
309    /// The exclusive end of the range (or None if unbounded).
310    pub end: Option<T>,
311}
312
313impl<T: RangeElement> Default for Range<T> {
314    fn default() -> Self {
315        Self::new()
316    }
317}
318
319impl<T: RangeElement> Range<T> {
320    /// Creates a new unbounded range (`[UNBOUNDED, UNBOUNDED)`).
321    pub fn new() -> Self {
322        Self {
323            start: None,
324            end: None,
325        }
326    }
327
328    /// Sets the value of [start][Self::start].
329    pub fn set_start<V: Into<T>>(mut self, v: V) -> Self {
330        self.start = Some(v.into());
331        self
332    }
333
334    /// Sets or clears the value of [start][Self::start].
335    pub fn set_or_clear_start(mut self, v: Option<T>) -> Self {
336        self.start = v;
337        self
338    }
339
340    /// Sets the value of [end][Self::end].
341    pub fn set_end<V: Into<T>>(mut self, v: V) -> Self {
342        self.end = Some(v.into());
343        self
344    }
345
346    /// Sets or clears the value of [end][Self::end].
347    pub fn set_or_clear_end(mut self, v: Option<T>) -> Self {
348        self.end = v;
349        self
350    }
351}
352
353impl<T: RangeElement> FromSql for Range<T> {
354    fn from_value(value: crate::query::SqlValue) -> Result<Self, ConvertError> {
355        match value.inner {
356            SqlValueInner::String(s) => {
357                let trimmed = s.trim();
358                // Strip leading [ and trailing )
359                let content = trimmed
360                    .strip_prefix('[')
361                    .and_then(|c| c.strip_suffix(')'))
362                    .ok_or_else(|| {
363                        ConvertError::Convert(
364                            "invalid range format: missing enclosing brackets".into(),
365                        )
366                    })?;
367
368                // Split on the comma
369                let parts: Vec<&str> = content.split(',').collect();
370                if parts.len() != 2 {
371                    return Err(ConvertError::Convert(
372                        format!(
373                            "invalid range format: expected 2 parts, got {}",
374                            parts.len()
375                        )
376                        .into(),
377                    ));
378                }
379
380                let start_str = parts[0].trim();
381                let end_str = parts[1].trim();
382
383                let start = if start_str.is_empty() || start_str == "UNBOUNDED" {
384                    None
385                } else {
386                    Some(T::from_value(crate::query::SqlValue::from_inner(
387                        SqlValueInner::String(start_str.to_string()),
388                    ))?)
389                };
390
391                let end = if end_str.is_empty() || end_str == "UNBOUNDED" {
392                    None
393                } else {
394                    Some(T::from_value(crate::query::SqlValue::from_inner(
395                        SqlValueInner::String(end_str.to_string()),
396                    ))?)
397                };
398
399                Ok(Range { start, end })
400            }
401            SqlValueInner::Null => Err(ConvertError::NotNull),
402            #[cfg(any(
403                google_cloud_unstable_bigquery_arrow,
404                google_cloud_unstable_bigquery_storage_read
405            ))]
406            SqlValueInner::Arrow(cell) => {
407                if cell.is_null() {
408                    return Err(ConvertError::NotNull);
409                }
410                let take = |key: &str| -> Result<Option<T>, ConvertError> {
411                    let field_cell = cell.struct_field_cell(&key)?;
412                    Option::<T>::from_value(crate::query::SqlValue::from_inner(
413                        SqlValueInner::Arrow(field_cell),
414                    ))
415                };
416                let start = take("start")?;
417                let end = take("end")?;
418                Ok(Range { start, end })
419            }
420            other => Err(ConvertError::type_mismatch("string", &other)),
421        }
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use test_case::test_case;
429
430    #[derive(Debug, PartialEq)]
431    enum TestConvertError {
432        NotNull,
433        TypeMismatch(String),
434        Convert,
435        MissingField(String),
436    }
437
438    impl From<ConvertError> for TestConvertError {
439        fn from(err: ConvertError) -> Self {
440            match err {
441                ConvertError::NotNull => Self::NotNull,
442                ConvertError::TypeMismatch { expected, .. } => Self::TypeMismatch(expected),
443                ConvertError::Convert(_) => Self::Convert,
444                ConvertError::MissingField(f) => Self::MissingField(f),
445            }
446        }
447    }
448
449    #[test_case(wkt::Value::String("1-2 3 4:05:06.789123456".to_string()) => Ok(Interval { years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6, nanos: 789_123_456 }) ; "valid interval with nanos")]
450    #[test_case(wkt::Value::String("0-0 0 0:00:00".to_string()) => Ok(Interval { years: 0, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0, nanos: 0 }) ; "zero interval")]
451    #[test_case(wkt::Value::String("0-0 1 2:30:45.123456".to_string()) => Ok(Interval { years: 0, months: 0, days: 1, hours: 2, minutes: 30, seconds: 45, nanos: 123_456_000 }) ; "valid interval from integration test")]
452    #[test_case(wkt::Value::String("1-2 3 4:5:6".to_string()) => Ok(Interval { years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6, nanos: 0 }) ; "unpadded time without subseconds")]
453    #[test_case(wkt::Value::String("1-2 3 4:5:6.5".to_string()) => Ok(Interval { years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6, nanos: 500_000_000 }) ; "unpadded time with short subsecond")]
454    #[test_case(wkt::Value::String("-1-2 3 -4:5:6.123".to_string()) => Ok(Interval { years: -1, months: -2, days: 3, hours: -4, minutes: -5, seconds: -6, nanos: -123_000_000 }) ; "mixed signs interval")]
455    #[test_case(wkt::Value::String("0-0 0 1:1:1.000000001".to_string()) => Ok(Interval { years: 0, months: 0, days: 0, hours: 1, minutes: 1, seconds: 1, nanos: 1 }) ; "single nanosecond")]
456    #[test_case(wkt::Value::String("-1-2 -3 -4:05:06.123".to_string()) => Ok(Interval { years: -1, months: -2, days: -3, hours: -4, minutes: -5, seconds: -6, nanos: -123_000_000 }) ; "all negative interval")]
457    #[test_case(wkt::Value::String("0-0 0 0:00:00.1234567899".to_string()) => Ok(Interval { years: 0, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0, nanos: 123_456_789 }) ; "truncated nanos")]
458    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null interval")]
459    #[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string".to_string())) ; "type mismatch interval")]
460    #[test_case(wkt::Value::String("".to_string()) => Err(TestConvertError::Convert) ; "empty interval string")]
461    #[test_case(wkt::Value::String("1-2 3".to_string()) => Err(TestConvertError::Convert) ; "invalid interval parts count")]
462    #[test_case(wkt::Value::String("1 3 4:05:06".to_string()) => Err(TestConvertError::Convert) ; "invalid year-month format")]
463    #[test_case(wkt::Value::String("0-0 0 744:0:0".to_string()) => Ok(Interval { years: 0, months: 0, days: 0, hours: 744, minutes: 0, seconds: 0, nanos: 0 }) ; "hours beyond a day")]
464    #[test_case(wkt::Value::String("0-0 0 25:0:0".to_string()) => Ok(Interval { years: 0, months: 0, days: 0, hours: 25, minutes: 0, seconds: 0, nanos: 0 }) ; "interval 25 hour")]
465    #[test_case(wkt::Value::String("0-0 0 -744:0:0".to_string()) => Ok(Interval { years: 0, months: 0, days: 0, hours: -744, minutes: 0, seconds: 0, nanos: 0 }) ; "negative hours beyond a day")]
466    #[test_case(wkt::Value::String("1-2 3 4:05".to_string()) => Err(TestConvertError::Convert) ; "invalid time format")]
467    #[test_case(wkt::Value::String("1-2 3 4:05:06:07".to_string()) => Err(TestConvertError::Convert) ; "too many time parts")]
468    #[test_case(wkt::Value::String("1-2 3 4:05:06.x".to_string()) => Err(TestConvertError::Convert) ; "invalid subsecond")]
469    fn test_from_sql_interval(value: wkt::Value) -> Result<Interval, TestConvertError> {
470        FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
471    }
472
473    #[test_case(wkt::Value::String("[2026-05-28, 2026-05-29)".to_string()) => Ok(Range { start: Some(google_cloud_type::model::Date::new().set_year(2026).set_month(5).set_day(28)), end: Some(google_cloud_type::model::Date::new().set_year(2026).set_month(5).set_day(29)) }) ; "date range bounded")]
474    #[test_case(wkt::Value::String("[2026-05-28, UNBOUNDED)".to_string()) => Ok(Range { start: Some(google_cloud_type::model::Date::new().set_year(2026).set_month(5).set_day(28)), end: None }) ; "date range unbounded end")]
475    #[test_case(wkt::Value::String("[UNBOUNDED, 2026-05-29)".to_string()) => Ok(Range { start: None, end: Some(google_cloud_type::model::Date::new().set_year(2026).set_month(5).set_day(29)) }) ; "date range unbounded start")]
476    #[test_case(wkt::Value::String("[UNBOUNDED, UNBOUNDED)".to_string()) => Ok(Range { start: None, end: None }) ; "date range unbounded both")]
477    #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null range")]
478    #[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string".to_string())) ; "range type mismatch")]
479    #[test_case(wkt::Value::String("[2026-05-28)".to_string()) => Err(TestConvertError::Convert) ; "range invalid format one part")]
480    #[test_case(wkt::Value::String("[2026-05-28, 2026-05-29, 2026-05-30)".to_string()) => Err(TestConvertError::Convert) ; "range invalid format three parts")]
481    #[test_case(wkt::Value::String("[".to_string()) => Err(TestConvertError::Convert) ; "range too short")]
482    #[test_case(wkt::Value::String("2026-05-28, 2026-05-29".to_string()) => Err(TestConvertError::Convert) ; "range missing brackets")]
483    #[test_case(wkt::Value::String("(2026-05-28, 2026-05-29)".to_string()) => Err(TestConvertError::Convert) ; "range invalid leading parenthesis")]
484    #[test_case(wkt::Value::String("[2026-05-28, 2026-05-29]".to_string()) => Err(TestConvertError::Convert) ; "range invalid trailing square bracket")]
485    #[test_case(wkt::Value::String("[invalid-start, 2026-05-29)".to_string()) => Err(TestConvertError::Convert) ; "range invalid start element")]
486    #[test_case(wkt::Value::String("[2026-05-28, invalid-end)".to_string()) => Err(TestConvertError::Convert) ; "range invalid end element")]
487    fn test_from_sql_range(
488        value: wkt::Value,
489    ) -> Result<Range<google_cloud_type::model::Date>, TestConvertError> {
490        FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
491    }
492
493    #[test_case(wkt::Value::String("[2026-05-28T15:30:00, 2026-05-29T15:30:00)".to_string()) => Ok(Range { start: Some(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)), end: Some(google_cloud_type::model::DateTime::new().set_year(2026).set_month(5).set_day(29).set_hours(15).set_minutes(30).set_seconds(0).set_nanos(0)) }) ; "datetime range bounded")]
494    fn test_from_sql_datetime_range(
495        value: wkt::Value,
496    ) -> Result<Range<google_cloud_type::model::DateTime>, TestConvertError> {
497        FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
498    }
499
500    #[test_case(wkt::Value::String("[1779982200000000, UNBOUNDED)".to_string()) => Ok(Range { start: Some(wkt::Timestamp::clamp(1779982200, 0)), end: None }) ; "timestamp range unbounded end")]
501    fn test_from_sql_timestamp_range(
502        value: wkt::Value,
503    ) -> Result<Range<wkt::Timestamp>, TestConvertError> {
504        FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
505    }
506
507    #[test]
508    fn test_interval_setters() {
509        let interval = Interval::new()
510            .set_years(1)
511            .set_months(2)
512            .set_days(3)
513            .set_hours(4)
514            .set_minutes(5)
515            .set_seconds(6)
516            .set_nanos(789);
517        assert_eq!(
518            interval,
519            Interval {
520                years: 1,
521                months: 2,
522                days: 3,
523                hours: 4,
524                minutes: 5,
525                seconds: 6,
526                nanos: 789,
527            }
528        );
529    }
530
531    #[test]
532    fn test_range_setters() {
533        let d1 = google_cloud_type::model::Date::new()
534            .set_year(2026)
535            .set_month(5)
536            .set_day(28);
537        let d2 = google_cloud_type::model::Date::new()
538            .set_year(2026)
539            .set_month(5)
540            .set_day(29);
541        let d3 = google_cloud_type::model::Date::new()
542            .set_year(2026)
543            .set_month(5)
544            .set_day(30);
545
546        let range = Range::<google_cloud_type::model::Date>::new()
547            .set_start(d1.clone())
548            .set_end(d2.clone());
549        assert_eq!(
550            range,
551            Range {
552                start: Some(d1),
553                end: Some(d2),
554            }
555        );
556
557        let cleared = range
558            .set_or_clear_start(None)
559            .set_or_clear_end(Some(d3.clone()));
560        assert_eq!(
561            cleared,
562            Range {
563                start: None,
564                end: Some(d3),
565            }
566        );
567    }
568
569    #[cfg(any(
570        google_cloud_unstable_bigquery_arrow,
571        google_cloud_unstable_bigquery_storage_read
572    ))]
573    mod arrow_tests {
574        use super::*;
575        use crate::query::SqlValue;
576        use crate::query::from_sql::ArrowCell;
577        use arrow::array::{
578            ArrayRef, BooleanArray, Date32Array, Int64Array, IntervalMonthDayNanoArray,
579            StructArray, TimestampMicrosecondArray,
580        };
581        use arrow::buffer::NullBuffer;
582        use arrow::datatypes::{DataType, Field, Fields, IntervalMonthDayNano};
583        use google_cloud_type::model::{Date, DateTime};
584        use std::sync::Arc;
585        use test_case::test_case;
586
587        fn arrow_val(arr: ArrayRef, row_idx: usize) -> SqlValue {
588            SqlValue::from_inner(SqlValueInner::Arrow(ArrowCell::new(arr, row_idx)))
589        }
590
591        fn interval_arr(val: Option<IntervalMonthDayNano>) -> ArrayRef {
592            Arc::new(IntervalMonthDayNanoArray::from(vec![val]))
593        }
594
595        fn date_range_struct_arr(
596            start: Option<i32>,
597            end: Option<i32>,
598            nulls: Option<NullBuffer>,
599        ) -> ArrayRef {
600            let fields = Fields::from(vec![
601                Field::new("start", DataType::Date32, true),
602                Field::new("end", DataType::Date32, true),
603            ]);
604            let start_col: ArrayRef = Arc::new(Date32Array::from(vec![start]));
605            let end_col: ArrayRef = Arc::new(Date32Array::from(vec![end]));
606            Arc::new(StructArray::new(fields, vec![start_col, end_col], nulls))
607        }
608
609        fn struct_arr_with_fields(names: &[&str], cols: Vec<ArrayRef>) -> ArrayRef {
610            let fields = Fields::from(
611                names
612                    .iter()
613                    .zip(cols.iter())
614                    .map(|(name, col)| Field::new(*name, col.data_type().clone(), true))
615                    .collect::<Vec<_>>(),
616            );
617            Arc::new(StructArray::new(fields, cols, None))
618        }
619
620        #[test_case(
621            interval_arr(Some(IntervalMonthDayNano::new(
622                14,
623                3,
624                ((4 * 3600 + 5 * 60 + 6) * 1_000_000_000) + 789_123_456,
625            ))) => Ok(Interval {
626                years: 1,
627                months: 2,
628                days: 3,
629                hours: 4,
630                minutes: 5,
631                seconds: 6,
632                nanos: 789_123_456,
633            }) ; "positive interval with nanos"
634        )]
635        #[test_case(
636            interval_arr(Some(IntervalMonthDayNano::new(0, 0, 0))) => Ok(Interval::new()) ; "zero interval"
637        )]
638        #[test_case(
639            interval_arr(Some(IntervalMonthDayNano::new(
640                -14,
641                3,
642                -(((4 * 3600 + 5 * 60 + 6) * 1_000_000_000) + 123_000_000),
643            ))) => Ok(Interval {
644                years: -1,
645                months: -2,
646                days: 3,
647                hours: -4,
648                minutes: -5,
649                seconds: -6,
650                nanos: -123_000_000,
651            }) ; "mixed signs interval"
652        )]
653        #[test_case(
654            interval_arr(Some(IntervalMonthDayNano::new(
655                -14,
656                -3,
657                -(((4 * 3600 + 5 * 60 + 6) * 1_000_000_000) + 123_000_000),
658            ))) => Ok(Interval {
659                years: -1,
660                months: -2,
661                days: -3,
662                hours: -4,
663                minutes: -5,
664                seconds: -6,
665                nanos: -123_000_000,
666            }) ; "all negative interval"
667        )]
668        #[test_case(
669            interval_arr(Some(IntervalMonthDayNano::new(0, 0, 744 * 3600 * 1_000_000_000)))
670            => Ok(Interval::new().set_hours(744)) ; "hours beyond a day"
671        )]
672        #[test_case(
673            interval_arr(Some(IntervalMonthDayNano::new(0, 0, -744 * 3600 * 1_000_000_000)))
674            => Ok(Interval::new().set_hours(-744)) ; "negative hours beyond a day"
675        )]
676        #[test_case(interval_arr(None) => Err(TestConvertError::NotNull) ; "null interval")]
677        #[test_case(
678            Arc::new(Int64Array::from(vec![123]))
679            => Err(TestConvertError::TypeMismatch("IntervalMonthDayNanoArray".to_string())) ; "type mismatch interval"
680        )]
681        fn test_from_sql_arrow_interval(arr: ArrayRef) -> Result<Interval, TestConvertError> {
682            Interval::from_value(arrow_val(arr, 0)).map_err(TestConvertError::from)
683        }
684
685        #[test_case(
686            date_range_struct_arr(Some(20_601), Some(20_602), None)
687            => Ok(Range {
688                start: Some(Date::new().set_year(2026).set_month(5).set_day(28)),
689                end: Some(Date::new().set_year(2026).set_month(5).set_day(29)),
690            }) ; "bounded date range"
691        )]
692        #[test_case(
693            date_range_struct_arr(Some(20_601), None, None)
694            => Ok(Range {
695                start: Some(Date::new().set_year(2026).set_month(5).set_day(28)),
696                end: None,
697            }) ; "unbounded end"
698        )]
699        #[test_case(
700            date_range_struct_arr(None, Some(20_602), None)
701            => Ok(Range {
702                start: None,
703                end: Some(Date::new().set_year(2026).set_month(5).set_day(29)),
704            }) ; "unbounded start"
705        )]
706        #[test_case(
707            date_range_struct_arr(None, None, None)
708            => Ok(Range { start: None, end: None }) ; "unbounded both"
709        )]
710        #[test_case(
711            date_range_struct_arr(Some(20_601), Some(20_602), Some(NullBuffer::from(vec![false])))
712            => Err(TestConvertError::NotNull) ; "null struct range"
713        )]
714        #[test_case(
715            Arc::new(Int64Array::from(vec![123]))
716            => Err(TestConvertError::TypeMismatch("StructArray".to_string())) ; "type mismatch range"
717        )]
718        #[test_case(
719            struct_arr_with_fields(&["end"], vec![Arc::new(Date32Array::from(vec![Some(20_602)]))])
720            => Err(TestConvertError::MissingField("start".to_string())) ; "missing start field"
721        )]
722        #[test_case(
723            struct_arr_with_fields(&["start"], vec![Arc::new(Date32Array::from(vec![Some(20_601)]))])
724            => Err(TestConvertError::MissingField("end".to_string())) ; "missing end field"
725        )]
726        #[test_case(
727            struct_arr_with_fields(
728                &["start", "end"],
729                vec![
730                    Arc::new(BooleanArray::from(vec![Some(true)])),
731                    Arc::new(Date32Array::from(vec![Some(20_602)])),
732                ],
733            )
734            => Err(TestConvertError::TypeMismatch("Date32Array".to_string())) ; "invalid start element type"
735        )]
736        #[test_case(
737            struct_arr_with_fields(
738                &["start", "end"],
739                vec![
740                    Arc::new(Date32Array::from(vec![Some(20_601)])),
741                    Arc::new(BooleanArray::from(vec![Some(false)])),
742                ],
743            )
744            => Err(TestConvertError::TypeMismatch("Date32Array".to_string())) ; "invalid end element type"
745        )]
746        fn test_from_sql_arrow_date_range(arr: ArrayRef) -> Result<Range<Date>, TestConvertError> {
747            Range::<Date>::from_value(arrow_val(arr, 0)).map_err(TestConvertError::from)
748        }
749
750        #[test]
751        fn test_from_sql_arrow_datetime_and_timestamp_range() {
752            let arr = struct_arr_with_fields(
753                &["start", "end"],
754                vec![
755                    Arc::new(TimestampMicrosecondArray::from(vec![Some(
756                        1_779_982_200_000_000,
757                    )])),
758                    Arc::new(TimestampMicrosecondArray::from(vec![None])),
759                ],
760            );
761
762            let dt_range = Range::<DateTime>::from_value(arrow_val(arr.clone(), 0))
763                .expect("valid datetime range");
764            assert_eq!(
765                dt_range,
766                Range {
767                    start: Some(
768                        DateTime::new()
769                            .set_year(2026)
770                            .set_month(5)
771                            .set_day(28)
772                            .set_hours(15)
773                            .set_minutes(30)
774                            .set_seconds(0)
775                            .set_nanos(0)
776                    ),
777                    end: None,
778                }
779            );
780
781            let ts_range = Range::<wkt::Timestamp>::from_value(arrow_val(arr, 0))
782                .expect("valid timestamp range");
783            assert_eq!(
784                ts_range,
785                Range {
786                    start: Some(wkt::Timestamp::clamp(1_779_982_200, 0)),
787                    end: None,
788                }
789            );
790        }
791    }
792}