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
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!(),