diff --git a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts index b5cded8c41..21a441eefc 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts @@ -583,6 +583,19 @@ describe("splitSqlStatementRanges", () => { expect(rangeSqlTexts(splitSqlStatementRanges(oracleIssue2405PlSql, "oracle"))).toEqual([oracleIssue2405PlSql]); }); + it("keeps Oracle anonymous blocks with local PROCEDURE/FUNCTION declarations together (#9634)", () => { + const cases = [ + "DECLARE\nPROCEDURE local_proc IS\nBEGIN\n NULL;\nEND;\n\nBEGIN\nlocal_proc;\nEND;", + "DECLARE\n v NUMBER;\n PROCEDURE p1 IS BEGIN NULL; END;\n FUNCTION f1 RETURN NUMBER IS BEGIN RETURN 1; END f1;\nBEGIN\n p1;\n v := f1;\nEND;", + "DECLARE\n PROCEDURE fwd(x NUMBER);\n PROCEDURE fwd(x NUMBER) IS BEGIN NULL; END;\nBEGIN\n fwd(1);\nEND;", + "DECLARE\n PROCEDURE outer_p IS\n PROCEDURE inner_p IS BEGIN NULL; END;\n BEGIN\n inner_p;\n END;\nBEGIN\n outer_p;\nEND;", + ]; + for (const sql of cases) { + expect(rangeSqlTexts(splitSqlStatementRanges(sql, "oracle"))).toEqual([sql]); + } + expect(rangeSqlTexts(splitSqlStatementRanges(`${cases[0]}\nSELECT 1 FROM dual;`, "oracle"))).toEqual([cases[0], "SELECT 1 FROM dual"]); + }); + it("keeps ArgoDB PL/SQL procedure bodies together (frontend splitter, mirrors backend argo_split tests)", () => { expect(rangeSqlTexts(splitSqlStatementRanges(argoProcedureFixture, "argo"))).toEqual([argoProcedureFixture]); expect(hasMultipleExecutionTargets(argoProcedureFixture, "argo")).toBe(false); diff --git a/apps/desktop/src/lib/sql/sqlStatementRanges.ts b/apps/desktop/src/lib/sql/sqlStatementRanges.ts index 5add59c837..74995760a9 100644 --- a/apps/desktop/src/lib/sql/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sql/sqlStatementRanges.ts @@ -1988,6 +1988,7 @@ function oraclePlSqlBlockEnd(sql: string): number | null { for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index]; if (token.kind === "semicolon") { + if (stack[stack.length - 1] === "ROUTINE_HEADER") stack.pop(); const complete = objectKind !== null ? stack.length === 0 : sawBegin && stack.length === 0; if (complete) return token.to; continue; @@ -1998,12 +1999,21 @@ function oraclePlSqlBlockEnd(sql: string): number | null { if (stack[stack.length - 1] !== "DECLARATION") stack.push("DECLARATION"); continue; } + // Local PROCEDURE/FUNCTION in a DECLARE section owns its own BEGIN..END. + if ((token.value === "PROCEDURE" || token.value === "FUNCTION") && (stack[stack.length - 1] === "DECLARATION" || stack[stack.length - 1] === "ROUTINE")) { + stack.push("ROUTINE_HEADER"); + continue; + } + if ((token.value === "IS" || token.value === "AS") && stack[stack.length - 1] === "ROUTINE_HEADER") { + stack[stack.length - 1] = "ROUTINE"; + continue; + } if (token.value === "BEGIN") { if (tokens[index - 1]?.kind === "word" && tokens[index - 1]?.value === "TRANSACTION") continue; const previous = previousWordToken(tokens, index); if (previous === "END") continue; sawBegin = true; - if (stack[stack.length - 1] === "DECLARATION") stack[stack.length - 1] = "BLOCK"; + if (stack[stack.length - 1] === "DECLARATION" || stack[stack.length - 1] === "ROUTINE") stack[stack.length - 1] = "BLOCK"; else stack.push("BLOCK"); continue; } diff --git a/crates/dbx-sql/src/sql.rs b/crates/dbx-sql/src/sql.rs index 96e8c521ce..d57e07196b 100644 --- a/crates/dbx-sql/src/sql.rs +++ b/crates/dbx-sql/src/sql.rs @@ -2535,11 +2535,30 @@ impl OraclePlSqlBlock { for (index, token) in self.tokens.iter().enumerate() { if token.is_semicolon() { + if matches!(scopes.last(), Some(OraclePlSqlScope::RoutineHeader)) { + scopes.pop(); + } continue; } - if token.is_word("BEGIN") { - scopes.push(OraclePlSqlScope::Block); + if token.is_word("DECLARE") { + if !matches!(scopes.last(), Some(OraclePlSqlScope::Declaration)) { + scopes.push(OraclePlSqlScope::Declaration); + } + } else if token.is_any_word(&["PROCEDURE", "FUNCTION"]) + && matches!(scopes.last(), Some(OraclePlSqlScope::Declaration | OraclePlSqlScope::Routine)) + { + scopes.push(OraclePlSqlScope::RoutineHeader); + } else if token.is_any_word(&["IS", "AS"]) && matches!(scopes.last(), Some(OraclePlSqlScope::RoutineHeader)) + { + *scopes.last_mut().unwrap() = OraclePlSqlScope::Routine; + } else if token.is_word("BEGIN") { + // A local routine's BEGIN belongs to that routine, not to the outer block. + if matches!(scopes.last(), Some(OraclePlSqlScope::Declaration | OraclePlSqlScope::Routine)) { + *scopes.last_mut().unwrap() = OraclePlSqlScope::Block; + } else { + scopes.push(OraclePlSqlScope::Block); + } saw_begin = true; complete = false; } else if token.is_word("CASE") { @@ -2631,6 +2650,12 @@ enum OraclePlSqlCreateObjectKind { #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum OraclePlSqlScope { Object, + /// DECLARE section awaiting its own BEGIN. + Declaration, + /// Local PROCEDURE/FUNCTION header, before IS/AS (a `;` here is a forward declaration). + RoutineHeader, + /// Local PROCEDURE/FUNCTION after IS/AS, awaiting its own BEGIN. + Routine, Block, Case, } @@ -3833,6 +3858,25 @@ SELECT 2 FROM DUMMY;"; ); } + #[test] + fn oracle_split_keeps_anonymous_block_with_local_routines_together() { + let cases = [ + "DECLARE\nPROCEDURE local_proc IS\nBEGIN\n NULL;\nEND;\n\nBEGIN\nlocal_proc;\nEND;", + "DECLARE\n v NUMBER;\n PROCEDURE p1 IS BEGIN NULL; END;\n FUNCTION f1 RETURN NUMBER IS BEGIN RETURN 1; END f1;\nBEGIN\n p1;\n v := f1;\nEND;", + "DECLARE\n PROCEDURE fwd(x NUMBER);\n PROCEDURE fwd(x NUMBER) IS BEGIN NULL; END;\nBEGIN\n fwd(1);\nEND;", + "DECLARE\n PROCEDURE outer_p IS\n PROCEDURE inner_p IS BEGIN NULL; END;\n BEGIN\n inner_p;\n END;\nBEGIN\n outer_p;\nEND;", + ]; + for sql in cases { + assert_eq!(split_sql_statements_for_database(sql, DatabaseType::Oracle), vec![sql.to_string()], "{sql}"); + } + let block = cases[0]; + let sql = format!("{block}\nSELECT 1 FROM dual;"); + assert_eq!( + split_sql_statements_for_database(&sql, DatabaseType::Oracle), + vec![block.to_string(), "SELECT 1 FROM dual".to_string()] + ); + } + #[test] fn oracle_like_split_keeps_anonymous_plsql_block_together() { let sql = "\