agent_patch
runs/2026-02-21__03-36-16__gpt-5-1-codex-mini/flux-pr-2185/flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/agent-logs/agent.patch
12889 bytes
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/src/ast/query.rs b/app/src/ast/query.rs
index bb2d889..236928c 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/src/ast/query.rs
+++ b/app/src/ast/query.rs
@@ -1208,18 +1208,26 @@ 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 predicate that seeds the traversal.
+ pub start_with: Option<Expr>,
+ /// CONNECT BY expressions describing parent-child relationships.
pub relationships: Vec<Expr>,
+ /// NOCYCLE modifier presence, see Oracle documentation.
+ pub nocycle: bool,
}
impl fmt::Display for ConnectBy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ if let Some(condition) = &self.start_with {
+ write!(f, "START WITH {condition} ")?;
+ }
+ write!(f, "CONNECT BY")?;
+ if self.nocycle {
+ write!(f, " NOCYCLE")?;
+ }
write!(
f,
- "START WITH {condition} CONNECT BY {relationships}",
- condition = self.condition,
+ " {relationships}",
relationships = display_comma_separated(&self.relationships)
)
}
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/src/ast/spans.rs b/app/src/ast/spans.rs
index ffc96ed..7638d79 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/src/ast/spans.rs
+++ b/app/src/ast/spans.rs
@@ -2283,13 +2283,11 @@ impl Spanned for Select {
impl Spanned for ConnectBy {
fn span(&self) -> Span {
- let ConnectBy {
- condition,
- relationships,
- } = self;
-
union_spans(
- core::iter::once(condition.span()).chain(relationships.iter().map(|item| item.span())),
+ self.start_with
+ .iter()
+ .map(|expr| expr.span())
+ .chain(self.relationships.iter().map(|item| item.span())),
)
}
}
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/src/dialect/oracle.rs b/app/src/dialect/oracle.rs
index a72d5d7..eb65b5c 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/src/dialect/oracle.rs
+++ b/app/src/dialect/oracle.rs
@@ -18,6 +18,8 @@
use log::debug;
use crate::{
+ ast::{Expr, Ident},
+ keywords::Keyword,
parser::{Parser, ParserError},
tokenizer::Token,
};
@@ -103,4 +105,14 @@ impl Dialect for OracleDialect {
fn supports_comment_optimizer_hint(&self) -> bool {
true
}
+
+ fn parse_prefix(&self, parser: &mut Parser) -> Option<Result<Expr, ParserError>> {
+ if parser.parse_keyword(Keyword::CONNECT_BY_ROOT) {
+ return Some(parser.parse_expr().map(|expr| Expr::Prefixed {
+ prefix: Ident::new("CONNECT_BY_ROOT"),
+ value: Box::new(expr),
+ }));
+ }
+ None
+ }
}
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/src/parser/mod.rs b/app/src/parser/mod.rs
index bddafd1..cf71628 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/src/parser/mod.rs
+++ b/app/src/parser/mod.rs
@@ -14280,25 +14280,47 @@ 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 nocycle = false;
+
+ loop {
+ if self.parse_keywords(&[Keyword::START, Keyword::WITH]) {
+ if start_with.is_some() {
+ return parser_err!(
+ "Multiple START WITH clauses",
+ 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",
+ self.peek_token().span.start
+ );
+ }
+ nocycle = self.parse_keyword(Keyword::NOCYCLE);
+ relationships = Some(self.with_state(ParserState::ConnectBy, |parser| {
+ parser.parse_comma_separated(Parser::parse_expr)
+ })?);
+ continue;
+ }
+
+ break;
+ }
+
+ let relationships = relationships.ok_or_else(|| {
+ parser_err!("Expected CONNECT BY clause", self.peek_token().span.start)
+ })?;
+
Ok(ConnectBy {
- condition,
+ start_with,
relationships,
+ nocycle,
})
}
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/tests/sqlparser_common.rs b/app/tests/sqlparser_common.rs
index b442ec4..112aecf 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__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",
))))),
}],
+ nocycle: 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",
))))),
}],
+ nocycle: false,
}),
flavor: SelectFlavor::Standard,
}
@@ -12805,6 +12807,64 @@ fn parse_connect_by() {
);
}
+#[test]
+fn parse_connect_by_without_start() {
+ let sql = concat!(
+ "SELECT employee_id, manager_id, title FROM employees ",
+ "CONNECT BY manager_id = PRIOR employee_id ",
+ "ORDER BY employee_id"
+ );
+
+ let select = all_dialects_where(|d| d.supports_connect_by()).verified_only_select(sql);
+ let connect_by = select.connect_by.expect("expected CONNECT BY clause");
+ assert!(connect_by.start_with.is_none());
+ assert!(!connect_by.nocycle);
+
+ let expected_relationship = 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"))))),
+ };
+ assert_eq!(connect_by.relationships, vec![expected_relationship]);
+}
+
+#[test]
+fn parse_connect_by_nocycle() {
+ let sql = concat!(
+ "SELECT employee_id, manager_id, title FROM employees ",
+ "START WITH title = 'president' ",
+ "CONNECT BY NOCYCLE manager_id = PRIOR employee_id ",
+ "ORDER BY employee_id"
+ );
+ let canonical = concat!(
+ "SELECT employee_id, manager_id, title FROM employees ",
+ "START WITH title = 'president' ",
+ "CONNECT BY NOCYCLE manager_id = PRIOR employee_id ",
+ "ORDER BY employee_id"
+ );
+
+ let select = all_dialects_where(|d| d.supports_connect_by())
+ .verified_only_select_with_canonical(sql, canonical);
+ let connect_by = select.connect_by.expect("expected CONNECT BY clause");
+ assert!(connect_by.nocycle);
+
+ let expected_start = 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(),
+ )),
+ };
+ assert_eq!(connect_by.start_with, Some(expected_start.clone()));
+
+ let expected_relationship = 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"))))),
+ };
+ assert_eq!(connect_by.relationships, vec![expected_relationship]);
+}
+
#[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-21__03-36-16__gpt-5-1-codex-mini/app/tests/sqlparser_oracle.rs b/app/tests/sqlparser_oracle.rs
index 1c12f86..98cc14c 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/tests/sqlparser_oracle.rs
+++ b/app/tests/sqlparser_oracle.rs
@@ -21,7 +21,7 @@
use pretty_assertions::assert_eq;
use sqlparser::{
- ast::{BinaryOperator, Expr, Ident, QuoteDelimitedString, Value, ValueWithSpan},
+ ast::{BinaryOperator, Expr, Ident, QuoteDelimitedString, SelectItem, Value, ValueWithSpan},
dialect::OracleDialect,
parser::ParserError,
tokenizer::Span,
@@ -388,3 +388,39 @@ fn test_optimizer_hints() {
VALUES (ps.person_id, ps.first_name, ps.last_name, ps.title)",
);
}
+
+#[test]
+fn parse_connect_by_root_operator_oracle() {
+ let sql = "SELECT CONNECT_BY_ROOT name AS root_name FROM dual";
+ let select = oracle().verified_only_select(sql);
+ match &select.projection[0] {
+ SelectItem::ExprWithAlias { expr, alias } => {
+ assert_eq!(
+ expr,
+ &Expr::Prefixed {
+ prefix: Ident::new("CONNECT_BY_ROOT"),
+ value: Box::new(Expr::Identifier(Ident::new("name")))
+ }
+ );
+ assert_eq!(alias, &Ident::new("root_name"));
+ }
+ _ => panic!("expected ExprWithAlias"),
+ }
+
+ let sql = "SELECT CONNECT_BY_ROOT name FROM dual";
+ let select = oracle().verified_only_select(sql);
+ assert_eq!(
+ select.projection[0],
+ SelectItem::UnnamedExpr(Expr::Prefixed {
+ prefix: Ident::new("CONNECT_BY_ROOT"),
+ value: Box::new(Expr::Identifier(Ident::new("name")))
+ })
+ );
+
+ let sql = "SELECT CONNECT_BY_ROOT FROM dual";
+ let err = oracle().parse_sql_statements(sql);
+ assert_eq!(
+ err.unwrap_err().to_string(),
+ "sql parser error: Expected an expression, found: FROM"
+ );
+}