From 28b795e7a989a8217e0822a203e80bbfa44f99ab Mon Sep 17 00:00:00 2001 From: zipg Date: Tue, 15 Sep 2026 18:09:10 +0800 Subject: [PATCH] fix(sql): keep GaussDB merge actions in statement range --- .../__tests__/sql/sqlStatementRanges.spec.ts | 36 +++++++++++++++++++ .../desktop/src/lib/sql/sqlStatementRanges.ts | 10 ++++++ 2 files changed, 46 insertions(+) diff --git a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts index a0524bf715..2926feb33b 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts @@ -180,6 +180,36 @@ END; SELECT 1 AS after_procedure; SELECT 2 AS final_statement;`; +const gaussDbMergeWithRecursiveUsing = `MERGE INTO TP_PIM_USER_RES a +USING ( + SELECT * FROM ( + WITH RECURSIVE src AS ( + SELECT userid, rptcode, appcde + FROM sjbase.ta_sys_rpt_aduser + WHERE appcde IN ('OSS', '1104') + ), + grp_oss AS ( + SELECT s.userid, r.grp_id FROM src s + JOIN SJOSSRPT.TP_OSS_CORE_RPT r ON r.code = s.rptcode + WHERE s.appcde = 'OSS' + UNION ALL + SELECT t.userid, p.grp_id FROM grp_oss t + JOIN SJOSSRPT.TP_OSS_CORE_GRP c ON c.grp_id = t.grp_id + JOIN SJOSSRPT.TP_OSS_CORE_GRP p ON p.grp_id = c.up_grpid + ) + SELECT DISTINCT u.userid, 'RES_OSS_RPT' AS restyp_code, u.res_id, u.appcde + FROM ( + SELECT s.userid, s.rptcode AS res_id, s.appcde FROM src s + UNION + SELECT g.userid, g.grp_id AS res_id, 'OSS' AS appcde FROM grp_oss g + ) u + ) t +) b +ON (a.user_id = b.userid AND a.restyp_code = b.restyp_code AND a.res_id = b.res_id AND a.appcde = b.appcde) +WHEN NOT MATCHED THEN + INSERT (user_id, restyp_code, res_id, appcde) + VALUES (b.userid, b.restyp_code, b.res_id, b.appcde);`; + const xuguProgrammableObjectFixtures = [ `CREATE OR REPLACE PROCEDURE dbx_xugu_procedure AS v_value INTEGER; @@ -1388,6 +1418,12 @@ FROM orders;`; expect(statementRangeAtCursor(gaussDbIssue4573Script, indexOf(gaussDbIssue4573Script, "count(*)"), "gaussdb")?.sql.trim()).toBe(expected); }); + it("keeps a GaussDB MERGE with a recursive USING query together", () => { + for (const needle of ["MERGE INTO", "WITH RECURSIVE", "WHEN NOT MATCHED", "VALUES (b.userid)"]) { + expect(statementRangeAtCursor(gaussDbMergeWithRecursiveUsing, indexOf(gaussDbMergeWithRecursiveUsing, needle), "gaussdb")?.sql.trim()).toBe(gaussDbMergeWithRecursiveUsing.slice(0, -1)); + } + }); + 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 6c85c5404a..ac1ca16763 100644 --- a/apps/desktop/src/lib/sql/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sql/sqlStatementRanges.ts @@ -803,6 +803,10 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d continue; } + if (currentBodyKeyword === "MERGE" && isMergeActionContinuation(sql, statement.from, lineStart.from, lineStart.keyword, databaseType, parameterOptions)) { + continue; + } + if (currentBodyKeyword === "ALTER" && isClickHouseAlterTableUpdateContinuation(sql, boundaries[boundaries.length - 1].from, lineStart.from, lineStart.keyword, databaseType)) { // ClickHouse mutations use UPDATE as the first ALTER TABLE action, not as // a standalone statement. Keep this dialect-specific to preserve soft boundaries elsewhere. @@ -1068,6 +1072,12 @@ function isMysqlAlterTableTruncatePartitionContinuation(sql: string, statementFr return nextSqlWord(sql, lineStartFrom + keyword.length, databaseType) === "PARTITION"; } +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; + const words = topLevelWordsBefore(sql, statementFrom, lineStartFrom, 5, databaseType, parameterOptions); + return words[words.length - 1] === "THEN" && words.includes("WHEN") && words.includes("MATCHED"); +} + function startsWithMysqlCreateTable(sql: string, statementFrom: number): boolean { const text = sql.slice(statementFrom, statementFrom + 256); return /^CREATE\s+(?:TEMPORARY\s+)?TABLE\b/i.test(text);