agent_patch
runs/2026-02-28__18-27-22__gpt-5-3-codex/flux-pr-2011/flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/agent-logs/agent.patch
8814 bytes
diff --git a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/ast/mod.rs b/app/src/ast/mod.rs
index cd93785..b56174c 100644
--- a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/ast/mod.rs
+++ b/app/src/ast/mod.rs
@@ -4218,7 +4218,7 @@ pub enum Statement {
on: Box<Expr>,
/// Specifies the actions to perform when values match or do not match.
clauses: Vec<MergeClause>,
- // Specifies the output to save changes in MSSQL
+ // Specifies an OUTPUT/RETURNING clause
output: Option<OutputClause>,
},
/// ```sql
@@ -9108,23 +9108,41 @@ impl Display for MergeClause {
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct OutputClause {
+ pub output_keyword: OutputKeyword,
pub select_items: Vec<SelectItem>,
- pub into_table: SelectInto,
+ pub into_table: Option<SelectInto>,
+}
+
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum OutputKeyword {
+ Output,
+ Returning,
+}
+
+impl fmt::Display for OutputKeyword {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ OutputKeyword::Output => f.write_str("OUTPUT"),
+ OutputKeyword::Returning => f.write_str("RETURNING"),
+ }
+ }
}
impl fmt::Display for OutputClause {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let OutputClause {
+ output_keyword,
select_items,
into_table,
} = self;
- write!(
- f,
- "OUTPUT {} {}",
- display_comma_separated(select_items),
- into_table
- )
+ write!(f, "{output_keyword} {}", display_comma_separated(select_items))?;
+ if let Some(into_table) = into_table {
+ write!(f, " INTO {into_table}")?;
+ }
+ Ok(())
}
}
diff --git a/tmp/agent-patch-flux-pr-2011.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 2ef456b..967af85 100644
--- a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/ast/query.rs
+++ b/app/src/ast/query.rs
@@ -161,6 +161,7 @@ pub enum SetExpr {
Insert(Statement),
Update(Statement),
Delete(Statement),
+ Merge(Statement),
Table(Box<Table>),
}
@@ -188,6 +189,7 @@ impl fmt::Display for SetExpr {
SetExpr::Insert(v) => v.fmt(f),
SetExpr::Update(v) => v.fmt(f),
SetExpr::Delete(v) => v.fmt(f),
+ SetExpr::Merge(v) => v.fmt(f),
SetExpr::Table(t) => t.fmt(f),
SetExpr::SetOperation {
left,
diff --git a/tmp/agent-patch-flux-pr-2011.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 add6c39..3976175 100644
--- a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/ast/spans.rs
+++ b/app/src/ast/spans.rs
@@ -214,6 +214,7 @@ impl Spanned for SetExpr {
SetExpr::Table(_) => Span::empty(),
SetExpr::Update(statement) => statement.span(),
SetExpr::Delete(statement) => statement.span(),
+ SetExpr::Merge(statement) => statement.span(),
}
}
}
diff --git a/tmp/agent-patch-flux-pr-2011.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 c4c72e9..2eceb77 100644
--- a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/src/parser/mod.rs
+++ b/app/src/parser/mod.rs
@@ -11508,6 +11508,13 @@ impl<'a> Parser<'a> {
Ok(Box::new(SetExpr::Delete(self.parse_delete()?)))
}
+ /// Parse a MERGE statement, returning a `Box`ed SetExpr
+ ///
+ /// This is used to reduce the size of the stack frames in debug builds
+ fn parse_merge_setexpr_boxed(&mut self) -> Result<Box<SetExpr>, ParserError> {
+ Ok(Box::new(SetExpr::Merge(self.parse_merge()?)))
+ }
+
pub fn parse_delete(&mut self) -> Result<Statement, ParserError> {
let (tables, with_from_keyword) = if !self.parse_keyword(Keyword::FROM) {
// `FROM` keyword is optional in BigQuery SQL.
@@ -11719,6 +11726,20 @@ impl<'a> Parser<'a> {
pipe_operators: vec![],
}
.into())
+ } else if self.parse_keyword(Keyword::MERGE) {
+ Ok(Query {
+ with,
+ body: self.parse_merge_setexpr_boxed()?,
+ limit_clause: None,
+ order_by: None,
+ fetch: None,
+ locks: vec![],
+ for_clause: None,
+ settings: None,
+ format_clause: None,
+ pipe_operators: vec![],
+ }
+ .into())
} else {
let body = self.parse_query_body(self.dialect.prec_unknown())?;
@@ -16572,12 +16593,22 @@ impl<'a> Parser<'a> {
}
fn parse_output(&mut self) -> Result<OutputClause, ParserError> {
- self.expect_keyword_is(Keyword::OUTPUT)?;
+ let output_keyword = if self.parse_keyword(Keyword::OUTPUT) {
+ OutputKeyword::Output
+ } else if self.parse_keyword(Keyword::RETURNING) {
+ OutputKeyword::Returning
+ } else {
+ return self.expected("OUTPUT or RETURNING", self.peek_token());
+ };
let select_items = self.parse_projection()?;
- self.expect_keyword_is(Keyword::INTO)?;
- let into_table = self.parse_select_into()?;
+ let into_table = if self.parse_keyword(Keyword::INTO) {
+ Some(self.parse_select_into()?)
+ } else {
+ None
+ };
Ok(OutputClause {
+ output_keyword,
select_items,
into_table,
})
@@ -16609,7 +16640,8 @@ impl<'a> Parser<'a> {
self.expect_keyword_is(Keyword::ON)?;
let on = self.parse_expr()?;
let clauses = self.parse_merge_clauses()?;
- let output = if self.peek_keyword(Keyword::OUTPUT) {
+ let output = if self.peek_keyword(Keyword::OUTPUT) || self.peek_keyword(Keyword::RETURNING)
+ {
Some(self.parse_output()?)
} else {
None
diff --git a/tmp/agent-patch-flux-pr-2011.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 54ad173..9a9ada3 100644
--- a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-3-codex/app/tests/sqlparser_common.rs
+++ b/app/tests/sqlparser_common.rs
@@ -7656,6 +7656,16 @@ fn parse_cte_in_data_modification_statements() {
}
other => panic!("Expected: INSERT, got: {other:?}"),
}
+
+ match verified_stmt(
+ "WITH s AS (SELECT 1) MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN DELETE",
+ ) {
+ Statement::Query(query) => {
+ assert_eq!(query.with.unwrap().to_string(), "WITH s AS (SELECT 1)");
+ assert!(matches!(*query.body, SetExpr::Merge(_)));
+ }
+ other => panic!("Expected: MERGE, got: {other:?}"),
+ }
}
#[test]
@@ -9915,6 +9925,42 @@ fn test_merge_with_output() {
verified_stmt(sql);
}
+#[test]
+fn test_merge_with_output_without_into() {
+ let sql = "MERGE INTO target_table USING source_table \
+ ON target_table.id = source_table.oooid \
+ WHEN MATCHED THEN \
+ UPDATE SET target_table.description = source_table.description \
+ OUTPUT inserted.*";
+
+ let stmt = verified_stmt(sql);
+ if let Statement::Merge { output, .. } = stmt {
+ let output = output.expect("expected output clause");
+ assert_eq!(output.output_keyword, OutputKeyword::Output);
+ assert!(output.into_table.is_none());
+ } else {
+ panic!("Expected MERGE statement");
+ }
+}
+
+#[test]
+fn test_merge_with_returning() {
+ let sql = "MERGE INTO target_table USING source_table \
+ ON target_table.id = source_table.oooid \
+ WHEN MATCHED THEN \
+ UPDATE SET target_table.description = source_table.description \
+ RETURNING target_table.id";
+
+ let stmt = verified_stmt(sql);
+ if let Statement::Merge { output, .. } = stmt {
+ let output = output.expect("expected output/returning clause");
+ assert_eq!(output.output_keyword, OutputKeyword::Returning);
+ assert!(output.into_table.is_none());
+ } else {
+ panic!("Expected MERGE statement");
+ }
+}
+
#[test]
fn test_merge_into_using_table() {
let sql = "MERGE INTO target_table USING source_table \