Skip to content

Commit ebe9107

Browse files
committed
Support for projection prefix operator (CONNECT_BY_ROOT)
1 parent 896c088 commit ebe9107

File tree

8 files changed

+100
-20
lines changed

8 files changed

+100
-20
lines changed

src/ast/mod.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -929,12 +929,14 @@ pub enum Expr {
929929
Nested(Box<Expr>),
930930
/// A literal value, such as string, number, date or NULL
931931
Value(ValueWithSpan),
932+
/// Prefixed expression, e.g. introducer strings, projection prefix
932933
/// <https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html>
933-
IntroducedString {
934-
introducer: String,
934+
/// <https://docs.snowflake.com/en/sql-reference/constructs/connect-by>
935+
Prefixed {
936+
prefix: Ident,
935937
/// The value of the constant.
936938
/// Hint: you can unwrap the string value using `value.into_string()`.
937-
value: Value,
939+
value: Box<Expr>,
938940
},
939941
/// A constant of form `<data_type> 'value'`.
940942
/// This can represent ANSI SQL `DATE`, `TIME`, and `TIMESTAMP` literals (such as `DATE '2020-01-01'`),
@@ -1654,7 +1656,7 @@ impl fmt::Display for Expr {
16541656
Expr::Collate { expr, collation } => write!(f, "{expr} COLLATE {collation}"),
16551657
Expr::Nested(ast) => write!(f, "({ast})"),
16561658
Expr::Value(v) => write!(f, "{v}"),
1657-
Expr::IntroducedString { introducer, value } => write!(f, "{introducer} {value}"),
1659+
Expr::Prefixed { prefix, value } => write!(f, "{prefix} {value}"),
16581660
Expr::TypedString { data_type, value } => {
16591661
write!(f, "{data_type}")?;
16601662
write!(f, " {value}")

src/ast/spans.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1544,7 +1544,7 @@ impl Spanned for Expr {
15441544
.map(|items| union_spans(items.iter().map(|i| i.span()))),
15451545
),
15461546
),
1547-
Expr::IntroducedString { value, .. } => value.span(),
1547+
Expr::Prefixed { value, .. } => value.span(),
15481548
Expr::Case {
15491549
operand,
15501550
conditions,

src/dialect/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,12 @@ pub trait Dialect: Debug + Any {
888888
keywords::RESERVED_FOR_TABLE_FACTOR
889889
}
890890

891+
/// Returns reserved keywords that may prefix a select item expression
892+
/// e.g. `SELECT CONNECT_BY_ROOT name FROM Tbl2` (Snowflake)
893+
fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] {
894+
&[]
895+
}
896+
891897
/// Returns true if this dialect supports the `TABLESAMPLE` option
892898
/// before the table alias option. For example:
893899
///

src/dialect/snowflake.rs

+6
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ use alloc::{format, vec};
4444
use super::keywords::RESERVED_FOR_IDENTIFIER;
4545
use sqlparser::ast::StorageSerializationPolicy;
4646

47+
const RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR: [Keyword; 1] = [Keyword::CONNECT_BY_ROOT];
4748
/// A [`Dialect`] for [Snowflake](https://www.snowflake.com/)
4849
#[derive(Debug, Default)]
4950
pub struct SnowflakeDialect;
@@ -346,6 +347,11 @@ impl Dialect for SnowflakeDialect {
346347
fn supports_group_by_expr(&self) -> bool {
347348
true
348349
}
350+
351+
/// See: <https://docs.snowflake.com/en/sql-reference/constructs/connect-by>
352+
fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] {
353+
&RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR
354+
}
349355
}
350356

351357
fn parse_file_staging_command(kw: Keyword, parser: &mut Parser) -> Result<Statement, ParserError> {

src/keywords.rs

+1
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ define_keywords!(
207207
CONNECT,
208208
CONNECTION,
209209
CONNECTOR,
210+
CONNECT_BY_ROOT,
210211
CONSTRAINT,
211212
CONTAINS,
212213
CONTINUE,

src/parser/mod.rs

+39-12
Original file line numberDiff line numberDiff line change
@@ -1392,9 +1392,9 @@ impl<'a> Parser<'a> {
13921392
| Token::HexStringLiteral(_)
13931393
if w.value.starts_with('_') =>
13941394
{
1395-
Ok(Expr::IntroducedString {
1396-
introducer: w.value.clone(),
1397-
value: self.parse_introduced_string_value()?,
1395+
Ok(Expr::Prefixed {
1396+
prefix: w.clone().into_ident(w_span),
1397+
value: self.parse_introduced_string_expr()?.into(),
13981398
})
13991399
}
14001400
// string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html
@@ -1403,9 +1403,9 @@ impl<'a> Parser<'a> {
14031403
| Token::HexStringLiteral(_)
14041404
if w.value.starts_with('_') =>
14051405
{
1406-
Ok(Expr::IntroducedString {
1407-
introducer: w.value.clone(),
1408-
value: self.parse_introduced_string_value()?,
1406+
Ok(Expr::Prefixed {
1407+
prefix: w.clone().into_ident(w_span),
1408+
value: self.parse_introduced_string_expr()?.into(),
14091409
})
14101410
}
14111411
Token::Arrow if self.dialect.supports_lambda_functions() => {
@@ -8970,13 +8970,19 @@ impl<'a> Parser<'a> {
89708970
}
89718971
}
89728972

8973-
fn parse_introduced_string_value(&mut self) -> Result<Value, ParserError> {
8973+
fn parse_introduced_string_expr(&mut self) -> Result<Expr, ParserError> {
89748974
let next_token = self.next_token();
89758975
let span = next_token.span;
89768976
match next_token.token {
8977-
Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())),
8978-
Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())),
8979-
Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())),
8977+
Token::SingleQuotedString(ref s) => Ok(Expr::Value(
8978+
Value::SingleQuotedString(s.to_string()).with_span(span),
8979+
)),
8980+
Token::DoubleQuotedString(ref s) => Ok(Expr::Value(
8981+
Value::DoubleQuotedString(s.to_string()).with_span(span),
8982+
)),
8983+
Token::HexStringLiteral(ref s) => Ok(Expr::Value(
8984+
Value::HexStringLiteral(s.to_string()).with_span(span),
8985+
)),
89808986
unexpected => self.expected(
89818987
"a string value",
89828988
TokenWithSpan {
@@ -13778,6 +13784,13 @@ impl<'a> Parser<'a> {
1377813784

1377913785
/// Parse a comma-delimited list of projections after SELECT
1378013786
pub fn parse_select_item(&mut self) -> Result<SelectItem, ParserError> {
13787+
let prefix = self
13788+
.parse_one_of_keywords(
13789+
self.dialect
13790+
.get_reserved_keywords_for_select_item_operator(),
13791+
)
13792+
.map(|keyword| Ident::new(format!("{:?}", keyword)));
13793+
1378113794
match self.parse_wildcard_expr()? {
1378213795
Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard(
1378313796
SelectItemQualifiedWildcardKind::ObjectName(prefix),
@@ -13822,8 +13835,11 @@ impl<'a> Parser<'a> {
1382213835
expr => self
1382313836
.maybe_parse_select_item_alias()
1382413837
.map(|alias| match alias {
13825-
Some(alias) => SelectItem::ExprWithAlias { expr, alias },
13826-
None => SelectItem::UnnamedExpr(expr),
13838+
Some(alias) => SelectItem::ExprWithAlias {
13839+
expr: prefixed_expr(expr, prefix),
13840+
alias,
13841+
},
13842+
None => SelectItem::UnnamedExpr(prefixed_expr(expr, prefix)),
1382713843
}),
1382813844
}
1382913845
}
@@ -15168,6 +15184,17 @@ impl<'a> Parser<'a> {
1516815184
}
1516915185
}
1517015186

15187+
fn prefixed_expr(expr: Expr, prefix: Option<Ident>) -> Expr {
15188+
if let Some(prefix) = prefix {
15189+
Expr::Prefixed {
15190+
prefix,
15191+
value: Box::new(expr),
15192+
}
15193+
} else {
15194+
expr
15195+
}
15196+
}
15197+
1517115198
impl Word {
1517215199
#[deprecated(since = "0.54.0", note = "please use `into_ident` instead")]
1517315200
pub fn to_ident(&self, span: Span) -> Ident {

tests/sqlparser_mysql.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -3020,9 +3020,12 @@ fn parse_hex_string_introducer() {
30203020
distinct: None,
30213021
top: None,
30223022
top_before_distinct: false,
3023-
projection: vec![SelectItem::UnnamedExpr(Expr::IntroducedString {
3024-
introducer: "_latin1".to_string(),
3025-
value: Value::HexStringLiteral("4D7953514C".to_string())
3023+
projection: vec![SelectItem::UnnamedExpr(Expr::Prefixed {
3024+
prefix: Ident::from("_latin1"),
3025+
value: Expr::Value(
3026+
Value::HexStringLiteral("4D7953514C".to_string()).with_empty_span()
3027+
)
3028+
.into(),
30263029
})],
30273030
from: vec![],
30283031
lateral_views: vec![],

tests/sqlparser_snowflake.rs

+35
Original file line numberDiff line numberDiff line change
@@ -3573,3 +3573,38 @@ fn test_alter_session_followed_by_statement() {
35733573
_ => panic!("Unexpected statements: {:?}", stmts),
35743574
}
35753575
}
3576+
3577+
#[test]
3578+
fn parse_connect_by_root_operator() {
3579+
let sql = "SELECT CONNECT_BY_ROOT name AS root_name FROM Tbl1";
3580+
3581+
match snowflake().verified_stmt(sql) {
3582+
Statement::Query(query) => {
3583+
assert_eq!(
3584+
query.body.as_select().unwrap().projection[0],
3585+
SelectItem::ExprWithAlias {
3586+
expr: Expr::Prefixed {
3587+
prefix: Ident::new("CONNECT_BY_ROOT"),
3588+
value: Box::new(Expr::Identifier(Ident::new("name")))
3589+
},
3590+
alias: Ident::new("root_name"),
3591+
}
3592+
);
3593+
}
3594+
_ => unreachable!(),
3595+
}
3596+
3597+
let sql = "SELECT CONNECT_BY_ROOT name FROM Tbl2";
3598+
match snowflake().verified_stmt(sql) {
3599+
Statement::Query(query) => {
3600+
assert_eq!(
3601+
query.body.as_select().unwrap().projection[0],
3602+
SelectItem::UnnamedExpr(Expr::Prefixed {
3603+
prefix: Ident::new("CONNECT_BY_ROOT"),
3604+
value: Box::new(Expr::Identifier(Ident::new("name")))
3605+
})
3606+
);
3607+
}
3608+
_ => unreachable!(),
3609+
}
3610+
}

0 commit comments

Comments
 (0)