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 08448cabe..4e4278633 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, + /// [START WITH ..] CONNECT BY .. + pub connect_by: Vec, /// 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)?; } + for clause in &self.connect_by { + SpaceOrNewline.fmt(f)?; + clause.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(()) } } @@ -1094,24 +1094,60 @@ 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 enum ConnectByKind { /// CONNECT BY - pub relationships: Vec, + 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, + }, + + /// 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, + }, } -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}") + } + } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 60c983fa1..c5da95ce9 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, @@ -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()), + } } } diff --git a/src/dialect/oracle.rs b/src/dialect/oracle.rs index 7ff932628..d404e0109 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::{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)] @@ -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/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..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 @@ -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, }); } @@ -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![])); @@ -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, @@ -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 { - 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, 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` 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 69524ff99..dff6fb5ca 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, @@ -12628,53 +12628,7 @@ fn parse_map_access_expr() { #[test] fn parse_connect_by() { - let expect_query = 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: Some(ConnectBy { - condition: 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", - ))))), - }], - }), - flavor: SelectFlavor::Standard, - }; + let dialects = all_dialects_where(|d| d.supports_connect_by()); let connect_by_1 = concat!( "SELECT employee_id, manager_id, title FROM employees ", @@ -12684,8 +12638,62 @@ fn parse_connect_by() { ); assert_eq!( - all_dialects_where(|d| d.supports_connect_by()).verified_only_select(connect_by_1), - expect_query + 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 { + start_token: AttachedToken::empty(), + condition: 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 { + connect_token: AttachedToken::empty(), + nocycle: false, + 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", + ))))), + }], + } + ], + flavor: SelectFlavor::Standard, + } ); // CONNECT BY can come before START WITH @@ -12696,9 +12704,62 @@ 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), - 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 { + connect_token: AttachedToken::empty(), + nocycle: false, + 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", + ))))), + }], + }, + 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( + Value::SingleQuotedString("president".to_owned()).with_empty_span(), + )), + } + .into() + }, + ], + flavor: SelectFlavor::Standard, + } ); // WHERE must come before CONNECT BY @@ -12710,7 +12771,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, @@ -12744,22 +12805,30 @@ fn parse_connect_by() { qualify: None, window_before_qualify: false, value_table_mode: None, - connect_by: Some(ConnectBy { - condition: 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() - )), + connect_by: vec![ + 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( + (Value::SingleQuotedString("president".to_owned(),)).with_empty_span() + )), + } + .into() }, - 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", - ))))), - }], - }), + ConnectByKind::ConnectBy { + connect_token: AttachedToken::empty(), + nocycle: false, + 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", + ))))), + }], + } + ], flavor: SelectFlavor::Standard, } ); @@ -12771,7 +12840,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"); @@ -12785,6 +12854,60 @@ 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!( + dialects.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: 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(), + }], + }], + 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( + "SELECT 0 FROM t WHERE 1 = 1 CONNECT BY 2 = 2 START WITH 'a' = 'a' GROUP BY 3", + ); } #[test] @@ -13682,7 +13805,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, @@ -15814,7 +15937,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_oracle.rs b/tests/sqlparser_oracle.rs index 1c12f868f..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 \ @@ -388,3 +388,29 @@ 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\"", + ); +} 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, }))), }),