agent_patch
runs/2026-02-21__03-36-16__gpt-5-1-codex-mini/flux-pr-2011/flux-pr-2011.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/agent-logs/agent.patch
10146 bytes
diff --git a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/src/ast/mod.rs b/app/src/ast/mod.rs
index cd93785..411c344 100644
--- a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-21__03-36-16__gpt-5-1-codex-mini/app/src/ast/mod.rs
+++ b/app/src/ast/mod.rs
@@ -4218,8 +4218,8 @@ 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
- output: Option<OutputClause>,
+ /// Specifies the output or returning clause to save changes.
+ output: Option<MergeOutputClause>,
},
/// ```sql
/// CACHE [ FLAG ] TABLE <table_name> [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ <query> ]
@@ -9099,32 +9099,40 @@ impl Display for MergeClause {
}
}
-/// A Output Clause in the end of a 'MERGE' Statement
-///
-/// Example:
-/// OUTPUT $action, deleted.* INTO dbo.temp_products;
-/// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql)
+/// The clause following a `MERGE` statement that can emit rows like
+/// `OUTPUT ... INTO ...` (SQL Server) or `RETURNING ...` (PostgreSQL).
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
-pub struct OutputClause {
- pub select_items: Vec<SelectItem>,
- pub into_table: SelectInto,
+pub enum MergeOutputClause {
+ /// SQL Server-style OUTPUT clause with optional INTO.
+ Output {
+ select_items: Vec<SelectItem>,
+ into_table: Option<SelectInto>,
+ },
+ /// PostgreSQL-style RETURNING clause.
+ Returning {
+ select_items: Vec<SelectItem>,
+ },
}
-impl fmt::Display for OutputClause {
+impl fmt::Display for MergeOutputClause {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let OutputClause {
- select_items,
- into_table,
- } = self;
-
- write!(
- f,
- "OUTPUT {} {}",
- display_comma_separated(select_items),
- into_table
- )
+ match self {
+ MergeOutputClause::Output {
+ select_items,
+ into_table,
+ } => {
+ write!(f, "OUTPUT {}", display_comma_separated(select_items))?;
+ if let Some(into_table) = into_table {
+ write!(f, " {}", into_table)?;
+ }
+ Ok(())
+ }
+ MergeOutputClause::Returning { select_items } => {
+ write!(f, "RETURNING {}", display_comma_separated(select_items))
+ }
+ }
}
}
diff --git a/tmp/agent-patch-flux-pr-2011.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 2ef456b..967af85 100644
--- a/tmp/agent-patch-flux-pr-2011.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
@@ -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-21__03-36-16__gpt-5-1-codex-mini/app/src/parser/mod.rs b/app/src/parser/mod.rs
index c4c72e9..753d5e0 100644
--- a/tmp/agent-patch-flux-pr-2011.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
@@ -622,7 +622,10 @@ impl<'a> Parser<'a> {
Keyword::DEALLOCATE => self.parse_deallocate(),
Keyword::EXECUTE | Keyword::EXEC => self.parse_execute(),
Keyword::PREPARE => self.parse_prepare(),
- Keyword::MERGE => self.parse_merge(),
+ Keyword::MERGE => {
+ self.prev_token();
+ self.parse_merge()
+ }
// `LISTEN`, `UNLISTEN` and `NOTIFY` are Postgres-specific
// syntaxes. They are used for Postgres statement.
Keyword::LISTEN if self.dialect.supports_listen_notify() => self.parse_listen(),
@@ -12200,6 +12203,8 @@ impl<'a> Parser<'a> {
} else if self.parse_keyword(Keyword::VALUES) {
let is_mysql = dialect_of!(self is MySqlDialect);
SetExpr::Values(self.parse_values(is_mysql)?)
+ } else if self.peek_keyword(Keyword::MERGE) {
+ SetExpr::Merge(self.parse_merge()?)
} else if self.parse_keyword(Keyword::TABLE) {
SetExpr::Table(Box::new(self.parse_as_table()?))
} else {
@@ -16571,18 +16576,6 @@ impl<'a> Parser<'a> {
Ok(clauses)
}
- fn parse_output(&mut self) -> Result<OutputClause, ParserError> {
- self.expect_keyword_is(Keyword::OUTPUT)?;
- let select_items = self.parse_projection()?;
- self.expect_keyword_is(Keyword::INTO)?;
- let into_table = self.parse_select_into()?;
-
- Ok(OutputClause {
- select_items,
- into_table,
- })
- }
-
fn parse_select_into(&mut self) -> Result<SelectInto, ParserError> {
let temporary = self
.parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY])
@@ -16600,6 +16593,8 @@ impl<'a> Parser<'a> {
}
pub fn parse_merge(&mut self) -> Result<Statement, ParserError> {
+ self.expect_keyword(Keyword::MERGE)?;
+
let into = self.parse_keyword(Keyword::INTO);
let table = self.parse_table_factor()?;
@@ -16609,8 +16604,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) {
- Some(self.parse_output()?)
+ let output = if self.peek_keyword(Keyword::OUTPUT) || self.peek_keyword(Keyword::RETURNING) {
+ Some(self.parse_merge_output_clause()?)
} else {
None
};
@@ -16625,6 +16620,25 @@ impl<'a> Parser<'a> {
})
}
+ fn parse_merge_output_clause(&mut self) -> Result<MergeOutputClause, ParserError> {
+ if self.parse_keyword(Keyword::OUTPUT) {
+ let select_items = self.parse_projection()?;
+ let into_table = if self.parse_keyword(Keyword::INTO) {
+ Some(self.parse_select_into()?)
+ } else {
+ None
+ };
+ Ok(MergeOutputClause::Output {
+ select_items,
+ into_table,
+ })
+ } else {
+ self.expect_keyword_is(Keyword::RETURNING)?;
+ let select_items = self.parse_projection()?;
+ Ok(MergeOutputClause::Returning { select_items })
+ }
+ }
+
fn parse_pragma_value(&mut self) -> Result<Value, ParserError> {
match self.parse_value()?.value {
v @ Value::SingleQuotedString(_) => Ok(v),
diff --git a/tmp/agent-patch-flux-pr-2011.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 54ad173..588b4ab 100644
--- a/tmp/agent-patch-flux-pr-2011.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
@@ -9915,6 +9915,60 @@ fn test_merge_with_output() {
verified_stmt(sql);
}
+#[test]
+fn test_merge_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.id";
+
+ match verified_stmt(sql) {
+ Statement::Merge {
+ output: Some(MergeOutputClause::Output {
+ select_items,
+ into_table,
+ }),
+ ..
+ } => {
+ assert!(into_table.is_none());
+ assert_eq!(
+ select_items,
+ vec![SelectItem::UnnamedExpr(Expr::CompoundIdentifier(vec![
+ Ident::new("inserted"),
+ Ident::new("id"),
+ ]))]
+ );
+ }
+ _ => unreachable!(),
+ }
+}
+
+#[test]
+fn test_merge_returning_clause() {
+ 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";
+
+ match verified_stmt(sql) {
+ Statement::Merge {
+ output: Some(MergeOutputClause::Returning { select_items }),
+ ..
+ } => {
+ assert_eq!(
+ select_items,
+ vec![SelectItem::UnnamedExpr(Expr::CompoundIdentifier(vec![
+ Ident::new("target_table"),
+ Ident::new("id"),
+ ]))]
+ );
+ }
+ _ => unreachable!(),
+ }
+}
+
#[test]
fn test_merge_into_using_table() {
let sql = "MERGE INTO target_table USING source_table \
@@ -9927,6 +9981,18 @@ fn test_merge_into_using_table() {
verified_stmt(sql);
}
+#[test]
+fn test_merge_as_query_body() {
+ let sql = "WITH cte AS (SELECT 1) MERGE INTO target_table AS tgt \
+ USING source_table AS src \
+ ON tgt.id = src.id \
+ WHEN MATCHED THEN UPDATE SET tgt.description = src.description";
+
+ let query = verified_query(sql);
+ assert!(query.with.is_some());
+ assert_matches!(query.body.as_ref(), SetExpr::Merge(_));
+}
+
#[test]
fn test_merge_with_delimiter() {
let sql = "MERGE INTO target_table USING source_table \