mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-08-22 15:04:04 +00:00
Fix INTERVAL
parsing to support expressions and units via dialect (#1398)
This commit is contained in:
parent
1bed87a8ea
commit
aa714e3447
10 changed files with 331 additions and 134 deletions
|
@ -24,4 +24,8 @@ impl Dialect for AnsiDialect {
|
||||||
fn is_identifier_part(&self, ch: char) -> bool {
|
fn is_identifier_part(&self, ch: char) -> bool {
|
||||||
ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_'
|
ch.is_ascii_lowercase() || ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_interval_qualifier(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,4 +63,8 @@ impl Dialect for BigQueryDialect {
|
||||||
fn supports_select_wildcard_except(&self) -> bool {
|
fn supports_select_wildcard_except(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_interval_qualifier(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,4 +37,8 @@ impl Dialect for ClickHouseDialect {
|
||||||
fn describe_requires_table_keyword(&self) -> bool {
|
fn describe_requires_table_keyword(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_interval_qualifier(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,8 @@ impl Dialect for DatabricksDialect {
|
||||||
fn supports_select_wildcard_except(&self) -> bool {
|
fn supports_select_wildcard_except(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_interval_qualifier(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,4 +42,8 @@ impl Dialect for HiveDialect {
|
||||||
fn supports_numeric_prefix(&self) -> bool {
|
fn supports_numeric_prefix(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_interval_qualifier(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -515,6 +515,27 @@ pub trait Dialect: Debug + Any {
|
||||||
fn supports_create_index_with_clause(&self) -> bool {
|
fn supports_create_index_with_clause(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether `INTERVAL` expressions require units (called "qualifiers" in the ANSI SQL spec) to be specified,
|
||||||
|
/// e.g. `INTERVAL 1 DAY` vs `INTERVAL 1`.
|
||||||
|
///
|
||||||
|
/// Expressions within intervals (e.g. `INTERVAL '1' + '1' DAY`) are only allowed when units are required.
|
||||||
|
///
|
||||||
|
/// See <https://github.com/sqlparser-rs/sqlparser-rs/pull/1398> for more information.
|
||||||
|
///
|
||||||
|
/// When `true`:
|
||||||
|
/// * `INTERVAL '1' DAY` is VALID
|
||||||
|
/// * `INTERVAL 1 + 1 DAY` is VALID
|
||||||
|
/// * `INTERVAL '1' + '1' DAY` is VALID
|
||||||
|
/// * `INTERVAL '1'` is INVALID
|
||||||
|
///
|
||||||
|
/// When `false`:
|
||||||
|
/// * `INTERVAL '1'` is VALID
|
||||||
|
/// * `INTERVAL '1' DAY` is VALID — unit is not required, but still allowed
|
||||||
|
/// * `INTERVAL 1 + 1 DAY` is INVALID
|
||||||
|
fn require_interval_qualifier(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This represents the operators for which precedence must be defined
|
/// This represents the operators for which precedence must be defined
|
||||||
|
|
|
@ -84,6 +84,10 @@ impl Dialect for MySqlDialect {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_interval_qualifier(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `LOCK TABLES`
|
/// `LOCK TABLES`
|
||||||
|
|
|
@ -56,15 +56,6 @@ macro_rules! parser_err {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a successful result if the optional expression is some
|
|
||||||
macro_rules! return_ok_if_some {
|
|
||||||
($e:expr) => {{
|
|
||||||
if let Some(v) = $e {
|
|
||||||
return Ok(v);
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
/// Implementation [`RecursionCounter`] if std is available
|
/// Implementation [`RecursionCounter`] if std is available
|
||||||
mod recursion {
|
mod recursion {
|
||||||
|
@ -928,35 +919,6 @@ impl<'a> Parser<'a> {
|
||||||
Ok(expr)
|
Ok(expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_interval_expr(&mut self) -> Result<Expr, ParserError> {
|
|
||||||
let precedence = self.dialect.prec_unknown();
|
|
||||||
let mut expr = self.parse_prefix()?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let next_precedence = self.get_next_interval_precedence()?;
|
|
||||||
|
|
||||||
if precedence >= next_precedence {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
expr = self.parse_infix(expr, next_precedence)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the precedence of the next token, with AND, OR, and XOR.
|
|
||||||
pub fn get_next_interval_precedence(&self) -> Result<u8, ParserError> {
|
|
||||||
let token = self.peek_token();
|
|
||||||
|
|
||||||
match token.token {
|
|
||||||
Token::Word(w) if w.keyword == Keyword::AND => Ok(self.dialect.prec_unknown()),
|
|
||||||
Token::Word(w) if w.keyword == Keyword::OR => Ok(self.dialect.prec_unknown()),
|
|
||||||
Token::Word(w) if w.keyword == Keyword::XOR => Ok(self.dialect.prec_unknown()),
|
|
||||||
_ => self.get_next_precedence(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_assert(&mut self) -> Result<Statement, ParserError> {
|
pub fn parse_assert(&mut self) -> Result<Statement, ParserError> {
|
||||||
let condition = self.parse_expr()?;
|
let condition = self.parse_expr()?;
|
||||||
let message = if self.parse_keyword(Keyword::AS) {
|
let message = if self.parse_keyword(Keyword::AS) {
|
||||||
|
@ -1004,7 +966,7 @@ impl<'a> Parser<'a> {
|
||||||
// name is not followed by a string literal, but in fact in PostgreSQL it is a valid
|
// name is not followed by a string literal, but in fact in PostgreSQL it is a valid
|
||||||
// expression that should parse as the column name "date".
|
// expression that should parse as the column name "date".
|
||||||
let loc = self.peek_token().location;
|
let loc = self.peek_token().location;
|
||||||
return_ok_if_some!(self.maybe_parse(|parser| {
|
let opt_expr = self.maybe_parse(|parser| {
|
||||||
match parser.parse_data_type()? {
|
match parser.parse_data_type()? {
|
||||||
DataType::Interval => parser.parse_interval(),
|
DataType::Interval => parser.parse_interval(),
|
||||||
// PostgreSQL allows almost any identifier to be used as custom data type name,
|
// PostgreSQL allows almost any identifier to be used as custom data type name,
|
||||||
|
@ -1020,7 +982,11 @@ impl<'a> Parser<'a> {
|
||||||
value: parser.parse_literal_string()?,
|
value: parser.parse_literal_string()?,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
if let Some(expr) = opt_expr {
|
||||||
|
return Ok(expr);
|
||||||
|
}
|
||||||
|
|
||||||
let next_token = self.next_token();
|
let next_token = self.next_token();
|
||||||
let expr = match next_token.token {
|
let expr = match next_token.token {
|
||||||
|
@ -2110,52 +2076,32 @@ impl<'a> Parser<'a> {
|
||||||
// don't currently try to parse it. (The sign can instead be included
|
// don't currently try to parse it. (The sign can instead be included
|
||||||
// inside the value string.)
|
// inside the value string.)
|
||||||
|
|
||||||
// The first token in an interval is a string literal which specifies
|
// to match the different flavours of INTERVAL syntax, we only allow expressions
|
||||||
// the duration of the interval.
|
// if the dialect requires an interval qualifier,
|
||||||
let value = self.parse_interval_expr()?;
|
// see https://github.com/sqlparser-rs/sqlparser-rs/pull/1398 for more details
|
||||||
|
let value = if self.dialect.require_interval_qualifier() {
|
||||||
|
// parse a whole expression so `INTERVAL 1 + 1 DAY` is valid
|
||||||
|
self.parse_expr()?
|
||||||
|
} else {
|
||||||
|
// parse a prefix expression so `INTERVAL 1 DAY` is valid, but `INTERVAL 1 + 1 DAY` is not
|
||||||
|
// this also means that `INTERVAL '5 days' > INTERVAL '1 day'` treated properly
|
||||||
|
self.parse_prefix()?
|
||||||
|
};
|
||||||
|
|
||||||
// Following the string literal is a qualifier which indicates the units
|
// Following the string literal is a qualifier which indicates the units
|
||||||
// of the duration specified in the string literal.
|
// of the duration specified in the string literal.
|
||||||
//
|
//
|
||||||
// Note that PostgreSQL allows omitting the qualifier, so we provide
|
// Note that PostgreSQL allows omitting the qualifier, so we provide
|
||||||
// this more general implementation.
|
// this more general implementation.
|
||||||
let leading_field = match self.peek_token().token {
|
let leading_field = if self.next_token_is_temporal_unit() {
|
||||||
Token::Word(kw)
|
Some(self.parse_date_time_field()?)
|
||||||
if [
|
} else if self.dialect.require_interval_qualifier() {
|
||||||
Keyword::YEAR,
|
return parser_err!(
|
||||||
Keyword::MONTH,
|
"INTERVAL requires a unit after the literal value",
|
||||||
Keyword::WEEK,
|
self.peek_token().location
|
||||||
Keyword::DAY,
|
);
|
||||||
Keyword::HOUR,
|
} else {
|
||||||
Keyword::MINUTE,
|
None
|
||||||
Keyword::SECOND,
|
|
||||||
Keyword::CENTURY,
|
|
||||||
Keyword::DECADE,
|
|
||||||
Keyword::DOW,
|
|
||||||
Keyword::DOY,
|
|
||||||
Keyword::EPOCH,
|
|
||||||
Keyword::ISODOW,
|
|
||||||
Keyword::ISOYEAR,
|
|
||||||
Keyword::JULIAN,
|
|
||||||
Keyword::MICROSECOND,
|
|
||||||
Keyword::MICROSECONDS,
|
|
||||||
Keyword::MILLENIUM,
|
|
||||||
Keyword::MILLENNIUM,
|
|
||||||
Keyword::MILLISECOND,
|
|
||||||
Keyword::MILLISECONDS,
|
|
||||||
Keyword::NANOSECOND,
|
|
||||||
Keyword::NANOSECONDS,
|
|
||||||
Keyword::QUARTER,
|
|
||||||
Keyword::TIMEZONE,
|
|
||||||
Keyword::TIMEZONE_HOUR,
|
|
||||||
Keyword::TIMEZONE_MINUTE,
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.any(|d| kw.keyword == *d) =>
|
|
||||||
{
|
|
||||||
Some(self.parse_date_time_field()?)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let (leading_precision, last_field, fsec_precision) =
|
let (leading_precision, last_field, fsec_precision) =
|
||||||
|
@ -2192,6 +2138,45 @@ impl<'a> Parser<'a> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Peek at the next token and determine if it is a temporal unit
|
||||||
|
/// like `second`.
|
||||||
|
pub fn next_token_is_temporal_unit(&mut self) -> bool {
|
||||||
|
if let Token::Word(word) = self.peek_token().token {
|
||||||
|
matches!(
|
||||||
|
word.keyword,
|
||||||
|
Keyword::YEAR
|
||||||
|
| Keyword::MONTH
|
||||||
|
| Keyword::WEEK
|
||||||
|
| Keyword::DAY
|
||||||
|
| Keyword::HOUR
|
||||||
|
| Keyword::MINUTE
|
||||||
|
| Keyword::SECOND
|
||||||
|
| Keyword::CENTURY
|
||||||
|
| Keyword::DECADE
|
||||||
|
| Keyword::DOW
|
||||||
|
| Keyword::DOY
|
||||||
|
| Keyword::EPOCH
|
||||||
|
| Keyword::ISODOW
|
||||||
|
| Keyword::ISOYEAR
|
||||||
|
| Keyword::JULIAN
|
||||||
|
| Keyword::MICROSECOND
|
||||||
|
| Keyword::MICROSECONDS
|
||||||
|
| Keyword::MILLENIUM
|
||||||
|
| Keyword::MILLENNIUM
|
||||||
|
| Keyword::MILLISECOND
|
||||||
|
| Keyword::MILLISECONDS
|
||||||
|
| Keyword::NANOSECOND
|
||||||
|
| Keyword::NANOSECONDS
|
||||||
|
| Keyword::QUARTER
|
||||||
|
| Keyword::TIMEZONE
|
||||||
|
| Keyword::TIMEZONE_HOUR
|
||||||
|
| Keyword::TIMEZONE_MINUTE
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Bigquery specific: Parse a struct literal
|
/// Bigquery specific: Parse a struct literal
|
||||||
/// Syntax
|
/// Syntax
|
||||||
/// ```sql
|
/// ```sql
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod test_utils;
|
mod test_utils;
|
||||||
|
|
||||||
use sqlparser::ast;
|
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use sqlparser::ast::*;
|
use sqlparser::ast::*;
|
||||||
|
@ -830,16 +829,14 @@ fn parse_typed_struct_syntax_bigquery() {
|
||||||
expr_from_projection(&select.projection[3])
|
expr_from_projection(&select.projection[3])
|
||||||
);
|
);
|
||||||
|
|
||||||
let sql = r#"SELECT STRUCT<INTERVAL>(INTERVAL '1-2 3 4:5:6.789999'), STRUCT<JSON>(JSON '{"class" : {"students" : [{"name" : "Jane"}]}}')"#;
|
let sql = r#"SELECT STRUCT<INTERVAL>(INTERVAL '2' HOUR), STRUCT<JSON>(JSON '{"class" : {"students" : [{"name" : "Jane"}]}}')"#;
|
||||||
let select = bigquery().verified_only_select(sql);
|
let select = bigquery().verified_only_select(sql);
|
||||||
assert_eq!(2, select.projection.len());
|
assert_eq!(2, select.projection.len());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&Expr::Struct {
|
&Expr::Struct {
|
||||||
values: vec![Expr::Interval(ast::Interval {
|
values: vec![Expr::Interval(Interval {
|
||||||
value: Box::new(Expr::Value(Value::SingleQuotedString(
|
value: Box::new(Expr::Value(Value::SingleQuotedString("2".to_string()))),
|
||||||
"1-2 3 4:5:6.789999".to_string()
|
leading_field: Some(DateTimeField::Hour),
|
||||||
))),
|
|
||||||
leading_field: None,
|
|
||||||
leading_precision: None,
|
leading_precision: None,
|
||||||
last_field: None,
|
last_field: None,
|
||||||
fractional_seconds_precision: None
|
fractional_seconds_precision: None
|
||||||
|
@ -1141,16 +1138,14 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
|
||||||
expr_from_projection(&select.projection[3])
|
expr_from_projection(&select.projection[3])
|
||||||
);
|
);
|
||||||
|
|
||||||
let sql = r#"SELECT STRUCT<INTERVAL>(INTERVAL '1-2 3 4:5:6.789999'), STRUCT<JSON>(JSON '{"class" : {"students" : [{"name" : "Jane"}]}}')"#;
|
let sql = r#"SELECT STRUCT<INTERVAL>(INTERVAL '1' MONTH), STRUCT<JSON>(JSON '{"class" : {"students" : [{"name" : "Jane"}]}}')"#;
|
||||||
let select = bigquery_and_generic().verified_only_select(sql);
|
let select = bigquery_and_generic().verified_only_select(sql);
|
||||||
assert_eq!(2, select.projection.len());
|
assert_eq!(2, select.projection.len());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&Expr::Struct {
|
&Expr::Struct {
|
||||||
values: vec![Expr::Interval(ast::Interval {
|
values: vec![Expr::Interval(Interval {
|
||||||
value: Box::new(Expr::Value(Value::SingleQuotedString(
|
value: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))),
|
||||||
"1-2 3 4:5:6.789999".to_string()
|
leading_field: Some(DateTimeField::Month),
|
||||||
))),
|
|
||||||
leading_field: None,
|
|
||||||
leading_precision: None,
|
leading_precision: None,
|
||||||
last_field: None,
|
last_field: None,
|
||||||
fractional_seconds_precision: None
|
fractional_seconds_precision: None
|
||||||
|
|
|
@ -4472,7 +4472,8 @@ fn parse_window_functions() {
|
||||||
sum(qux) OVER (ORDER BY a \
|
sum(qux) OVER (ORDER BY a \
|
||||||
GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) \
|
GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING) \
|
||||||
FROM foo";
|
FROM foo";
|
||||||
let select = verified_only_select(sql);
|
let dialects = all_dialects_except(|d| d.require_interval_qualifier());
|
||||||
|
let select = dialects.verified_only_select(sql);
|
||||||
|
|
||||||
const EXPECTED_PROJ_QTY: usize = 7;
|
const EXPECTED_PROJ_QTY: usize = 7;
|
||||||
assert_eq!(EXPECTED_PROJ_QTY, select.projection.len());
|
assert_eq!(EXPECTED_PROJ_QTY, select.projection.len());
|
||||||
|
@ -4886,7 +4887,9 @@ fn parse_literal_timestamp_with_time_zone() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_interval() {
|
fn parse_interval_all() {
|
||||||
|
// these intervals expressions all work with both variants of INTERVAL
|
||||||
|
|
||||||
let sql = "SELECT INTERVAL '1-1' YEAR TO MONTH";
|
let sql = "SELECT INTERVAL '1-1' YEAR TO MONTH";
|
||||||
let select = verified_only_select(sql);
|
let select = verified_only_select(sql);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -4954,23 +4957,6 @@ fn parse_interval() {
|
||||||
expr_from_projection(only(&select.projection)),
|
expr_from_projection(only(&select.projection)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let sql = "SELECT INTERVAL 1 + 1 DAY";
|
|
||||||
let select = verified_only_select(sql);
|
|
||||||
assert_eq!(
|
|
||||||
&Expr::Interval(Interval {
|
|
||||||
value: Box::new(Expr::BinaryOp {
|
|
||||||
left: Box::new(Expr::Value(number("1"))),
|
|
||||||
op: BinaryOperator::Plus,
|
|
||||||
right: Box::new(Expr::Value(number("1"))),
|
|
||||||
}),
|
|
||||||
leading_field: Some(DateTimeField::Day),
|
|
||||||
leading_precision: None,
|
|
||||||
last_field: None,
|
|
||||||
fractional_seconds_precision: None,
|
|
||||||
}),
|
|
||||||
expr_from_projection(only(&select.projection)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let sql = "SELECT INTERVAL '10' HOUR (1)";
|
let sql = "SELECT INTERVAL '10' HOUR (1)";
|
||||||
let select = verified_only_select(sql);
|
let select = verified_only_select(sql);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -4984,21 +4970,6 @@ fn parse_interval() {
|
||||||
expr_from_projection(only(&select.projection)),
|
expr_from_projection(only(&select.projection)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let sql = "SELECT INTERVAL '1 DAY'";
|
|
||||||
let select = verified_only_select(sql);
|
|
||||||
assert_eq!(
|
|
||||||
&Expr::Interval(Interval {
|
|
||||||
value: Box::new(Expr::Value(Value::SingleQuotedString(String::from(
|
|
||||||
"1 DAY"
|
|
||||||
)))),
|
|
||||||
leading_field: None,
|
|
||||||
leading_precision: None,
|
|
||||||
last_field: None,
|
|
||||||
fractional_seconds_precision: None,
|
|
||||||
}),
|
|
||||||
expr_from_projection(only(&select.projection)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = parse_sql_statements("SELECT INTERVAL '1' SECOND TO SECOND");
|
let result = parse_sql_statements("SELECT INTERVAL '1' SECOND TO SECOND");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ParserError::ParserError("Expected: end of statement, found: SECOND".to_string()),
|
ParserError::ParserError("Expected: end of statement, found: SECOND".to_string()),
|
||||||
|
@ -5024,12 +4995,212 @@ fn parse_interval() {
|
||||||
verified_only_select("SELECT INTERVAL '1' HOUR TO MINUTE");
|
verified_only_select("SELECT INTERVAL '1' HOUR TO MINUTE");
|
||||||
verified_only_select("SELECT INTERVAL '1' HOUR TO SECOND");
|
verified_only_select("SELECT INTERVAL '1' HOUR TO SECOND");
|
||||||
verified_only_select("SELECT INTERVAL '1' MINUTE TO SECOND");
|
verified_only_select("SELECT INTERVAL '1' MINUTE TO SECOND");
|
||||||
verified_only_select("SELECT INTERVAL '1 YEAR'");
|
verified_only_select("SELECT INTERVAL 1 YEAR");
|
||||||
verified_only_select("SELECT INTERVAL '1 YEAR' AS one_year");
|
verified_only_select("SELECT INTERVAL 1 MONTH");
|
||||||
one_statement_parses_to(
|
verified_only_select("SELECT INTERVAL 1 DAY");
|
||||||
|
verified_only_select("SELECT INTERVAL 1 HOUR");
|
||||||
|
verified_only_select("SELECT INTERVAL 1 MINUTE");
|
||||||
|
verified_only_select("SELECT INTERVAL 1 SECOND");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_interval_dont_require_unit() {
|
||||||
|
let dialects = all_dialects_except(|d| d.require_interval_qualifier());
|
||||||
|
|
||||||
|
let sql = "SELECT INTERVAL '1 DAY'";
|
||||||
|
let select = dialects.verified_only_select(sql);
|
||||||
|
assert_eq!(
|
||||||
|
&Expr::Interval(Interval {
|
||||||
|
value: Box::new(Expr::Value(Value::SingleQuotedString(String::from(
|
||||||
|
"1 DAY"
|
||||||
|
)))),
|
||||||
|
leading_field: None,
|
||||||
|
leading_precision: None,
|
||||||
|
last_field: None,
|
||||||
|
fractional_seconds_precision: None,
|
||||||
|
}),
|
||||||
|
expr_from_projection(only(&select.projection)),
|
||||||
|
);
|
||||||
|
dialects.verified_only_select("SELECT INTERVAL '1 YEAR'");
|
||||||
|
dialects.verified_only_select("SELECT INTERVAL '1 MONTH'");
|
||||||
|
dialects.verified_only_select("SELECT INTERVAL '1 DAY'");
|
||||||
|
dialects.verified_only_select("SELECT INTERVAL '1 HOUR'");
|
||||||
|
dialects.verified_only_select("SELECT INTERVAL '1 MINUTE'");
|
||||||
|
dialects.verified_only_select("SELECT INTERVAL '1 SECOND'");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_interval_require_unit() {
|
||||||
|
let dialects = all_dialects_where(|d| d.require_interval_qualifier());
|
||||||
|
|
||||||
|
let sql = "SELECT INTERVAL '1 DAY'";
|
||||||
|
let err = dialects.parse_sql_statements(sql).unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"sql parser error: INTERVAL requires a unit after the literal value"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_interval_require_qualifier() {
|
||||||
|
let dialects = all_dialects_where(|d| d.require_interval_qualifier());
|
||||||
|
|
||||||
|
let sql = "SELECT INTERVAL 1 + 1 DAY";
|
||||||
|
let select = dialects.verified_only_select(sql);
|
||||||
|
assert_eq!(
|
||||||
|
expr_from_projection(only(&select.projection)),
|
||||||
|
&Expr::Interval(Interval {
|
||||||
|
value: Box::new(Expr::BinaryOp {
|
||||||
|
left: Box::new(Expr::Value(number("1"))),
|
||||||
|
op: BinaryOperator::Plus,
|
||||||
|
right: Box::new(Expr::Value(number("1"))),
|
||||||
|
}),
|
||||||
|
leading_field: Some(DateTimeField::Day),
|
||||||
|
leading_precision: None,
|
||||||
|
last_field: None,
|
||||||
|
fractional_seconds_precision: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let sql = "SELECT INTERVAL '1' + '1' DAY";
|
||||||
|
let select = dialects.verified_only_select(sql);
|
||||||
|
assert_eq!(
|
||||||
|
expr_from_projection(only(&select.projection)),
|
||||||
|
&Expr::Interval(Interval {
|
||||||
|
value: Box::new(Expr::BinaryOp {
|
||||||
|
left: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))),
|
||||||
|
op: BinaryOperator::Plus,
|
||||||
|
right: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))),
|
||||||
|
}),
|
||||||
|
leading_field: Some(DateTimeField::Day),
|
||||||
|
leading_precision: None,
|
||||||
|
last_field: None,
|
||||||
|
fractional_seconds_precision: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let sql = "SELECT INTERVAL '1' + '2' - '3' DAY";
|
||||||
|
let select = dialects.verified_only_select(sql);
|
||||||
|
assert_eq!(
|
||||||
|
expr_from_projection(only(&select.projection)),
|
||||||
|
&Expr::Interval(Interval {
|
||||||
|
value: Box::new(Expr::BinaryOp {
|
||||||
|
left: Box::new(Expr::BinaryOp {
|
||||||
|
left: Box::new(Expr::Value(Value::SingleQuotedString("1".to_string()))),
|
||||||
|
op: BinaryOperator::Plus,
|
||||||
|
right: Box::new(Expr::Value(Value::SingleQuotedString("2".to_string()))),
|
||||||
|
}),
|
||||||
|
op: BinaryOperator::Minus,
|
||||||
|
right: Box::new(Expr::Value(Value::SingleQuotedString("3".to_string()))),
|
||||||
|
}),
|
||||||
|
leading_field: Some(DateTimeField::Day),
|
||||||
|
leading_precision: None,
|
||||||
|
last_field: None,
|
||||||
|
fractional_seconds_precision: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_interval_disallow_interval_expr() {
|
||||||
|
let dialects = all_dialects_except(|d| d.require_interval_qualifier());
|
||||||
|
|
||||||
|
let sql = "SELECT INTERVAL '1 DAY'";
|
||||||
|
let select = dialects.verified_only_select(sql);
|
||||||
|
assert_eq!(
|
||||||
|
expr_from_projection(only(&select.projection)),
|
||||||
|
&Expr::Interval(Interval {
|
||||||
|
value: Box::new(Expr::Value(Value::SingleQuotedString(String::from(
|
||||||
|
"1 DAY"
|
||||||
|
)))),
|
||||||
|
leading_field: None,
|
||||||
|
leading_precision: None,
|
||||||
|
last_field: None,
|
||||||
|
fractional_seconds_precision: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
dialects.verified_only_select("SELECT INTERVAL '1 YEAR'");
|
||||||
|
dialects.verified_only_select("SELECT INTERVAL '1 YEAR' AS one_year");
|
||||||
|
dialects.one_statement_parses_to(
|
||||||
"SELECT INTERVAL '1 YEAR' one_year",
|
"SELECT INTERVAL '1 YEAR' one_year",
|
||||||
"SELECT INTERVAL '1 YEAR' AS one_year",
|
"SELECT INTERVAL '1 YEAR' AS one_year",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let sql = "SELECT INTERVAL '1 DAY' > INTERVAL '1 SECOND'";
|
||||||
|
let select = dialects.verified_only_select(sql);
|
||||||
|
assert_eq!(
|
||||||
|
expr_from_projection(only(&select.projection)),
|
||||||
|
&Expr::BinaryOp {
|
||||||
|
left: Box::new(Expr::Interval(Interval {
|
||||||
|
value: Box::new(Expr::Value(Value::SingleQuotedString(String::from(
|
||||||
|
"1 DAY"
|
||||||
|
)))),
|
||||||
|
leading_field: None,
|
||||||
|
leading_precision: None,
|
||||||
|
last_field: None,
|
||||||
|
fractional_seconds_precision: None,
|
||||||
|
})),
|
||||||
|
op: BinaryOperator::Gt,
|
||||||
|
right: Box::new(Expr::Interval(Interval {
|
||||||
|
value: Box::new(Expr::Value(Value::SingleQuotedString(String::from(
|
||||||
|
"1 SECOND"
|
||||||
|
)))),
|
||||||
|
leading_field: None,
|
||||||
|
leading_precision: None,
|
||||||
|
last_field: None,
|
||||||
|
fractional_seconds_precision: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interval_disallow_interval_expr_gt() {
|
||||||
|
let dialects = all_dialects_except(|d| d.require_interval_qualifier());
|
||||||
|
let expr = dialects.verified_expr("INTERVAL '1 second' > x");
|
||||||
|
assert_eq!(
|
||||||
|
expr,
|
||||||
|
Expr::BinaryOp {
|
||||||
|
left: Box::new(Expr::Interval(Interval {
|
||||||
|
value: Box::new(Expr::Value(Value::SingleQuotedString(
|
||||||
|
"1 second".to_string()
|
||||||
|
))),
|
||||||
|
leading_field: None,
|
||||||
|
leading_precision: None,
|
||||||
|
last_field: None,
|
||||||
|
fractional_seconds_precision: None,
|
||||||
|
},)),
|
||||||
|
op: BinaryOperator::Gt,
|
||||||
|
right: Box::new(Expr::Identifier(Ident {
|
||||||
|
value: "x".to_string(),
|
||||||
|
quote_style: None,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interval_disallow_interval_expr_double_colon() {
|
||||||
|
let dialects = all_dialects_except(|d| d.require_interval_qualifier());
|
||||||
|
let expr = dialects.verified_expr("INTERVAL '1 second'::TEXT");
|
||||||
|
assert_eq!(
|
||||||
|
expr,
|
||||||
|
Expr::Cast {
|
||||||
|
kind: CastKind::DoubleColon,
|
||||||
|
expr: Box::new(Expr::Interval(Interval {
|
||||||
|
value: Box::new(Expr::Value(Value::SingleQuotedString(
|
||||||
|
"1 second".to_string()
|
||||||
|
))),
|
||||||
|
leading_field: None,
|
||||||
|
leading_precision: None,
|
||||||
|
last_field: None,
|
||||||
|
fractional_seconds_precision: None,
|
||||||
|
})),
|
||||||
|
data_type: DataType::Text,
|
||||||
|
format: None,
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -5038,7 +5209,8 @@ fn parse_interval_and_or_xor() {
|
||||||
WHERE d3_date > d1_date + INTERVAL '5 days' \
|
WHERE d3_date > d1_date + INTERVAL '5 days' \
|
||||||
AND d2_date > d1_date + INTERVAL '3 days'";
|
AND d2_date > d1_date + INTERVAL '3 days'";
|
||||||
|
|
||||||
let actual_ast = Parser::parse_sql(&GenericDialect {}, sql).unwrap();
|
let dialects = all_dialects_except(|d| d.require_interval_qualifier());
|
||||||
|
let actual_ast = dialects.parse_sql_statements(sql).unwrap();
|
||||||
|
|
||||||
let expected_ast = vec![Statement::Query(Box::new(Query {
|
let expected_ast = vec![Statement::Query(Box::new(Query {
|
||||||
with: None,
|
with: None,
|
||||||
|
@ -5140,19 +5312,19 @@ fn parse_interval_and_or_xor() {
|
||||||
|
|
||||||
assert_eq!(actual_ast, expected_ast);
|
assert_eq!(actual_ast, expected_ast);
|
||||||
|
|
||||||
verified_stmt(
|
dialects.verified_stmt(
|
||||||
"SELECT col FROM test \
|
"SELECT col FROM test \
|
||||||
WHERE d3_date > d1_date + INTERVAL '5 days' \
|
WHERE d3_date > d1_date + INTERVAL '5 days' \
|
||||||
AND d2_date > d1_date + INTERVAL '3 days'",
|
AND d2_date > d1_date + INTERVAL '3 days'",
|
||||||
);
|
);
|
||||||
|
|
||||||
verified_stmt(
|
dialects.verified_stmt(
|
||||||
"SELECT col FROM test \
|
"SELECT col FROM test \
|
||||||
WHERE d3_date > d1_date + INTERVAL '5 days' \
|
WHERE d3_date > d1_date + INTERVAL '5 days' \
|
||||||
OR d2_date > d1_date + INTERVAL '3 days'",
|
OR d2_date > d1_date + INTERVAL '3 days'",
|
||||||
);
|
);
|
||||||
|
|
||||||
verified_stmt(
|
dialects.verified_stmt(
|
||||||
"SELECT col FROM test \
|
"SELECT col FROM test \
|
||||||
WHERE d3_date > d1_date + INTERVAL '5 days' \
|
WHERE d3_date > d1_date + INTERVAL '5 days' \
|
||||||
XOR d2_date > d1_date + INTERVAL '3 days'",
|
XOR d2_date > d1_date + INTERVAL '3 days'",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue