Skip to content

Commit 2b5bdce

Browse files
authored
Snowflake: Add support for CONNECT_BY_ROOT (#1780)
1 parent 4e392f5 commit 2b5bdce

File tree

8 files changed

+107
-20
lines changed

8 files changed

+107
-20
lines changed

src/ast/mod.rs

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

src/ast/spans.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1543,7 +1543,7 @@ impl Spanned for Expr {
15431543
.map(|items| union_spans(items.iter().map(|i| i.span()))),
15441544
),
15451545
),
1546-
Expr::IntroducedString { value, .. } => value.span(),
1546+
Expr::Prefixed { value, .. } => value.span(),
15471547
Expr::Case {
15481548
operand,
15491549
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
@@ -1388,9 +1388,9 @@ impl<'a> Parser<'a> {
13881388
| Token::HexStringLiteral(_)
13891389
if w.value.starts_with('_') =>
13901390
{
1391-
Ok(Expr::IntroducedString {
1392-
introducer: w.value.clone(),
1393-
value: self.parse_introduced_string_value()?,
1391+
Ok(Expr::Prefixed {
1392+
prefix: w.clone().into_ident(w_span),
1393+
value: self.parse_introduced_string_expr()?.into(),
13941394
})
13951395
}
13961396
// string introducer https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html
@@ -1399,9 +1399,9 @@ impl<'a> Parser<'a> {
13991399
| Token::HexStringLiteral(_)
14001400
if w.value.starts_with('_') =>
14011401
{
1402-
Ok(Expr::IntroducedString {
1403-
introducer: w.value.clone(),
1404-
value: self.parse_introduced_string_value()?,
1402+
Ok(Expr::Prefixed {
1403+
prefix: w.clone().into_ident(w_span),
1404+
value: self.parse_introduced_string_expr()?.into(),
14051405
})
14061406
}
14071407
Token::Arrow if self.dialect.supports_lambda_functions() => {
@@ -9035,13 +9035,19 @@ impl<'a> Parser<'a> {
90359035
}
90369036
}
90379037

9038-
fn parse_introduced_string_value(&mut self) -> Result<Value, ParserError> {
9038+
fn parse_introduced_string_expr(&mut self) -> Result<Expr, ParserError> {
90399039
let next_token = self.next_token();
90409040
let span = next_token.span;
90419041
match next_token.token {
9042-
Token::SingleQuotedString(ref s) => Ok(Value::SingleQuotedString(s.to_string())),
9043-
Token::DoubleQuotedString(ref s) => Ok(Value::DoubleQuotedString(s.to_string())),
9044-
Token::HexStringLiteral(ref s) => Ok(Value::HexStringLiteral(s.to_string())),
9042+
Token::SingleQuotedString(ref s) => Ok(Expr::Value(
9043+
Value::SingleQuotedString(s.to_string()).with_span(span),
9044+
)),
9045+
Token::DoubleQuotedString(ref s) => Ok(Expr::Value(
9046+
Value::DoubleQuotedString(s.to_string()).with_span(span),
9047+
)),
9048+
Token::HexStringLiteral(ref s) => Ok(Expr::Value(
9049+
Value::HexStringLiteral(s.to_string()).with_span(span),
9050+
)),
90459051
unexpected => self.expected(
90469052
"a string value",
90479053
TokenWithSpan {
@@ -13968,6 +13974,13 @@ impl<'a> Parser<'a> {
1396813974

1396913975
/// Parse a comma-delimited list of projections after SELECT
1397013976
pub fn parse_select_item(&mut self) -> Result<SelectItem, ParserError> {
13977+
let prefix = self
13978+
.parse_one_of_keywords(
13979+
self.dialect
13980+
.get_reserved_keywords_for_select_item_operator(),
13981+
)
13982+
.map(|keyword| Ident::new(format!("{:?}", keyword)));
13983+
1397113984
match self.parse_wildcard_expr()? {
1397213985
Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard(
1397313986
SelectItemQualifiedWildcardKind::ObjectName(prefix),
@@ -14012,8 +14025,11 @@ impl<'a> Parser<'a> {
1401214025
expr => self
1401314026
.maybe_parse_select_item_alias()
1401414027
.map(|alias| match alias {
14015-
Some(alias) => SelectItem::ExprWithAlias { expr, alias },
14016-
None => SelectItem::UnnamedExpr(expr),
14028+
Some(alias) => SelectItem::ExprWithAlias {
14029+
expr: maybe_prefixed_expr(expr, prefix),
14030+
alias,
14031+
},
14032+
None => SelectItem::UnnamedExpr(maybe_prefixed_expr(expr, prefix)),
1401714033
}),
1401814034
}
1401914035
}
@@ -15375,6 +15391,17 @@ impl<'a> Parser<'a> {
1537515391
}
1537615392
}
1537715393

15394+
fn maybe_prefixed_expr(expr: Expr, prefix: Option<Ident>) -> Expr {
15395+
if let Some(prefix) = prefix {
15396+
Expr::Prefixed {
15397+
prefix,
15398+
value: Box::new(expr),
15399+
}
15400+
} else {
15401+
expr
15402+
}
15403+
}
15404+
1537815405
impl Word {
1537915406
#[deprecated(since = "0.54.0", note = "please use `into_ident` instead")]
1538015407
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

+42
Original file line numberDiff line numberDiff line change
@@ -3983,3 +3983,45 @@ fn test_nested_join_without_parentheses() {
39833983
}],
39843984
);
39853985
}
3986+
3987+
#[test]
3988+
fn parse_connect_by_root_operator() {
3989+
let sql = "SELECT CONNECT_BY_ROOT name AS root_name FROM Tbl1";
3990+
3991+
match snowflake().verified_stmt(sql) {
3992+
Statement::Query(query) => {
3993+
assert_eq!(
3994+
query.body.as_select().unwrap().projection[0],
3995+
SelectItem::ExprWithAlias {
3996+
expr: Expr::Prefixed {
3997+
prefix: Ident::new("CONNECT_BY_ROOT"),
3998+
value: Box::new(Expr::Identifier(Ident::new("name")))
3999+
},
4000+
alias: Ident::new("root_name"),
4001+
}
4002+
);
4003+
}
4004+
_ => unreachable!(),
4005+
}
4006+
4007+
let sql = "SELECT CONNECT_BY_ROOT name FROM Tbl2";
4008+
match snowflake().verified_stmt(sql) {
4009+
Statement::Query(query) => {
4010+
assert_eq!(
4011+
query.body.as_select().unwrap().projection[0],
4012+
SelectItem::UnnamedExpr(Expr::Prefixed {
4013+
prefix: Ident::new("CONNECT_BY_ROOT"),
4014+
value: Box::new(Expr::Identifier(Ident::new("name")))
4015+
})
4016+
);
4017+
}
4018+
_ => unreachable!(),
4019+
}
4020+
4021+
let sql = "SELECT CONNECT_BY_ROOT FROM Tbl2";
4022+
let res = snowflake().parse_sql_statements(sql);
4023+
assert_eq!(
4024+
res.unwrap_err().to_string(),
4025+
"sql parser error: Expected an expression, found: FROM"
4026+
);
4027+
}

0 commit comments

Comments
 (0)