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 other => Err(ConvertError::type_mismatch("string", &other)),
219 }
220 }
221}
222
223mod sealed {
224 pub trait RangeElement {}
226
227 impl RangeElement for google_cloud_type::model::Date {}
228 impl RangeElement for google_cloud_type::model::DateTime {}
229 impl RangeElement for wkt::Timestamp {}
230}
231
232pub trait RangeElement: FromSql + sealed::RangeElement {}
240
241impl RangeElement for google_cloud_type::model::Date {}
242impl RangeElement for google_cloud_type::model::DateTime {}
243impl RangeElement for wkt::Timestamp {}
244
245#[derive(Clone, Debug, PartialEq)]
275#[non_exhaustive]
276pub struct Range<T: RangeElement> {
277 pub start: Option<T>,
279 pub end: Option<T>,
281}
282
283impl<T: RangeElement> Default for Range<T> {
284 fn default() -> Self {
285 Self::new()
286 }
287}
288
289impl<T: RangeElement> Range<T> {
290 pub fn new() -> Self {
292 Self {
293 start: None,
294 end: None,
295 }
296 }
297
298 pub fn set_start<V: Into<T>>(mut self, v: V) -> Self {
300 self.start = Some(v.into());
301 self
302 }
303
304 pub fn set_or_clear_start(mut self, v: Option<T>) -> Self {
306 self.start = v;
307 self
308 }
309
310 pub fn set_end<V: Into<T>>(mut self, v: V) -> Self {
312 self.end = Some(v.into());
313 self
314 }
315
316 pub fn set_or_clear_end(mut self, v: Option<T>) -> Self {
318 self.end = v;
319 self
320 }
321}
322
323impl<T: RangeElement> FromSql for Range<T> {
324 fn from_value(value: crate::query::SqlValue) -> Result<Self, ConvertError> {
325 match value.inner {
326 SqlValueInner::String(s) => {
327 let trimmed = s.trim();
328 let content = trimmed
330 .strip_prefix('[')
331 .and_then(|c| c.strip_suffix(')'))
332 .ok_or_else(|| {
333 ConvertError::Convert(
334 "invalid range format: missing enclosing brackets".into(),
335 )
336 })?;
337
338 let parts: Vec<&str> = content.split(',').collect();
340 if parts.len() != 2 {
341 return Err(ConvertError::Convert(
342 format!(
343 "invalid range format: expected 2 parts, got {}",
344 parts.len()
345 )
346 .into(),
347 ));
348 }
349
350 let start_str = parts[0].trim();
351 let end_str = parts[1].trim();
352
353 let start = if start_str.is_empty() || start_str == "UNBOUNDED" {
354 None
355 } else {
356 Some(T::from_value(crate::query::SqlValue::from_inner(
357 SqlValueInner::String(start_str.to_string()),
358 ))?)
359 };
360
361 let end = if end_str.is_empty() || end_str == "UNBOUNDED" {
362 None
363 } else {
364 Some(T::from_value(crate::query::SqlValue::from_inner(
365 SqlValueInner::String(end_str.to_string()),
366 ))?)
367 };
368
369 Ok(Range { start, end })
370 }
371 SqlValueInner::Null => Err(ConvertError::NotNull),
372 other => Err(ConvertError::type_mismatch("string", &other)),
373 }
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use test_case::test_case;
381
382 #[derive(Debug, PartialEq)]
383 enum TestConvertError {
384 NotNull,
385 TypeMismatch(String),
386 Convert(String),
387 }
388
389 impl From<ConvertError> for TestConvertError {
390 fn from(err: ConvertError) -> Self {
391 match err {
392 ConvertError::NotNull => Self::NotNull,
393 ConvertError::TypeMismatch { expected, .. } => Self::TypeMismatch(expected),
394 ConvertError::Convert(e) => Self::Convert(e.to_string()),
395 ConvertError::MissingField(f) => Self::Convert(format!("missing field: {f}")),
396 }
397 }
398 }
399
400 #[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")]
401 #[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")]
402 #[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")]
403 #[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")]
404 #[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")]
405 #[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")]
406 #[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")]
407 #[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")]
408 #[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")]
409 #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null interval")]
410 #[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string".to_string())) ; "type mismatch interval")]
411 #[test_case(wkt::Value::String("".to_string()) => Err(TestConvertError::Convert("invalid interval format: expected 3 parts, got ``".to_string())) ; "empty interval string")]
412 #[test_case(wkt::Value::String("1-2 3".to_string()) => Err(TestConvertError::Convert("invalid interval format: expected 3 parts, got `1-2 3`".to_string())) ; "invalid interval parts count")]
413 #[test_case(wkt::Value::String("1 3 4:05:06".to_string()) => Err(TestConvertError::Convert("invalid interval year-month format".to_string())) ; "invalid year-month format")]
414 #[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")]
415 #[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")]
416 #[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")]
417 #[test_case(wkt::Value::String("1-2 3 4:05".to_string()) => Err(TestConvertError::Convert("invalid interval time format".to_string())) ; "invalid time format")]
418 #[test_case(wkt::Value::String("1-2 3 4:05:06:07".to_string()) => Err(TestConvertError::Convert("invalid interval time format".to_string())) ; "too many time parts")]
419 #[test_case(wkt::Value::String("1-2 3 4:05:06.x".to_string()) => Err(TestConvertError::Convert("invalid digit found in string".to_string())) ; "invalid subsecond")]
420 fn test_from_sql_interval(value: wkt::Value) -> Result<Interval, TestConvertError> {
421 FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
422 }
423
424 #[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")]
425 #[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")]
426 #[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")]
427 #[test_case(wkt::Value::String("[UNBOUNDED, UNBOUNDED)".to_string()) => Ok(Range { start: None, end: None }) ; "date range unbounded both")]
428 #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null range")]
429 #[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string".to_string())) ; "range type mismatch")]
430 #[test_case(wkt::Value::String("[2026-05-28)".to_string()) => Err(TestConvertError::Convert("invalid range format: expected 2 parts, got 1".to_string())) ; "range invalid format one part")]
431 #[test_case(wkt::Value::String("[2026-05-28, 2026-05-29, 2026-05-30)".to_string()) => Err(TestConvertError::Convert("invalid range format: expected 2 parts, got 3".to_string())) ; "range invalid format three parts")]
432 #[test_case(wkt::Value::String("[".to_string()) => Err(TestConvertError::Convert("invalid range format: missing enclosing brackets".to_string())) ; "range too short")]
433 #[test_case(wkt::Value::String("2026-05-28, 2026-05-29".to_string()) => Err(TestConvertError::Convert("invalid range format: missing enclosing brackets".to_string())) ; "range missing brackets")]
434 #[test_case(wkt::Value::String("(2026-05-28, 2026-05-29)".to_string()) => Err(TestConvertError::Convert("invalid range format: missing enclosing brackets".to_string())) ; "range invalid leading parenthesis")]
435 #[test_case(wkt::Value::String("[2026-05-28, 2026-05-29]".to_string()) => Err(TestConvertError::Convert("invalid range format: missing enclosing brackets".to_string())) ; "range invalid trailing square bracket")]
436 #[test_case(wkt::Value::String("[invalid-start, 2026-05-29)".to_string()) => Err(TestConvertError::Convert("the 'year' component could not be parsed".to_string())) ; "range invalid start element")]
437 #[test_case(wkt::Value::String("[2026-05-28, invalid-end)".to_string()) => Err(TestConvertError::Convert("the 'year' component could not be parsed".to_string())) ; "range invalid end element")]
438 fn test_from_sql_range(
439 value: wkt::Value,
440 ) -> Result<Range<google_cloud_type::model::Date>, TestConvertError> {
441 FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
442 }
443
444 #[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")]
445 fn test_from_sql_datetime_range(
446 value: wkt::Value,
447 ) -> Result<Range<google_cloud_type::model::DateTime>, TestConvertError> {
448 FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
449 }
450
451 #[test_case(wkt::Value::String("[1779982200000000, UNBOUNDED)".to_string()) => Ok(Range { start: Some(wkt::Timestamp::clamp(1779982200, 0)), end: None }) ; "timestamp range unbounded end")]
452 fn test_from_sql_timestamp_range(
453 value: wkt::Value,
454 ) -> Result<Range<wkt::Timestamp>, TestConvertError> {
455 FromSql::from_value(crate::query::SqlValue::new(value)).map_err(TestConvertError::from)
456 }
457
458 #[test]
459 fn test_interval_setters() {
460 let interval = Interval::new()
461 .set_years(1)
462 .set_months(2)
463 .set_days(3)
464 .set_hours(4)
465 .set_minutes(5)
466 .set_seconds(6)
467 .set_nanos(789);
468 assert_eq!(
469 interval,
470 Interval {
471 years: 1,
472 months: 2,
473 days: 3,
474 hours: 4,
475 minutes: 5,
476 seconds: 6,
477 nanos: 789,
478 }
479 );
480 }
481
482 #[test]
483 fn test_range_setters() {
484 let d1 = google_cloud_type::model::Date::new()
485 .set_year(2026)
486 .set_month(5)
487 .set_day(28);
488 let d2 = google_cloud_type::model::Date::new()
489 .set_year(2026)
490 .set_month(5)
491 .set_day(29);
492 let d3 = google_cloud_type::model::Date::new()
493 .set_year(2026)
494 .set_month(5)
495 .set_day(30);
496
497 let range = Range::<google_cloud_type::model::Date>::new()
498 .set_start(d1.clone())
499 .set_end(d2.clone());
500 assert_eq!(
501 range,
502 Range {
503 start: Some(d1),
504 end: Some(d2),
505 }
506 );
507
508 let cleared = range
509 .set_or_clear_start(None)
510 .set_or_clear_end(Some(d3.clone()));
511 assert_eq!(
512 cleared,
513 Range {
514 start: None,
515 end: Some(d3),
516 }
517 );
518 }
519}