Skip to content

Commit 327e2c8

Browse files
committed
[Oracle] Optional START WITH for CONNECT BY
1 parent c8b7f7c commit 327e2c8

5 files changed

Lines changed: 82 additions & 16 deletions

File tree

src/ast/query.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,23 +1094,34 @@ impl fmt::Display for TableWithJoins {
10941094
/// Joins a table to itself to process hierarchical data in the table.
10951095
///
10961096
/// See <https://docs.snowflake.com/en/sql-reference/constructs/connect-by>.
1097+
/// See <https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Hierarchical-Queries.html>
10971098
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
10981099
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10991100
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11001101
pub struct ConnectBy {
11011102
/// START WITH
1102-
pub condition: Expr,
1103+
pub condition: Option<Expr>,
11031104
/// CONNECT BY
11041105
pub relationships: Vec<Expr>,
1106+
/// [CONNECT BY] NOCYCLE
1107+
pub nocycle: bool,
11051108
}
11061109

11071110
impl fmt::Display for ConnectBy {
11081111
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1112+
let Self {
1113+
condition,
1114+
relationships,
1115+
nocycle
1116+
} = self;
1117+
if let Some(condition) = condition {
1118+
write!(f, "START WITH {condition} ")?;
1119+
}
11091120
write!(
11101121
f,
1111-
"START WITH {condition} CONNECT BY {relationships}",
1112-
condition = self.condition,
1113-
relationships = display_comma_separated(&self.relationships)
1122+
"CONNECT BY {nocycle}{relationships}",
1123+
nocycle = if *nocycle { "NOCYCLE " } else { "" },
1124+
relationships = display_comma_separated(relationships)
11141125
)
11151126
}
11161127
}

src/ast/spans.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2285,10 +2285,11 @@ impl Spanned for ConnectBy {
22852285
let ConnectBy {
22862286
condition,
22872287
relationships,
2288+
nocycle: _
22882289
} = self;
22892290

22902291
union_spans(
2291-
core::iter::once(condition.span()).chain(relationships.iter().map(|item| item.span())),
2292+
condition.iter().map(Spanned::span).chain(relationships.iter().map(|item| item.span())),
22922293
)
22932294
}
22942295
}

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,7 @@ define_keywords!(
677677
NOCOMPRESS,
678678
NOCREATEDB,
679679
NOCREATEROLE,
680+
NOCYCLE,
680681
NOINHERIT,
681682
NOLOGIN,
682683
NONE,

src/parser/mod.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14185,25 +14185,31 @@ impl<'a> Parser<'a> {
1418514185

1418614186
/// Parse a `CONNECT BY` clause (Oracle-style hierarchical query support).
1418714187
pub fn parse_connect_by(&mut self) -> Result<ConnectBy, ParserError> {
14188-
let (condition, relationships) = if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) {
14189-
let relationships = self.with_state(ParserState::ConnectBy, |parser| {
14190-
parser.parse_comma_separated(Parser::parse_expr)
14188+
let (condition, relationships, nocycle) = if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) {
14189+
let (relationships, nocycle) = self.with_state(ParserState::ConnectBy, |parser| {
14190+
let nocycle = parser.parse_keyword(Keyword::NOCYCLE);
14191+
parser.parse_comma_separated(Parser::parse_expr).map(|exprs| (exprs, nocycle))
1419114192
})?;
14192-
self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
14193-
let condition = self.parse_expr()?;
14194-
(condition, relationships)
14193+
let condition = if self.parse_keywords(&[Keyword::START, Keyword::WITH]) {
14194+
Some(self.parse_expr()?)
14195+
} else {
14196+
None
14197+
};
14198+
(condition, relationships, nocycle)
1419514199
} else {
1419614200
self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
1419714201
let condition = self.parse_expr()?;
1419814202
self.expect_keywords(&[Keyword::CONNECT, Keyword::BY])?;
14203+
let nocycle = self.parse_keyword(Keyword::NOCYCLE);
1419914204
let relationships = self.with_state(ParserState::ConnectBy, |parser| {
1420014205
parser.parse_comma_separated(Parser::parse_expr)
1420114206
})?;
14202-
(condition, relationships)
14207+
(Some(condition), relationships, nocycle)
1420314208
};
1420414209
Ok(ConnectBy {
1420514210
condition,
1420614211
relationships,
14212+
nocycle
1420714213
})
1420814214
}
1420914215

tests/sqlparser_common.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12658,20 +12658,21 @@ fn parse_connect_by() {
1265812658
window_before_qualify: false,
1265912659
value_table_mode: None,
1266012660
connect_by: Some(ConnectBy {
12661-
condition: Expr::BinaryOp {
12661+
condition: Some(Expr::BinaryOp {
1266212662
left: Box::new(Expr::Identifier(Ident::new("title"))),
1266312663
op: BinaryOperator::Eq,
1266412664
right: Box::new(Expr::Value(
1266512665
Value::SingleQuotedString("president".to_owned()).with_empty_span(),
1266612666
)),
12667-
},
12667+
}),
1266812668
relationships: vec![Expr::BinaryOp {
1266912669
left: Box::new(Expr::Identifier(Ident::new("manager_id"))),
1267012670
op: BinaryOperator::Eq,
1267112671
right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new(
1267212672
"employee_id",
1267312673
))))),
1267412674
}],
12675+
nocycle: false,
1267512676
}),
1267612677
flavor: SelectFlavor::Standard,
1267712678
};
@@ -12745,20 +12746,21 @@ fn parse_connect_by() {
1274512746
window_before_qualify: false,
1274612747
value_table_mode: None,
1274712748
connect_by: Some(ConnectBy {
12748-
condition: Expr::BinaryOp {
12749+
condition: Some(Expr::BinaryOp {
1274912750
left: Box::new(Expr::Identifier(Ident::new("title"))),
1275012751
op: BinaryOperator::Eq,
1275112752
right: Box::new(Expr::Value(
1275212753
(Value::SingleQuotedString("president".to_owned(),)).with_empty_span()
1275312754
)),
12754-
},
12755+
}),
1275512756
relationships: vec![Expr::BinaryOp {
1275612757
left: Box::new(Expr::Identifier(Ident::new("manager_id"))),
1275712758
op: BinaryOperator::Eq,
1275812759
right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new(
1275912760
"employee_id",
1276012761
))))),
1276112762
}],
12763+
nocycle: false,
1276212764
}),
1276312765
flavor: SelectFlavor::Standard,
1276412766
}
@@ -12785,6 +12787,51 @@ fn parse_connect_by() {
1278512787
"prior"
1278612788
)))]
1278712789
);
12790+
12791+
// no START WITH and NOCYCLE
12792+
let connect_by_5 = "SELECT child, parent FROM t CONNECT BY NOCYCLE parent = PRIOR child";
12793+
assert_eq!(
12794+
all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_5),
12795+
Select {
12796+
select_token: AttachedToken::empty(),
12797+
optimizer_hint: None,
12798+
distinct: None,
12799+
top: None,
12800+
top_before_distinct: false,
12801+
projection: vec![
12802+
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("child"))),
12803+
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("parent"))),
12804+
],
12805+
exclude: None,
12806+
from: vec![TableWithJoins {
12807+
relation: table_from_name(ObjectName::from(vec![Ident::new("t")])),
12808+
joins: vec![],
12809+
}],
12810+
into: None,
12811+
lateral_views: vec![],
12812+
prewhere: None,
12813+
selection: None,
12814+
group_by: GroupByExpr::Expressions(vec![], vec![]),
12815+
cluster_by: vec![],
12816+
distribute_by: vec![],
12817+
sort_by: vec![],
12818+
having: None,
12819+
named_window: vec![],
12820+
qualify: None,
12821+
window_before_qualify: false,
12822+
value_table_mode: None,
12823+
connect_by: Some(ConnectBy {
12824+
condition: None,
12825+
relationships: vec![Expr::BinaryOp {
12826+
left: Expr::Identifier(Ident::new("parent")).into(),
12827+
op: BinaryOperator::Eq,
12828+
right: Expr::Prior(Expr::Identifier(Ident::new("child")).into()).into(),
12829+
}],
12830+
nocycle: true,
12831+
}),
12832+
flavor: SelectFlavor::Standard,
12833+
}
12834+
);
1278812835
}
1278912836

1279012837
#[test]

0 commit comments

Comments
 (0)