Skip to content
2 changes: 1 addition & 1 deletion src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub use self::dml::{
};
pub use self::operator::{BinaryOperator, UnaryOperator};
pub use self::query::{
AfterMatchSkip, ConnectBy, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode,
AfterMatchSkip, ConnectByKind, Cte, CteAsMaterialized, Distinct, EmptyMatchesMode,
ExceptSelectItem, ExcludeSelectItem, ExprWithAlias, ExprWithAliasAndOrderBy, Fetch, ForClause,
ForJson, ForXml, FormatClause, GroupByExpr, GroupByWithModifier, IdentWithAlias,
IlikeSelectItem, InputFormatClause, Interpolate, InterpolateExpr, Join, JoinConstraint,
Expand Down
70 changes: 53 additions & 17 deletions src/ast/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,8 @@ pub struct Select {
pub prewhere: Option<Expr>,
/// WHERE
pub selection: Option<Expr>,
/// [START WITH ..] CONNECT BY ..
pub connect_by: Vec<ConnectByKind>,
/// GROUP BY
pub group_by: GroupByExpr,
/// CLUSTER BY (Hive)
Expand All @@ -395,8 +397,6 @@ pub struct Select {
pub window_before_qualify: bool,
/// BigQuery syntax: `SELECT AS VALUE | SELECT AS STRUCT`
pub value_table_mode: Option<ValueTableMode>,
/// STARTING WITH .. CONNECT BY
pub connect_by: Option<ConnectBy>,
/// Was this a FROM-first query?
pub flavor: SelectFlavor,
}
Expand Down Expand Up @@ -475,6 +475,10 @@ impl fmt::Display for Select {
SpaceOrNewline.fmt(f)?;
Indent(selection).fmt(f)?;
}
for clause in &self.connect_by {
SpaceOrNewline.fmt(f)?;
clause.fmt(f)?;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i can't verify this change for snowflake, but this doc indicates to me that it's ok

match &self.group_by {
GroupByExpr::All(_) => {
SpaceOrNewline.fmt(f)?;
Expand Down Expand Up @@ -538,10 +542,6 @@ impl fmt::Display for Select {
display_comma_separated(&self.named_window).fmt(f)?;
}
}
if let Some(ref connect_by) = self.connect_by {
SpaceOrNewline.fmt(f)?;
connect_by.fmt(f)?;
}
Ok(())
}
}
Expand Down Expand Up @@ -1094,24 +1094,60 @@ impl fmt::Display for TableWithJoins {
/// Joins a table to itself to process hierarchical data in the table.
///
/// See <https://docs.snowflake.com/en/sql-reference/constructs/connect-by>.
/// See <https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Hierarchical-Queries.html>
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct ConnectBy {
/// START WITH
pub condition: Expr,
pub enum ConnectByKind {
/// CONNECT BY
pub relationships: Vec<Expr>,
ConnectBy {
/// the `CONNECT` token
connect_token: AttachedToken,

/// [CONNECT BY] NOCYCLE
///
/// 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)
nocycle: bool,

/// join conditions denoting the hierarchical relationship
relationships: Vec<Expr>,
},

/// START WITH
///
/// Optional on [Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Hierarchical-Queries.html#GUID-0118DF1D-B9A9-41EB-8556-C6E7D6A5A84E)
/// when comming _after_ the `CONNECT BY`.
StartWith {
/// the `START` token
start_token: AttachedToken,

/// condition selecting the root rows of the hierarchy
condition: Box<Expr>,
},
}

impl fmt::Display for ConnectBy {
impl fmt::Display for ConnectByKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"START WITH {condition} CONNECT BY {relationships}",
condition = self.condition,
relationships = display_comma_separated(&self.relationships)
)
match self {
ConnectByKind::ConnectBy {
connect_token: _,
nocycle,
relationships,
} => {
write!(
f,
"CONNECT BY {nocycle}{relationships}",
nocycle = if *nocycle { "NOCYCLE " } else { "" },
relationships = display_comma_separated(relationships)
)
}
ConnectByKind::StartWith {
start_token: _,
condition,
} => {
write!(f, "START WITH {condition}")
}
}
}
}

Expand Down
30 changes: 18 additions & 12 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use super::{
AlterIndexOperation, AlterTableOperation, Analyze, Array, Assignment, AssignmentTarget,
AttachedToken, BeginEndStatements, CaseStatement, CloseCursor, ClusteredIndex, ColumnDef,
ColumnOption, ColumnOptionDef, ConditionalStatementBlock, ConditionalStatements,
ConflictTarget, ConnectBy, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable,
ConflictTarget, ConnectByKind, ConstraintCharacteristics, CopySource, CreateIndex, CreateTable,
CreateTableOptions, Cte, Delete, DoUpdate, ExceptSelectItem, ExcludeSelectItem, Expr,
ExprWithAlias, Fetch, ForValues, FromTable, Function, FunctionArg, FunctionArgExpr,
FunctionArgumentClause, FunctionArgumentList, FunctionArguments, GroupByExpr, HavingBound,
Expand Down Expand Up @@ -2268,28 +2268,34 @@ impl Spanned for Select {
.chain(lateral_views.iter().map(|item| item.span()))
.chain(prewhere.iter().map(|item| item.span()))
.chain(selection.iter().map(|item| item.span()))
.chain(connect_by.iter().map(|item| item.span()))
.chain(core::iter::once(group_by.span()))
.chain(cluster_by.iter().map(|item| item.span()))
.chain(distribute_by.iter().map(|item| item.span()))
.chain(sort_by.iter().map(|item| item.span()))
.chain(having.iter().map(|item| item.span()))
.chain(named_window.iter().map(|item| item.span()))
.chain(qualify.iter().map(|item| item.span()))
.chain(connect_by.iter().map(|item| item.span())),
.chain(qualify.iter().map(|item| item.span())),
)
}
}

impl Spanned for ConnectBy {
impl Spanned for ConnectByKind {
fn span(&self) -> Span {
let ConnectBy {
condition,
relationships,
} = self;

union_spans(
core::iter::once(condition.span()).chain(relationships.iter().map(|item| item.span())),
)
match self {
ConnectByKind::ConnectBy {
connect_token,
nocycle: _,
relationships,
} => union_spans(
core::iter::once(connect_token.0.span())
.chain(relationships.last().iter().map(|item| item.span())),
),
ConnectByKind::StartWith {
start_token,
condition,
} => union_spans([start_token.0.span(), condition.span()].into_iter()),
}
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/dialect/oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ use crate::{
tokenizer::Token,
};

use super::{Dialect, Precedence};
use super::{keywords::Keyword, Dialect, Precedence};

const RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR: [Keyword; 1] = [Keyword::CONNECT_BY_ROOT];

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

fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] {
&RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR
}

fn supports_quote_delimited_string(&self) -> bool {
true
}
Expand Down
1 change: 1 addition & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ define_keywords!(
NOCOMPRESS,
NOCREATEDB,
NOCREATEROLE,
NOCYCLE,
NOINHERIT,
NOLOGIN,
NONE,
Expand Down
86 changes: 45 additions & 41 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4511,16 +4511,29 @@ impl<'a> Parser<'a> {
/// consumed and returns false
#[must_use]
pub fn parse_keywords(&mut self, keywords: &[Keyword]) -> bool {
let index = self.index;
for &keyword in keywords {
if !self.parse_keyword(keyword) {
// println!("parse_keywords aborting .. did not find {:?}", keyword);
// reset index and return immediately
self.index = index;
return false;
self.parse_keywords_(keywords).is_some()
}

/// Just like [Self::parse_keywords], but - upon success - returns the
/// token index of the first keyword.
#[must_use]
fn parse_keywords_(&mut self, keywords: &[Keyword]) -> Option<usize> {
let start_index = self.index;
let mut first_keyword_index = None;
match keywords {
[keyword, keywords @ ..] if self.parse_keyword(*keyword) => {
first_keyword_index = Some(self.index.saturating_sub(1));
for &keyword in keywords {
if !self.parse_keyword(keyword) {
self.index = start_index;
first_keyword_index = None;
break;
}
}
}
_ => {}
}
true
first_keyword_index
}

/// If the current token is one of the given `keywords`, returns the keyword
Expand Down Expand Up @@ -13902,7 +13915,7 @@ impl<'a> Parser<'a> {
window_before_qualify: false,
qualify: None,
value_table_mode: None,
connect_by: None,
connect_by: vec![],
flavor: SelectFlavor::FromFirstNoSelect,
});
}
Expand Down Expand Up @@ -14000,6 +14013,8 @@ impl<'a> Parser<'a> {
None
};

let connect_by = self.maybe_parse_connect_by()?;

let group_by = self
.parse_optional_group_by()?
.unwrap_or_else(|| GroupByExpr::Expressions(vec![], vec![]));
Expand Down Expand Up @@ -14052,17 +14067,6 @@ impl<'a> Parser<'a> {
Default::default()
};

let connect_by = if self.dialect.supports_connect_by()
&& self
.parse_one_of_keywords(&[Keyword::START, Keyword::CONNECT])
.is_some()
{
self.prev_token();
Some(self.parse_connect_by()?)
} else {
None
};

Ok(Select {
select_token: AttachedToken(select_token),
optimizer_hint,
Expand Down Expand Up @@ -14184,27 +14188,27 @@ impl<'a> Parser<'a> {
}

/// Parse a `CONNECT BY` clause (Oracle-style hierarchical query support).
pub fn parse_connect_by(&mut self) -> Result<ConnectBy, ParserError> {
let (condition, relationships) = if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) {
let relationships = self.with_state(ParserState::ConnectBy, |parser| {
parser.parse_comma_separated(Parser::parse_expr)
})?;
self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
let condition = self.parse_expr()?;
(condition, relationships)
} else {
self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
let condition = self.parse_expr()?;
self.expect_keywords(&[Keyword::CONNECT, Keyword::BY])?;
let relationships = self.with_state(ParserState::ConnectBy, |parser| {
parser.parse_comma_separated(Parser::parse_expr)
})?;
(condition, relationships)
};
Ok(ConnectBy {
condition,
relationships,
})
pub fn maybe_parse_connect_by(&mut self) -> Result<Vec<ConnectByKind>, ParserError> {
let mut clauses = Vec::with_capacity(2);
loop {
if let Some(idx) = self.parse_keywords_(&[Keyword::START, Keyword::WITH]) {
clauses.push(ConnectByKind::StartWith {
start_token: self.token_at(idx).clone().into(),
condition: self.parse_expr()?.into(),
});
} else if let Some(idx) = self.parse_keywords_(&[Keyword::CONNECT, Keyword::BY]) {
clauses.push(ConnectByKind::ConnectBy {
connect_token: self.token_at(idx).clone().into(),
nocycle: self.parse_keyword(Keyword::NOCYCLE),
relationships: self.with_state(ParserState::ConnectBy, |parser| {
parser.parse_comma_separated(Parser::parse_expr)
})?,
});
} else {
break;
}
}
Ok(clauses)
}

/// Parse `CREATE TABLE x AS TABLE y`
Expand Down
4 changes: 2 additions & 2 deletions tests/sqlparser_bigquery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2710,7 +2710,7 @@ fn test_export_data() {
qualify: None,
window_before_qualify: false,
value_table_mode: None,
connect_by: None,
connect_by: vec![],
flavor: SelectFlavor::Standard,
}))),
order_by: Some(OrderBy {
Expand Down Expand Up @@ -2815,7 +2815,7 @@ fn test_export_data() {
qualify: None,
window_before_qualify: false,
value_table_mode: None,
connect_by: None,
connect_by: vec![],
flavor: SelectFlavor::Standard,
}))),
order_by: Some(OrderBy {
Expand Down
2 changes: 1 addition & 1 deletion tests/sqlparser_clickhouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ fn parse_map_access_expr() {
window_before_qualify: false,
qualify: None,
value_table_mode: None,
connect_by: None,
connect_by: vec![],
flavor: SelectFlavor::Standard,
},
select
Expand Down
Loading
Loading