1use crate::error::ConvertError;
21use crate::query::FromSql;
22use crate::query::from_sql::SqlValueInner;
23
24#[derive(Clone, Debug, Default, PartialEq)]
53#[non_exhaustive]
54pub struct Interval {
55 pub years: i32,
57 pub months: i32,
59 pub days: i32,
61 pub hours: i32,
63 pub minutes: i32,
65 pub seconds: i32,
67 pub nanos: i32,
69}
70
71impl Interval {
72 pub fn new() -> Self {
74 Self::default()
75 }
76
77 pub fn set_years(mut self, v: i32) -> Self {
79 self.years = v;
80 self
81 }
82
83 pub fn set_months(mut self, v: i32) -> Self {
85 self.months = v;
86 self
87 }
88
89 pub fn set_days(mut self, v: i32) -> Self {
91 self.days = v;
92 self
93 }
94
95 pub fn set_hours(mut self, v: i32) -> Self {
97 self.hours = v;
98 self
99 }
100
101 pub fn set_minutes(mut self, v: i32) -> Self {
103 self.minutes = v;
104 self
105 }
106
107 pub fn set_seconds(mut self, v: i32) -> Self {
109 self.seconds = v;
110 self
111 }
112
113 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 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 let days = days_str
167 .parse::<i32>()
168 .map_err(|e| ConvertError::Convert(Box::new(e)))?;
169
170 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 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 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
262pub 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#[derive(Clone, Debug, PartialEq)]
305#[non_exhaustive]
306pub struct Range<T: RangeElement> {
307 pub start: Option<T>,
309 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 pub fn new() -> Self {
322 Self {
323 start: None,
324 end: None,
325 }
326 }
327
328 pub fn set_start<V: Into<T>>(mut self, v: V) -> Self {
330 self.start = Some(v.into());
331 self
332 }
333
334 pub fn set_or_clear_start(mut self, v: Option<T>) -> Self {
336 self.start = v;
337 self
338 }
339
340 pub fn set_end<V: Into<T>>(mut self, v: V) -> Self {
342 self.end = Some(v.into());
343 self
344 }
345
346 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 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 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}