From 280af0212048b5884b1c1f0a0a75406c68a13ffd Mon Sep 17 00:00:00 2001 From: dienaso <89768641@qq.com> Date: Sat, 19 Sep 2026 10:55:37 +0800 Subject: [PATCH] fix(sql): keep merge update set line in statement continuation --- .../__tests__/sql/sqlStatementRanges.spec.ts | 39 +++++++++++++++++++ .../desktop/src/lib/sql/sqlStatementRanges.ts | 7 +++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts index d5a0553da6..32b7fbd564 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts @@ -1472,6 +1472,45 @@ FROM orders;`; } }); + it("keeps an Oracle MERGE together when each action starts its own line (#9516)", () => { + const merge = `MERGE INTO bom_template t USING (SELECT id, no FROM stage_bom) s ON (t.id = s.id) +WHEN MATCHED THEN +UPDATE SET + t.no = s.no +WHERE + t.no IS NULL +WHEN NOT MATCHED THEN +INSERT (id, no) +VALUES (s.id, s.no)`; + for (const needle of ["MERGE INTO", "UPDATE SET", "WHERE", "INSERT (id, no)", "VALUES (s.id, s.no)"]) { + expect(statementRangeAtCursor(merge, indexOf(merge, needle), "oracle")?.sql.trim()).toBe(merge); + } + expect(rangeSqlTexts(executableStatementRanges(merge, "oracle"))).toEqual([merge]); + }); + + it("keeps an Oracle MERGE delete action that starts its own line together", () => { + const merge = `MERGE INTO bom_template t USING (SELECT id FROM stage_bom) s ON (t.id = s.id) +WHEN MATCHED THEN +DELETE WHERE t.no IS NULL`; + expect(rangeSqlTexts(executableStatementRanges(merge, "oracle"))).toEqual([merge]); + }); + + it("keeps an Oracle MERGE together when the UPDATE action's SET starts its own line", () => { + const merge = `MERGE INTO bom_template t USING (SELECT id, no FROM stage_bom) s ON (t.id = s.id) +WHEN MATCHED THEN +UPDATE +SET t.no = s.no`; + const update = "UPDATE bom_template\nSET no = 'x'"; + expect(rangeSqlTexts(executableStatementRanges(merge, "oracle"))).toEqual([merge]); + expect(rangeSqlTexts(executableStatementRanges(`${merge};\n\n${update};`, "oracle"))).toEqual([merge, update]); + }); + + it("still splits a standalone UPDATE that follows a terminated MERGE", () => { + const merge = "MERGE INTO bom_template t USING (SELECT id FROM stage_bom) s ON (t.id = s.id)\nWHEN MATCHED THEN UPDATE SET t.no = s.no"; + const update = "UPDATE bom_template SET no = 'x'"; + expect(rangeSqlTexts(executableStatementRanges(`${merge};\n\n${update};`, "oracle"))).toEqual([merge, update]); + }); + it("returns the full SAP HANA DO block for cursors inside nested statements", () => { const range = statementRangeAtCursor(sapHanaDoBlockFixture, indexOf(sapHanaDoBlockFixture, "Result"), "saphana"); diff --git a/apps/desktop/src/lib/sql/sqlStatementRanges.ts b/apps/desktop/src/lib/sql/sqlStatementRanges.ts index b25c3d43f3..c97e3fa452 100644 --- a/apps/desktop/src/lib/sql/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sql/sqlStatementRanges.ts @@ -1106,8 +1106,13 @@ function isMysqlAlterTableTruncatePartitionContinuation(sql: string, statementFr } function isMergeActionContinuation(sql: string, statementFrom: number, lineStartFrom: number, keyword: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): boolean { - if (keyword !== "INSERT" || !startsWithSqlWords(sql, statementFrom, ["MERGE"], databaseType, parameterOptions)) return false; + // Oracle (and friends) allow each MERGE action on its own line after + // `WHEN ... MATCHED THEN`, e.g. `UPDATE SET ...` (#9516); only INSERT was + // recognized, so UPDATE/DELETE action lines split the statement in two. + if (keyword !== "INSERT" && keyword !== "UPDATE" && keyword !== "DELETE" && keyword !== "SET") return false; + if (!startsWithSqlWords(sql, statementFrom, ["MERGE"], databaseType, parameterOptions)) return false; const words = topLevelWordsBefore(sql, statementFrom, lineStartFrom, 5, databaseType, parameterOptions); + if (keyword === "SET") return words[words.length - 1] === "UPDATE" && words.includes("THEN") && words.includes("MATCHED"); return words[words.length - 1] === "THEN" && words.includes("WHEN") && words.includes("MATCHED"); }