From 327e2c80dc5a12c479cb6f359f696c7dd49eac2a Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Fri, 30 Jan 2026 09:20:20 +0100 Subject: [PATCH 01/13] [Oracle] Optional START WITH for CONNECT BY --- src/ast/query.rs | 19 +++++++++++--- src/ast/spans.rs | 3 ++- src/keywords.rs | 1 + src/parser/mod.rs | 20 +++++++++----- tests/sqlparser_common.rs | 55 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 08448cabe..465807944 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1094,23 +1094,34 @@ impl fmt::Display for TableWithJoins { /// Joins a table to itself to process hierarchical data in the table. /// /// See . +/// See #[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 condition: Option, /// CONNECT BY pub relationships: Vec, + /// [CONNECT BY] NOCYCLE + pub nocycle: bool, } impl fmt::Display for ConnectBy { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let Self { + condition, + relationships, + nocycle + } = self; + if let Some(condition) = condition { + write!(f, "START WITH {condition} ")?; + } write!( f, - "START WITH {condition} CONNECT BY {relationships}", - condition = self.condition, - relationships = display_comma_separated(&self.relationships) + "CONNECT BY {nocycle}{relationships}", + nocycle = if *nocycle { "NOCYCLE " } else { "" }, + relationships = display_comma_separated(relationships) ) } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 60c983fa1..2be477cac 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2285,10 +2285,11 @@ impl Spanned for ConnectBy { let ConnectBy { condition, relationships, + nocycle: _ } = self; union_spans( - core::iter::once(condition.span()).chain(relationships.iter().map(|item| item.span())), + condition.iter().map(Spanned::span).chain(relationships.iter().map(|item| item.span())), ) } } diff --git a/src/keywords.rs b/src/keywords.rs index 964e4b388..08e6926a8 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -677,6 +677,7 @@ define_keywords!( NOCOMPRESS, NOCREATEDB, NOCREATEROLE, + NOCYCLE, NOINHERIT, NOLOGIN, NONE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 276311431..948b58722 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14185,25 +14185,31 @@ impl<'a> Parser<'a> { /// Parse a `CONNECT BY` clause (Oracle-style hierarchical query support). pub fn parse_connect_by(&mut self) -> Result { - 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) + let (condition, relationships, nocycle) = if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) { + let (relationships, nocycle) = self.with_state(ParserState::ConnectBy, |parser| { + let nocycle = parser.parse_keyword(Keyword::NOCYCLE); + parser.parse_comma_separated(Parser::parse_expr).map(|exprs| (exprs, nocycle)) })?; - self.expect_keywords(&[Keyword::START, Keyword::WITH])?; - let condition = self.parse_expr()?; - (condition, relationships) + let condition = if self.parse_keywords(&[Keyword::START, Keyword::WITH]) { + Some(self.parse_expr()?) + } else { + None + }; + (condition, relationships, nocycle) } else { self.expect_keywords(&[Keyword::START, Keyword::WITH])?; let condition = self.parse_expr()?; self.expect_keywords(&[Keyword::CONNECT, Keyword::BY])?; + let nocycle = self.parse_keyword(Keyword::NOCYCLE); let relationships = self.with_state(ParserState::ConnectBy, |parser| { parser.parse_comma_separated(Parser::parse_expr) })?; - (condition, relationships) + (Some(condition), relationships, nocycle) }; Ok(ConnectBy { condition, relationships, + nocycle }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 69524ff99..b39d90819 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12658,13 +12658,13 @@ fn parse_connect_by() { window_before_qualify: false, value_table_mode: None, connect_by: Some(ConnectBy { - condition: Expr::BinaryOp { + condition: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, right: Box::new(Expr::Value( Value::SingleQuotedString("president".to_owned()).with_empty_span(), )), - }, + }), relationships: vec![Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("manager_id"))), op: BinaryOperator::Eq, @@ -12672,6 +12672,7 @@ fn parse_connect_by() { "employee_id", ))))), }], + nocycle: false, }), flavor: SelectFlavor::Standard, }; @@ -12745,13 +12746,13 @@ fn parse_connect_by() { window_before_qualify: false, value_table_mode: None, connect_by: Some(ConnectBy { - condition: Expr::BinaryOp { + condition: Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, right: Box::new(Expr::Value( (Value::SingleQuotedString("president".to_owned(),)).with_empty_span() )), - }, + }), relationships: vec![Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("manager_id"))), op: BinaryOperator::Eq, @@ -12759,6 +12760,7 @@ fn parse_connect_by() { "employee_id", ))))), }], + nocycle: false, }), flavor: SelectFlavor::Standard, } @@ -12785,6 +12787,51 @@ fn parse_connect_by() { "prior" )))] ); + + // no START WITH and NOCYCLE + let connect_by_5 = "SELECT child, parent FROM t CONNECT BY NOCYCLE parent = PRIOR child"; + assert_eq!( + all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_5), + Select { + select_token: AttachedToken::empty(), + optimizer_hint: None, + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![ + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("child"))), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("parent"))), + ], + exclude: None, + from: vec![TableWithJoins { + relation: table_from_name(ObjectName::from(vec![Ident::new("t")])), + joins: vec![], + }], + into: None, + lateral_views: vec![], + prewhere: None, + selection: None, + group_by: GroupByExpr::Expressions(vec![], vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + window_before_qualify: false, + value_table_mode: None, + connect_by: Some(ConnectBy { + condition: None, + relationships: vec![Expr::BinaryOp { + left: Expr::Identifier(Ident::new("parent")).into(), + op: BinaryOperator::Eq, + right: Expr::Prior(Expr::Identifier(Ident::new("child")).into()).into(), + }], + nocycle: true, + }), + flavor: SelectFlavor::Standard, + } + ); } #[test] From c86c6798a991af7616ba22661f3ebc58555afb1a Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Fri, 30 Jan 2026 10:59:59 +0100 Subject: [PATCH 02/13] [Oracle] Parse CONNECT BY before GROUP BY --- src/ast/query.rs | 12 ++++++------ src/parser/mod.rs | 22 +++++++++++----------- tests/sqlparser_common.rs | 20 ++++++++++++++------ 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 465807944..dfcd1eb15 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -374,6 +374,8 @@ pub struct Select { pub prewhere: Option, /// WHERE pub selection: Option, + /// STARTING WITH .. CONNECT BY + pub connect_by: Option, /// GROUP BY pub group_by: GroupByExpr, /// CLUSTER BY (Hive) @@ -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, - /// STARTING WITH .. CONNECT BY - pub connect_by: Option, /// Was this a FROM-first query? pub flavor: SelectFlavor, } @@ -475,6 +475,10 @@ impl fmt::Display for Select { SpaceOrNewline.fmt(f)?; Indent(selection).fmt(f)?; } + if let Some(ref connect_by) = self.connect_by { + SpaceOrNewline.fmt(f)?; + connect_by.fmt(f)?; + } match &self.group_by { GroupByExpr::All(_) => { SpaceOrNewline.fmt(f)?; @@ -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(()) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 948b58722..76e07647c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14000,6 +14000,17 @@ impl<'a> Parser<'a> { None }; + 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 + }; + let group_by = self .parse_optional_group_by()? .unwrap_or_else(|| GroupByExpr::Expressions(vec![], vec![])); @@ -14052,17 +14063,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, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b39d90819..a7eb7425c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12628,6 +12628,8 @@ fn parse_map_access_expr() { #[test] fn parse_connect_by() { + let dialects = all_dialects_where(|d| d.supports_connect_by()); + let expect_query = Select { select_token: AttachedToken::empty(), optimizer_hint: None, @@ -12685,7 +12687,7 @@ fn parse_connect_by() { ); assert_eq!( - all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_1), + dialects.verified_only_select(connect_by_1), expect_query ); @@ -12697,8 +12699,7 @@ fn parse_connect_by() { "ORDER BY employee_id" ); assert_eq!( - all_dialects_where(|d| d.supports_connect_by()) - .verified_only_select_with_canonical(connect_by_2, connect_by_1), + dialects.verified_only_select_with_canonical(connect_by_2, connect_by_1), expect_query ); @@ -12711,7 +12712,7 @@ fn parse_connect_by() { "ORDER BY employee_id" ); assert_eq!( - all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_3), + dialects.verified_only_select(connect_by_3), Select { select_token: AttachedToken::empty(), optimizer_hint: None, @@ -12773,7 +12774,7 @@ fn parse_connect_by() { "WHERE employee_id <> 42 ", "ORDER BY employee_id" ); - all_dialects_where(|d| d.supports_connect_by()) + dialects .parse_sql_statements(connect_by_4) .expect_err("should have failed"); @@ -12791,7 +12792,7 @@ fn parse_connect_by() { // no START WITH and NOCYCLE let connect_by_5 = "SELECT child, parent FROM t CONNECT BY NOCYCLE parent = PRIOR child"; assert_eq!( - all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_5), + dialects.verified_only_select(connect_by_5), Select { select_token: AttachedToken::empty(), optimizer_hint: None, @@ -12832,6 +12833,13 @@ fn parse_connect_by() { flavor: SelectFlavor::Standard, } ); + + // ~ CONNECT BY after WHERE and before GROUP BY + dialects.verified_only_select("SELECT 0 FROM t WHERE 1 = 1 CONNECT BY 2 = 2 GROUP BY 3"); + dialects.verified_only_select("SELECT 0 FROM t WHERE 1 = 1 START WITH 'a' = 'a' CONNECT BY 2 = 2 GROUP BY 3"); + dialects.verified_only_select_with_canonical( + "SELECT 0 FROM t WHERE 1 = 1 CONNECT BY 2 = 2 START WITH 'a' = 'a' GROUP BY 3", + "SELECT 0 FROM t WHERE 1 = 1 START WITH 'a' = 'a' CONNECT BY 2 = 2 GROUP BY 3"); } #[test] From ad2b0fa4d2129873d8b244006657a64f8202dd7e Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Fri, 30 Jan 2026 13:39:51 +0100 Subject: [PATCH 03/13] [Oracle] Support CONNECT_BY_ROOT --- src/dialect/oracle.rs | 8 +++++++- tests/sqlparser_oracle.rs | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/dialect/oracle.rs b/src/dialect/oracle.rs index 7ff932628..0b3f06db4 100644 --- a/src/dialect/oracle.rs +++ b/src/dialect/oracle.rs @@ -22,7 +22,9 @@ use crate::{ tokenizer::Token, }; -use super::{Dialect, Precedence}; +use super::{Dialect, Precedence, keywords::Keyword}; + +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)] @@ -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 } diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 1c12f868f..7a4292664 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -388,3 +388,27 @@ fn test_optimizer_hints() { VALUES (ps.person_id, ps.first_name, ps.last_name, ps.title)", ); } + +#[test] +fn test_connect_by() { + let oracle_dialect = oracle(); + + oracle_dialect.verified_only_select( + "SELECT last_name AS \"Employee\", CONNECT_BY_ISCYCLE AS \"Cycle\", \ + LEVEL, \ + SYS_CONNECT_BY_PATH(last_name, '/') AS \"Path\" \ + FROM employees \ + WHERE level <= 3 AND department_id = 80 \ + START WITH last_name = 'King' \ + CONNECT BY NOCYCLE PRIOR employee_id = manager_id AND LEVEL <= 4 \ + ORDER BY \"Employee\", \"Cycle\", LEVEL, \"Path\""); + + // ~ CONNECT_BY_ROOT + oracle_dialect.verified_only_select( + "SELECT last_name AS \"Employee\", CONNECT_BY_ROOT last_name AS \"Manager\", \ + LEVEL - 1 AS \"Pathlen\", SYS_CONNECT_BY_PATH(last_name, '/') AS \"Path\" \ + FROM employees \ + WHERE LEVEL > 1 AND department_id = 110 \ + CONNECT BY PRIOR employee_id = manager_id \ + ORDER BY \"Employee\", \"Manager\", \"Pathlen\", \"Path\""); +} From 0807255674d244dc54c485d68e7bdf5cb6b85e8e Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Fri, 30 Jan 2026 13:40:31 +0100 Subject: [PATCH 04/13] Cargo fmt --- src/ast/query.rs | 2 +- src/ast/spans.rs | 7 +++++-- src/dialect/oracle.rs | 2 +- src/parser/mod.rs | 10 +++++++--- tests/sqlparser_common.rs | 12 ++++++------ tests/sqlparser_oracle.rs | 6 ++++-- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index dfcd1eb15..3f90f6ae5 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1112,7 +1112,7 @@ impl fmt::Display for ConnectBy { let Self { condition, relationships, - nocycle + nocycle, } = self; if let Some(condition) = condition { write!(f, "START WITH {condition} ")?; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 2be477cac..8cc0dc0ac 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2285,11 +2285,14 @@ impl Spanned for ConnectBy { let ConnectBy { condition, relationships, - nocycle: _ + nocycle: _, } = self; union_spans( - condition.iter().map(Spanned::span).chain(relationships.iter().map(|item| item.span())), + condition + .iter() + .map(Spanned::span) + .chain(relationships.iter().map(|item| item.span())), ) } } diff --git a/src/dialect/oracle.rs b/src/dialect/oracle.rs index 0b3f06db4..d404e0109 100644 --- a/src/dialect/oracle.rs +++ b/src/dialect/oracle.rs @@ -22,7 +22,7 @@ use crate::{ tokenizer::Token, }; -use super::{Dialect, Precedence, keywords::Keyword}; +use super::{keywords::Keyword, Dialect, Precedence}; const RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR: [Keyword; 1] = [Keyword::CONNECT_BY_ROOT]; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 76e07647c..c5a44a2bc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14185,10 +14185,14 @@ impl<'a> Parser<'a> { /// Parse a `CONNECT BY` clause (Oracle-style hierarchical query support). pub fn parse_connect_by(&mut self) -> Result { - let (condition, relationships, nocycle) = if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) { + let (condition, relationships, nocycle) = if self + .parse_keywords(&[Keyword::CONNECT, Keyword::BY]) + { let (relationships, nocycle) = self.with_state(ParserState::ConnectBy, |parser| { let nocycle = parser.parse_keyword(Keyword::NOCYCLE); - parser.parse_comma_separated(Parser::parse_expr).map(|exprs| (exprs, nocycle)) + parser + .parse_comma_separated(Parser::parse_expr) + .map(|exprs| (exprs, nocycle)) })?; let condition = if self.parse_keywords(&[Keyword::START, Keyword::WITH]) { Some(self.parse_expr()?) @@ -14209,7 +14213,7 @@ impl<'a> Parser<'a> { Ok(ConnectBy { condition, relationships, - nocycle + nocycle, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a7eb7425c..474282646 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12686,10 +12686,7 @@ fn parse_connect_by() { "ORDER BY employee_id" ); - assert_eq!( - dialects.verified_only_select(connect_by_1), - expect_query - ); + assert_eq!(dialects.verified_only_select(connect_by_1), expect_query); // CONNECT BY can come before START WITH let connect_by_2 = concat!( @@ -12836,10 +12833,13 @@ fn parse_connect_by() { // ~ CONNECT BY after WHERE and before GROUP BY dialects.verified_only_select("SELECT 0 FROM t WHERE 1 = 1 CONNECT BY 2 = 2 GROUP BY 3"); - dialects.verified_only_select("SELECT 0 FROM t WHERE 1 = 1 START WITH 'a' = 'a' CONNECT BY 2 = 2 GROUP BY 3"); + dialects.verified_only_select( + "SELECT 0 FROM t WHERE 1 = 1 START WITH 'a' = 'a' CONNECT BY 2 = 2 GROUP BY 3", + ); dialects.verified_only_select_with_canonical( "SELECT 0 FROM t WHERE 1 = 1 CONNECT BY 2 = 2 START WITH 'a' = 'a' GROUP BY 3", - "SELECT 0 FROM t WHERE 1 = 1 START WITH 'a' = 'a' CONNECT BY 2 = 2 GROUP BY 3"); + "SELECT 0 FROM t WHERE 1 = 1 START WITH 'a' = 'a' CONNECT BY 2 = 2 GROUP BY 3", + ); } #[test] diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 7a4292664..2ffba63b7 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -401,7 +401,8 @@ fn test_connect_by() { WHERE level <= 3 AND department_id = 80 \ START WITH last_name = 'King' \ CONNECT BY NOCYCLE PRIOR employee_id = manager_id AND LEVEL <= 4 \ - ORDER BY \"Employee\", \"Cycle\", LEVEL, \"Path\""); + ORDER BY \"Employee\", \"Cycle\", LEVEL, \"Path\"", + ); // ~ CONNECT_BY_ROOT oracle_dialect.verified_only_select( @@ -410,5 +411,6 @@ fn test_connect_by() { FROM employees \ WHERE LEVEL > 1 AND department_id = 110 \ CONNECT BY PRIOR employee_id = manager_id \ - ORDER BY \"Employee\", \"Manager\", \"Pathlen\", \"Path\""); + ORDER BY \"Employee\", \"Manager\", \"Pathlen\", \"Path\"", + ); } From b044c4882054657b81cb98a3f2a8c65cc4c5258e Mon Sep 17 00:00:00 2001 From: xitep Date: Fri, 30 Jan 2026 14:20:42 +0100 Subject: [PATCH 05/13] Apply suggestion from @xitep --- src/ast/query.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 3f90f6ae5..12c833978 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -374,7 +374,7 @@ pub struct Select { pub prewhere: Option, /// WHERE pub selection: Option, - /// STARTING WITH .. CONNECT BY + /// START WITH .. CONNECT BY pub connect_by: Option, /// GROUP BY pub group_by: GroupByExpr, From 004c0fcc47502f76296c92f9cff1a4259900ac40 Mon Sep 17 00:00:00 2001 From: xitep Date: Fri, 30 Jan 2026 14:24:59 +0100 Subject: [PATCH 06/13] Update src/ast/query.rs --- src/ast/query.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 12c833978..78a633df7 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -374,7 +374,7 @@ pub struct Select { pub prewhere: Option, /// WHERE pub selection: Option, - /// START WITH .. CONNECT BY + /// [START WITH ..] CONNECT BY .. pub connect_by: Option, /// GROUP BY pub group_by: GroupByExpr, From 11ddcef302f62afb3ac0afb9c623b3410ba95f01 Mon Sep 17 00:00:00 2001 From: xitep Date: Fri, 30 Jan 2026 17:23:48 +0100 Subject: [PATCH 07/13] Update tests/sqlparser_common.rs Co-authored-by: Ifeanyi Ubah --- tests/sqlparser_common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 474282646..87a21127c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12831,7 +12831,7 @@ fn parse_connect_by() { } ); - // ~ CONNECT BY after WHERE and before GROUP BY + // CONNECT BY after WHERE and before GROUP BY dialects.verified_only_select("SELECT 0 FROM t WHERE 1 = 1 CONNECT BY 2 = 2 GROUP BY 3"); dialects.verified_only_select( "SELECT 0 FROM t WHERE 1 = 1 START WITH 'a' = 'a' CONNECT BY 2 = 2 GROUP BY 3", From bdb151e7f06978be7676068e8b6eae7ec4700a4e Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Fri, 30 Jan 2026 17:25:55 +0100 Subject: [PATCH 08/13] Prefer "peek" over "consume/rewind" token --- src/parser/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c5a44a2bc..73a5b60ba 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14002,10 +14002,9 @@ impl<'a> Parser<'a> { let connect_by = if self.dialect.supports_connect_by() && self - .parse_one_of_keywords(&[Keyword::START, Keyword::CONNECT]) + .peek_one_of_keywords(&[Keyword::START, Keyword::CONNECT]) .is_some() { - self.prev_token(); Some(self.parse_connect_by()?) } else { None From 77b9603e64fb2510ac3b15881a68eb6392aa22b1 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Fri, 30 Jan 2026 17:26:05 +0100 Subject: [PATCH 09/13] Syntax docs --- src/ast/query.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ast/query.rs b/src/ast/query.rs index 78a633df7..cbafb6274 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1100,10 +1100,17 @@ impl fmt::Display for TableWithJoins { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct ConnectBy { /// 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`. pub condition: Option, + /// CONNECT BY pub relationships: Vec, + /// [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) pub nocycle: bool, } From cf272e76e870123d9a3c90537727efdc6a762620 Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Fri, 30 Jan 2026 18:27:36 +0100 Subject: [PATCH 10/13] Preserve order of START WITH / CONNECT BY; simplify parsing --- src/ast/mod.rs | 2 +- src/ast/query.rs | 54 +++++------ src/ast/spans.rs | 22 ++--- src/parser/mod.rs | 57 ++++-------- tests/sqlparser_bigquery.rs | 4 +- tests/sqlparser_clickhouse.rs | 2 +- tests/sqlparser_common.rs | 166 ++++++++++++++++++++++------------ tests/sqlparser_duckdb.rs | 4 +- tests/sqlparser_mssql.rs | 6 +- tests/sqlparser_mysql.rs | 16 ++-- tests/sqlparser_postgres.rs | 6 +- 11 files changed, 178 insertions(+), 161 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index f255e5f3f..7dda84b09 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -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, diff --git a/src/ast/query.rs b/src/ast/query.rs index cbafb6274..93705d334 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -375,7 +375,7 @@ pub struct Select { /// WHERE pub selection: Option, /// [START WITH ..] CONNECT BY .. - pub connect_by: Option, + pub connect_by: Vec, /// GROUP BY pub group_by: GroupByExpr, /// CLUSTER BY (Hive) @@ -475,9 +475,9 @@ impl fmt::Display for Select { SpaceOrNewline.fmt(f)?; Indent(selection).fmt(f)?; } - if let Some(ref connect_by) = self.connect_by { + for clause in &self.connect_by { SpaceOrNewline.fmt(f)?; - connect_by.fmt(f)?; + clause.fmt(f)?; } match &self.group_by { GroupByExpr::All(_) => { @@ -1098,38 +1098,38 @@ impl fmt::Display for TableWithJoins { #[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 { +pub enum ConnectByKind { + /// CONNECT BY + ConnectBy { + /// the join conditions denoting the hierarchical relationship + relationships: Vec, + + /// [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, + }, + /// 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`. - pub condition: Option, - - /// CONNECT BY - pub relationships: Vec, - - /// [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) - pub nocycle: bool, + StartWith(Box), } -impl fmt::Display for ConnectBy { +impl fmt::Display for ConnectByKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let Self { - condition, - relationships, - nocycle, - } = self; - if let Some(condition) = condition { - write!(f, "START WITH {condition} ")?; + match self { + ConnectByKind::ConnectBy { relationships, nocycle } => { + write!(f, "CONNECT BY {nocycle}{relationships}", + nocycle = if *nocycle { "NOCYCLE " } else { "" }, + relationships = display_comma_separated(relationships) + ) + }, + ConnectByKind::StartWith(condition) => { + write!(f, "START WITH {condition}") + }, } - write!( - f, - "CONNECT BY {nocycle}{relationships}", - nocycle = if *nocycle { "NOCYCLE " } else { "" }, - relationships = display_comma_separated(relationships) - ) } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 8cc0dc0ac..918b0a330 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -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, @@ -2280,20 +2280,14 @@ impl Spanned for Select { } } -impl Spanned for ConnectBy { +impl Spanned for ConnectByKind { fn span(&self) -> Span { - let ConnectBy { - condition, - relationships, - nocycle: _, - } = self; - - union_spans( - condition - .iter() - .map(Spanned::span) - .chain(relationships.iter().map(|item| item.span())), - ) + match self { + ConnectByKind::ConnectBy { relationships, nocycle: _ } => { + union_spans(relationships.iter().map(|item| item.span())) + } + ConnectByKind::StartWith(expr) => expr.span(), + } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 73a5b60ba..5584403c4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13902,7 +13902,7 @@ impl<'a> Parser<'a> { window_before_qualify: false, qualify: None, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::FromFirstNoSelect, }); } @@ -14000,15 +14000,7 @@ impl<'a> Parser<'a> { None }; - let connect_by = if self.dialect.supports_connect_by() - && self - .peek_one_of_keywords(&[Keyword::START, Keyword::CONNECT]) - .is_some() - { - Some(self.parse_connect_by()?) - } else { - None - }; + let connect_by = self.maybe_parse_connect_by()?; let group_by = self .parse_optional_group_by()? @@ -14183,37 +14175,22 @@ impl<'a> Parser<'a> { } /// Parse a `CONNECT BY` clause (Oracle-style hierarchical query support). - pub fn parse_connect_by(&mut self) -> Result { - let (condition, relationships, nocycle) = if self - .parse_keywords(&[Keyword::CONNECT, Keyword::BY]) - { - let (relationships, nocycle) = self.with_state(ParserState::ConnectBy, |parser| { - let nocycle = parser.parse_keyword(Keyword::NOCYCLE); - parser - .parse_comma_separated(Parser::parse_expr) - .map(|exprs| (exprs, nocycle)) - })?; - let condition = if self.parse_keywords(&[Keyword::START, Keyword::WITH]) { - Some(self.parse_expr()?) + pub fn maybe_parse_connect_by(&mut self) -> Result, ParserError> { + let mut clauses = Vec::with_capacity(2); + loop { + if self.parse_keywords(&[Keyword::START, Keyword::WITH]) { + clauses.push(ConnectByKind::StartWith(self.parse_expr()?.into())); + } else if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) { + let nocycle = self.parse_keyword(Keyword::NOCYCLE); + let relationships = self.with_state(ParserState::ConnectBy, |parser| { + parser.parse_comma_separated(Parser::parse_expr) + })?; + clauses.push(ConnectByKind::ConnectBy { relationships, nocycle }); } else { - None - }; - (condition, relationships, nocycle) - } else { - self.expect_keywords(&[Keyword::START, Keyword::WITH])?; - let condition = self.parse_expr()?; - self.expect_keywords(&[Keyword::CONNECT, Keyword::BY])?; - let nocycle = self.parse_keyword(Keyword::NOCYCLE); - let relationships = self.with_state(ParserState::ConnectBy, |parser| { - parser.parse_comma_separated(Parser::parse_expr) - })?; - (Some(condition), relationships, nocycle) - }; - Ok(ConnectBy { - condition, - relationships, - nocycle, - }) + break; + } + } + Ok(clauses) } /// Parse `CREATE TABLE x AS TABLE y` diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index fb28b4d21..45cac5f6e 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -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 { @@ -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 { diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index ac31a2783..0687ae7a9 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -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 diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 87a21127c..9a0a65877 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -500,7 +500,7 @@ fn parse_update_set_from() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -5947,7 +5947,7 @@ fn test_parse_named_window() { qualify: None, window_before_qualify: true, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }; assert_eq!(actual_select_only, expected); @@ -6621,7 +6621,7 @@ fn parse_interval_and_or_xor() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -8958,7 +8958,7 @@ fn lateral_function() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }; assert_eq!(actual_select_only, expected); @@ -9945,7 +9945,7 @@ fn parse_merge() { window_before_qualify: false, qualify: None, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -12344,7 +12344,7 @@ fn parse_unload() { window_before_qualify: false, qualify: None, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), with: None, @@ -12630,7 +12630,14 @@ fn parse_map_access_expr() { fn parse_connect_by() { let dialects = all_dialects_where(|d| d.supports_connect_by()); - let expect_query = Select { + let connect_by_1 = concat!( + "SELECT employee_id, manager_id, title FROM employees ", + "START WITH title = 'president' ", + "CONNECT BY manager_id = PRIOR employee_id ", + "ORDER BY employee_id" + ); + + assert_eq!(dialects.verified_only_select(connect_by_1), Select { select_token: AttachedToken::empty(), optimizer_hint: None, distinct: None, @@ -12659,34 +12666,26 @@ fn parse_connect_by() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: Some(ConnectBy { - condition: Some(Expr::BinaryOp { + connect_by: vec![ + ConnectByKind::StartWith(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, right: Box::new(Expr::Value( Value::SingleQuotedString("president".to_owned()).with_empty_span(), )), - }), - relationships: vec![Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("manager_id"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new( - "employee_id", - ))))), + }.into()), + ConnectByKind::ConnectBy { + relationships: vec![Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("manager_id"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new( + "employee_id", + ))))), + }], + nocycle: false, }], - nocycle: false, - }), flavor: SelectFlavor::Standard, - }; - - let connect_by_1 = concat!( - "SELECT employee_id, manager_id, title FROM employees ", - "START WITH title = 'president' ", - "CONNECT BY manager_id = PRIOR employee_id ", - "ORDER BY employee_id" - ); - - assert_eq!(dialects.verified_only_select(connect_by_1), expect_query); + }); // CONNECT BY can come before START WITH let connect_by_2 = concat!( @@ -12696,8 +12695,56 @@ fn parse_connect_by() { "ORDER BY employee_id" ); assert_eq!( - dialects.verified_only_select_with_canonical(connect_by_2, connect_by_1), - expect_query + dialects.verified_only_select(connect_by_2), + Select { + select_token: AttachedToken::empty(), + optimizer_hint: None, + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![ + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("employee_id"))), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), + ], + exclude: None, + from: vec![TableWithJoins { + relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), + joins: vec![], + }], + into: None, + lateral_views: vec![], + prewhere: None, + selection: None, + group_by: GroupByExpr::Expressions(vec![], vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + window_before_qualify: false, + value_table_mode: None, + connect_by: vec![ + ConnectByKind::ConnectBy { + relationships: vec![Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("manager_id"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new( + "employee_id", + ))))), + }], + nocycle: false, + }, + ConnectByKind::StartWith(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("title"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value( + Value::SingleQuotedString("president".to_owned()).with_empty_span(), + )), + }.into())], + flavor: SelectFlavor::Standard, + } ); // WHERE must come before CONNECT BY @@ -12743,23 +12790,24 @@ fn parse_connect_by() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: Some(ConnectBy { - condition: Some(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("title"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::Value( - (Value::SingleQuotedString("president".to_owned(),)).with_empty_span() - )), - }), - relationships: vec![Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("manager_id"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new( - "employee_id", - ))))), + connect_by: vec![ + ConnectByKind::StartWith(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("title"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value( + (Value::SingleQuotedString("president".to_owned(),)).with_empty_span() + )), + }.into()), + ConnectByKind::ConnectBy { + relationships: vec![Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("manager_id"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new( + "employee_id", + ))))), + }], + nocycle: false, }], - nocycle: false, - }), flavor: SelectFlavor::Standard, } ); @@ -12818,15 +12866,15 @@ fn parse_connect_by() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: Some(ConnectBy { - condition: None, - relationships: vec![Expr::BinaryOp { - left: Expr::Identifier(Ident::new("parent")).into(), - op: BinaryOperator::Eq, - right: Expr::Prior(Expr::Identifier(Ident::new("child")).into()).into(), + connect_by: vec![ + ConnectByKind::ConnectBy { + relationships: vec![Expr::BinaryOp { + left: Expr::Identifier(Ident::new("parent")).into(), + op: BinaryOperator::Eq, + right: Expr::Prior(Expr::Identifier(Ident::new("child")).into()).into(), + }], + nocycle: true, }], - nocycle: true, - }), flavor: SelectFlavor::Standard, } ); @@ -12836,10 +12884,8 @@ fn parse_connect_by() { dialects.verified_only_select( "SELECT 0 FROM t WHERE 1 = 1 START WITH 'a' = 'a' CONNECT BY 2 = 2 GROUP BY 3", ); - dialects.verified_only_select_with_canonical( - "SELECT 0 FROM t WHERE 1 = 1 CONNECT BY 2 = 2 START WITH 'a' = 'a' GROUP BY 3", - "SELECT 0 FROM t WHERE 1 = 1 START WITH 'a' = 'a' CONNECT BY 2 = 2 GROUP BY 3", - ); + dialects.verified_only_select( + "SELECT 0 FROM t WHERE 1 = 1 CONNECT BY 2 = 2 START WITH 'a' = 'a' GROUP BY 3"); } #[test] @@ -13737,7 +13783,7 @@ fn test_extract_seconds_ok() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -15869,7 +15915,7 @@ fn test_select_from_first() { window_before_qualify: false, qualify: None, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor, }))), order_by: None, diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 7cc710de2..8f7d5d0f7 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -293,7 +293,7 @@ fn test_select_union_by_name() { window_before_qualify: false, qualify: None, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), right: Box::::new(SetExpr::Select(Box::new(Select { @@ -325,7 +325,7 @@ fn test_select_union_by_name() { window_before_qualify: false, qualify: None, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), }); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 7ef4ce85c..76aee34af 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -163,7 +163,7 @@ fn parse_create_procedure() { window_before_qualify: false, qualify: None, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))) }))], @@ -1390,7 +1390,7 @@ fn parse_substring_in_select() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -1533,7 +1533,7 @@ fn parse_mssql_declare() { window_before_qualify: false, qualify: None, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))) })) diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 80aed5bfe..bd628d827 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1459,7 +1459,7 @@ fn parse_escaped_quote_identifiers_with_escape() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -1515,7 +1515,7 @@ fn parse_escaped_quote_identifiers_with_no_escape() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -1563,7 +1563,7 @@ fn parse_escaped_backticks_with_escape() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -1615,7 +1615,7 @@ fn parse_escaped_backticks_with_no_escape() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -2438,7 +2438,7 @@ fn parse_select_with_numeric_prefix_column_name() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))) ); @@ -2614,7 +2614,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))) ); @@ -3261,7 +3261,7 @@ fn parse_substring_in_select() { window_before_qualify: false, qualify: None, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -3570,7 +3570,7 @@ fn parse_hex_string_introducer() { qualify: None, value_table_mode: None, into: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 54e9ee0c0..0389522df 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1323,7 +1323,7 @@ fn parse_copy_to() { sort_by: vec![], qualify: None, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), order_by: None, @@ -3087,7 +3087,7 @@ fn parse_array_subquery_expr() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), right: Box::new(SetExpr::Select(Box::new(Select { @@ -3114,7 +3114,7 @@ fn parse_array_subquery_expr() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: None, + connect_by: vec![], flavor: SelectFlavor::Standard, }))), }), From 4c135f4f67aed3b90f2b8afe052fc2e6d9e5392b Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Fri, 30 Jan 2026 18:28:25 +0100 Subject: [PATCH 11/13] Cargo fmt --- src/ast/query.rs | 13 +++- src/ast/spans.rs | 7 +- src/parser/mod.rs | 5 +- tests/sqlparser_common.rs | 149 +++++++++++++++++++++----------------- 4 files changed, 99 insertions(+), 75 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 93705d334..34e7163c9 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1120,15 +1120,20 @@ pub enum ConnectByKind { impl fmt::Display for ConnectByKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - ConnectByKind::ConnectBy { relationships, nocycle } => { - write!(f, "CONNECT BY {nocycle}{relationships}", + ConnectByKind::ConnectBy { + relationships, + nocycle, + } => { + write!( + f, + "CONNECT BY {nocycle}{relationships}", nocycle = if *nocycle { "NOCYCLE " } else { "" }, relationships = display_comma_separated(relationships) ) - }, + } ConnectByKind::StartWith(condition) => { write!(f, "START WITH {condition}") - }, + } } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 918b0a330..b83d60585 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2283,9 +2283,10 @@ impl Spanned for Select { impl Spanned for ConnectByKind { fn span(&self) -> Span { match self { - ConnectByKind::ConnectBy { relationships, nocycle: _ } => { - union_spans(relationships.iter().map(|item| item.span())) - } + ConnectByKind::ConnectBy { + relationships, + nocycle: _, + } => union_spans(relationships.iter().map(|item| item.span())), ConnectByKind::StartWith(expr) => expr.span(), } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5584403c4..d3fdba154 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14185,7 +14185,10 @@ impl<'a> Parser<'a> { let relationships = self.with_state(ParserState::ConnectBy, |parser| { parser.parse_comma_separated(Parser::parse_expr) })?; - clauses.push(ConnectByKind::ConnectBy { relationships, nocycle }); + clauses.push(ConnectByKind::ConnectBy { + relationships, + nocycle, + }); } else { break; } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9a0a65877..00b990211 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12637,55 +12637,62 @@ fn parse_connect_by() { "ORDER BY employee_id" ); - assert_eq!(dialects.verified_only_select(connect_by_1), Select { - select_token: AttachedToken::empty(), - optimizer_hint: None, - distinct: None, - top: None, - top_before_distinct: false, - projection: vec![ - SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("employee_id"))), - SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), - SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), - ], - exclude: None, - from: vec![TableWithJoins { - relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), - joins: vec![], - }], - into: None, - lateral_views: vec![], - prewhere: None, - selection: None, - group_by: GroupByExpr::Expressions(vec![], vec![]), - cluster_by: vec![], - distribute_by: vec![], - sort_by: vec![], - having: None, - named_window: vec![], - qualify: None, - window_before_qualify: false, - value_table_mode: None, - connect_by: vec![ - ConnectByKind::StartWith(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("title"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::Value( - Value::SingleQuotedString("president".to_owned()).with_empty_span(), - )), - }.into()), - ConnectByKind::ConnectBy { - relationships: vec![Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("manager_id"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new( - "employee_id", - ))))), - }], - nocycle: false, + assert_eq!( + dialects.verified_only_select(connect_by_1), + Select { + select_token: AttachedToken::empty(), + optimizer_hint: None, + distinct: None, + top: None, + top_before_distinct: false, + projection: vec![ + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("employee_id"))), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("manager_id"))), + SelectItem::UnnamedExpr(Expr::Identifier(Ident::new("title"))), + ], + exclude: None, + from: vec![TableWithJoins { + relation: table_from_name(ObjectName::from(vec![Ident::new("employees")])), + joins: vec![], }], - flavor: SelectFlavor::Standard, - }); + into: None, + lateral_views: vec![], + prewhere: None, + selection: None, + group_by: GroupByExpr::Expressions(vec![], vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + window_before_qualify: false, + value_table_mode: None, + connect_by: vec![ + ConnectByKind::StartWith( + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("title"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value( + Value::SingleQuotedString("president".to_owned()).with_empty_span(), + )), + } + .into() + ), + ConnectByKind::ConnectBy { + relationships: vec![Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("manager_id"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new( + "employee_id", + ))))), + }], + nocycle: false, + } + ], + flavor: SelectFlavor::Standard, + } + ); // CONNECT BY can come before START WITH let connect_by_2 = concat!( @@ -12736,13 +12743,17 @@ fn parse_connect_by() { }], nocycle: false, }, - ConnectByKind::StartWith(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("title"))), - op: BinaryOperator::Eq, - right: Box::new(Expr::Value( - Value::SingleQuotedString("president".to_owned()).with_empty_span(), - )), - }.into())], + ConnectByKind::StartWith( + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("title"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::Value( + Value::SingleQuotedString("president".to_owned()).with_empty_span(), + )), + } + .into() + ) + ], flavor: SelectFlavor::Standard, } ); @@ -12791,13 +12802,16 @@ fn parse_connect_by() { window_before_qualify: false, value_table_mode: None, connect_by: vec![ - ConnectByKind::StartWith(Expr::BinaryOp { + ConnectByKind::StartWith( + Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, right: Box::new(Expr::Value( (Value::SingleQuotedString("president".to_owned(),)).with_empty_span() )), - }.into()), + } + .into() + ), ConnectByKind::ConnectBy { relationships: vec![Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("manager_id"))), @@ -12807,7 +12821,8 @@ fn parse_connect_by() { ))))), }], nocycle: false, - }], + } + ], flavor: SelectFlavor::Standard, } ); @@ -12866,15 +12881,14 @@ fn parse_connect_by() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: vec![ - ConnectByKind::ConnectBy { - relationships: vec![Expr::BinaryOp { - left: Expr::Identifier(Ident::new("parent")).into(), - op: BinaryOperator::Eq, - right: Expr::Prior(Expr::Identifier(Ident::new("child")).into()).into(), - }], - nocycle: true, + connect_by: vec![ConnectByKind::ConnectBy { + relationships: vec![Expr::BinaryOp { + left: Expr::Identifier(Ident::new("parent")).into(), + op: BinaryOperator::Eq, + right: Expr::Prior(Expr::Identifier(Ident::new("child")).into()).into(), }], + nocycle: true, + }], flavor: SelectFlavor::Standard, } ); @@ -12885,7 +12899,8 @@ fn parse_connect_by() { "SELECT 0 FROM t WHERE 1 = 1 START WITH 'a' = 'a' CONNECT BY 2 = 2 GROUP BY 3", ); dialects.verified_only_select( - "SELECT 0 FROM t WHERE 1 = 1 CONNECT BY 2 = 2 START WITH 'a' = 'a' GROUP BY 3"); + "SELECT 0 FROM t WHERE 1 = 1 CONNECT BY 2 = 2 START WITH 'a' = 'a' GROUP BY 3", + ); } #[test] From 5e0d2ed53e70e08ead1baaff940adb5da14ae1ee Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Fri, 30 Jan 2026 18:40:59 +0100 Subject: [PATCH 12/13] Comment style --- tests/sqlparser_oracle.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/sqlparser_oracle.rs b/tests/sqlparser_oracle.rs index 2ffba63b7..0dbccdb5e 100644 --- a/tests/sqlparser_oracle.rs +++ b/tests/sqlparser_oracle.rs @@ -209,7 +209,7 @@ fn parse_quote_delimited_string() { #[test] fn parse_invalid_quote_delimited_strings() { let dialect = all_dialects_where(|d| d.supports_quote_delimited_string()); - // ~ invalid quote delimiter + // invalid quote delimiter for q in [' ', '\t', '\r', '\n'] { assert_eq!( dialect.parse_sql_statements(&format!("SELECT Q'{q}abc{q}' FROM dual")), @@ -219,7 +219,7 @@ fn parse_invalid_quote_delimited_strings() { "with quote char {q:?}" ); } - // ~ invalid eof after quote + // invalid eof after quote assert_eq!( dialect.parse_sql_statements("SELECT Q'"), Err(ParserError::TokenizerError( @@ -227,7 +227,7 @@ fn parse_invalid_quote_delimited_strings() { )), "with EOF quote char" ); - // ~ unterminated string + // unterminated string assert_eq!( dialect.parse_sql_statements("SELECT Q'|asdfa...."), Err(ParserError::TokenizerError( @@ -338,7 +338,7 @@ fn parse_national_quote_delimited_string_but_is_a_word() { fn test_optimizer_hints() { let oracle_dialect = oracle(); - // ~ selects + // selects let select = oracle_dialect.verified_only_select_with_canonical( "SELECT /*+one two three*/ /*+not a hint!*/ 1 FROM dual", "SELECT /*+one two three*/ 1 FROM dual", @@ -369,16 +369,16 @@ fn test_optimizer_hints() { Some(" one two three /* asdf */\n") ); - // ~ inserts + // inserts oracle_dialect.verified_stmt("INSERT /*+ append */ INTO t1 SELECT * FROM all_objects"); - // ~ updates + // updates oracle_dialect.verified_stmt("UPDATE /*+ DISABLE_PARALLEL_DML */ table_name SET column1 = 1"); - // ~ deletes + // deletes oracle_dialect.verified_stmt("DELETE --+ ENABLE_PARALLEL_DML\n FROM table_name"); - // ~ merges + // merges oracle_dialect.verified_stmt( "MERGE /*+ CLUSTERING */ INTO people_target pt \ USING people_source ps \ @@ -404,7 +404,7 @@ fn test_connect_by() { ORDER BY \"Employee\", \"Cycle\", LEVEL, \"Path\"", ); - // ~ CONNECT_BY_ROOT + // CONNECT_BY_ROOT oracle_dialect.verified_only_select( "SELECT last_name AS \"Employee\", CONNECT_BY_ROOT last_name AS \"Manager\", \ LEVEL - 1 AS \"Pathlen\", SYS_CONNECT_BY_PATH(last_name, '/') AS \"Path\" \ From 5310f2d8393cad189c20d6daf2d493925e56abbd Mon Sep 17 00:00:00 2001 From: Petr Novotnik Date: Sat, 31 Jan 2026 12:26:37 +0100 Subject: [PATCH 13/13] Begin tokens of START WITH / CONNECT BY clauses --- src/ast/query.rs | 23 ++++++++++++++---- src/ast/spans.rs | 17 ++++++++++---- src/parser/mod.rs | 49 +++++++++++++++++++++++++-------------- tests/sqlparser_common.rs | 33 +++++++++++++++----------- 4 files changed, 82 insertions(+), 40 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 34e7163c9..4e4278633 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1101,28 +1101,38 @@ impl fmt::Display for TableWithJoins { pub enum ConnectByKind { /// CONNECT BY ConnectBy { - /// the join conditions denoting the hierarchical relationship - relationships: Vec, + /// 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, }, /// 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(Box), + StartWith { + /// the `START` token + start_token: AttachedToken, + + /// condition selecting the root rows of the hierarchy + condition: Box, + }, } impl fmt::Display for ConnectByKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ConnectByKind::ConnectBy { - relationships, + connect_token: _, nocycle, + relationships, } => { write!( f, @@ -1131,7 +1141,10 @@ impl fmt::Display for ConnectByKind { relationships = display_comma_separated(relationships) ) } - ConnectByKind::StartWith(condition) => { + ConnectByKind::StartWith { + start_token: _, + condition, + } => { write!(f, "START WITH {condition}") } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index b83d60585..c5da95ce9 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2268,14 +2268,14 @@ 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())), ) } } @@ -2284,10 +2284,17 @@ impl Spanned for ConnectByKind { fn span(&self) -> Span { match self { ConnectByKind::ConnectBy { - relationships, + connect_token, nocycle: _, - } => union_spans(relationships.iter().map(|item| item.span())), - ConnectByKind::StartWith(expr) => expr.span(), + 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()), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d3fdba154..69441ddbd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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 { + 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 @@ -14178,16 +14191,18 @@ impl<'a> Parser<'a> { pub fn maybe_parse_connect_by(&mut self) -> Result, ParserError> { let mut clauses = Vec::with_capacity(2); loop { - if self.parse_keywords(&[Keyword::START, Keyword::WITH]) { - clauses.push(ConnectByKind::StartWith(self.parse_expr()?.into())); - } else if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) { - let nocycle = self.parse_keyword(Keyword::NOCYCLE); - let relationships = self.with_state(ParserState::ConnectBy, |parser| { - parser.parse_comma_separated(Parser::parse_expr) - })?; + 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 { - relationships, - nocycle, + 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; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 00b990211..dff6fb5ca 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -12669,8 +12669,9 @@ fn parse_connect_by() { window_before_qualify: false, value_table_mode: None, connect_by: vec![ - ConnectByKind::StartWith( - Expr::BinaryOp { + ConnectByKind::StartWith { + start_token: AttachedToken::empty(), + condition: Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, right: Box::new(Expr::Value( @@ -12678,8 +12679,10 @@ fn parse_connect_by() { )), } .into() - ), + }, ConnectByKind::ConnectBy { + connect_token: AttachedToken::empty(), + nocycle: false, relationships: vec![Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("manager_id"))), op: BinaryOperator::Eq, @@ -12687,7 +12690,6 @@ fn parse_connect_by() { "employee_id", ))))), }], - nocycle: false, } ], flavor: SelectFlavor::Standard, @@ -12734,6 +12736,8 @@ fn parse_connect_by() { value_table_mode: None, connect_by: vec![ ConnectByKind::ConnectBy { + connect_token: AttachedToken::empty(), + nocycle: false, relationships: vec![Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("manager_id"))), op: BinaryOperator::Eq, @@ -12741,10 +12745,10 @@ fn parse_connect_by() { "employee_id", ))))), }], - nocycle: false, }, - ConnectByKind::StartWith( - Expr::BinaryOp { + ConnectByKind::StartWith { + start_token: AttachedToken::empty(), + condition: Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, right: Box::new(Expr::Value( @@ -12752,7 +12756,7 @@ fn parse_connect_by() { )), } .into() - ) + }, ], flavor: SelectFlavor::Standard, } @@ -12802,8 +12806,9 @@ fn parse_connect_by() { window_before_qualify: false, value_table_mode: None, connect_by: vec![ - ConnectByKind::StartWith( - Expr::BinaryOp { + ConnectByKind::StartWith { + start_token: AttachedToken::empty(), + condition: Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("title"))), op: BinaryOperator::Eq, right: Box::new(Expr::Value( @@ -12811,8 +12816,10 @@ fn parse_connect_by() { )), } .into() - ), + }, ConnectByKind::ConnectBy { + connect_token: AttachedToken::empty(), + nocycle: false, relationships: vec![Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("manager_id"))), op: BinaryOperator::Eq, @@ -12820,7 +12827,6 @@ fn parse_connect_by() { "employee_id", ))))), }], - nocycle: false, } ], flavor: SelectFlavor::Standard, @@ -12882,12 +12888,13 @@ fn parse_connect_by() { window_before_qualify: false, value_table_mode: None, connect_by: vec![ConnectByKind::ConnectBy { + connect_token: AttachedToken::empty(), + nocycle: true, relationships: vec![Expr::BinaryOp { left: Expr::Identifier(Ident::new("parent")).into(), op: BinaryOperator::Eq, right: Expr::Prior(Expr::Identifier(Ident::new("child")).into()).into(), }], - nocycle: true, }], flavor: SelectFlavor::Standard, }