Skip to content

Commit 31d62c8

Browse files
etgarperetsayman-sigma
authored andcommitted
Add ODBC escape syntax support for time expressions (apache#1953)
1 parent 04de185 commit 31d62c8

7 files changed

Lines changed: 259 additions & 110 deletions

File tree

src/ast/mod.rs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,12 +1037,7 @@ pub enum Expr {
10371037
/// A constant of form `<data_type> 'value'`.
10381038
/// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`),
10391039
/// as well as constants of other types (a non-standard PostgreSQL extension).
1040-
TypedString {
1041-
data_type: DataType,
1042-
/// The value of the constant.
1043-
/// Hint: you can unwrap the string value using `value.into_string()`.
1044-
value: ValueWithSpan,
1045-
},
1040+
TypedString(TypedString),
10461041
/// Scalar function call e.g. `LEFT(foo, 5)`
10471042
Function(Function),
10481043
/// `CASE [<operand>] WHEN <condition> THEN <result> ... [ELSE <result>] END`
@@ -1768,10 +1763,7 @@ impl fmt::Display for Expr {
17681763
Expr::Nested(ast) => write!(f, "({ast})"),
17691764
Expr::Value(v) => write!(f, "{v}"),
17701765
Expr::Prefixed { prefix, value } => write!(f, "{prefix} {value}"),
1771-
Expr::TypedString { data_type, value } => {
1772-
write!(f, "{data_type}")?;
1773-
write!(f, " {value}")
1774-
}
1766+
Expr::TypedString(ts) => ts.fmt(f),
17751767
Expr::Function(fun) => fun.fmt(f),
17761768
Expr::Case {
17771769
case_token: _,
@@ -7479,6 +7471,52 @@ pub struct DropDomain {
74797471
pub drop_behavior: Option<DropBehavior>,
74807472
}
74817473

7474+
/// A constant of form `<data_type> 'value'`.
7475+
/// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`),
7476+
/// as well as constants of other types (a non-standard PostgreSQL extension).
7477+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
7478+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7479+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
7480+
pub struct TypedString {
7481+
pub data_type: DataType,
7482+
/// The value of the constant.
7483+
/// Hint: you can unwrap the string value using `value.into_string()`.
7484+
pub value: ValueWithSpan,
7485+
/// Flags whether this TypedString uses the [ODBC syntax].
7486+
///
7487+
/// Example:
7488+
/// ```sql
7489+
/// -- An ODBC date literal:
7490+
/// SELECT {d '2025-07-16'}
7491+
/// -- This is equivalent to the standard ANSI SQL literal:
7492+
/// SELECT DATE '2025-07-16'
7493+
///
7494+
/// [ODBC syntax]: https://learn.microsoft.com/en-us/sql/odbc/reference/develop-app/date-time-and-timestamp-literals?view=sql-server-2017
7495+
pub uses_odbc_syntax: bool,
7496+
}
7497+
7498+
impl fmt::Display for TypedString {
7499+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
7500+
let data_type = &self.data_type;
7501+
let value = &self.value;
7502+
match self.uses_odbc_syntax {
7503+
false => {
7504+
write!(f, "{data_type}")?;
7505+
write!(f, " {value}")
7506+
}
7507+
true => {
7508+
let prefix = match data_type {
7509+
DataType::Date => "d",
7510+
DataType::Time(..) => "t",
7511+
DataType::Timestamp(..) => "ts",
7512+
_ => "?",
7513+
};
7514+
write!(f, "{{{prefix} {value}}}")
7515+
}
7516+
}
7517+
}
7518+
}
7519+
74827520
/// A function call
74837521
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
74847522
#[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;
@@ -1530,7 +1530,7 @@ impl Spanned for Expr {
15301530
.union(&union_spans(collation.0.iter().map(|i| i.span()))),
15311531
Expr::Nested(expr) => expr.span(),
15321532
Expr::Value(value) => value.span(),
1533-
Expr::TypedString { value, .. } => value.span(),
1533+
Expr::TypedString(TypedString { value, .. }) => value.span(),
15341534
Expr::Function(function) => function.span(),
15351535
Expr::GroupingSets(vec) => {
15361536
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)