1use std::collections::HashMap;
16use std::ops::ControlFlow;
17use std::time::Duration as StdDuration;
18
19use itertools::Itertools;
20use lazy_static::lazy_static;
21use regex::Regex;
22use sqlparser::ast::{DataType, Expr, Interval, Value};
23
24use crate::statements::transform::TransformRule;
25
26lazy_static! {
27 static ref INTERVAL_ABBREVIATION_PATTERN: Regex = Regex::new(r"([+-]?\d+|[a-zA-Z]+|\+|-)").unwrap();
29
30 static ref IS_VALID_ISO_8601_PREFIX_PATTERN: Regex = Regex::new(r"^[-]?[Pp]").unwrap();
32
33 static ref INTERVAL_ABBREVIATION_MAPPING: HashMap<&'static str, &'static str> = HashMap::from([
34 ("y","years"),
35 ("mon","months"),
36 ("w","weeks"),
37 ("d","days"),
38 ("h","hours"),
39 ("m","minutes"),
40 ("s","seconds"),
41 ("millis","milliseconds"),
42 ("ms","milliseconds"),
43 ("us","microseconds"),
44 ("ns","nanoseconds"),
45 ]);
46}
47
48pub(crate) struct ExpandIntervalTransformRule;
64
65impl TransformRule for ExpandIntervalTransformRule {
66 fn visit_expr(&self, expr: &mut Expr) -> ControlFlow<()> {
72 match expr {
73 Expr::Interval(interval) => match &*interval.value {
74 Expr::Value(Value::SingleQuotedString(value))
75 | Expr::Value(Value::DoubleQuotedString(value)) => {
76 if let Some(normalized_name) = normalize_interval_name(value) {
77 *expr = update_existing_interval_with_value(
78 interval,
79 single_quoted_string_expr(normalized_name),
80 );
81 }
82 }
83 Expr::BinaryOp { left, op, right } => match &**left {
84 Expr::Value(Value::SingleQuotedString(value))
85 | Expr::Value(Value::DoubleQuotedString(value)) => {
86 if let Some(normalized_name) = normalize_interval_name(value) {
87 let new_expr_value = Box::new(Expr::BinaryOp {
88 left: single_quoted_string_expr(normalized_name),
89 op: op.clone(),
90 right: right.clone(),
91 });
92 *expr = update_existing_interval_with_value(interval, new_expr_value);
93 }
94 }
95 _ => {}
96 },
97 _ => {}
98 },
99 Expr::Cast {
100 expr: cast_exp,
101 data_type,
102 kind,
103 format,
104 } => {
105 if DataType::Interval == *data_type {
106 match &**cast_exp {
107 Expr::Value(Value::SingleQuotedString(value))
108 | Expr::Value(Value::DoubleQuotedString(value)) => {
109 let interval_value =
110 normalize_interval_name(value).unwrap_or_else(|| value.to_string());
111 *expr = Expr::Cast {
112 kind: kind.clone(),
113 expr: single_quoted_string_expr(interval_value),
114 data_type: DataType::Interval,
115 format: std::mem::take(format),
116 }
117 }
118 _ => {}
119 }
120 }
121 }
122 _ => {}
123 }
124 ControlFlow::<()>::Continue(())
125 }
126}
127
128fn single_quoted_string_expr(string: String) -> Box<Expr> {
129 Box::new(Expr::Value(Value::SingleQuotedString(string)))
130}
131
132fn update_existing_interval_with_value(interval: &Interval, value: Box<Expr>) -> Expr {
133 Expr::Interval(Interval {
134 value,
135 leading_field: interval.leading_field.clone(),
136 leading_precision: interval.leading_precision,
137 last_field: interval.last_field.clone(),
138 fractional_seconds_precision: interval.fractional_seconds_precision,
139 })
140}
141
142fn normalize_interval_name(interval_str: &str) -> Option<String> {
152 if interval_str.contains(char::is_whitespace) {
153 return None;
154 }
155
156 if IS_VALID_ISO_8601_PREFIX_PATTERN.is_match(interval_str) {
157 return parse_iso8601_interval(interval_str);
158 }
159
160 expand_interval_abbreviation(interval_str)
161}
162
163fn parse_iso8601_interval(signed_iso: &str) -> Option<String> {
164 let (is_negative, unsigned_iso) = if let Some(stripped) = signed_iso.strip_prefix('-') {
165 (true, stripped)
166 } else {
167 (false, signed_iso)
168 };
169
170 match iso8601::duration(&unsigned_iso.to_uppercase()) {
171 Ok(duration) => {
172 let millis = StdDuration::from(duration).as_millis();
173 let sign = if is_negative { "-" } else { "" };
174 Some(format!("{}{} milliseconds", sign, millis))
175 }
176 Err(_) => None,
177 }
178}
179
180fn expand_interval_abbreviation(interval_str: &str) -> Option<String> {
181 Some(
182 INTERVAL_ABBREVIATION_PATTERN
183 .find_iter(interval_str)
184 .map(|mat| {
185 let mat_str = mat.as_str();
186 *INTERVAL_ABBREVIATION_MAPPING
187 .get(mat_str)
188 .unwrap_or(&mat_str)
189 })
190 .join(" "),
191 )
192}
193
194#[cfg(test)]
195mod tests {
196 use std::ops::ControlFlow;
197
198 use sqlparser::ast::{BinaryOperator, CastKind, DataType, Expr, Interval, Value};
199
200 use crate::statements::transform::expand_interval::{
201 normalize_interval_name, single_quoted_string_expr, ExpandIntervalTransformRule,
202 };
203 use crate::statements::transform::TransformRule;
204
205 fn create_interval(value: Box<Expr>) -> Expr {
206 Expr::Interval(Interval {
207 value,
208 leading_field: None,
209 leading_precision: None,
210 last_field: None,
211 fractional_seconds_precision: None,
212 })
213 }
214
215 #[test]
216 fn test_transform_interval_basic_conversions() {
217 let test_cases = vec![
218 ("1y", "1 years"),
219 ("4mon", "4 months"),
220 ("-3w", "-3 weeks"),
221 ("55h", "55 hours"),
222 ("3d", "3 days"),
223 ("5s", "5 seconds"),
224 ("2m", "2 minutes"),
225 ("100millis", "100 milliseconds"),
226 ("200ms", "200 milliseconds"),
227 ("350us", "350 microseconds"),
228 ("400ns", "400 nanoseconds"),
229 ];
230 for (input, expected) in test_cases {
231 let result = normalize_interval_name(input).unwrap();
232 assert_eq!(result, expected);
233 }
234
235 let test_cases = vec!["1 year 2 months 3 days 4 hours", "-2 months"];
236 for input in test_cases {
237 assert_eq!(normalize_interval_name(input), None);
238 }
239 }
240
241 #[test]
242 fn test_transform_interval_compound_conversions() {
243 let test_cases = vec![
244 ("2y4mon6w", "2 years 4 months 6 weeks"),
245 ("5d3h1m", "5 days 3 hours 1 minutes"),
246 (
247 "10s312ms789ns",
248 "10 seconds 312 milliseconds 789 nanoseconds",
249 ),
250 (
251 "23millis987us754ns",
252 "23 milliseconds 987 microseconds 754 nanoseconds",
253 ),
254 ("-1d-5h", "-1 days -5 hours"),
255 ("-2y-4mon-6w", "-2 years -4 months -6 weeks"),
256 ("-5d-3h-1m", "-5 days -3 hours -1 minutes"),
257 (
258 "-10s-312ms-789ns",
259 "-10 seconds -312 milliseconds -789 nanoseconds",
260 ),
261 (
262 "-23millis-987us-754ns",
263 "-23 milliseconds -987 microseconds -754 nanoseconds",
264 ),
265 ];
266 for (input, expected) in test_cases {
267 let result = normalize_interval_name(input).unwrap();
268 assert_eq!(result, expected);
269 }
270 }
271
272 #[test]
273 fn test_iso8601_format() {
274 assert_eq!(
275 normalize_interval_name("P1Y2M3DT4H5M6S"),
276 Some("36993906000 milliseconds".to_string())
277 );
278 assert_eq!(
279 normalize_interval_name("p3y3m700dt133h17m36.789s"),
280 Some("163343856789 milliseconds".to_string())
281 );
282 assert_eq!(
283 normalize_interval_name("-P1Y2M3DT4H5M6S"),
284 Some("-36993906000 milliseconds".to_string())
285 );
286 assert_eq!(normalize_interval_name("P1_INVALID_ISO8601"), None);
287 }
288
289 #[test]
290 fn test_visit_expr_when_interval_is_single_quoted_string_abbr_expr() {
291 let interval_transformation_rule = ExpandIntervalTransformRule {};
292
293 let mut string_expr = create_interval(single_quoted_string_expr("5y".to_string()));
294
295 let control_flow = interval_transformation_rule.visit_expr(&mut string_expr);
296
297 assert_eq!(control_flow, ControlFlow::Continue(()));
298 assert_eq!(
299 string_expr,
300 Expr::Interval(Interval {
301 value: Box::new(Expr::Value(Value::SingleQuotedString(
302 "5 years".to_string()
303 ))),
304 leading_field: None,
305 leading_precision: None,
306 last_field: None,
307 fractional_seconds_precision: None,
308 })
309 );
310 }
311
312 #[test]
313 fn test_visit_expr_when_interval_is_single_quoted_string_iso8601_expr() {
314 let interval_transformation_rule = ExpandIntervalTransformRule {};
315
316 let mut string_expr =
317 create_interval(single_quoted_string_expr("P1Y2M3DT4H5M6S".to_string()));
318
319 let control_flow = interval_transformation_rule.visit_expr(&mut string_expr);
320
321 assert_eq!(control_flow, ControlFlow::Continue(()));
322 assert_eq!(
323 string_expr,
324 Expr::Interval(Interval {
325 value: Box::new(Expr::Value(Value::SingleQuotedString(
326 "36993906000 milliseconds".to_string()
327 ))),
328 leading_field: None,
329 leading_precision: None,
330 last_field: None,
331 fractional_seconds_precision: None,
332 })
333 );
334 }
335
336 #[test]
337 fn test_visit_expr_when_interval_is_binary_op() {
338 let interval_transformation_rule = ExpandIntervalTransformRule {};
339
340 let binary_op = Box::new(Expr::BinaryOp {
341 left: single_quoted_string_expr("2d".to_string()),
342 op: BinaryOperator::Minus,
343 right: Box::new(create_interval(single_quoted_string_expr("1d".to_string()))),
344 });
345 let mut binary_op_expr = create_interval(binary_op);
346 let control_flow = interval_transformation_rule.visit_expr(&mut binary_op_expr);
347
348 assert_eq!(control_flow, ControlFlow::Continue(()));
349 assert_eq!(
350 binary_op_expr,
351 Expr::Interval(Interval {
352 value: Box::new(Expr::BinaryOp {
353 left: single_quoted_string_expr("2 days".to_string()),
354 op: BinaryOperator::Minus,
355 right: Box::new(Expr::Interval(Interval {
356 value: single_quoted_string_expr("1d".to_string()),
357 leading_field: None,
358 leading_precision: None,
359 last_field: None,
360 fractional_seconds_precision: None,
361 })),
362 }),
363 leading_field: None,
364 leading_precision: None,
365 last_field: None,
366 fractional_seconds_precision: None,
367 })
368 );
369 }
370
371 #[test]
372 fn test_visit_expr_when_cast_expr() {
373 let interval_transformation_rule = ExpandIntervalTransformRule {};
374
375 let mut cast_to_interval_expr = Expr::Cast {
376 expr: single_quoted_string_expr("3y2mon".to_string()),
377 data_type: DataType::Interval,
378 format: None,
379 kind: sqlparser::ast::CastKind::Cast,
380 };
381
382 let control_flow = interval_transformation_rule.visit_expr(&mut cast_to_interval_expr);
383
384 assert_eq!(control_flow, ControlFlow::Continue(()));
385 assert_eq!(
386 cast_to_interval_expr,
387 Expr::Cast {
388 kind: CastKind::Cast,
389 expr: Box::new(Expr::Value(Value::SingleQuotedString(
390 "3 years 2 months".to_string()
391 ))),
392 data_type: DataType::Interval,
393 format: None,
394 }
395 );
396
397 let mut cast_to_i64_expr = Expr::Cast {
398 expr: single_quoted_string_expr("5".to_string()),
399 data_type: DataType::Int64,
400 format: None,
401 kind: sqlparser::ast::CastKind::Cast,
402 };
403 let control_flow = interval_transformation_rule.visit_expr(&mut cast_to_i64_expr);
404 assert_eq!(control_flow, ControlFlow::Continue(()));
405 assert_eq!(
406 cast_to_i64_expr,
407 Expr::Cast {
408 expr: single_quoted_string_expr("5".to_string()),
409 data_type: DataType::Int64,
410 format: None,
411 kind: sqlparser::ast::CastKind::Cast,
412 }
413 );
414 }
415}