1use crate::error::ConvertError;
21use crate::query::FromSql;
22use crate::query::from_sql::parse_time;
23
24#[derive(Clone, Debug, Default, PartialEq)]
53pub struct Interval {
54 pub years: i32,
56 pub months: i32,
58 pub days: i32,
60 pub hours: i32,
62 pub minutes: i32,
64 pub seconds: i32,
66 pub nanos: i32,
68}
69
70impl FromSql for Interval {
71 fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
72 match value {
73 wkt::Value::String(s) => {
74 let mut parts = s.split_whitespace();
75 let ym_str = parts.next();
76 let days_str = parts.next();
77 let time_str = parts.next();
78 let extra = parts.next();
79
80 let (ym_str, days_str, time_str) = match (ym_str, days_str, time_str, extra) {
81 (Some(ym), Some(d), Some(t), None) => (ym, d, t),
82 _ => {
83 return Err(ConvertError::Convert(
84 format!("invalid interval format: expected 3 parts, got `{s}`").into(),
85 ));
86 }
87 };
88
89 let ym_neg = ym_str.starts_with('-');
91 let ym_content = if ym_neg { &ym_str[1..] } else { ym_str };
92 let mut ym_parts = ym_content.split('-');
93 let y_str = ym_parts.next();
94 let m_str = ym_parts.next();
95 let ym_extra = ym_parts.next();
96
97 let (y_str, m_str) = match (y_str, m_str, ym_extra) {
98 (Some(y), Some(m), None) => (y, m),
99 _ => {
100 return Err(ConvertError::Convert(
101 "invalid interval year-month format".into(),
102 ));
103 }
104 };
105 let ym_sign = if ym_neg { -1 } else { 1 };
106 let years = y_str
107 .parse::<i32>()
108 .map_err(|e| ConvertError::Convert(Box::new(e)))?
109 * ym_sign;
110 let months = m_str
111 .parse::<i32>()
112 .map_err(|e| ConvertError::Convert(Box::new(e)))?
113 * ym_sign;
114
115 let days = days_str
117 .parse::<i32>()
118 .map_err(|e| ConvertError::Convert(Box::new(e)))?;
119
120 let time_neg = time_str.starts_with('-');
122 let time_content = if time_neg { &time_str[1..] } else { time_str };
123 let t = parse_time(time_content)?;
124 let time_sign = if time_neg { -1 } else { 1 };
125 let hours = t.hour() as i32 * time_sign;
126 let minutes = t.minute() as i32 * time_sign;
127 let seconds = t.second() as i32 * time_sign;
128 let nanos = t.nanosecond() as i32 * time_sign;
129
130 Ok(Interval {
131 years,
132 months,
133 days,
134 hours,
135 minutes,
136 seconds,
137 nanos,
138 })
139 }
140 wkt::Value::Null => Err(ConvertError::NotNull),
141 other => Err(ConvertError::TypeMismatch {
142 expected: "string",
143 got: other,
144 }),
145 }
146 }
147}
148
149#[derive(Clone, Debug, PartialEq)]
179pub struct Range<T> {
180 pub start: Option<T>,
182 pub end: Option<T>,
184}
185
186impl<T: FromSql> FromSql for Range<T> {
187 fn from_sql(value: wkt::Value) -> Result<Self, ConvertError> {
188 match value {
189 wkt::Value::String(s) => {
190 let trimmed = s.trim();
191 let content = trimmed
193 .strip_prefix('[')
194 .and_then(|c| c.strip_suffix(')'))
195 .ok_or_else(|| {
196 ConvertError::Convert(
197 "invalid range format: missing enclosing brackets".into(),
198 )
199 })?;
200
201 let parts: Vec<&str> = content.split(',').collect();
203 if parts.len() != 2 {
204 return Err(ConvertError::Convert(
205 format!(
206 "invalid range format: expected 2 parts, got {}",
207 parts.len()
208 )
209 .into(),
210 ));
211 }
212
213 let start_str = parts[0].trim();
214 let end_str = parts[1].trim();
215
216 let start = if start_str.is_empty() || start_str == "UNBOUNDED" {
217 None
218 } else {
219 Some(T::from_sql(wkt::Value::String(start_str.to_string()))?)
220 };
221
222 let end = if end_str.is_empty() || end_str == "UNBOUNDED" {
223 None
224 } else {
225 Some(T::from_sql(wkt::Value::String(end_str.to_string()))?)
226 };
227
228 Ok(Range { start, end })
229 }
230 wkt::Value::Null => Err(ConvertError::NotNull),
231 other => Err(ConvertError::TypeMismatch {
232 expected: "string",
233 got: other,
234 }),
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use test_case::test_case;
243
244 #[derive(Debug, PartialEq)]
245 enum TestConvertError {
246 NotNull,
247 TypeMismatch(&'static str),
248 Convert(String),
249 }
250
251 impl From<ConvertError> for TestConvertError {
252 fn from(err: ConvertError) -> Self {
253 match err {
254 ConvertError::NotNull => Self::NotNull,
255 ConvertError::TypeMismatch { expected, .. } => Self::TypeMismatch(expected),
256 ConvertError::Convert(e) => Self::Convert(e.to_string()),
257 ConvertError::MissingField(f) => Self::Convert(format!("missing field: {f}")),
258 }
259 }
260 }
261
262 #[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")]
263 #[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")]
264 #[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")]
265 #[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")]
266 #[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")]
267 #[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")]
268 #[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")]
269 #[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")]
270 #[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")]
271 #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null interval")]
272 #[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string")) ; "type mismatch interval")]
273 #[test_case(wkt::Value::String("".to_string()) => Err(TestConvertError::Convert("invalid interval format: expected 3 parts, got ``".to_string())) ; "empty interval string")]
274 #[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")]
275 #[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")]
276 #[test_case(wkt::Value::String("1-2 3 4:05".to_string()) => Err(TestConvertError::Convert("a character literal was not valid".to_string())) ; "invalid time format")]
277 fn test_from_sql_interval(value: wkt::Value) -> Result<Interval, TestConvertError> {
278 FromSql::from_sql(value).map_err(TestConvertError::from)
279 }
280
281 #[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")]
282 #[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")]
283 #[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")]
284 #[test_case(wkt::Value::String("[UNBOUNDED, UNBOUNDED)".to_string()) => Ok(Range { start: None, end: None }) ; "date range unbounded both")]
285 #[test_case(wkt::Value::Null => Err(TestConvertError::NotNull) ; "null range")]
286 #[test_case(wkt::Value::Number(123.into()) => Err(TestConvertError::TypeMismatch("string")) ; "range type mismatch")]
287 #[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")]
288 #[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")]
289 #[test_case(wkt::Value::String("[".to_string()) => Err(TestConvertError::Convert("invalid range format: missing enclosing brackets".to_string())) ; "range too short")]
290 #[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")]
291 #[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")]
292 #[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")]
293 fn test_from_sql_range(
294 value: wkt::Value,
295 ) -> Result<Range<google_cloud_type::model::Date>, TestConvertError> {
296 FromSql::from_sql(value).map_err(TestConvertError::from)
297 }
298}