Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sql): improve interval expression, support shortened version #4182

Merged
4 changes: 4 additions & 0 deletions src/sql/src/statements/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ use sqlparser::ast::{visit_expressions_mut, Expr};

use crate::error::Result;
use crate::statements::statement::Statement;
mod expand_interval;
mod type_alias;

use expand_interval::ExpandIntervalTransformRule;
pub use type_alias::get_data_type_by_alias_name;
use type_alias::TypeAliasTransformRule;

lazy_static! {
/// [TransformRule] registry
static ref RULES: Vec<Arc<dyn TransformRule>> = vec![
Arc::new(ExpandIntervalTransformRule{}),
Arc::new(TypeAliasTransformRule{}),
];
}
Expand Down
143 changes: 143 additions & 0 deletions src/sql/src/statements/transform/expand_interval.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::collections::HashMap;
use std::ops::ControlFlow;

use itertools::Itertools;
use lazy_static::lazy_static;
use sqlparser::ast::{Expr, Interval, Value};

use crate::statements::transform::TransformRule;

lazy_static! {
static ref INTERVAL_SHORT_NAME_MAPPING: HashMap<&'static str, &'static str> = HashMap::from([
("y", " years"),
("m", " months"),
etolbakov marked this conversation as resolved.
Show resolved Hide resolved
("w", " weeks"),
("d", " days"),
("h", " hours"),
("min", " minutes"),
("s", " seconds"),
("millis", " milliseconds"),
("mils", " milliseconds"),
("ms", " microseconds"),
("ns", " nanoseconds"),
etolbakov marked this conversation as resolved.
Show resolved Hide resolved
]);
}

/// 'Interval' expression transformer
/// - `y` for `years`
/// - `m` for `months`
/// - `w` for `weeks`
/// - `d` for `days`
/// - `h` for `hours`
/// - `m` for `minutes`
/// - `s` for `seconds`
/// - `millis` for `milliseconds`
/// - `mils` for `milliseconds`
/// - `ms` for `microseconds`
/// - `ns` for `nanoseconds`
/// Required for use cases that use the shortened version of Interval declaration,
/// f.e `select interval '1h'` or `select interval '3w'`
pub(crate) struct ExpandIntervalTransformRule;

impl TransformRule for ExpandIntervalTransformRule {
fn visit_expr(&self, expr: &mut Expr) -> ControlFlow<()> {
if let Expr::Interval(Interval {
value,
leading_field,
leading_precision,
last_field,
fractional_seconds_precision,
}) = expr
{
if let Expr::Value(Value::SingleQuotedString(item)) = *value.clone() {
etolbakov marked this conversation as resolved.
Show resolved Hide resolved
if !item.contains(|c: char| c.is_whitespace()) {
*expr = Expr::Interval(Interval {
value: Box::new(Expr::Value(Value::SingleQuotedString(
expand_interval_name(item.as_str()),
))),
leading_field: leading_field.clone(),
leading_precision: *leading_precision,
last_field: last_field.clone(),
fractional_seconds_precision: *fractional_seconds_precision,
});
}
}
}
ControlFlow::<()>::Continue(())
}
}

/// Removes the first character (assumed to be a sign) and all digits from the `interval_str`.
/// Returns an interval's short name (e.g., "y", "h", "min").
fn get_interval_short_name(interval_str: &str) -> String {
let mut short_name = String::new();
for c in interval_str.chars().dropping(1) {
if !c.is_ascii_digit() {
short_name.push(c);
}
}
short_name
}

/// Expands a short interval name to its full name.
/// Returns an interval's full name (e.g., "years", "hours", "minutes") for existing mapping
/// or the `interval_str` as is
fn expand_interval_name(interval_str: &str) -> String {
let short_name = get_interval_short_name(interval_str);
match INTERVAL_SHORT_NAME_MAPPING.get(short_name.as_str()) {
etolbakov marked this conversation as resolved.
Show resolved Hide resolved
Some(extended_name) => interval_str.replace(short_name.as_str(), extended_name),
None => interval_str.to_string(),
}
}

#[cfg(test)]
mod tests {
use crate::statements::transform::expand_interval::{
expand_interval_name, get_interval_short_name,
};

#[test]
fn test_drop_digits() {
assert_eq!(get_interval_short_name("100h"), "h");
assert_eq!(get_interval_short_name("55y"), "y");
assert_eq!(get_interval_short_name("-2m"), "m");
}
#[test]
fn test_transform_interval_conversions() {
let test_cases = vec![
("1y", "1 years"),
("4m", "4 months"),
("3w", "3 weeks"),
("55h", "55 hours"),
("3d", "3 days"),
("5s", "5 seconds"),
("2min", "2 minutes"),
("100millis", "100 milliseconds"),
("150mils", "150 milliseconds"),
("200ms", "200 microseconds"),
("400ns", "400 nanoseconds"),
("10x", "10x"),
("2 years", "2 years"),
("4 months", "4 months"),
("7 weeks", "7 weeks"),
etolbakov marked this conversation as resolved.
Show resolved Hide resolved
];
for (input, expected) in test_cases {
let result = expand_interval_name(input);
assert_eq!(result, expected);
}
}
}
2 changes: 1 addition & 1 deletion src/sql/src/statements/transform/type_alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ impl TransformRule for TypeAliasTransformRule {

fn replace_type_alias(data_type: &mut DataType) {
match data_type {
// TODO(dennis): The sqlparser latest version contains the Int8 alias for postres Bigint.
// TODO(dennis): The sqlparser latest version contains the Int8 alias for Postgres Bigint.
etolbakov marked this conversation as resolved.
Show resolved Hide resolved
// Which means 8 bytes in postgres (not 8 bits). If we upgrade the sqlparser, need to process it.
// See https://docs.rs/sqlparser/latest/sqlparser/ast/enum.DataType.html#variant.Int8
DataType::Custom(name, tokens) if name.0.len() == 1 && tokens.is_empty() => {
Expand Down
12 changes: 10 additions & 2 deletions tests/cases/standalone/common/types/interval/interval.result
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ SELECT INTERVAL '1.5 year';
| 0 years 18 mons 0 days 0 hours 0 mins 0.000000000 secs |
+---------------------------------------------------------+

SELECT INTERVAL '55h';

+--------------------------------------------------------+
| IntervalMonthDayNano("198000000000000") |
+--------------------------------------------------------+
| 0 years 0 mons 0 days 55 hours 0 mins 0.000000000 secs |
+--------------------------------------------------------+

SELECT INTERVAL '-2 months';

+---------------------------------------------------------+
Expand Down Expand Up @@ -277,9 +285,9 @@ This was likely caused by a bug in DataFusion's code and we would welcome that y

SELECT SUM(interval_value) from intervals;

Error: 3000(PlanQuery), Failed to plan SQL: Error during planning: Execution error: User-defined coercion failed with Execution("Sum not supported for Interval(MonthDayNano)") and No function matches the given name and argument types 'SUM(Interval(MonthDayNano))'. You might need to add explicit type casts.
Error: 3000(PlanQuery), Failed to plan SQL: Error during planning: No function matches the given name and argument types 'SUM(Interval(MonthDayNano))'. You might need to add explicit type casts.
Candidate functions:
SUM(UserDefined)
SUM(Int8/Int16/Int32/Int64/UInt8/UInt16/UInt32/UInt64/Float32/Float64)

SELECT AVG(interval_value) from intervals;

Expand Down
2 changes: 2 additions & 0 deletions tests/cases/standalone/common/types/interval/interval.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ SELECT INTERVAL '1 year 2 months 3 days 4 hours 5 minutes 6 seconds 100 microsec

SELECT INTERVAL '1.5 year';

SELECT INTERVAL '55h';
etolbakov marked this conversation as resolved.
Show resolved Hide resolved

SELECT INTERVAL '-2 months';

SELECT INTERVAL '1 year 2 months 3 days 4 hours' + INTERVAL '1 year';
Expand Down
Loading