Add ODBC escape syntax support for time expressions (#1953)

This commit is contained in:
etgarperets 2025-07-29 13:37:04 +03:00 committed by GitHub
parent 97a5b61a73
commit bde269b56f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 259 additions and 110 deletions

View file

@ -1014,12 +1014,7 @@ pub enum Expr {
/// A constant of form `<data_type> 'value'`. /// A constant of form `<data_type> 'value'`.
/// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`), /// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`),
/// as well as constants of other types (a non-standard PostgreSQL extension). /// as well as constants of other types (a non-standard PostgreSQL extension).
TypedString { TypedString(TypedString),
data_type: DataType,
/// The value of the constant.
/// Hint: you can unwrap the string value using `value.into_string()`.
value: ValueWithSpan,
},
/// Scalar function call e.g. `LEFT(foo, 5)` /// Scalar function call e.g. `LEFT(foo, 5)`
Function(Function), Function(Function),
/// `CASE [<operand>] WHEN <condition> THEN <result> ... [ELSE <result>] END` /// `CASE [<operand>] WHEN <condition> THEN <result> ... [ELSE <result>] END`
@ -1734,10 +1729,7 @@ impl fmt::Display for Expr {
Expr::Nested(ast) => write!(f, "({ast})"), Expr::Nested(ast) => write!(f, "({ast})"),
Expr::Value(v) => write!(f, "{v}"), Expr::Value(v) => write!(f, "{v}"),
Expr::Prefixed { prefix, value } => write!(f, "{prefix} {value}"), Expr::Prefixed { prefix, value } => write!(f, "{prefix} {value}"),
Expr::TypedString { data_type, value } => { Expr::TypedString(ts) => ts.fmt(f),
write!(f, "{data_type}")?;
write!(f, " {value}")
}
Expr::Function(fun) => fun.fmt(f), Expr::Function(fun) => fun.fmt(f),
Expr::Case { Expr::Case {
case_token: _, case_token: _,
@ -7450,6 +7442,52 @@ pub struct DropDomain {
pub drop_behavior: Option<DropBehavior>, pub drop_behavior: Option<DropBehavior>,
} }
/// A constant of form `<data_type> 'value'`.
/// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`),
/// as well as constants of other types (a non-standard PostgreSQL extension).
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct TypedString {
pub data_type: DataType,
/// The value of the constant.
/// Hint: you can unwrap the string value using `value.into_string()`.
pub value: ValueWithSpan,
/// Flags whether this TypedString uses the [ODBC syntax].
///
/// Example:
/// ```sql
/// -- An ODBC date literal:
/// SELECT {d '2025-07-16'}
/// -- This is equivalent to the standard ANSI SQL literal:
/// SELECT DATE '2025-07-16'
///
/// [ODBC syntax]: https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/date-time-and-timestamp-literals?view=sql-server-2017
pub uses_odbc_syntax: bool,
}
impl fmt::Display for TypedString {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let data_type = &self.data_type;
let value = &self.value;
match self.uses_odbc_syntax {
false => {
write!(f, "{data_type}")?;
write!(f, " {value}")
}
true => {
let prefix = match data_type {
DataType::Date => "d",
DataType::Time(..) => "t",
DataType::Timestamp(..) => "ts",
_ => "?",
};
write!(f, "{{{prefix} {value}}}")
}
}
}
}
/// A function call /// A function call
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

View file

@ -15,7 +15,7 @@
// specific language governing permissions and limitations // specific language governing permissions and limitations
// under the License. // under the License.
use crate::ast::{query::SelectItemQualifiedWildcardKind, ColumnOptions, ExportData}; use crate::ast::{query::SelectItemQualifiedWildcardKind, ColumnOptions, ExportData, TypedString};
use core::iter; use core::iter;
use crate::tokenizer::Span; use crate::tokenizer::Span;
@ -1525,7 +1525,7 @@ impl Spanned for Expr {
.union(&union_spans(collation.0.iter().map(|i| i.span()))), .union(&union_spans(collation.0.iter().map(|i| i.span()))),
Expr::Nested(expr) => expr.span(), Expr::Nested(expr) => expr.span(),
Expr::Value(value) => value.span(), Expr::Value(value) => value.span(),
Expr::TypedString { value, .. } => value.span(), Expr::TypedString(TypedString { value, .. }) => value.span(),
Expr::Function(function) => function.span(), Expr::Function(function) => function.span(),
Expr::GroupingSets(vec) => { Expr::GroupingSets(vec) => {
union_spans(vec.iter().flat_map(|i| i.iter().map(|k| k.span()))) union_spans(vec.iter().flat_map(|i| i.iter().map(|k| k.span())))

View file

@ -1543,10 +1543,11 @@ impl<'a> Parser<'a> {
// an unary negation `NOT ('a' LIKE 'b')`. To solve this, we don't accept the // an unary negation `NOT ('a' LIKE 'b')`. To solve this, we don't accept the
// `type 'string'` syntax for the custom data types at all. // `type 'string'` syntax for the custom data types at all.
DataType::Custom(..) => parser_err!("dummy", loc), DataType::Custom(..) => parser_err!("dummy", loc),
data_type => Ok(Expr::TypedString { data_type => Ok(Expr::TypedString(TypedString {
data_type, data_type,
value: parser.parse_value()?, value: parser.parse_value()?,
}), uses_odbc_syntax: false,
})),
} }
})?; })?;
@ -1732,10 +1733,11 @@ impl<'a> Parser<'a> {
} }
fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result<Expr, ParserError> { fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result<Expr, ParserError> {
Ok(Expr::TypedString { Ok(Expr::TypedString(TypedString {
data_type: DataType::GeometricType(kind), data_type: DataType::GeometricType(kind),
value: self.parse_value()?, value: self.parse_value()?,
}) uses_odbc_syntax: false,
}))
} }
/// Try to parse an [Expr::CompoundFieldAccess] like `a.b.c` or `a.b[1].c`. /// Try to parse an [Expr::CompoundFieldAccess] like `a.b.c` or `a.b[1].c`.
@ -2032,6 +2034,50 @@ impl<'a> Parser<'a> {
}) })
} }
/// Tries to parse the body of an [ODBC escaping sequence]
/// i.e. without the enclosing braces
/// Currently implemented:
/// Scalar Function Calls
/// Date, Time, and Timestamp Literals
/// See <https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/escape-sequences-in-odbc?view=sql-server-2017>
fn maybe_parse_odbc_body(&mut self) -> Result<Option<Expr>, ParserError> {
// Attempt 1: Try to parse it as a function.
if let Some(expr) = self.maybe_parse_odbc_fn_body()? {
return Ok(Some(expr));
}
// Attempt 2: Try to parse it as a Date, Time or Timestamp Literal
self.maybe_parse_odbc_body_datetime()
}
/// Tries to parse the body of an [ODBC Date, Time, and Timestamp Literals] call.
///
/// ```sql
/// {d '2025-07-17'}
/// {t '14:12:01'}
/// {ts '2025-07-17 14:12:01'}
/// ```
///
/// [ODBC Date, Time, and Timestamp Literals]:
/// https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/date-time-and-timestamp-literals?view=sql-server-2017
fn maybe_parse_odbc_body_datetime(&mut self) -> Result<Option<Expr>, ParserError> {
self.maybe_parse(|p| {
let token = p.next_token().clone();
let word_string = token.token.to_string();
let data_type = match word_string.as_str() {
"t" => DataType::Time(None, TimezoneInfo::None),
"d" => DataType::Date,
"ts" => DataType::Timestamp(None, TimezoneInfo::None),
_ => return p.expected("ODBC datetime keyword (t, d, or ts)", token),
};
let value = p.parse_value()?;
Ok(Expr::TypedString(TypedString {
data_type,
value,
uses_odbc_syntax: true,
}))
})
}
/// Tries to parse the body of an [ODBC function] call. /// Tries to parse the body of an [ODBC function] call.
/// i.e. without the enclosing braces /// i.e. without the enclosing braces
/// ///
@ -2786,7 +2832,7 @@ impl<'a> Parser<'a> {
fn parse_lbrace_expr(&mut self) -> Result<Expr, ParserError> { fn parse_lbrace_expr(&mut self) -> Result<Expr, ParserError> {
let token = self.expect_token(&Token::LBrace)?; let token = self.expect_token(&Token::LBrace)?;
if let Some(fn_expr) = self.maybe_parse_odbc_fn_body()? { if let Some(fn_expr) = self.maybe_parse_odbc_body()? {
self.expect_token(&Token::RBrace)?; self.expect_token(&Token::RBrace)?;
return Ok(fn_expr); return Ok(fn_expr);
} }

View file

@ -906,13 +906,14 @@ fn parse_typed_struct_syntax_bigquery() {
); );
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::Datetime(None), data_type: DataType::Datetime(None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()),
span: Span::empty(), span: Span::empty(),
}, },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::Datetime(None), field_type: DataType::Datetime(None),
@ -968,15 +969,16 @@ fn parse_typed_struct_syntax_bigquery() {
); );
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::JSON, data_type: DataType::JSON,
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString( value: Value::SingleQuotedString(
r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into()
), ),
span: Span::empty(), span: Span::empty(),
} },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::JSON, field_type: DataType::JSON,
@ -1004,7 +1006,7 @@ fn parse_typed_struct_syntax_bigquery() {
); );
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::Timestamp(None, TimezoneInfo::None), data_type: DataType::Timestamp(None, TimezoneInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString( value: Value::SingleQuotedString(
@ -1012,7 +1014,8 @@ fn parse_typed_struct_syntax_bigquery() {
), ),
span: Span::empty(), span: Span::empty(),
}, },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::Timestamp(None, TimezoneInfo::None), field_type: DataType::Timestamp(None, TimezoneInfo::None),
@ -1024,13 +1027,14 @@ fn parse_typed_struct_syntax_bigquery() {
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::Time(None, TimezoneInfo::None), data_type: DataType::Time(None, TimezoneInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("15:30:00".into()), value: Value::SingleQuotedString("15:30:00".into()),
span: Span::empty(), span: Span::empty(),
} },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::Time(None, TimezoneInfo::None), field_type: DataType::Time(None, TimezoneInfo::None),
@ -1045,13 +1049,14 @@ fn parse_typed_struct_syntax_bigquery() {
assert_eq!(2, select.projection.len()); assert_eq!(2, select.projection.len());
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::Numeric(ExactNumberInfo::None), data_type: DataType::Numeric(ExactNumberInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1".into()), value: Value::SingleQuotedString("1".into()),
span: Span::empty(), span: Span::empty(),
} },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::Numeric(ExactNumberInfo::None), field_type: DataType::Numeric(ExactNumberInfo::None),
@ -1062,13 +1067,14 @@ fn parse_typed_struct_syntax_bigquery() {
); );
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None), data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1".into()), value: Value::SingleQuotedString("1".into()),
span: Span::empty(), span: Span::empty(),
} },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::BigNumeric(ExactNumberInfo::None), field_type: DataType::BigNumeric(ExactNumberInfo::None),
@ -1239,13 +1245,14 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
); );
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::Datetime(None), data_type: DataType::Datetime(None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()),
span: Span::empty(), span: Span::empty(),
} },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::Datetime(None), field_type: DataType::Datetime(None),
@ -1301,15 +1308,16 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
); );
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::JSON, data_type: DataType::JSON,
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString( value: Value::SingleQuotedString(
r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into() r#"{"class" : {"students" : [{"name" : "Jane"}]}}"#.into()
), ),
span: Span::empty(), span: Span::empty(),
} },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::JSON, field_type: DataType::JSON,
@ -1337,15 +1345,16 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
); );
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::Timestamp(None, TimezoneInfo::None), data_type: DataType::Timestamp(None, TimezoneInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString( value: Value::SingleQuotedString(
"2008-12-25 15:30:00 America/Los_Angeles".into() "2008-12-25 15:30:00 America/Los_Angeles".into()
), ),
span: Span::empty(), span: Span::empty(),
} },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::Timestamp(None, TimezoneInfo::None), field_type: DataType::Timestamp(None, TimezoneInfo::None),
@ -1357,13 +1366,14 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::Time(None, TimezoneInfo::None), data_type: DataType::Time(None, TimezoneInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("15:30:00".into()), value: Value::SingleQuotedString("15:30:00".into()),
span: Span::empty(), span: Span::empty(),
} },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::Time(None, TimezoneInfo::None), field_type: DataType::Time(None, TimezoneInfo::None),
@ -1378,13 +1388,14 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
assert_eq!(2, select.projection.len()); assert_eq!(2, select.projection.len());
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::Numeric(ExactNumberInfo::None), data_type: DataType::Numeric(ExactNumberInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1".into()), value: Value::SingleQuotedString("1".into()),
span: Span::empty(), span: Span::empty(),
} },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::Numeric(ExactNumberInfo::None), field_type: DataType::Numeric(ExactNumberInfo::None),
@ -1395,13 +1406,14 @@ fn parse_typed_struct_syntax_bigquery_and_generic() {
); );
assert_eq!( assert_eq!(
&Expr::Struct { &Expr::Struct {
values: vec![Expr::TypedString { values: vec![Expr::TypedString(TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None), data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1".into()), value: Value::SingleQuotedString("1".into()),
span: Span::empty(), span: Span::empty(),
} },
}], uses_odbc_syntax: false
})],
fields: vec![StructField { fields: vec![StructField {
field_name: None, field_name: None,
field_type: DataType::BigNumeric(ExactNumberInfo::None), field_type: DataType::BigNumeric(ExactNumberInfo::None),
@ -2433,13 +2445,14 @@ fn test_triple_quote_typed_strings() {
let expr = bigquery().verified_expr(r#"JSON """{"foo":"bar's"}""""#); let expr = bigquery().verified_expr(r#"JSON """{"foo":"bar's"}""""#);
assert_eq!( assert_eq!(
Expr::TypedString { Expr::TypedString(TypedString {
data_type: DataType::JSON, data_type: DataType::JSON,
value: ValueWithSpan { value: ValueWithSpan {
value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()), value: Value::TripleDoubleQuotedString(r#"{"foo":"bar's"}"#.into()),
span: Span::empty(), span: Span::empty(),
}
}, },
uses_odbc_syntax: false
}),
expr expr
); );
} }

View file

@ -5914,13 +5914,14 @@ fn parse_literal_date() {
let sql = "SELECT DATE '1999-01-01'"; let sql = "SELECT DATE '1999-01-01'";
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::Date, data_type: DataType::Date,
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01".into()), value: Value::SingleQuotedString("1999-01-01".into()),
span: Span::empty(), span: Span::empty(),
}
}, },
uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
} }
@ -5930,13 +5931,14 @@ fn parse_literal_time() {
let sql = "SELECT TIME '01:23:34'"; let sql = "SELECT TIME '01:23:34'";
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::Time(None, TimezoneInfo::None), data_type: DataType::Time(None, TimezoneInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("01:23:34".into()), value: Value::SingleQuotedString("01:23:34".into()),
span: Span::empty(), span: Span::empty(),
}, },
}, uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
} }
@ -5946,13 +5948,14 @@ fn parse_literal_datetime() {
let sql = "SELECT DATETIME '1999-01-01 01:23:34.45'"; let sql = "SELECT DATETIME '1999-01-01 01:23:34.45'";
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::Datetime(None), data_type: DataType::Datetime(None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()), value: Value::SingleQuotedString("1999-01-01 01:23:34.45".into()),
span: Span::empty(), span: Span::empty(),
}, },
}, uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
} }
@ -5962,13 +5965,14 @@ fn parse_literal_timestamp_without_time_zone() {
let sql = "SELECT TIMESTAMP '1999-01-01 01:23:34'"; let sql = "SELECT TIMESTAMP '1999-01-01 01:23:34'";
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::Timestamp(None, TimezoneInfo::None), data_type: DataType::Timestamp(None, TimezoneInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01 01:23:34".into()), value: Value::SingleQuotedString("1999-01-01 01:23:34".into()),
span: Span::empty(), span: Span::empty(),
}, },
}, uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
@ -5980,13 +5984,14 @@ fn parse_literal_timestamp_with_time_zone() {
let sql = "SELECT TIMESTAMPTZ '1999-01-01 01:23:34Z'"; let sql = "SELECT TIMESTAMPTZ '1999-01-01 01:23:34Z'";
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::Timestamp(None, TimezoneInfo::Tz), data_type: DataType::Timestamp(None, TimezoneInfo::Tz),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()), value: Value::SingleQuotedString("1999-01-01 01:23:34Z".into()),
span: Span::empty(), span: Span::empty(),
}, },
}, uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
@ -6556,7 +6561,7 @@ fn parse_json_keyword() {
}'"#; }'"#;
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::JSON, data_type: DataType::JSON,
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString( value: Value::SingleQuotedString(
@ -6583,8 +6588,9 @@ fn parse_json_keyword() {
.to_string() .to_string()
), ),
span: Span::empty(), span: Span::empty(),
}
}, },
uses_odbc_syntax: false,
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
} }
@ -6593,17 +6599,23 @@ fn parse_json_keyword() {
fn parse_typed_strings() { fn parse_typed_strings() {
let expr = verified_expr(r#"JSON '{"foo":"bar"}'"#); let expr = verified_expr(r#"JSON '{"foo":"bar"}'"#);
assert_eq!( assert_eq!(
Expr::TypedString { Expr::TypedString(TypedString {
data_type: DataType::JSON, data_type: DataType::JSON,
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()), value: Value::SingleQuotedString(r#"{"foo":"bar"}"#.into()),
span: Span::empty(), span: Span::empty(),
}
}, },
uses_odbc_syntax: false
}),
expr expr
); );
if let Expr::TypedString { data_type, value } = expr { if let Expr::TypedString(TypedString {
data_type,
value,
uses_odbc_syntax: false,
}) = expr
{
assert_eq!(DataType::JSON, data_type); assert_eq!(DataType::JSON, data_type);
assert_eq!(r#"{"foo":"bar"}"#, value.into_string().unwrap()); assert_eq!(r#"{"foo":"bar"}"#, value.into_string().unwrap());
} }
@ -6614,13 +6626,14 @@ fn parse_bignumeric_keyword() {
let sql = r#"SELECT BIGNUMERIC '0'"#; let sql = r#"SELECT BIGNUMERIC '0'"#;
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None), data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString(r#"0"#.into()), value: Value::SingleQuotedString(r#"0"#.into()),
span: Span::empty(), span: Span::empty(),
}
}, },
uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
verified_stmt("SELECT BIGNUMERIC '0'"); verified_stmt("SELECT BIGNUMERIC '0'");
@ -6628,13 +6641,14 @@ fn parse_bignumeric_keyword() {
let sql = r#"SELECT BIGNUMERIC '123456'"#; let sql = r#"SELECT BIGNUMERIC '123456'"#;
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None), data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString(r#"123456"#.into()), value: Value::SingleQuotedString(r#"123456"#.into()),
span: Span::empty(), span: Span::empty(),
}
}, },
uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
verified_stmt("SELECT BIGNUMERIC '123456'"); verified_stmt("SELECT BIGNUMERIC '123456'");
@ -6642,13 +6656,14 @@ fn parse_bignumeric_keyword() {
let sql = r#"SELECT BIGNUMERIC '-3.14'"#; let sql = r#"SELECT BIGNUMERIC '-3.14'"#;
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None), data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString(r#"-3.14"#.into()), value: Value::SingleQuotedString(r#"-3.14"#.into()),
span: Span::empty(), span: Span::empty(),
}
}, },
uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
verified_stmt("SELECT BIGNUMERIC '-3.14'"); verified_stmt("SELECT BIGNUMERIC '-3.14'");
@ -6656,13 +6671,14 @@ fn parse_bignumeric_keyword() {
let sql = r#"SELECT BIGNUMERIC '-0.54321'"#; let sql = r#"SELECT BIGNUMERIC '-0.54321'"#;
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None), data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString(r#"-0.54321"#.into()), value: Value::SingleQuotedString(r#"-0.54321"#.into()),
span: Span::empty(), span: Span::empty(),
}
}, },
uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
verified_stmt("SELECT BIGNUMERIC '-0.54321'"); verified_stmt("SELECT BIGNUMERIC '-0.54321'");
@ -6670,13 +6686,14 @@ fn parse_bignumeric_keyword() {
let sql = r#"SELECT BIGNUMERIC '1.23456e05'"#; let sql = r#"SELECT BIGNUMERIC '1.23456e05'"#;
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None), data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString(r#"1.23456e05"#.into()), value: Value::SingleQuotedString(r#"1.23456e05"#.into()),
span: Span::empty(), span: Span::empty(),
}
}, },
uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
verified_stmt("SELECT BIGNUMERIC '1.23456e05'"); verified_stmt("SELECT BIGNUMERIC '1.23456e05'");
@ -6684,13 +6701,14 @@ fn parse_bignumeric_keyword() {
let sql = r#"SELECT BIGNUMERIC '-9.876e-3'"#; let sql = r#"SELECT BIGNUMERIC '-9.876e-3'"#;
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
&Expr::TypedString { &Expr::TypedString(TypedString {
data_type: DataType::BigNumeric(ExactNumberInfo::None), data_type: DataType::BigNumeric(ExactNumberInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString(r#"-9.876e-3"#.into()), value: Value::SingleQuotedString(r#"-9.876e-3"#.into()),
span: Span::empty(), span: Span::empty(),
}
}, },
uses_odbc_syntax: false
}),
expr_from_projection(only(&select.projection)), expr_from_projection(only(&select.projection)),
); );
verified_stmt("SELECT BIGNUMERIC '-9.876e-3'"); verified_stmt("SELECT BIGNUMERIC '-9.876e-3'");
@ -15015,83 +15033,90 @@ fn test_geometry_type() {
let sql = "point '1,2'"; let sql = "point '1,2'";
assert_eq!( assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString { Expr::TypedString(TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Point), data_type: DataType::GeometricType(GeometricTypeKind::Point),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1,2".to_string()), value: Value::SingleQuotedString("1,2".to_string()),
span: Span::empty(), span: Span::empty(),
}, },
} uses_odbc_syntax: false
})
); );
let sql = "line '1,2,3,4'"; let sql = "line '1,2,3,4'";
assert_eq!( assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString { Expr::TypedString(TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Line), data_type: DataType::GeometricType(GeometricTypeKind::Line),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3,4".to_string()), value: Value::SingleQuotedString("1,2,3,4".to_string()),
span: Span::empty(), span: Span::empty(),
}, },
} uses_odbc_syntax: false
})
); );
let sql = "path '1,2,3,4'"; let sql = "path '1,2,3,4'";
assert_eq!( assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString { Expr::TypedString(TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::GeometricPath), data_type: DataType::GeometricType(GeometricTypeKind::GeometricPath),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3,4".to_string()), value: Value::SingleQuotedString("1,2,3,4".to_string()),
span: Span::empty(), span: Span::empty(),
}, },
} uses_odbc_syntax: false
})
); );
let sql = "box '1,2,3,4'"; let sql = "box '1,2,3,4'";
assert_eq!( assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString { Expr::TypedString(TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::GeometricBox), data_type: DataType::GeometricType(GeometricTypeKind::GeometricBox),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3,4".to_string()), value: Value::SingleQuotedString("1,2,3,4".to_string()),
span: Span::empty(), span: Span::empty(),
}, },
} uses_odbc_syntax: false
})
); );
let sql = "circle '1,2,3'"; let sql = "circle '1,2,3'";
assert_eq!( assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString { Expr::TypedString(TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Circle), data_type: DataType::GeometricType(GeometricTypeKind::Circle),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3".to_string()), value: Value::SingleQuotedString("1,2,3".to_string()),
span: Span::empty(), span: Span::empty(),
}, },
} uses_odbc_syntax: false
})
); );
let sql = "polygon '1,2,3,4'"; let sql = "polygon '1,2,3,4'";
assert_eq!( assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString { Expr::TypedString(TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::Polygon), data_type: DataType::GeometricType(GeometricTypeKind::Polygon),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3,4".to_string()), value: Value::SingleQuotedString("1,2,3,4".to_string()),
span: Span::empty(), span: Span::empty(),
}, },
} uses_odbc_syntax: false
})
); );
let sql = "lseg '1,2,3,4'"; let sql = "lseg '1,2,3,4'";
assert_eq!( assert_eq!(
all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql), all_dialects_where(|d| d.supports_geometric_types()).verified_expr(sql),
Expr::TypedString { Expr::TypedString(TypedString {
data_type: DataType::GeometricType(GeometricTypeKind::LineSegment), data_type: DataType::GeometricType(GeometricTypeKind::LineSegment),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("1,2,3,4".to_string()), value: Value::SingleQuotedString("1,2,3,4".to_string()),
span: Span::empty(), span: Span::empty(),
}, },
} uses_odbc_syntax: false
})
); );
} }
#[test] #[test]
@ -16291,6 +16316,31 @@ fn parse_notnull() {
notnull_unsupported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL"); notnull_unsupported_dialects.expr_parses_to("NOT NULL NOTNULL", "NOT NULL");
} }
#[test]
fn parse_odbc_time_date_timestamp() {
// Supported statements
let sql_d = "SELECT {d '2025-07-17'}, category_name FROM categories";
let _ = all_dialects().verified_stmt(sql_d);
let sql_t = "SELECT {t '14:12:01'}, category_name FROM categories";
let _ = all_dialects().verified_stmt(sql_t);
let sql_ts = "SELECT {ts '2025-07-17 14:12:01'}, category_name FROM categories";
let _ = all_dialects().verified_stmt(sql_ts);
// Unsupported statement
let supports_dictionary = all_dialects_where(|d| d.supports_dictionary_syntax());
let dictionary_unsupported = all_dialects_where(|d| !d.supports_dictionary_syntax());
let sql = "SELECT {tt '14:12:01'} FROM foo";
let res = supports_dictionary.parse_sql_statements(sql);
let res_dict = dictionary_unsupported.parse_sql_statements(sql);
assert_eq!(
ParserError::ParserError("Expected: :, found: '14:12:01'".to_string()),
res.unwrap_err()
);
assert_eq!(
ParserError::ParserError("Expected: an expression, found: {".to_string()),
res_dict.unwrap_err()
);
}
#[test] #[test]
fn parse_create_user() { fn parse_create_user() {
let create = verified_stmt("CREATE USER u1"); let create = verified_stmt("CREATE USER u1");

View file

@ -327,13 +327,14 @@ fn data_type_timestamp_ntz() {
// Literal // Literal
assert_eq!( assert_eq!(
databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"), databricks().verified_expr("TIMESTAMP_NTZ '2025-03-29T18:52:00'"),
Expr::TypedString { Expr::TypedString(TypedString {
data_type: DataType::TimestampNtz, data_type: DataType::TimestampNtz,
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()), value: Value::SingleQuotedString("2025-03-29T18:52:00".to_owned()),
span: Span::empty(), span: Span::empty(),
} },
} uses_odbc_syntax: false
})
); );
// Cast // Cast

View file

@ -4723,7 +4723,7 @@ fn parse_dollar_quoted_string() {
quote_style: None, quote_style: None,
span: Span::empty(), span: Span::empty(),
}, },
} },
); );
assert_eq!( assert_eq!(
@ -5296,13 +5296,14 @@ fn parse_at_time_zone() {
// check precedence // check precedence
let expr = Expr::BinaryOp { let expr = Expr::BinaryOp {
left: Box::new(Expr::AtTimeZone { left: Box::new(Expr::AtTimeZone {
timestamp: Box::new(Expr::TypedString { timestamp: Box::new(Expr::TypedString(TypedString {
data_type: DataType::Timestamp(None, TimezoneInfo::None), data_type: DataType::Timestamp(None, TimezoneInfo::None),
value: ValueWithSpan { value: ValueWithSpan {
value: Value::SingleQuotedString("2001-09-28 01:00".to_string()), value: Value::SingleQuotedString("2001-09-28 01:00".to_string()),
span: Span::empty(), span: Span::empty(),
}, },
}), uses_odbc_syntax: false,
})),
time_zone: Box::new(Expr::Cast { time_zone: Box::new(Expr::Cast {
kind: CastKind::DoubleColon, kind: CastKind::DoubleColon,
expr: Box::new(Expr::Value( expr: Box::new(Expr::Value(