use crate::error::ConvertError;
use crate::query::FromSql;
use crate::query::from_sql::SqlValueInner;
#[derive(Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct Interval {
pub years: i32,
pub months: i32,
pub days: i32,
pub hours: i32,
pub minutes: i32,
pub seconds: i32,
pub nanos: i32,
}
impl Interval {
pub fn new() -> Self {
Self::default()
}
pub fn set_years(mut self, v: i32) -> Self {
self.years = v;
self
}
pub fn set_months(mut self, v: i32) -> Self {
self.months = v;
self
}
pub fn set_days(mut self, v: i32) -> Self {
self.days = v;
self
}
pub fn set_hours(mut self, v: i32) -> Self {
self.hours = v;
self
}
pub fn set_minutes(mut self, v: i32) -> Self {
self.minutes = v;
self
}
pub fn set_seconds(mut self, v: i32) -> Self {
self.seconds = v;
self
}
pub fn set_nanos(mut self, v: i32) -> Self {
self.nanos = v;
self
}
}
impl FromSql for Interval {
fn from_value(value: crate::query::SqlValue) -> Result<Self, ConvertError> {
match value.inner {
SqlValueInner::String(s) => {
let mut parts = s.split_whitespace();
let ym_str = parts.next();
let days_str = parts.next();
let time_str = parts.next();
let extra = parts.next();
let (ym_str, days_str, time_str) = match (ym_str, days_str, time_str, extra) {
(Some(ym), Some(d), Some(t), None) => (ym, d, t),
_ => {
return Err(ConvertError::Convert(
format!("invalid interval format: expected 3 parts, got `{s}`").into(),
));
}
};
let ym_neg = ym_str.starts_with('-');
let ym_content = if ym_neg { &ym_str[1..] } else { ym_str };
let mut ym_parts = ym_content.split('-');
let y_str = ym_parts.next();
let m_str = ym_parts.next();
let ym_extra = ym_parts.next();
let (y_str, m_str) = match (y_str, m_str, ym_extra) {
(Some(y), Some(m), None) => (y, m),
_ => {
return Err(ConvertError::Convert(
"invalid interval year-month format".into(),
));
}
};
let ym_sign = if ym_neg { -1 } else { 1 };
let years = y_str
.parse::<i32>()
.map_err(|e| ConvertError::Convert(Box::new(e)))?
* ym_sign;
let months = m_str
.parse::<i32>()
.map_err(|e| ConvertError::Convert(Box::new(e)))?
* ym_sign;
let days = days_str
.parse::<i32>()
.map_err(|e| ConvertError::Convert(Box::new(e)))?;
let time_neg = time_str.starts_with('-');
let time_content = if time_neg { &time_str[1..] } else { time_str };
let (hms, frac) = time_content.split_once('.').unwrap_or((time_content, "0"));
let mut hms_parts = hms.split(':');
let (h_str, m_str, s_str) = match (
hms_parts.next(),
hms_parts.next(),
hms_parts.next(),
hms_parts.next(),
) {
(Some(h), Some(m), Some(s), None) => (h, m, s),
_ => {
return Err(ConvertError::Convert("invalid interval time format".into()));
}
};
let time_sign = if time_neg { -1 } else { 1 };
let hours = h_str
.parse::<i32>()
.map_err(|e| ConvertError::Convert(Box::new(e)))?
* time_sign;
let minutes = m_str
.parse::<i32>()
.map_err(|e| ConvertError::Convert(Box::new(e)))?
* time_sign;
let seconds = s_str
.parse::<i32>()
.map_err(|e| ConvertError::Convert(Box::new(e)))?
* time_sign;
let nanos = format!("{frac:0<9.9}")
.parse::<i32>()
.map_err(|e| ConvertError::Convert(Box::new(e)))?
* time_sign;
Ok(Interval {
years,
months,
days,
hours,
minutes,
seconds,
nanos,
})
}
SqlValueInner::Null => Err(ConvertError::NotNull),
#[cfg(any(
google_cloud_unstable_bigquery_arrow,
google_cloud_unstable_bigquery_storage_read
))]
SqlValueInner::Arrow(cell) => {
let v = cell.as_interval()?;
let ym_sign = if v.months < 0 { -1 } else { 1 };
let total_months = v.months.unsigned_abs();
let years = (total_months / 12) as i32 * ym_sign;
let months = (total_months % 12) as i32 * ym_sign;
let time_sign = if v.nanoseconds < 0 { -1 } else { 1 };
let total_nanos = v.nanoseconds.unsigned_abs();
let nanos = (total_nanos % 1_000_000_000) as i32 * time_sign;
let total_secs = total_nanos / 1_000_000_000;
let seconds = (total_secs % 60) as i32 * time_sign;
let total_mins = total_secs / 60;
let minutes = (total_mins % 60) as i32 * time_sign;
let hours = (total_mins / 60) as i32 * time_sign;
Ok(Interval {
years,
months,
days: v.days,
hours,
minutes,
seconds,
nanos,
})
}
other => Err(ConvertError::type_mismatch("string", &other)),
}
}
}
mod sealed {
pub trait RangeElement {}
impl RangeElement for google_cloud_type::model::Date {}
impl RangeElement for google_cloud_type::model::DateTime {}
impl RangeElement for wkt::Timestamp {}
}
pub trait RangeElement: FromSql + sealed::RangeElement {}
impl RangeElement for google_cloud_type::model::Date {}
impl RangeElement for google_cloud_type::model::DateTime {}
impl RangeElement for wkt::Timestamp {}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Range<T: RangeElement> {
pub start: Option<T>,
pub end: Option<T>,
}
impl<T: RangeElement> Default for Range<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: RangeElement> Range<T> {
pub fn new() -> Self {
Self {
start: None,
end: None,
}
}
pub fn set_start<V: Into<T>>(mut self, v: V) -> Self {
self.start = Some(v.into());
self
}
pub fn set_or_clear_start(mut self, v: Option<T>) -> Self {
self.start = v;
self
}
pub fn set_end<V: Into<T>>(mut self, v: V) -> Self {
self.end = Some(v.into());
self
}
pub fn set_or_clear_end(mut self, v: Option<T>) -> Self {
self.end = v;
self
}
}
impl<T: RangeElement> FromSql for Range<T> {
fn from_value(value: crate::query::SqlValue) -> Result<Self, ConvertError> {
match value.inner {
SqlValueInner::String(s) => {
let trimmed = s.trim();
let content = trimmed
.strip_prefix('[')
.and_then(|c| c.strip_suffix(')'))
.ok_or_else(|| {
ConvertError::Convert(
"invalid range format: missing enclosing brackets".into(),
)
})?;
let parts: Vec<&str> = content.split(',').collect();
if parts.len() != 2 {
return Err(ConvertError::Convert(
format!(
"invalid range format: expected 2 parts, got {}",
parts.len()
)
.into(),
));
}
let start_str = parts[0].trim();
let end_str = parts[1].trim();
let start = if start_str.is_empty() || start_str == "UNBOUNDED" {
None
} else {
Some(T::from_value(crate::query::SqlValue::from_inner(
SqlValueInner::String(start_str.to_string()),
))?)
};
let end = if end_str.is_empty() || end_str == "UNBOUNDED" {
None
} else {
Some(T::from_value(crate::query::SqlValue::from_inner(
SqlValueInner::String(end_str.to_string()),
))?)
};
Ok(Range { start, end })
}
SqlValueInner::Null => Err(ConvertError::NotNull),
#[cfg(any(
google_cloud_unstable_bigquery_arrow,
google_cloud_unstable_bigquery_storage_read
))]
SqlValueInner::Arrow(cell) => {
if cell.is_null() {
return Err(ConvertError::NotNull);
}
let take = |key: &str| -> Result<Option<T>, ConvertError> {
let field_cell = cell.struct_field_cell(&key)?;
Option::<T>::from_value(crate::query::SqlValue::from_inner(
SqlValueInner::Arrow(field_cell),
))
};
let start = take("start")?;
let end = take("end")?;
Ok(Range { start, end })
}
other => Err(ConvertError::type_mismatch("string", &other)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[derive(Debug, PartialEq)]
enum TestConvertError {
NotNull,
TypeMismatch(String),
Convert,
MissingField(String),
}
impl From<ConvertError> for TestConvertError {
fn from(err: ConvertError) -> Self {
match err {
ConvertError::NotNull => Self::NotNull,
ConvertError::TypeMismatch { expected, .. } => Self::TypeMismatch(expected),
ConvertError::Convert(_) => Self::Convert,
ConvertError::MissingField(f) => Self::MissingField(f),
}
}
}
#[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")]
#[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")]
#[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")]
#[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")]
#[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")]
#[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")]
#[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")]
#[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")]
#[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")]
#[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null interval")]
#[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string".to_string())) ; "type mismatch interval")]
#[test_case(wkt::Value::String("".to_string()) => Err(TestConvertError::Convert) ; "empty interval string")]
#[test_case(wkt::Value::String("1-2 3".to_string()) => Err(TestConvertError::Convert) ; "invalid interval parts count")]
#[test_case(wkt::Value::String("1 3 4:05:06".to_string()) => Err(TestConvertError::Convert) ; "invalid year-month format")]
#[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")]
#[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")]
#[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")]
#[test_case(wkt::Value::String("1-2 3 4:05".to_string()) => Err(TestConvertError::Convert) ; "invalid time format")]
#[test_case(wkt::Value::String("1-2 3 4:05:06:07".to_string()) => Err(TestConvertError::Convert) ; "too many time parts")]
#[test_case(wkt::Value::String("1-2 3 4:05:06.x".to_string()) => Err(TestConvertError::Convert) ; "invalid subsecond")]
fn test_from_sql_interval(value: wkt::Value) -> Result<Interval, TestConvertError> {
FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
}
#[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")]
#[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")]
#[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")]
#[test_case(wkt::Value::String("[UNBOUNDED, UNBOUNDED)".to_string()) => Ok(Range { start: None, end: None }) ; "date range unbounded both")]
#[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null range")]
#[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string".to_string())) ; "range type mismatch")]
#[test_case(wkt::Value::String("[2026-05-28)".to_string()) => Err(TestConvertError::Convert) ; "range invalid format one part")]
#[test_case(wkt::Value::String("[2026-05-28, 2026-05-29, 2026-05-30)".to_string()) => Err(TestConvertError::Convert) ; "range invalid format three parts")]
#[test_case(wkt::Value::String("[".to_string()) => Err(TestConvertError::Convert) ; "range too short")]
#[test_case(wkt::Value::String("2026-05-28, 2026-05-29".to_string()) => Err(TestConvertError::Convert) ; "range missing brackets")]
#[test_case(wkt::Value::String("(2026-05-28, 2026-05-29)".to_string()) => Err(TestConvertError::Convert) ; "range invalid leading parenthesis")]
#[test_case(wkt::Value::String("[2026-05-28, 2026-05-29]".to_string()) => Err(TestConvertError::Convert) ; "range invalid trailing square bracket")]
#[test_case(wkt::Value::String("[invalid-start, 2026-05-29)".to_string()) => Err(TestConvertError::Convert) ; "range invalid start element")]
#[test_case(wkt::Value::String("[2026-05-28, invalid-end)".to_string()) => Err(TestConvertError::Convert) ; "range invalid end element")]
fn test_from_sql_range(
value: wkt::Value,
) -> Result<Range<google_cloud_type::model::Date>, TestConvertError> {
FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
}
#[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")]
fn test_from_sql_datetime_range(
value: wkt::Value,
) -> Result<Range<google_cloud_type::model::DateTime>, TestConvertError> {
FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
}
#[test_case(wkt::Value::String("[1779982200000000, UNBOUNDED)".to_string()) => Ok(Range { start: Some(wkt::Timestamp::clamp(1779982200, 0)), end: None }) ; "timestamp range unbounded end")]
fn test_from_sql_timestamp_range(
value: wkt::Value,
) -> Result<Range<wkt::Timestamp>, TestConvertError> {
FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
}
#[test]
fn test_interval_setters() {
let interval = Interval::new()
.set_years(1)
.set_months(2)
.set_days(3)
.set_hours(4)
.set_minutes(5)
.set_seconds(6)
.set_nanos(789);
assert_eq!(
interval,
Interval {
years: 1,
months: 2,
days: 3,
hours: 4,
minutes: 5,
seconds: 6,
nanos: 789,
}
);
}
#[test]
fn test_range_setters() {
let d1 = google_cloud_type::model::Date::new()
.set_year(2026)
.set_month(5)
.set_day(28);
let d2 = google_cloud_type::model::Date::new()
.set_year(2026)
.set_month(5)
.set_day(29);
let d3 = google_cloud_type::model::Date::new()
.set_year(2026)
.set_month(5)
.set_day(30);
let range = Range::<google_cloud_type::model::Date>::new()
.set_start(d1.clone())
.set_end(d2.clone());
assert_eq!(
range,
Range {
start: Some(d1),
end: Some(d2),
}
);
let cleared = range
.set_or_clear_start(None)
.set_or_clear_end(Some(d3.clone()));
assert_eq!(
cleared,
Range {
start: None,
end: Some(d3),
}
);
}
#[cfg(any(
google_cloud_unstable_bigquery_arrow,
google_cloud_unstable_bigquery_storage_read
))]
mod arrow_tests {
use super::*;
use crate::query::SqlValue;
use crate::query::from_sql::ArrowCell;
use arrow::array::{
ArrayRef, BooleanArray, Date32Array, Int64Array, IntervalMonthDayNanoArray,
StructArray, TimestampMicrosecondArray,
};
use arrow::buffer::NullBuffer;
use arrow::datatypes::{DataType, Field, Fields, IntervalMonthDayNano};
use google_cloud_type::model::{Date, DateTime};
use std::sync::Arc;
use test_case::test_case;
fn arrow_val(arr: ArrayRef, row_idx: usize) -> SqlValue {
SqlValue::from_inner(SqlValueInner::Arrow(ArrowCell::new(arr, row_idx)))
}
fn interval_arr(val: Option<IntervalMonthDayNano>) -> ArrayRef {
Arc::new(IntervalMonthDayNanoArray::from(vec![val]))
}
fn date_range_struct_arr(
start: Option<i32>,
end: Option<i32>,
nulls: Option<NullBuffer>,
) -> ArrayRef {
let fields = Fields::from(vec![
Field::new("start", DataType::Date32, true),
Field::new("end", DataType::Date32, true),
]);
let start_col: ArrayRef = Arc::new(Date32Array::from(vec![start]));
let end_col: ArrayRef = Arc::new(Date32Array::from(vec![end]));
Arc::new(StructArray::new(fields, vec![start_col, end_col], nulls))
}
fn struct_arr_with_fields(names: &[&str], cols: Vec<ArrayRef>) -> ArrayRef {
let fields = Fields::from(
names
.iter()
.zip(cols.iter())
.map(|(name, col)| Field::new(*name, col.data_type().clone(), true))
.collect::<Vec<_>>(),
);
Arc::new(StructArray::new(fields, cols, None))
}
#[test_case(
interval_arr(Some(IntervalMonthDayNano::new(
14,
3,
((4 * 3600 + 5 * 60 + 6) * 1_000_000_000) + 789_123_456,
))) => Ok(Interval {
years: 1,
months: 2,
days: 3,
hours: 4,
minutes: 5,
seconds: 6,
nanos: 789_123_456,
}) ; "positive interval with nanos"
)]
#[test_case(
interval_arr(Some(IntervalMonthDayNano::new(0, 0, 0))) => Ok(Interval::new()) ; "zero interval"
)]
#[test_case(
interval_arr(Some(IntervalMonthDayNano::new(
-14,
3,
-(((4 * 3600 + 5 * 60 + 6) * 1_000_000_000) + 123_000_000),
))) => Ok(Interval {
years: -1,
months: -2,
days: 3,
hours: -4,
minutes: -5,
seconds: -6,
nanos: -123_000_000,
}) ; "mixed signs interval"
)]
#[test_case(
interval_arr(Some(IntervalMonthDayNano::new(
-14,
-3,
-(((4 * 3600 + 5 * 60 + 6) * 1_000_000_000) + 123_000_000),
))) => Ok(Interval {
years: -1,
months: -2,
days: -3,
hours: -4,
minutes: -5,
seconds: -6,
nanos: -123_000_000,
}) ; "all negative interval"
)]
#[test_case(
interval_arr(Some(IntervalMonthDayNano::new(0, 0, 744 * 3600 * 1_000_000_000)))
=> Ok(Interval::new().set_hours(744)) ; "hours beyond a day"
)]
#[test_case(
interval_arr(Some(IntervalMonthDayNano::new(0, 0, -744 * 3600 * 1_000_000_000)))
=> Ok(Interval::new().set_hours(-744)) ; "negative hours beyond a day"
)]
#[test_case(interval_arr(None) => Err(TestConvertError::NotNull) ; "null interval")]
#[test_case(
Arc::new(Int64Array::from(vec![123]))
=> Err(TestConvertError::TypeMismatch("IntervalMonthDayNanoArray".to_string())) ; "type mismatch interval"
)]
fn test_from_sql_arrow_interval(arr: ArrayRef) -> Result<Interval, TestConvertError> {
Interval::from_value(arrow_val(arr, 0)).map_err(TestConvertError::from)
}
#[test_case(
date_range_struct_arr(Some(20_601), Some(20_602), None)
=> Ok(Range {
start: Some(Date::new().set_year(2026).set_month(5).set_day(28)),
end: Some(Date::new().set_year(2026).set_month(5).set_day(29)),
}) ; "bounded date range"
)]
#[test_case(
date_range_struct_arr(Some(20_601), None, None)
=> Ok(Range {
start: Some(Date::new().set_year(2026).set_month(5).set_day(28)),
end: None,
}) ; "unbounded end"
)]
#[test_case(
date_range_struct_arr(None, Some(20_602), None)
=> Ok(Range {
start: None,
end: Some(Date::new().set_year(2026).set_month(5).set_day(29)),
}) ; "unbounded start"
)]
#[test_case(
date_range_struct_arr(None, None, None)
=> Ok(Range { start: None, end: None }) ; "unbounded both"
)]
#[test_case(
date_range_struct_arr(Some(20_601), Some(20_602), Some(NullBuffer::from(vec![false])))
=> Err(TestConvertError::NotNull) ; "null struct range"
)]
#[test_case(
Arc::new(Int64Array::from(vec![123]))
=> Err(TestConvertError::TypeMismatch("StructArray".to_string())) ; "type mismatch range"
)]
#[test_case(
struct_arr_with_fields(&["end"], vec![Arc::new(Date32Array::from(vec![Some(20_602)]))])
=> Err(TestConvertError::MissingField("start".to_string())) ; "missing start field"
)]
#[test_case(
struct_arr_with_fields(&["start"], vec![Arc::new(Date32Array::from(vec![Some(20_601)]))])
=> Err(TestConvertError::MissingField("end".to_string())) ; "missing end field"
)]
#[test_case(
struct_arr_with_fields(
&["start", "end"],
vec![
Arc::new(BooleanArray::from(vec![Some(true)])),
Arc::new(Date32Array::from(vec![Some(20_602)])),
],
)
=> Err(TestConvertError::TypeMismatch("Date32Array".to_string())) ; "invalid start element type"
)]
#[test_case(
struct_arr_with_fields(
&["start", "end"],
vec![
Arc::new(Date32Array::from(vec![Some(20_601)])),
Arc::new(BooleanArray::from(vec![Some(false)])),
],
)
=> Err(TestConvertError::TypeMismatch("Date32Array".to_string())) ; "invalid end element type"
)]
fn test_from_sql_arrow_date_range(arr: ArrayRef) -> Result<Range<Date>, TestConvertError> {
Range::<Date>::from_value(arrow_val(arr, 0)).map_err(TestConvertError::from)
}
#[test]
fn test_from_sql_arrow_datetime_and_timestamp_range() {
let arr = struct_arr_with_fields(
&["start", "end"],
vec![
Arc::new(TimestampMicrosecondArray::from(vec![Some(
1_779_982_200_000_000,
)])),
Arc::new(TimestampMicrosecondArray::from(vec![None])),
],
);
let dt_range = Range::<DateTime>::from_value(arrow_val(arr.clone(), 0))
.expect("valid datetime range");
assert_eq!(
dt_range,
Range {
start: Some(
DateTime::new()
.set_year(2026)
.set_month(5)
.set_day(28)
.set_hours(15)
.set_minutes(30)
.set_seconds(0)
.set_nanos(0)
),
end: None,
}
);
let ts_range = Range::<wkt::Timestamp>::from_value(arrow_val(arr, 0))
.expect("valid timestamp range");
assert_eq!(
ts_range,
Range {
start: Some(wkt::Timestamp::clamp(1_779_982_200, 0)),
end: None,
}
);
}
}
}