Skip to content

Commit abb2cf7

Browse files
committed
[Oracle] Optional START WITH for CONNECT BY
1 parent 0c19e08 commit abb2cf7

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
@@ -1204,23 +1204,34 @@ impl fmt::Display for TableWithJoins {
12041204
/// Joins a table to itself to process hierarchical data in the table.
12051205
///
12061206
/// See <https://docs.snowflake.com/en/sql-reference/constructs/connect-by>.
1207+
/// See <https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Hierarchical-Queries.html>
12071208
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
12081209
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12091210
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
12101211
pub struct ConnectBy {
12111212
/// START WITH
1212-
pub condition: Expr,
1213+
pub condition: Option<Expr>,
12131214
/// CONNECT BY
12141215
pub relationships: Vec<Expr>,
1216+
/// [CONNECT BY] NOCYCLE
1217+
pub nocycle: bool,
12151218
}
12161219

12171220
impl fmt::Display for ConnectBy {
12181221
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1222+
let Self {
1223+
condition,
1224+
relationships,
1225+
nocycle
1226+
} = self;
1227+
if let Some(condition) = condition {
1228+
write!(f, "START WITH {condition} ")?;
1229+
}
12191230
write!(
12201231
f,
1221-
"START WITH {condition} CONNECT BY {relationships}",
1222-
condition = self.condition,
1223-
relationships = display_comma_separated(&self.relationships)
1232+
"CONNECT BY {nocycle}{relationships}",
1233+
nocycle = if *nocycle { "NOCYCLE " } else { "" },
1234+
relationships = display_comma_separated(relationships)
12241235
)
12251236
}
12261237
}

src/ast/spans.rs

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

22912292
union_spans(
2292-
core::iter::once(condition.span()).chain(relationships.iter().map(|item| item.span())),
2293+
condition.iter().map(Spanned::span).chain(relationships.iter().map(|item| item.span())),
22932294
)
22942295
}
22952296
}

src/keywords.rs

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

src/parser/mod.rs

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

1428114281
/// Parse a `CONNECT BY` clause (Oracle-style hierarchical query support).
1428214282
pub fn parse_connect_by(&mut self) -> Result<ConnectBy, ParserError> {
14283-
let (condition, relationships) = if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) {
14284-
let relationships = self.with_state(ParserState::ConnectBy, |parser| {
14285-
parser.parse_comma_separated(Parser::parse_expr)
14283+
let (condition, relationships, nocycle) = if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) {
14284+
let (relationships, nocycle) = self.with_state(ParserState::ConnectBy, |parser| {
14285+
let nocycle = parser.parse_keyword(Keyword::NOCYCLE);
14286+
parser.parse_comma_separated(Parser::parse_expr).map(|exprs| (exprs, nocycle))
1428614287
})?;
14287-
self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
14288-
let condition = self.parse_expr()?;
14289-
(condition, relationships)
14288+
let condition = if self.parse_keywords(&[Keyword::START, Keyword::WITH]) {
14289+
Some(self.parse_expr()?)
14290+
} else {
14291+
None
14292+
};
14293+
(condition, relationships, nocycle)
1429014294
} else {
1429114295
self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
1429214296
let condition = self.parse_expr()?;
1429314297
self.expect_keywords(&[Keyword::CONNECT, Keyword::BY])?;
14298+
let nocycle = self.parse_keyword(Keyword::NOCYCLE);
1429414299
let relationships = self.with_state(ParserState::ConnectBy, |parser| {
1429514300
parser.parse_comma_separated(Parser::parse_expr)
1429614301
})?;
14297-
(condition, relationships)
14302+
(Some(condition), relationships, nocycle)
1429814303
};
1429914304
Ok(ConnectBy {
1430014305
condition,
1430114306
relationships,
14307+
nocycle
1430214308
})
1430314309
}
1430414310

tests/sqlparser_common.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12675,20 +12675,21 @@ fn parse_connect_by() {
1267512675
window_before_qualify: false,
1267612676
value_table_mode: None,
1267712677
connect_by: Some(ConnectBy {
12678-
condition: Expr::BinaryOp {
12678+
condition: Some(Expr::BinaryOp {
1267912679
left: Box::new(Expr::Identifier(Ident::new("title"))),
1268012680
op: BinaryOperator::Eq,
1268112681
right: Box::new(Expr::Value(
1268212682
Value::SingleQuotedString("president".to_owned()).with_empty_span(),
1268312683
)),
12684-
},
12684+
}),
1268512685
relationships: vec![Expr::BinaryOp {
1268612686
left: Box::new(Expr::Identifier(Ident::new("manager_id"))),
1268712687
op: BinaryOperator::Eq,
1268812688
right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new(
1268912689
"employee_id",
1269012690
))))),
1269112691
}],
12692+
nocycle: false,
1269212693
}),
1269312694
flavor: SelectFlavor::Standard,
1269412695
};
@@ -12763,20 +12764,21 @@ fn parse_connect_by() {
1276312764
window_before_qualify: false,
1276412765
value_table_mode: None,
1276512766
connect_by: Some(ConnectBy {
12766-
condition: Expr::BinaryOp {
12767+
condition: Some(Expr::BinaryOp {
1276712768
left: Box::new(Expr::Identifier(Ident::new("title"))),
1276812769
op: BinaryOperator::Eq,
1276912770
right: Box::new(Expr::Value(
1277012771
(Value::SingleQuotedString("president".to_owned(),)).with_empty_span()
1277112772
)),
12772-
},
12773+
}),
1277312774
relationships: vec![Expr::BinaryOp {
1277412775
left: Box::new(Expr::Identifier(Ident::new("manager_id"))),
1277512776
op: BinaryOperator::Eq,
1277612777
right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new(
1277712778
"employee_id",
1277812779
))))),
1277912780
}],
12781+
nocycle: false,
1278012782
}),
1278112783
flavor: SelectFlavor::Standard,
1278212784
}
@@ -12803,6 +12805,51 @@ fn parse_connect_by() {
1280312805
"prior"
1280412806
)))]
1280512807
);
12808+
12809+
// no START WITH and NOCYCLE
12810+
let connect_by_5 = "SELECT child, parent FROM t CONNECT BY NOCYCLE parent = PRIOR child";
12811+
assert_eq!(
12812+
all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_5),
12813+
Select {
12814+
select_token: AttachedToken::empty(),
12815+
optimizer_hint: None,
12816+
distinct: None,
12817+
top: None,
12818+
top_before_distinct: false,
12819+
projection: vec![
12820+
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("child"))),
12821+
SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("parent"))),
12822+
],
12823+
exclude: None,
12824+
from: vec![TableWithJoins {
12825+
relation: table_from_name(ObjectName::from(vec![Ident::new("t")])),
12826+
joins: vec![],
12827+
}],
12828+
into: None,
12829+
lateral_views: vec![],
12830+
prewhere: None,
12831+
selection: None,
12832+
group_by: GroupByExpr::Expressions(vec![], vec![]),
12833+
cluster_by: vec![],
12834+
distribute_by: vec![],
12835+
sort_by: vec![],
12836+
having: None,
12837+
named_window: vec![],
12838+
qualify: None,
12839+
window_before_qualify: false,
12840+
value_table_mode: None,
12841+
connect_by: Some(ConnectBy {
12842+
condition: None,
12843+
relationships: vec![Expr::BinaryOp {
12844+
left: Expr::Identifier(Ident::new("parent")).into(),
12845+
op: BinaryOperator::Eq,
12846+
right: Expr::Prior(Expr::Identifier(Ident::new("child")).into()).into(),
12847+
}],
12848+
nocycle: true,
12849+
}),
12850+
flavor: SelectFlavor::Standard,
12851+
}
12852+
);
1280612853
}
1280712854

1280812855
#[test]

0 commit comments

Comments
 (0)