Skip to content

Commit d7f56e8

Browse files
xitepiffyio
andauthored
[Oracle] Support hierarchical queries (apache#2185)
Co-authored-by: Ifeanyi Ubah <ify1992@yahoo.com>
1 parent 0c19e08 commit d7f56e8

File tree

14 files changed

+376
-175
lines changed

14 files changed

+376
-175
lines changed

src/ast/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub use self::dml::{
8787
};
8888
pub use self::operator::{BinaryOperator, UnaryOperator};
8989
pub use self::query::{
90-
AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode,
90+
AfterMatchSkip, ConnectByKind, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode,
9191
ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, ExprWithAliasAndOrderBy, Fetch, ForClause,
9292
ForJson, ForXml, FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias,
9393
IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint,

src/ast/query.rs

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,8 @@ pub struct Select {
480480
pub prewhere: Option<Expr>,
481481
/// WHERE
482482
pub selection: Option<Expr>,
483+
/// [START WITH ..] CONNECT BY ..
484+
pub connect_by: Vec<ConnectByKind>,
483485
/// GROUP BY
484486
pub group_by: GroupByExpr,
485487
/// CLUSTER BY (Hive)
@@ -501,8 +503,6 @@ pub struct Select {
501503
pub window_before_qualify: bool,
502504
/// BigQuery syntax: `SELECT AS VALUE | SELECT AS STRUCT`
503505
pub value_table_mode: Option<ValueTableMode>,
504-
/// STARTING WITH .. CONNECT BY
505-
pub connect_by: Option<ConnectBy>,
506506
/// Was this a FROM-first query?
507507
pub flavor: SelectFlavor,
508508
}
@@ -585,6 +585,10 @@ impl fmt::Display for Select {
585585
SpaceOrNewline.fmt(f)?;
586586
Indent(selection).fmt(f)?;
587587
}
588+
for clause in &self.connect_by {
589+
SpaceOrNewline.fmt(f)?;
590+
clause.fmt(f)?;
591+
}
588592
match &self.group_by {
589593
GroupByExpr::All(_) => {
590594
SpaceOrNewline.fmt(f)?;
@@ -648,10 +652,6 @@ impl fmt::Display for Select {
648652
display_comma_separated(&self.named_window).fmt(f)?;
649653
}
650654
}
651-
if let Some(ref connect_by) = self.connect_by {
652-
SpaceOrNewline.fmt(f)?;
653-
connect_by.fmt(f)?;
654-
}
655655
Ok(())
656656
}
657657
}
@@ -1204,24 +1204,60 @@ 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))]
1210-
pub struct ConnectBy {
1211-
/// START WITH
1212-
pub condition: Expr,
1211+
pub enum ConnectByKind {
12131212
/// CONNECT BY
1214-
pub relationships: Vec<Expr>,
1213+
ConnectBy {
1214+
/// the `CONNECT` token
1215+
connect_token: AttachedToken,
1216+
1217+
/// [CONNECT BY] NOCYCLE
1218+
///
1219+
/// Optional on [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Hierarchical-Queries.html#GUID-0118DF1D-B9A9-41EB-8556-C6E7D6A5A84E__GUID-5377971A-F518-47E4-8781-F06FEB3EF993)
1220+
nocycle: bool,
1221+
1222+
/// join conditions denoting the hierarchical relationship
1223+
relationships: Vec<Expr>,
1224+
},
1225+
1226+
/// START WITH
1227+
///
1228+
/// Optional on [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Hierarchical-Queries.html#GUID-0118DF1D-B9A9-41EB-8556-C6E7D6A5A84E)
1229+
/// when comming _after_ the `CONNECT BY`.
1230+
StartWith {
1231+
/// the `START` token
1232+
start_token: AttachedToken,
1233+
1234+
/// condition selecting the root rows of the hierarchy
1235+
condition: Box<Expr>,
1236+
},
12151237
}
12161238

1217-
impl fmt::Display for ConnectBy {
1239+
impl fmt::Display for ConnectByKind {
12181240
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1219-
write!(
1220-
f,
1221-
"START WITH {condition} CONNECT BY {relationships}",
1222-
condition = self.condition,
1223-
relationships = display_comma_separated(&self.relationships)
1224-
)
1241+
match self {
1242+
ConnectByKind::ConnectBy {
1243+
connect_token: _,
1244+
nocycle,
1245+
relationships,
1246+
} => {
1247+
write!(
1248+
f,
1249+
"CONNECT BY {nocycle}{relationships}",
1250+
nocycle = if *nocycle { "NOCYCLE " } else { "" },
1251+
relationships = display_comma_separated(relationships)
1252+
)
1253+
}
1254+
ConnectByKind::StartWith {
1255+
start_token: _,
1256+
condition,
1257+
} => {
1258+
write!(f, "START WITH {condition}")
1259+
}
1260+
}
12251261
}
12261262
}
12271263

src/ast/spans.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use super::{
3232
AlterIndexOperation, AlterTableOperation, Analyze, Array, Assignment, AssignmentTarget,
3333
AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef,
3434
ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements,
35-
ConflictTarget, ConnectBy, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable,
35+
ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable,
3636
CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr,
3737
ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr,
3838
FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound,
@@ -2269,28 +2269,34 @@ impl Spanned for Select {
22692269
.chain(lateral_views.iter().map(|item| item.span()))
22702270
.chain(prewhere.iter().map(|item| item.span()))
22712271
.chain(selection.iter().map(|item| item.span()))
2272+
.chain(connect_by.iter().map(|item| item.span()))
22722273
.chain(core::iter::once(group_by.span()))
22732274
.chain(cluster_by.iter().map(|item| item.span()))
22742275
.chain(distribute_by.iter().map(|item| item.span()))
22752276
.chain(sort_by.iter().map(|item| item.span()))
22762277
.chain(having.iter().map(|item| item.span()))
22772278
.chain(named_window.iter().map(|item| item.span()))
2278-
.chain(qualify.iter().map(|item| item.span()))
2279-
.chain(connect_by.iter().map(|item| item.span())),
2279+
.chain(qualify.iter().map(|item| item.span())),
22802280
)
22812281
}
22822282
}
22832283

2284-
impl Spanned for ConnectBy {
2284+
impl Spanned for ConnectByKind {
22852285
fn span(&self) -> Span {
2286-
let ConnectBy {
2287-
condition,
2288-
relationships,
2289-
} = self;
2290-
2291-
union_spans(
2292-
core::iter::once(condition.span()).chain(relationships.iter().map(|item| item.span())),
2293-
)
2286+
match self {
2287+
ConnectByKind::ConnectBy {
2288+
connect_token,
2289+
nocycle: _,
2290+
relationships,
2291+
} => union_spans(
2292+
core::iter::once(connect_token.0.span())
2293+
.chain(relationships.last().iter().map(|item| item.span())),
2294+
),
2295+
ConnectByKind::StartWith {
2296+
start_token,
2297+
condition,
2298+
} => union_spans([start_token.0.span(), condition.span()].into_iter()),
2299+
}
22942300
}
22952301
}
22962302

src/dialect/oracle.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ use crate::{
2222
tokenizer::Token,
2323
};
2424

25-
use super::{Dialect, Precedence};
25+
use super::{keywords::Keyword, Dialect, Precedence};
26+
27+
const RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR: [Keyword; 1] = [Keyword::CONNECT_BY_ROOT];
2628

2729
/// A [`Dialect`] for [Oracle Databases](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/index.html)
2830
#[derive(Debug, Default)]
@@ -96,6 +98,10 @@ impl Dialect for OracleDialect {
9698
true
9799
}
98100

101+
fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] {
102+
&RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR
103+
}
104+
99105
fn supports_quote_delimited_string(&self) -> bool {
100106
true
101107
}

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: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4521,16 +4521,25 @@ impl<'a> Parser<'a> {
45214521
/// consumed and returns false
45224522
#[must_use]
45234523
pub fn parse_keywords(&mut self, keywords: &[Keyword]) -> bool {
4524-
let index = self.index;
4524+
self.parse_keywords_indexed(keywords).is_some()
4525+
}
4526+
4527+
/// Just like [Self::parse_keywords], but - upon success - returns the
4528+
/// token index of the first keyword.
4529+
#[must_use]
4530+
fn parse_keywords_indexed(&mut self, keywords: &[Keyword]) -> Option<usize> {
4531+
let start_index = self.index;
4532+
let mut first_keyword_index = None;
45254533
for &keyword in keywords {
45264534
if !self.parse_keyword(keyword) {
4527-
// println!("parse_keywords aborting .. did not find {:?}", keyword);
4528-
// reset index and return immediately
4529-
self.index = index;
4530-
return false;
4535+
self.index = start_index;
4536+
return None;
4537+
}
4538+
if first_keyword_index.is_none() {
4539+
first_keyword_index = Some(self.index.saturating_sub(1));
45314540
}
45324541
}
4533-
true
4542+
first_keyword_index
45344543
}
45354544

45364545
/// If the current token is one of the given `keywords`, returns the keyword
@@ -13921,7 +13930,7 @@ impl<'a> Parser<'a> {
1392113930
window_before_qualify: false,
1392213931
qualify: None,
1392313932
value_table_mode: None,
13924-
connect_by: None,
13933+
connect_by: vec![],
1392513934
flavor: SelectFlavor::FromFirstNoSelect,
1392613935
});
1392713936
}
@@ -14032,6 +14041,8 @@ impl<'a> Parser<'a> {
1403214041
None
1403314042
};
1403414043

14044+
let connect_by = self.maybe_parse_connect_by()?;
14045+
1403514046
let group_by = self
1403614047
.parse_optional_group_by()?
1403714048
.unwrap_or_else(|| GroupByExpr::Expressions(vec![], vec![]));
@@ -14084,17 +14095,6 @@ impl<'a> Parser<'a> {
1408414095
Default::default()
1408514096
};
1408614097

14087-
let connect_by = if self.dialect.supports_connect_by()
14088-
&& self
14089-
.parse_one_of_keywords(&[Keyword::START, Keyword::CONNECT])
14090-
.is_some()
14091-
{
14092-
self.prev_token();
14093-
Some(self.parse_connect_by()?)
14094-
} else {
14095-
None
14096-
};
14097-
1409814098
Ok(Select {
1409914099
select_token: AttachedToken(select_token),
1410014100
optimizer_hint,
@@ -14279,27 +14279,28 @@ impl<'a> Parser<'a> {
1427914279
}
1428014280

1428114281
/// Parse a `CONNECT BY` clause (Oracle-style hierarchical query support).
14282-
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)
14286-
})?;
14287-
self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
14288-
let condition = self.parse_expr()?;
14289-
(condition, relationships)
14290-
} else {
14291-
self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
14292-
let condition = self.parse_expr()?;
14293-
self.expect_keywords(&[Keyword::CONNECT, Keyword::BY])?;
14294-
let relationships = self.with_state(ParserState::ConnectBy, |parser| {
14295-
parser.parse_comma_separated(Parser::parse_expr)
14296-
})?;
14297-
(condition, relationships)
14298-
};
14299-
Ok(ConnectBy {
14300-
condition,
14301-
relationships,
14302-
})
14282+
pub fn maybe_parse_connect_by(&mut self) -> Result<Vec<ConnectByKind>, ParserError> {
14283+
let mut clauses = Vec::with_capacity(2);
14284+
loop {
14285+
if let Some(idx) = self.parse_keywords_indexed(&[Keyword::START, Keyword::WITH]) {
14286+
clauses.push(ConnectByKind::StartWith {
14287+
start_token: self.token_at(idx).clone().into(),
14288+
condition: self.parse_expr()?.into(),
14289+
});
14290+
} else if let Some(idx) = self.parse_keywords_indexed(&[Keyword::CONNECT, Keyword::BY])
14291+
{
14292+
clauses.push(ConnectByKind::ConnectBy {
14293+
connect_token: self.token_at(idx).clone().into(),
14294+
nocycle: self.parse_keyword(Keyword::NOCYCLE),
14295+
relationships: self.with_state(ParserState::ConnectBy, |parser| {
14296+
parser.parse_comma_separated(Parser::parse_expr)
14297+
})?,
14298+
});
14299+
} else {
14300+
break;
14301+
}
14302+
}
14303+
Ok(clauses)
1430314304
}
1430414305

1430514306
/// Parse `CREATE TABLE x AS TABLE y`

tests/sqlparser_bigquery.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2711,7 +2711,7 @@ fn test_export_data() {
27112711
qualify: None,
27122712
window_before_qualify: false,
27132713
value_table_mode: None,
2714-
connect_by: None,
2714+
connect_by: vec![],
27152715
flavor: SelectFlavor::Standard,
27162716
}))),
27172717
order_by: Some(OrderBy {
@@ -2817,7 +2817,7 @@ fn test_export_data() {
28172817
qualify: None,
28182818
window_before_qualify: false,
28192819
value_table_mode: None,
2820-
connect_by: None,
2820+
connect_by: vec![],
28212821
flavor: SelectFlavor::Standard,
28222822
}))),
28232823
order_by: Some(OrderBy {

tests/sqlparser_clickhouse.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ fn parse_map_access_expr() {
103103
window_before_qualify: false,
104104
qualify: None,
105105
value_table_mode: None,
106-
connect_by: None,
106+
connect_by: vec![],
107107
flavor: SelectFlavor::Standard,
108108
},
109109
select

0 commit comments

Comments
 (0)