Skip to content

Commit bde269b

Browse files
authored
Add ODBC escape syntax support for time expressions (#1953)
1 parent 97a5b61 commit bde269b

File tree

7 files changed

+259
-110
lines changed

7 files changed

+259
-110
lines changed

src/ast/mod.rs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,12 +1014,7 @@ pub enum Expr {
10141014
/// A constant of form `<data_type> 'value'`.
10151015
/// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`),
10161016
/// as well as constants of other types (a non-standard PostgreSQL extension).
1017-
TypedString {
1018-
data_type: DataType,
1019-
/// The value of the constant.
1020-
/// Hint: you can unwrap the string value using `value.into_string()`.
1021-
value: ValueWithSpan,
1022-
},
1017+
TypedString(TypedString),
10231018
/// Scalar function call e.g. `LEFT(foo, 5)`
10241019
Function(Function),
10251020
/// `CASE [<operand>] WHEN <condition> THEN <result> ... [ELSE <result>] END`
@@ -1734,10 +1729,7 @@ impl fmt::Display for Expr {
17341729
Expr::Nested(ast) => write!(f, "({ast})"),
17351730
Expr::Value(v) => write!(f, "{v}"),
17361731
Expr::Prefixed { prefix, value } => write!(f, "{prefix} {value}"),
1737-
Expr::TypedString { data_type, value } => {
1738-
write!(f, "{data_type}")?;
1739-
write!(f, " {value}")
1740-
}
1732+
Expr::TypedString(ts) => ts.fmt(f),
17411733
Expr::Function(fun) => fun.fmt(f),
17421734
Expr::Case {
17431735
case_token: _,
@@ -7450,6 +7442,52 @@ pub struct DropDomain {
74507442
pub drop_behavior: Option<DropBehavior>,
74517443
}
74527444

7445+
/// A constant of form `<data_type> 'value'`.
7446+
/// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`),
7447+
/// as well as constants of other types (a non-standard PostgreSQL extension).
7448+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
7449+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7450+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
7451+
pub struct TypedString {
7452+
pub data_type: DataType,
7453+
/// The value of the constant.
7454+
/// Hint: you can unwrap the string value using `value.into_string()`.
7455+
pub value: ValueWithSpan,
7456+
/// Flags whether this TypedString uses the [ODBC syntax].
7457+
///
7458+
/// Example:
7459+
/// ```sql
7460+
/// -- An ODBC date literal:
7461+
/// SELECT {d '2025-07-16'}
7462+
/// -- This is equivalent to the standard ANSI SQL literal:
7463+
/// SELECT DATE '2025-07-16'
7464+
///
7465+
/// [ODBC syntax]: https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/date-time-and-timestamp-literals?view=sql-server-2017
7466+
pub uses_odbc_syntax: bool,
7467+
}
7468+
7469+
impl fmt::Display for TypedString {
7470+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
7471+
let data_type = &self.data_type;
7472+
let value = &self.value;
7473+
match self.uses_odbc_syntax {
7474+
false => {
7475+
write!(f, "{data_type}")?;
7476+
write!(f, " {value}")
7477+
}
7478+
true => {
7479+
let prefix = match data_type {
7480+
DataType::Date => "d",
7481+
DataType::Time(..) => "t",
7482+
DataType::Timestamp(..) => "ts",
7483+
_ => "?",
7484+
};
7485+
write!(f, "{{{prefix} {value}}}")
7486+
}
7487+
}
7488+
}
7489+
}
7490+
74537491
/// A function call
74547492
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
74557493
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

src/ast/spans.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use crate::ast::{query::SelectItemQualifiedWildcardKind, ColumnOptions, ExportData};
18+
use crate::ast::{query::SelectItemQualifiedWildcardKind, ColumnOptions, ExportData, TypedString};
1919
use core::iter;
2020

2121
use crate::tokenizer::Span;
@@ -1525,7 +1525,7 @@ impl Spanned for Expr {
15251525
.union(&union_spans(collation.0.iter().map(|i| i.span()))),
15261526
Expr::Nested(expr) => expr.span(),
15271527
Expr::Value(value) => value.span(),
1528-
Expr::TypedString { value, .. } => value.span(),
1528+
Expr::TypedString(TypedString { value, .. }) => value.span(),
15291529
Expr::Function(function) => function.span(),
15301530
Expr::GroupingSets(vec) => {
15311531
union_spans(vec.iter().flat_map(|i| i.iter().map(|k| k.span())))

src/parser/mod.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,10 +1543,11 @@ impl<'a> Parser<'a> {
15431543
// an unary negation `NOT ('a' LIKE 'b')`. To solve this, we don't accept the
15441544
// `type 'string'` syntax for the custom data types at all.
15451545
DataType::Custom(..) => parser_err!("dummy", loc),
1546-
data_type => Ok(Expr::TypedString {
1546+
data_type => Ok(Expr::TypedString(TypedString {
15471547
data_type,
15481548
value: parser.parse_value()?,
1549-
}),
1549+
uses_odbc_syntax: false,
1550+
})),
15501551
}
15511552
})?;
15521553

@@ -1732,10 +1733,11 @@ impl<'a> Parser<'a> {
17321733
}
17331734

17341735
fn parse_geometric_type(&mut self, kind: GeometricTypeKind) -> Result<Expr, ParserError> {
1735-
Ok(Expr::TypedString {
1736+
Ok(Expr::TypedString(TypedString {
17361737
data_type: DataType::GeometricType(kind),
17371738
value: self.parse_value()?,
1738-
})
1739+
uses_odbc_syntax: false,
1740+
}))
17391741
}
17401742

17411743
/// Try to parse an [Expr::CompoundFieldAccess] like `a.b.c` or `a.b[1].c`.
@@ -2032,6 +2034,50 @@ impl<'a> Parser<'a> {
20322034
})
20332035
}
20342036

2037+
/// Tries to parse the body of an [ODBC escaping sequence]
2038+
/// i.e. without the enclosing braces
2039+
/// Currently implemented:
2040+
/// Scalar Function Calls
2041+
/// Date, Time, and Timestamp Literals
2042+
/// See <https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/escape-sequences-in-odbc?view=sql-server-2017>
2043+
fn maybe_parse_odbc_body(&mut self) -> Result<Option<Expr>, ParserError> {
2044+
// Attempt 1: Try to parse it as a function.
2045+
if let Some(expr) = self.maybe_parse_odbc_fn_body()? {
2046+
return Ok(Some(expr));
2047+
}
2048+
// Attempt 2: Try to parse it as a Date, Time or Timestamp Literal
2049+
self.maybe_parse_odbc_body_datetime()
2050+
}
2051+
2052+
/// Tries to parse the body of an [ODBC Date, Time, and Timestamp Literals] call.
2053+
///
2054+
/// ```sql
2055+
/// {d '2025-07-17'}
2056+
/// {t '14:12:01'}
2057+
/// {ts '2025-07-17 14:12:01'}
2058+
/// ```
2059+
///
2060+
/// [ODBC Date, Time, and Timestamp Literals]:
2061+
/// https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/date-time-and-timestamp-literals?view=sql-server-2017
2062+
fn maybe_parse_odbc_body_datetime(&mut self) -> Result<Option<Expr>, ParserError> {
2063+
self.maybe_parse(|p| {
2064+
let token = p.next_token().clone();
2065+
let word_string = token.token.to_string();
2066+
let data_type = match word_string.as_str() {
2067+
"t" => DataType::Time(None, TimezoneInfo::None),
2068+
"d" => DataType::Date,
2069+
"ts" => DataType::Timestamp(None, TimezoneInfo::None),
2070+
_ => return p.expected("ODBC datetime keyword (t, d, or ts)", token),
2071+
};
2072+
let value = p.parse_value()?;
2073+
Ok(Expr::TypedString(TypedString {
2074+
data_type,
2075+
value,
2076+
uses_odbc_syntax: true,
2077+
}))
2078+
})
2079+
}
2080+
20352081
/// Tries to parse the body of an [ODBC function] call.
20362082
/// i.e. without the enclosing braces
20372083
///
@@ -2786,7 +2832,7 @@ impl<'a> Parser<'a> {
27862832
fn parse_lbrace_expr(&mut self) -> Result<Expr, ParserError> {
27872833
let token = self.expect_token(&Token::LBrace)?;
27882834

2789-
if let Some(fn_expr) = self.maybe_parse_odbc_fn_body()? {
2835+
if let Some(fn_expr) = self.maybe_parse_odbc_body()? {
27902836
self.expect_token(&Token::RBrace)?;
27912837
return Ok(fn_expr);
27922838
}

0 commit comments

Comments
 (0)