diff --git a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts index 5fd711bc59..201dc7ace0 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts @@ -89,6 +89,17 @@ BEGIN SELECT 1 + 2 INTO PRE_TRD_DATE FROM DUAL; END;`; +const oracleConsecutiveNestedBlocks = `CREATE OR REPLACE PROCEDURE dbx_consecutive_blocks AS +BEGIN + BEGIN + NULL; + END; + BEGIN + NULL; + END; + NULL; +END;`; + const gaussDbNestedProcedure = `CREATE OR REPLACE PROCEDURE public.dbx_issue_4318() AS BEGIN @@ -263,6 +274,18 @@ BEGIN END; SELECT 2;`; +const mysqlRoutineWithConsecutiveBlocksFixture = `CREATE PROCEDURE p_consecutive_blocks() +BEGIN + BEGIN + SELECT 1; + END; + BEGIN + SELECT 2; + END; + SELECT 3; +END; +SELECT 4;`; + const mysqlDelimitedRoutineFixture = `DELIMITER // CREATE PROCEDURE sp_insert_random_users(IN p_count INT) BEGIN @@ -285,6 +308,17 @@ BEGIN END; SELECT 2 FROM DUMMY;`; +const sapHanaDoBlockWithConsecutiveBlocksFixture = `DO +BEGIN + BEGIN + SELECT 1 AS "First" FROM DUMMY; + END; + BEGIN + SELECT 2 AS "Second" FROM DUMMY; + END; +END; +SELECT 3 FROM DUMMY;`; + describe("splitSqlStatementRanges", () => { it("splits multiple top-level statements", () => { const sql = "SELECT 1;\nSELECT 2;\nSELECT 3;"; @@ -379,6 +413,10 @@ describe("splitSqlStatementRanges", () => { expect(rangeSqlTexts(splitSqlStatementRanges(mysqlRoutineWithNestedCaseBlockFixture, "mysql"))).toEqual([mysqlRoutineWithNestedCaseBlockFixture.slice(0, mysqlRoutineWithNestedCaseBlockFixture.indexOf("\nSELECT 2;")).replace(/;$/, "").trim(), "SELECT 2"]); }); + it("keeps consecutive nested MySQL blocks inside their procedure", () => { + expect(rangeSqlTexts(splitSqlStatementRanges(mysqlRoutineWithConsecutiveBlocksFixture, "mysql"))).toEqual([mysqlRoutineWithConsecutiveBlocksFixture.slice(0, mysqlRoutineWithConsecutiveBlocksFixture.indexOf("\nSELECT 4;")).replace(/;$/, "").trim(), "SELECT 4"]); + }); + it("does not merge regular MySQL transaction statements as routine blocks", () => { const sql = "BEGIN; INSERT INTO t VALUES (1); COMMIT;"; expect(rangeSqlTexts(splitSqlStatementRanges(sql, "mysql"))).toEqual(["BEGIN", "INSERT INTO t VALUES (1)", "COMMIT"]); @@ -406,6 +444,32 @@ describe("splitSqlStatementRanges", () => { expect(rangeSqlTexts(splitSqlStatementRanges(oracleIssue2405PlSql, "oracle"))).toEqual([oracleIssue2405PlSql]); }); + it("keeps consecutive nested Oracle blocks inside their procedure", () => { + expect(rangeSqlTexts(splitSqlStatementRanges(`${oracleConsecutiveNestedBlocks}\nSELECT 1;`, "oracle"))).toEqual([oracleConsecutiveNestedBlocks, "SELECT 1"]); + }); + + it.each([ + ["BEGIN", "BEGIN\n NULL;\n END;"], + ["IF", "IF 1 = 1 THEN\n NULL;\n END IF;"], + ["LOOP", "LOOP\n EXIT;\n END LOOP;"], + ["CASE", "CASE WHEN 1 = 1 THEN\n NULL;\n END CASE;"], + ])("treats %s after a semicolon as a new Oracle scope", (_starter, scope) => { + const procedure = `CREATE OR REPLACE PROCEDURE dbx_delimited_scope AS +BEGIN + BEGIN + NULL; + END; + ${scope} + NULL; +END;`; + + expect(rangeSqlTexts(splitSqlStatementRanges(`${procedure}\nSELECT 1;`, "oracle"))).toEqual([procedure, "SELECT 1"]); + }); + + it("keeps BEGIN TRANSACTION outside Oracle PL/SQL block handling", () => { + expect(rangeSqlTexts(splitSqlStatementRanges("BEGIN TRANSACTION;\nSELECT 1;", "oracle"))).toEqual(["BEGIN TRANSACTION", "SELECT 1"]); + }); + it("splits large Dameng package bodies without repeated prefix parsing", () => { const body = Array.from({ length: 3000 }, (_, index) => ` v_value := v_value + ${index % 10};`).join("\n"); const packageBody = `CREATE OR REPLACE PACKAGE BODY app.big_pkg AS @@ -496,6 +560,10 @@ END;`; expect(ranges[0].sql).toContain('SELECT 1 AS "Result" FROM DUMMY;'); expect(ranges[0].sql).toContain("END;"); }); + + it("keeps consecutive nested SAP HANA blocks inside their DO block", () => { + expect(rangeSqlTexts(splitSqlStatementRanges(sapHanaDoBlockWithConsecutiveBlocksFixture, "saphana"))).toEqual([sapHanaDoBlockWithConsecutiveBlocksFixture.slice(0, sapHanaDoBlockWithConsecutiveBlocksFixture.indexOf("\nSELECT 3")), "SELECT 3 FROM DUMMY"]); + }); }); describe("statementRangeAtCursor", () => { diff --git a/apps/desktop/src/lib/sql/sqlStatementRanges.ts b/apps/desktop/src/lib/sql/sqlStatementRanges.ts index de92485b46..8c052ab1bd 100644 --- a/apps/desktop/src/lib/sql/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sql/sqlStatementRanges.ts @@ -1952,6 +1952,7 @@ function oraclePlSqlTokens(sql: string): OraclePlSqlToken[] { function previousWordToken(tokens: Array<{ kind: "word" | "semicolon"; value: string }>, index: number): string | null { for (let i = index - 1; i >= 0; i -= 1) { + if (tokens[i].kind === "semicolon") return null; if (tokens[i].kind === "word") return tokens[i].value; } return null; @@ -1959,6 +1960,7 @@ function previousWordToken(tokens: Array<{ kind: "word" | "semicolon"; value: st function nextWordToken(tokens: Array<{ kind: "word" | "semicolon"; value: string }>, index: number): string | null { for (let i = index + 1; i < tokens.length; i += 1) { + if (tokens[i].kind === "semicolon") return null; if (tokens[i].kind === "word") return tokens[i].value; } return null;