STET

agent_patch

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

14186 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/ast/query.rs b/app/src/ast/query.rs
index bb2d889..74f3541 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/ast/query.rs
+++ b/app/src/ast/query.rs
@@ -1209,19 +1209,25 @@ impl fmt::Display for TableWithJoins {
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
 pub struct ConnectBy {
     /// START WITH
-    pub condition: Expr,
+    pub condition: Option<Expr>,
+    /// CONNECT BY
+    pub no_cycle: bool,
     /// CONNECT BY
     pub relationships: Vec<Expr>,
 }
 
 impl fmt::Display for ConnectBy {
     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)
-        )
+        if let Some(condition) = &self.condition {
+            write!(f, "START WITH {condition} ")?;
+        }
+
+        write!(f, "CONNECT BY ")?;
+        if self.no_cycle {
+            write!(f, "NOCYCLE ")?;
+        }
+
+        write!(f, "{}", 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-4/app/src/ast/spans.rs b/app/src/ast/spans.rs
index ffc96ed..3367c77 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/ast/spans.rs
+++ b/app/src/ast/spans.rs
@@ -2285,11 +2285,15 @@ impl Spanned for ConnectBy {
     fn span(&self) -> Span {
         let ConnectBy {
             condition,
+            no_cycle: _,
             relationships,
         } = self;
 
         union_spans(
-            core::iter::once(condition.span()).chain(relationships.iter().map(|item| item.span())),
+            condition
+                .iter()
+                .map(|expr| expr.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-4/app/src/dialect/oracle.rs b/app/src/dialect/oracle.rs
index a72d5d7..0e53f10 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/dialect/oracle.rs
+++ b/app/src/dialect/oracle.rs
@@ -18,12 +18,15 @@
 use log::debug;
 
 use crate::{
+    keywords::Keyword,
     parser::{Parser, ParserError},
     tokenizer::Token,
 };
 
 use super::{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, Default)]
 pub struct OracleDialect;
@@ -103,4 +106,8 @@ impl Dialect for OracleDialect {
     fn supports_comment_optimizer_hint(&self) -> bool {
         true
     }
+
+    fn get_reserved_keywords_for_select_item_operator(&self) -> &[Keyword] {
+        &RESERVED_KEYWORDS_FOR_SELECT_ITEM_OPERATOR
+    }
 }
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/keywords.rs b/app/src/keywords.rs
index 2e26bda..f84f4d2 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/keywords.rs
+++ b/app/src/keywords.rs
@@ -678,6 +678,7 @@ define_keywords!(
     NOCOMPRESS,
     NOCREATEDB,
     NOCREATEROLE,
+    NOCYCLE,
     NOINHERIT,
     NOLOGIN,
     NONE,
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/parser/mod.rs b/app/src/parser/mod.rs
index bddafd1..9e8d38a 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/parser/mod.rs
+++ b/app/src/parser/mod.rs
@@ -1543,6 +1543,31 @@ 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
+                    .get_reserved_keywords_for_select_item_operator()
+                    .contains(&Keyword::CONNECT_BY_ROOT) =>
+            {
+                if matches!(
+                    self.peek_token_ref().token,
+                    Token::Word(Word {
+                        keyword: Keyword::FROM,
+                        ..
+                    })
+                ) {
+                    return parser_err!(
+                        "Expected an expression, found: FROM",
+                        self.peek_token_ref().span.start
+                    );
+                }
+
+                let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?;
+                Ok(Some(Expr::Prefixed {
+                    prefix: w.to_ident(w_span),
+                    value: 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,24 +14305,33 @@ 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 (condition, no_cycle, relationships) =
+            if self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) {
+                let no_cycle = self.parse_keyword(Keyword::NOCYCLE)
+                    || self.parse_keywords(&[Keyword::NO, Keyword::CYCLE]);
+                let relationships = self.with_state(ParserState::ConnectBy, |parser| {
+                    parser.parse_comma_separated(Parser::parse_expr)
+                })?;
+                let condition = if self.parse_keywords(&[Keyword::START, Keyword::WITH]) {
+                    Some(self.parse_expr()?)
+                } else {
+                    None
+                };
+                (condition, no_cycle, relationships)
+            } else {
+                self.expect_keywords(&[Keyword::START, Keyword::WITH])?;
+                let condition = Some(self.parse_expr()?);
+                self.expect_keywords(&[Keyword::CONNECT, Keyword::BY])?;
+                let no_cycle = self.parse_keyword(Keyword::NOCYCLE)
+                    || self.parse_keywords(&[Keyword::NO, Keyword::CYCLE]);
+                let relationships = self.with_state(ParserState::ConnectBy, |parser| {
+                    parser.parse_comma_separated(Parser::parse_expr)
+                })?;
+                (condition, no_cycle, relationships)
+            };
         Ok(ConnectBy {
             condition,
+            no_cycle,
             relationships,
         })
     }
@@ -17496,13 +17530,6 @@ impl<'a> Parser<'a> {
 
     /// Parse a comma-delimited list of projections after SELECT
     pub fn parse_select_item(&mut self) -> Result<SelectItem, ParserError> {
-        let prefix = self
-            .parse_one_of_keywords(
-                self.dialect
-                    .get_reserved_keywords_for_select_item_operator(),
-            )
-            .map(|keyword| Ident::new(format!("{keyword:?}")));
-
         match self.parse_wildcard_expr()? {
             Expr::QualifiedWildcard(prefix, token) => Ok(SelectItem::QualifiedWildcard(
                 SelectItemQualifiedWildcardKind::ObjectName(prefix),
@@ -17547,11 +17574,8 @@ impl<'a> Parser<'a> {
             expr => self
                 .maybe_parse_select_item_alias()
                 .map(|alias| match alias {
-                    Some(alias) => SelectItem::ExprWithAlias {
-                        expr: maybe_prefixed_expr(expr, prefix),
-                        alias,
-                    },
-                    None => SelectItem::UnnamedExpr(maybe_prefixed_expr(expr, prefix)),
+                    Some(alias) => SelectItem::ExprWithAlias { expr, alias },
+                    None => SelectItem::UnnamedExpr(expr),
                 }),
         }
     }
@@ -19401,17 +19425,6 @@ impl<'a> Parser<'a> {
     }
 }
 
-fn maybe_prefixed_expr(expr: Expr, prefix: Option<Ident>) -> Expr {
-    if let Some(prefix) = prefix {
-        Expr::Prefixed {
-            prefix,
-            value: Box::new(expr),
-        }
-    } else {
-        expr
-    }
-}
-
 impl Word {
     /// Convert a reference to this word into an [`Ident`] by cloning the value.
     ///
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/tests/sqlparser_common.rs b/app/tests/sqlparser_common.rs
index b442ec4..efd2c43 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/tests/sqlparser_common.rs
+++ b/app/tests/sqlparser_common.rs
@@ -12675,13 +12675,14 @@ 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(),
                 )),
-            },
+            }),
+            no_cycle: false,
             relationships: vec![Expr::BinaryOp {
                 left: Box::new(Expr::Identifier(Ident::new("manager_id"))),
                 op: BinaryOperator::Eq,
@@ -12763,13 +12764,14 @@ 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()
                     )),
-                },
+                }),
+                no_cycle: false,
                 relationships: vec![Expr::BinaryOp {
                     left: Box::new(Expr::Identifier(Ident::new("manager_id"))),
                     op: BinaryOperator::Eq,
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/tests/sqlparser_oracle.rs b/app/tests/sqlparser_oracle.rs
index 1c12f86..0f2f561 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/tests/sqlparser_oracle.rs
+++ b/app/tests/sqlparser_oracle.rs
@@ -388,3 +388,71 @@ fn test_optimizer_hints() {
                VALUES (ps.person_id, ps.first_name, ps.last_name, ps.title)",
     );
 }
+
+#[test]
+fn parse_connect_by_without_start_with_and_nocycle() {
+    let select = oracle().verified_only_select(
+        "SELECT employee_id FROM employees CONNECT BY NOCYCLE PRIOR employee_id = manager_id",
+    );
+    let connect_by = select.connect_by.expect("CONNECT BY should be parsed");
+
+    assert_eq!(connect_by.condition, None);
+    assert!(connect_by.no_cycle);
+    assert_eq!(
+        connect_by.relationships,
+        vec![Expr::BinaryOp {
+            left: Box::new(Expr::Prior(Box::new(Expr::Identifier(Ident::new(
+                "employee_id",
+            ))))),
+            op: BinaryOperator::Eq,
+            right: Box::new(Expr::Identifier(Ident::new("manager_id"))),
+        }]
+    );
+}
+
+#[test]
+fn parse_connect_by_nocycle_before_start_with() {
+    let select = oracle().verified_only_select_with_canonical(
+        "SELECT employee_id FROM employees CONNECT BY NOCYCLE PRIOR employee_id = manager_id START WITH employee_id = 100",
+        "SELECT employee_id FROM employees START WITH employee_id = 100 CONNECT BY NOCYCLE PRIOR employee_id = manager_id",
+    );
+    let connect_by = select.connect_by.expect("CONNECT BY should be parsed");
+
+    assert_eq!(
+        connect_by.condition,
+        Some(Expr::BinaryOp {
+            left: Box::new(Expr::Identifier(Ident::new("employee_id"))),
+            op: BinaryOperator::Eq,
+            right: Box::new(Expr::Value(number("100").into())),
+        })
+    );
+    assert!(connect_by.no_cycle);
+}
+
+#[test]
+fn parse_connect_by_root_operator_in_expression() {
+    let select = oracle().verified_only_select(
+        "SELECT CONNECT_BY_ROOT last_name || ' manages ' || last_name AS path FROM employees",
+    );
+
+    assert_eq!(
+        select.projection[0],
+        sqlparser::ast::SelectItem::ExprWithAlias {
+            expr: Expr::BinaryOp {
+                left: Box::new(Expr::BinaryOp {
+                    left: Box::new(Expr::Prefixed {
+                        prefix: Ident::new("CONNECT_BY_ROOT"),
+                        value: Box::new(Expr::Identifier(Ident::new("last_name"))),
+                    }),
+                    op: BinaryOperator::StringConcat,
+                    right: Box::new(Expr::Value(
+                        Value::SingleQuotedString(" manages ".into()).with_empty_span(),
+                    )),
+                }),
+                op: BinaryOperator::StringConcat,
+                right: Box::new(Expr::Identifier(Ident::new("last_name"))),
+            },
+            alias: Ident::new("path"),
+        }
+    );
+}