Skip to content

Commit 0b5722a

Browse files
authored
Support for CONNECT BY (apache#1138)
1 parent deaa6d8 commit 0b5722a

16 files changed

+319
-4
lines changed

src/ast/mod.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ pub use self::ddl::{
4040
pub use self::dml::{Delete, Insert};
4141
pub use self::operator::{BinaryOperator, UnaryOperator};
4242
pub use self::query::{
43-
AfterMatchSkip, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode, ExceptSelectItem,
44-
ExcludeSelectItem, Fetch, ForClause, ForJson, ForXml, GroupByExpr, IdentWithAlias,
45-
IlikeSelectItem, Join, JoinConstraint, JoinOperator, JsonTableColumn,
43+
AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode,
44+
ExceptSelectItem, ExcludeSelectItem, Fetch, ForClause, ForJson, ForXml, GroupByExpr,
45+
IdentWithAlias, IlikeSelectItem, Join, JoinConstraint, JoinOperator, JsonTableColumn,
4646
JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern,
4747
MatchRecognizeSymbol, Measure, NamedWindowDefinition, NonBlock, Offset, OffsetRows,
4848
OrderByExpr, Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement,
@@ -798,6 +798,8 @@ pub enum Expr {
798798
///
799799
/// See <https://docs.snowflake.com/en/sql-reference/constructs/where#joins-in-the-where-clause>.
800800
OuterJoin(Box<Expr>),
801+
/// A reference to the prior level in a CONNECT BY clause.
802+
Prior(Box<Expr>),
801803
}
802804

803805
impl fmt::Display for CastFormat {
@@ -1255,6 +1257,7 @@ impl fmt::Display for Expr {
12551257
Expr::OuterJoin(expr) => {
12561258
write!(f, "{expr} (+)")
12571259
}
1260+
Expr::Prior(expr) => write!(f, "PRIOR {expr}"),
12581261
}
12591262
}
12601263
}

src/ast/query.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ pub struct Select {
247247
pub qualify: Option<Expr>,
248248
/// BigQuery syntax: `SELECT AS VALUE | SELECT AS STRUCT`
249249
pub value_table_mode: Option<ValueTableMode>,
250+
/// STARTING WITH .. CONNECT BY
251+
pub connect_by: Option<ConnectBy>,
250252
}
251253

252254
impl fmt::Display for Select {
@@ -314,6 +316,9 @@ impl fmt::Display for Select {
314316
if let Some(ref qualify) = self.qualify {
315317
write!(f, " QUALIFY {qualify}")?;
316318
}
319+
if let Some(ref connect_by) = self.connect_by {
320+
write!(f, " {connect_by}")?;
321+
}
317322
Ok(())
318323
}
319324
}
@@ -731,6 +736,30 @@ impl fmt::Display for TableWithJoins {
731736
}
732737
}
733738

739+
/// Joins a table to itself to process hierarchical data in the table.
740+
///
741+
/// See <https://docs.snowflake.com/en/sql-reference/constructs/connect-by>.
742+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
743+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
744+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
745+
pub struct ConnectBy {
746+
/// START WITH
747+
pub condition: Expr,
748+
/// CONNECT BY
749+
pub relationships: Vec<Expr>,
750+
}
751+
752+
impl fmt::Display for ConnectBy {
753+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
754+
write!(
755+
f,
756+
"START WITH {condition} CONNECT BY {relationships}",
757+
condition = self.condition,
758+
relationships = display_comma_separated(&self.relationships)
759+
)
760+
}
761+
}
762+
734763
/// A table name or a parenthesized subquery with an optional alias
735764
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
736765
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

src/dialect/generic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ impl Dialect for GenericDialect {
3939
true
4040
}
4141

42+
fn supports_connect_by(&self) -> bool {
43+
true
44+
}
45+
4246
fn supports_match_recognize(&self) -> bool {
4347
true
4448
}

src/dialect/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ pub trait Dialect: Debug + Any {
154154
fn supports_group_by_expr(&self) -> bool {
155155
false
156156
}
157+
/// Returns true if the dialect supports CONNECT BY.
158+
fn supports_connect_by(&self) -> bool {
159+
false
160+
}
157161
/// Returns true if the dialect supports the MATCH_RECOGNIZE operation.
158162
fn supports_match_recognize(&self) -> bool {
159163
false

src/dialect/mssql.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,8 @@ impl Dialect for MsSqlDialect {
4040
fn convert_type_before_value(&self) -> bool {
4141
true
4242
}
43+
44+
fn supports_connect_by(&self) -> bool {
45+
true
46+
}
4347
}

src/dialect/redshift.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,8 @@ impl Dialect for RedshiftSqlDialect {
5959
fn convert_type_before_value(&self) -> bool {
6060
true
6161
}
62+
63+
fn supports_connect_by(&self) -> bool {
64+
true
65+
}
6266
}

src/dialect/snowflake.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ impl Dialect for SnowflakeDialect {
5555
true
5656
}
5757

58+
fn supports_connect_by(&self) -> bool {
59+
true
60+
}
61+
5862
fn supports_match_recognize(&self) -> bool {
5963
true
6064
}

src/keywords.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,9 @@ pub const RESERVED_FOR_TABLE_ALIAS: &[Keyword] = &[
819819
Keyword::FOR,
820820
// for MYSQL PARTITION SELECTION
821821
Keyword::PARTITION,
822+
// for Snowflake START WITH .. CONNECT BY
823+
Keyword::START,
824+
Keyword::CONNECT,
822825
// Reserved for snowflake MATCH_RECOGNIZE
823826
Keyword::MATCH_RECOGNIZE,
824827
];

src/parser/mod.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,22 @@ impl ParserOptions {
256256
}
257257
}
258258

259+
#[derive(Copy, Clone)]
260+
enum ParserState {
261+
/// The default state of the parser.
262+
Normal,
263+
/// The state when parsing a CONNECT BY expression. This allows parsing
264+
/// PRIOR expressions while still allowing prior as an identifier name
265+
/// in other contexts.
266+
ConnectBy,
267+
}
268+
259269
pub struct Parser<'a> {
260270
tokens: Vec<TokenWithLocation>,
261271
/// The index of the first unprocessed token in `self.tokens`
262272
index: usize,
273+
/// The current state of the parser.
274+
state: ParserState,
263275
/// The current dialect to use
264276
dialect: &'a dyn Dialect,
265277
/// Additional options that allow you to mix & match behavior
@@ -290,6 +302,7 @@ impl<'a> Parser<'a> {
290302
Self {
291303
tokens: vec![],
292304
index: 0,
305+
state: ParserState::Normal,
293306
dialect,
294307
recursion_counter: RecursionCounter::new(DEFAULT_REMAINING_DEPTH),
295308
options: ParserOptions::default(),
@@ -1040,6 +1053,10 @@ impl<'a> Parser<'a> {
10401053
self.prev_token();
10411054
self.parse_bigquery_struct_literal()
10421055
}
1056+
Keyword::PRIOR if matches!(self.state, ParserState::ConnectBy) => {
1057+
let expr = self.parse_subexpr(Self::PLUS_MINUS_PREC)?;
1058+
Ok(Expr::Prior(Box::new(expr)))
1059+
}
10431060
// Here `w` is a word, check if it's a part of a multi-part
10441061
// identifier, a function call, or a simple identifier:
10451062
_ => match self.peek_token().token {
@@ -7695,6 +7712,17 @@ impl<'a> Parser<'a> {
76957712
None
76967713
};
76977714

7715+
let connect_by = if self.dialect.supports_connect_by()
7716+
&& self
7717+
.parse_one_of_keywords(&[Keyword::START, Keyword::CONNECT])
7718+
.is_some()
7719+
{
7720+
self.prev_token();
7721+
Some(self.parse_connect_by()?)
7722+
} else {
7723+
None
7724+
};
7725+
76987726
Ok(Select {
76997727
distinct,
77007728
top,
@@ -7711,6 +7739,44 @@ impl<'a> Parser<'a> {
77117739
named_window: named_windows,
77127740
qualify,
77137741
value_table_mode,
7742+
connect_by,
7743+
})
7744+
}
7745+
7746+
/// Invoke `f` after first setting the parser's `ParserState` to `state`.
7747+
///
7748+
/// Upon return, restores the parser's state to what it started at.
7749+
fn with_state<T, F>(&mut self, state: ParserState, mut f: F) -> Result<T, ParserError>
7750+
where
7751+
F: FnMut(&mut Parser) -> Result<T, ParserError>,
7752+
{
7753+
let current_state = self.state;
7754+
self.state = state;
7755+
let res = f(self);
7756+
self.state = current_state;
7757+
res
7758+
}
7759+
7760+
pub fn parse_connect_by(&mut self) -> Result<ConnectBy, ParserError> {
7761+
let (condition, relationships) = if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) {
7762+
let relationships = self.with_state(ParserState::ConnectBy, |parser| {
7763+
parser.parse_comma_separated(Parser::parse_expr)
7764+
})?;
7765+
self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
7766+
let condition = self.parse_expr()?;
7767+
(condition, relationships)
7768+
} else {
7769+
self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
7770+
let condition = self.parse_expr()?;
7771+
self.expect_keywords(&[Keyword::CONNECT, Keyword::BY])?;
7772+
let relationships = self.with_state(ParserState::ConnectBy, |parser| {
7773+
parser.parse_comma_separated(Parser::parse_expr)
7774+
})?;
7775+
(condition, relationships)
7776+
};
7777+
Ok(ConnectBy {
7778+
condition,
7779+
relationships,
77147780
})
77157781
}
77167782

src/test_utils.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,16 @@ impl TestedDialects {
157157
}
158158
}
159159

160+
/// Ensures that `sql` parses as a single [Query], and that
161+
/// re-serializing the parse result matches the given canonical
162+
/// sql string.
163+
pub fn verified_query_with_canonical(&self, query: &str, canonical: &str) -> Query {
164+
match self.one_statement_parses_to(query, canonical) {
165+
Statement::Query(query) => *query,
166+
_ => panic!("Expected Query"),
167+
}
168+
}
169+
160170
/// Ensures that `sql` parses as a single [Select], and that
161171
/// re-serializing the parse result produces the same `sql`
162172
/// string (is not modified after a serialization round-trip).

0 commit comments

Comments
 (0)