STET

agent_patch

runs/2026-02-28__18-27-22__gpt-5-1-codex-mini/flux-pr-2185/flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/agent-logs/agent.patch

17959 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/changelog/0.60.0.md b/app/changelog/0.60.0.md
index c3297bd..26e23aa 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/changelog/0.60.0.md
+++ b/app/changelog/0.60.0.md
@@ -32,6 +32,7 @@ This release consists of 37 commits from 20 contributors. See credits at the end
 
 **Other:**
 
+- Support Oracle-style hierarchical queries with flexible `START WITH` / `CONNECT BY`, `NOCYCLE`, and the `CONNECT_BY_ROOT` operator [#2185](https://github.com/apache/datafusion-sqlparser-rs/pull/2185) (Codex)
 - Add support for INVISIBLE columns in MySQL [#2033](https://github.com/apache/datafusion-sqlparser-rs/pull/2033) (altmannmarcelo)
 - Link to actual change logs in CHANGELOG.md [#2040](https://github.com/apache/datafusion-sqlparser-rs/pull/2040) (lovasoa)
 - Snowflake: ALTER USER and KeyValueOptions Refactoring [#2035](https://github.com/apache/datafusion-sqlparser-rs/pull/2035) (yoavcloud)
@@ -95,4 +96,3 @@ Thank you to everyone who contributed to this release. Here is a breakdown of co
 ```
 
 Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release.
-
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/mod.rs b/app/src/ast/mod.rs
index a26c14e..5dfd858 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/mod.rs
+++ b/app/src/ast/mod.rs
@@ -1304,6 +1304,8 @@ pub enum Expr {
     OuterJoin(Box<Expr>),
     /// A reference to the prior level in a CONNECT BY clause.
     Prior(Box<Expr>),
+    /// The `CONNECT_BY_ROOT` operator.
+    ConnectByRoot(Box<Expr>),
     /// A lambda function.
     ///
     /// Syntax:
@@ -2107,46 +2109,47 @@ impl fmt::Display for Expr {
             Expr::Dictionary(fields) => {
                 write!(f, "{{{}}}", display_comma_separated(fields))
             }
-            Expr::Map(map) => {
-                write!(f, "{map}")
-            }
-            Expr::Array(set) => {
-                write!(f, "{set}")
-            }
-            Expr::JsonAccess { value, path } => {
-                write!(f, "{value}{path}")
-            }
-            Expr::AtTimeZone {
-                timestamp,
-                time_zone,
-            } => {
-                write!(f, "{timestamp} AT TIME ZONE {time_zone}")
-            }
-            Expr::Interval(interval) => {
-                write!(f, "{interval}")
-            }
-            Expr::MatchAgainst {
-                columns,
-                match_value: match_expr,
-                opt_search_modifier,
-            } => {
-                write!(f, "MATCH ({}) AGAINST ", display_comma_separated(columns),)?;
-
-                if let Some(search_modifier) = opt_search_modifier {
-                    write!(f, "({match_expr} {search_modifier})")?;
-                } else {
-                    write!(f, "({match_expr})")?;
-                }
+    Expr::Map(map) => {
+        write!(f, "{map}")
+    }
+    Expr::Array(set) => {
+        write!(f, "{set}")
+    }
+    Expr::JsonAccess { value, path } => {
+        write!(f, "{value}{path}")
+    }
+    Expr::AtTimeZone {
+        timestamp,
+        time_zone,
+    } => {
+        write!(f, "{timestamp} AT TIME ZONE {time_zone}")
+    }
+    Expr::Interval(interval) => {
+        write!(f, "{interval}")
+    }
+    Expr::MatchAgainst {
+        columns,
+        match_value: match_expr,
+        opt_search_modifier,
+    } => {
+        write!(f, "MATCH ({}) AGAINST ", display_comma_separated(columns),)?;
 
-                Ok(())
-            }
-            Expr::OuterJoin(expr) => {
-                write!(f, "{expr} (+)")
-            }
-            Expr::Prior(expr) => write!(f, "PRIOR {expr}"),
-            Expr::Lambda(lambda) => write!(f, "{lambda}"),
-            Expr::MemberOf(member_of) => write!(f, "{member_of}"),
+        if let Some(search_modifier) = opt_search_modifier {
+            write!(f, "({match_expr} {search_modifier})")?;
+        } else {
+            write!(f, "({match_expr})")?;
         }
+
+        Ok(())
+    }
+    Expr::OuterJoin(expr) => {
+        write!(f, "{expr} (+)")
+    }
+    Expr::Prior(expr) => write!(f, "PRIOR {expr}"),
+    Expr::ConnectByRoot(expr) => write!(f, "CONNECT_BY_ROOT {expr}"),
+    Expr::Lambda(lambda) => write!(f, "{lambda}"),
+    Expr::MemberOf(member_of) => write!(f, "{member_of}"),
+}
     }
 }
 
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/query.rs b/app/src/ast/query.rs
index bb2d889..5cfe6ef 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/query.rs
+++ b/app/src/ast/query.rs
@@ -1208,18 +1208,23 @@ impl fmt::Display for TableWithJoins {
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
 pub struct ConnectBy {
-    /// START WITH
-    pub condition: Expr,
-    /// CONNECT BY
+    /// Optional START WITH condition.
+    pub start_with: Option<Expr>,
+    /// CONNECT BY relationships.
     pub relationships: Vec<Expr>,
+    /// `NOCYCLE` flag.
+    pub no_cycle: bool,
 }
 
 impl fmt::Display for ConnectBy {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        if let Some(start_with) = &self.start_with {
+            write!(f, "START WITH {start_with} ")?;
+        }
         write!(
             f,
-            "START WITH {condition} CONNECT BY {relationships}",
-            condition = self.condition,
+            "CONNECT BY{}{relationships}",
+            if self.no_cycle { " NOCYCLE " } else { " " },
             relationships = display_comma_separated(&self.relationships)
         )
     }
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/spans.rs b/app/src/ast/spans.rs
index ffc96ed..0c675bc 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/ast/spans.rs
+++ b/app/src/ast/spans.rs
@@ -1620,6 +1620,7 @@ impl Spanned for Expr {
             ),
             Expr::OuterJoin(expr) => expr.span(),
             Expr::Prior(expr) => expr.span(),
+            Expr::ConnectByRoot(expr) => expr.span(),
             Expr::Lambda(_) => Span::empty(),
             Expr::MemberOf(member_of) => member_of.value.span().union(&member_of.array.span()),
         }
@@ -2284,12 +2285,16 @@ impl Spanned for Select {
 impl Spanned for ConnectBy {
     fn span(&self) -> Span {
         let ConnectBy {
-            condition,
+            start_with,
             relationships,
+            ..
         } = self;
 
         union_spans(
-            core::iter::once(condition.span()).chain(relationships.iter().map(|item| item.span())),
+            start_with
+                .iter()
+                .map(|item| item.span())
+                .chain(relationships.iter().map(|item| item.span())),
         )
     }
 }
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/dialect/snowflake.rs b/app/src/dialect/snowflake.rs
index e2d8cb2..2804abb 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/dialect/snowflake.rs
+++ b/app/src/dialect/snowflake.rs
@@ -49,8 +49,6 @@ use alloc::{format, vec};
 
 use super::keywords::RESERVED_FOR_IDENTIFIER;
 
-const RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR: [Keyword; 1] = [Keyword::CONNECT_BY_ROOT];
-
 // See: <https://docs.snowflake.com/en/sql-reference/reserved-keywords>
 const RESERVED_KEYWORDS_FOR_TABLE_FACTOR: &[Keyword] = &[
     Keyword::ALL,
@@ -579,11 +577,6 @@ impl Dialect for SnowflakeDialect {
         true
     }
 
-    /// See: <https://docs.snowflake.com/en/sql-reference/constructs/connect-by>
-    fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] {
-        &RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR
-    }
-
     fn supports_space_separated_column_options(&self) -> bool {
         true
     }
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/parser/mod.rs b/app/src/parser/mod.rs
index bddafd1..4412dac 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/src/parser/mod.rs
+++ b/app/src/parser/mod.rs
@@ -1543,6 +1543,10 @@ impl<'a> Parser<'a> {
                 let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?;
                 Ok(Some(Expr::Prior(Box::new(expr))))
             }
+            Keyword::CONNECT_BY_ROOT if self.dialect.supports_connect_by() => {
+                let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?;
+                Ok(Some(Expr::ConnectByRoot(Box::new(expr))))
+            }
             Keyword::MAP if *self.peek_token_ref() == Token::LBrace && self.dialect.support_map_literal_syntax() => {
                 Ok(Some(self.parse_duckdb_map_literal()?))
             }
@@ -14280,25 +14284,54 @@ 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)
+        let mut start_with = None;
+        let mut relationships = None;
+        let mut no_cycle = false;
+
+        loop {
+            if self.parse_keywords(&[Keyword::START, Keyword::WITH]) {
+                if start_with.is_some() {
+                    return parser_err!(
+                        \"Multiple START WITH clauses not allowed\",
+                        self.peek_token().span.start
+                    );
+                }
+                start_with = Some(self.parse_expr()?);
+                continue;
+            }
+
+            if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) {
+                if relationships.is_some() {
+                    return parser_err!(
+                        \"Multiple CONNECT BY clauses not allowed\",
+                        self.peek_token().span.start
+                    );
+                }
+                no_cycle = self.parse_keyword(Keyword::NOCYCLE);
+                let rels = self.with_state(ParserState::ConnectBy, |parser| {
+                    parser.parse_comma_separated(Parser::parse_expr)
+                })?;
+                relationships = Some(rels);
+                continue;
+            }
+
+            break;
+        }
+
+        let relationships = match relationships {
+            Some(rel) => rel,
+            None => {
+                return parser_err!(
+                    \"Expected CONNECT BY clause\",
+                    self.peek_token().span.start
+                )
+            }
         };
+
         Ok(ConnectBy {
-            condition,
+            start_with,
             relationships,
+            no_cycle,
         })
     }
 
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/tests/sqlparser_common.rs b/app/tests/sqlparser_common.rs
index b442ec4..830cb73 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/tests/sqlparser_common.rs
+++ b/app/tests/sqlparser_common.rs
@@ -12675,13 +12675,13 @@ fn parse_connect_by() {
         window_before_qualify: false,
         value_table_mode: None,
         connect_by: Some(ConnectBy {
-            condition: Expr::BinaryOp {
+            start_with: 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,
@@ -12689,6 +12689,7 @@ fn parse_connect_by() {
                     "employee_id",
                 ))))),
             }],
+            no_cycle: false,
         }),
         flavor: SelectFlavor::Standard,
     };
@@ -12763,13 +12764,13 @@ fn parse_connect_by() {
             window_before_qualify: false,
             value_table_mode: None,
             connect_by: Some(ConnectBy {
-                condition: Expr::BinaryOp {
+                start_with: 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,
@@ -12777,6 +12778,7 @@ fn parse_connect_by() {
                         "employee_id",
                     ))))),
                 }],
+                no_cycle: false,
             }),
             flavor: SelectFlavor::Standard,
         }
@@ -12805,6 +12807,65 @@ fn parse_connect_by() {
     );
 }
 
+#[test]
+fn parse_connect_by_without_start_with() {
+    let expected_connect_by = ConnectBy {
+        start_with: None,
+        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",
+            ))))),
+        }],
+        no_cycle: false,
+    };
+
+    let select = all_dialects_where(|d| d.supports_connect_by()).verified_only_select(
+        "SELECT employee_id, manager_id FROM employees \
+         CONNECT BY manager_id = PRIOR employee_id",
+    );
+
+    assert_eq!(select.connect_by, Some(expected_connect_by));
+}
+
+#[test]
+fn parse_connect_by_nocycle() {
+    let expected_connect_by = ConnectBy {
+        start_with: None,
+        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",
+            ))))),
+        }],
+        no_cycle: true,
+    };
+
+    let select = all_dialects_where(|d| d.supports_connect_by()).verified_only_select(
+        "SELECT employee_id, manager_id FROM employees \
+         CONNECT BY NOCYCLE manager_id = PRIOR employee_id",
+    );
+
+    assert_eq!(select.connect_by, Some(expected_connect_by));
+}
+
+#[test]
+fn parse_connect_by_root_expression() {
+    let select = all_dialects_where(|d| d.supports_connect_by()).verified_only_select(
+        "SELECT CONNECT_BY_ROOT name FROM employees \
+         CONNECT BY manager_id = PRIOR employee_id",
+    );
+
+    assert_eq!(
+        select.projection[0],
+        SelectItem::UnnamedExpr(Expr::ConnectByRoot(Box::new(Expr::Identifier(
+            Ident::new("name")
+        ))))
+    );
+}
+
 #[test]
 fn test_selective_aggregation() {
     let testing_dialects = all_dialects_where(|d| d.supports_filter_during_aggregation());
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/tests/sqlparser_snowflake.rs b/app/tests/sqlparser_snowflake.rs
index cb5af62..16afe51 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-1-codex-mini/app/tests/sqlparser_snowflake.rs
+++ b/app/tests/sqlparser_snowflake.rs
@@ -4187,13 +4187,10 @@ fn parse_connect_by_root_operator() {
         Statement::Query(query) => {
             assert_eq!(
                 query.body.as_select().unwrap().projection[0],
-                SelectItem::ExprWithAlias {
-                    expr: Expr::Prefixed {
-                        prefix: Ident::new("CONNECT_BY_ROOT"),
-                        value: Box::new(Expr::Identifier(Ident::new("name")))
-                    },
-                    alias: Ident::new("root_name"),
-                }
+            SelectItem::ExprWithAlias {
+                expr: Expr::ConnectByRoot(Box::new(Expr::Identifier(Ident::new("name")))),
+                alias: Ident::new("root_name"),
+            }
             );
         }
         _ => unreachable!(),
@@ -4204,10 +4201,9 @@ fn parse_connect_by_root_operator() {
         Statement::Query(query) => {
             assert_eq!(
                 query.body.as_select().unwrap().projection[0],
-                SelectItem::UnnamedExpr(Expr::Prefixed {
-                    prefix: Ident::new("CONNECT_BY_ROOT"),
-                    value: Box::new(Expr::Identifier(Ident::new("name")))
-                })
+            SelectItem::UnnamedExpr(Expr::ConnectByRoot(Box::new(Expr::Identifier(
+                Ident::new("name")
+            ))))
             );
         }
         _ => unreachable!(),