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
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"),
+ }
+ );
+}