STET

agent_patch

runs/2026-02-28__18-27-22__gpt-5-4/flux-pr-2011/flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-4/agent-logs/agent.patch

10484 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/src/ast/mod.rs b/app/src/ast/mod.rs
index cd93785..791b9e1 100644
--- a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-4/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 the rows returned from the merge operation.
         output: Option<OutputClause>,
     },
     /// ```sql
@@ -9099,32 +9099,51 @@ impl Display for MergeClause {
     }
 }
 
-/// A Output Clause in the end of a 'MERGE' Statement
+/// A MERGE result clause.
 ///
 /// Example:
 /// OUTPUT $action, deleted.* INTO dbo.temp_products;
+/// RETURNING id, description;
 /// [mssql](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql)
 #[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 mode: OutputMode,
     pub select_items: Vec<SelectItem>,
-    pub into_table: SelectInto,
+    pub into_table: Option<SelectInto>,
 }
 
 impl fmt::Display for OutputClause {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         let OutputClause {
+            mode,
             select_items,
             into_table,
         } = self;
 
-        write!(
-            f,
-            "OUTPUT {} {}",
-            display_comma_separated(select_items),
-            into_table
-        )
+        write!(f, "{mode} {}", display_comma_separated(select_items))?;
+        if let Some(into_table) = into_table {
+            write!(f, " {into_table}")?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum OutputMode {
+    Output,
+    Returning,
+}
+
+impl fmt::Display for OutputMode {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            OutputMode::Output => f.write_str("OUTPUT"),
+            OutputMode::Returning => f.write_str("RETURNING"),
+        }
     }
 }
 
diff --git a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-4/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-4/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-4/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-4/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-4/app/src/parser/mod.rs b/app/src/parser/mod.rs
index c4c72e9..b63e7a0 100644
--- a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-4/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 mode = if self.parse_keyword(Keyword::OUTPUT) {
+            OutputMode::Output
+        } else if self.parse_keyword(Keyword::RETURNING) {
+            OutputMode::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 {
+            mode,
             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_one_of_keywords(&[Keyword::OUTPUT, Keyword::RETURNING]).is_some()
+        {
             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-4/app/tests/sqlparser_common.rs b/app/tests/sqlparser_common.rs
index 54ad173..38b7676 100644
--- a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-4/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 x AS (SELECT 42) MERGE INTO t USING x ON t.id = x.id WHEN MATCHED THEN DELETE",
+    ) {
+        Statement::Query(query) => {
+            assert_eq!(query.with.unwrap().to_string(), "WITH x AS (SELECT 42)");
+            assert!(matches!(*query.body, SetExpr::Merge(_)));
+        }
+        other => panic!("Expected: MERGE, got: {other:?}"),
+    }
 }
 
 #[test]
@@ -9915,6 +9925,64 @@ 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.*";
+
+    match verified_stmt(sql) {
+        Statement::Merge {
+            output: Some(OutputClause {
+                mode,
+                select_items,
+                into_table,
+            }),
+            ..
+        } => {
+            assert_eq!(mode, OutputMode::Output);
+            assert_eq!(
+                select_items,
+                vec![SelectItem::QualifiedWildcard(
+                    SelectItemQualifiedWildcardKind::ObjectName(ObjectName::from(vec![
+                        Ident::new("inserted")
+                    ])),
+                    WildcardAdditionalOptions::default()
+                )]
+            );
+            assert_eq!(into_table, None);
+        }
+        other => panic!("Expected MERGE with OUTPUT, got: {other:?}"),
+    }
+}
+
+#[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, source_table.description";
+
+    match verified_stmt(sql) {
+        Statement::Merge {
+            output: Some(OutputClause {
+                mode,
+                select_items,
+                into_table,
+            }),
+            ..
+        } => {
+            assert_eq!(mode, OutputMode::Returning);
+            assert_eq!(select_items.len(), 2);
+            assert_eq!(into_table, None);
+        }
+        other => panic!("Expected MERGE with RETURNING, got: {other:?}"),
+    }
+}
+
 #[test]
 fn test_merge_into_using_table() {
     let sql = "MERGE INTO target_table USING source_table \
diff --git a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/tests/sqlparser_mssql.rs b/app/tests/sqlparser_mssql.rs
index a1e05d0..93cf87d 100644
--- a/tmp/agent-patch-flux-pr-2011.1-of-1.2026-02-28__18-27-22__gpt-5-4/app/tests/sqlparser_mssql.rs
+++ b/app/tests/sqlparser_mssql.rs
@@ -2373,6 +2373,16 @@ fn parse_mssql_merge_with_output() {
     ms_and_generic().verified_stmt(stmt);
 }
 
+#[test]
+fn parse_mssql_merge_with_output_without_into() {
+    let stmt = "MERGE dso.products AS t \
+        USING dsi.products AS \
+        s ON s.ProductID = t.ProductID \
+        WHEN MATCHED THEN UPDATE SET t.ProductName = s.ProductName \
+        OUTPUT $action, deleted.ProductID";
+    ms_and_generic().verified_stmt(stmt);
+}
+
 #[test]
 fn parse_create_trigger() {
     let create_trigger = "\