agent_patch
runs/2026-02-28__18-27-22__gpt-5-3-codex/flux-pr-2185/flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/agent-logs/agent.patch
12014 bytes
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/ast/query.rs b/app/src/ast/query.rs
index bb2d889..ae4e786 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/ast/query.rs
+++ b/app/src/ast/query.rs
@@ -501,7 +501,7 @@ pub struct Select {
pub window_before_qualify: bool,
/// BigQuery syntax: `SELECT AS VALUE | SELECT AS STRUCT`
pub value_table_mode: Option<ValueTableMode>,
- /// STARTING WITH .. CONNECT BY
+ /// START WITH .. CONNECT BY
pub connect_by: Option<ConnectBy>,
/// Was this a FROM-first query?
pub flavor: SelectFlavor,
@@ -1209,19 +1209,23 @@ impl fmt::Display for TableWithJoins {
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct ConnectBy {
/// START WITH
- pub condition: Expr,
+ pub start_with: Option<Expr>,
+ /// NOCYCLE
+ 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(start_with) = &self.start_with {
+ write!(f, "START WITH {start_with} ")?;
+ }
+ 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-3-codex/app/src/ast/spans.rs b/app/src/ast/spans.rs
index ffc96ed..3b1f768 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/ast/spans.rs
+++ b/app/src/ast/spans.rs
@@ -2284,12 +2284,16 @@ impl Spanned for Select {
impl Spanned for ConnectBy {
fn span(&self) -> Span {
let ConnectBy {
- condition,
+ start_with,
+ no_cycle: _,
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-3-codex/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-3-codex/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-3-codex/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-3-codex/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-3-codex/app/src/parser/mod.rs b/app/src/parser/mod.rs
index bddafd1..9845c46 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/parser/mod.rs
+++ b/app/src/parser/mod.rs
@@ -14280,24 +14280,32 @@ 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;
+
+ for _ in 0..2 {
+ if start_with.is_none() && self.parse_keywords(&[Keyword::START, Keyword::WITH]) {
+ start_with = Some(self.parse_expr()?);
+ continue;
+ }
+
+ if relationships.is_none() && self.parse_keywords(&[Keyword::CONNECT, Keyword::BY]) {
+ no_cycle = self.parse_keyword(Keyword::NOCYCLE);
+ relationships = Some(self.with_state(ParserState::ConnectBy, |parser| {
+ parser.parse_comma_separated(Parser::parse_expr)
+ })?);
+ continue;
+ }
+ }
+
+ let Some(relationships) = relationships else {
+ return self.expected("CONNECT BY", self.peek_token());
};
+
Ok(ConnectBy {
- condition,
+ start_with,
+ no_cycle,
relationships,
})
}
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/tests/sqlparser_common.rs b/app/tests/sqlparser_common.rs
index b442ec4..29f44b7 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/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 {
+ 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(),
)),
- },
+ }),
+ 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 {
+ 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()
)),
- },
+ }),
+ no_cycle: false,
relationships: vec![Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("manager_id"))),
op: BinaryOperator::Eq,
@@ -12793,6 +12795,58 @@ fn parse_connect_by() {
.parse_sql_statements(connect_by_4)
.expect_err("should have failed");
+ let connect_by_5 = concat!(
+ "SELECT employee_id, manager_id, title FROM employees ",
+ "CONNECT BY manager_id = PRIOR employee_id ",
+ "ORDER BY employee_id"
+ );
+ assert_eq!(
+ all_dialects_where(|d| d.supports_connect_by())
+ .verified_only_select_with_canonical(connect_by_5, connect_by_5)
+ .connect_by,
+ Some(ConnectBy {
+ start_with: None,
+ no_cycle: 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",
+ ))))),
+ }],
+ })
+ );
+
+ let connect_by_6 = 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"
+ );
+ assert_eq!(
+ all_dialects_where(|d| d.supports_connect_by())
+ .verified_only_select(connect_by_6)
+ .connect_by
+ .unwrap(),
+ ConnectBy {
+ 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(),
+ )),
+ }),
+ no_cycle: true,
+ 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",
+ ))))),
+ }],
+ }
+ );
+
// PRIOR expressions are only valid within a CONNECT BY, and the the token
// `prior` is valid as an identifier anywhere else.
assert_eq!(
diff --git a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/tests/sqlparser_oracle.rs b/app/tests/sqlparser_oracle.rs
index 1c12f86..7fa0d31 100644
--- a/tmp/agent-patch-flux-pr-2185.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/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,27 @@ fn test_optimizer_hints() {
VALUES (ps.person_id, ps.first_name, ps.last_name, ps.title)",
);
}
+
+#[test]
+fn parse_connect_by_root_operator() {
+ let sql = "SELECT CONNECT_BY_ROOT name AS root_name FROM tbl1";
+ assert_eq!(
+ oracle().verified_only_select(sql).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"),
+ }
+ );
+
+ let sql = "SELECT CONNECT_BY_ROOT name FROM tbl2";
+ assert_eq!(
+ oracle().verified_only_select(sql).projection[0],
+ SelectItem::UnnamedExpr(Expr::Prefixed {
+ prefix: Ident::new("CONNECT_BY_ROOT"),
+ value: Box::new(Expr::Identifier(Ident::new("name"))),
+ })
+ );
+}