sql/statements/transform/
expand_interval.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    /// Matches either one or more digits `(\d+)` or one or more ASCII characters `[a-zA-Z]` or plus/minus signs
28    static ref INTERVAL_ABBREVIATION_PATTERN: Regex = Regex::new(r"([+-]?\d+|[a-zA-Z]+|\+|-)").unwrap();
29
30    /// Checks if the provided string starts as ISO_8601 format string (case/sign independent)
31    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
48/// 'INTERVAL' abbreviation transformer
49/// - `y` for `years`
50/// - `mon` for `months`
51/// - `w` for `weeks`
52/// - `d` for `days`
53/// - `h` for `hours`
54/// - `m` for `minutes`
55/// - `s` for `seconds`
56/// - `millis` for `milliseconds`
57/// - `ms` for `milliseconds`
58/// - `us` for `microseconds`
59/// - `ns` for `nanoseconds`
60///
61/// Required for scenarios that use the shortened version of `INTERVAL`,
62///   f.e `SELECT INTERVAL '1h'` or `SELECT INTERVAL '3w2d'`
63pub(crate) struct ExpandIntervalTransformRule;
64
65impl TransformRule for ExpandIntervalTransformRule {
66    /// Applies transform rule for `Interval` type by extending the shortened version (e.g. '1h', '2d') or
67    /// converting ISO 8601 format strings (e.g., "P1Y2M3D")
68    /// In case when `Interval` has `BinaryOp` value (e.g. query like `SELECT INTERVAL '2h' - INTERVAL '1h'`)
69    /// it's AST has `left` part of type `Value::SingleQuotedString` which needs to be handled specifically.
70    /// To handle the `right` part which is `Interval` no extra steps are needed.
71    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
142/// Normalizes an interval expression string into the sql-compatible format.
143/// This function handles 2 types of input:
144/// 1. Abbreviated interval strings (e.g., "1y2mo3d")
145///    Returns an interval's full name (e.g., "years", "hours", "minutes") according to the `INTERVAL_ABBREVIATION_MAPPING`
146///    If the `interval_str` contains whitespaces, the interval name is considered to be in a full form.
147/// 2. ISO 8601 format strings (e.g., "P1Y2M3D"), case/sign independent
148///    Returns a number of milliseconds corresponding to ISO 8601 (e.g., "36525000 milliseconds")
149///
150/// Note: Hybrid format "1y 2 days 3h" is not supported.
151fn 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}