fix(xugu): preserve programmable object DDL execution

This commit is contained in:
Elias authored and GitHub committed 2026-07-26 11:56:19 +08:00
1 parent f56d3a86c1
commit 3836142f72
5 files changed
+660 -36

No files matched your search

@@ -80,6 +80,75 @@ BEGIN
NULL;
END;`;
const xuguProgrammableObjectFixtures = [
`CREATE OR REPLACE PROCEDURE dbx_xugu_procedure AS
v_value INTEGER;
BEGIN
v_value := 1;
END;`,
`CREATE PROCEDURE dbx_xugu_procedure_without_replace AS
v_value INTEGER;
BEGIN
v_value := 1;
END;`,
`CREATE OR REPLACE FUNCTION dbx_xugu_function RETURN INTEGER AS
BEGIN
RETURN 1;
END;`,
`CREATE FUNCTION dbx_xugu_function_without_replace RETURN INTEGER AS
BEGIN
RETURN 1;
END;`,
`CREATE OR REPLACE TRIGGER dbx_xugu_trigger
BEFORE INSERT ON dbx_xugu_events
FOR EACH ROW
BEGIN
NULL;
END;`,
`CREATE TRIGGER dbx_xugu_trigger_without_replace
BEFORE INSERT ON dbx_xugu_events
FOR EACH ROW
BEGIN
NULL;
END;`,
`CREATE OR REPLACE PACKAGE BODY dbx_xugu_package AS
PROCEDURE ping AS
BEGIN
NULL;
END ping;
END dbx_xugu_package;`,
`CREATE PACKAGE BODY dbx_xugu_package_without_replace AS
PROCEDURE ping AS
BEGIN
NULL;
END ping;
END dbx_xugu_package_without_replace;`,
`CREATE OR REPLACE FORCE PACKAGE BODY dbx_xugu_force_package AS
PROCEDURE ping AS
BEGIN
NULL;
END ping;
END dbx_xugu_force_package;`,
`CREATE OR REPLACE NOFORCE PACKAGE BODY dbx_xugu_noforce_package AS
PROCEDURE ping AS
BEGIN
NULL;
END ping;
END dbx_xugu_noforce_package;`,
`CREATE OR REPLACE TYPE BODY dbx_xugu_type AS
MEMBER PROCEDURE ping IS
BEGIN
NULL;
END;
END;`,
`CREATE TYPE BODY dbx_xugu_type_without_replace AS
MEMBER PROCEDURE ping IS
BEGIN
NULL;
END;
END;`,
];
const mysqlRoutineFixture = `CREATE PROCEDURE p()
BEGIN
SELECT 1;
@@ -223,6 +292,54 @@ describe("splitSqlStatementRanges", () => {
expect(rangeSqlTexts(splitSqlStatementRanges(gaussDbNestedProcedure, "gaussdb"))).toEqual([gaussDbNestedProcedure]);
});
it("keeps Xugu programmable object DDL together and retains its terminator", () => {
for (const sql of xuguProgrammableObjectFixtures) {
const ranges = splitSqlStatementRanges(`${sql}\nSELECT 1;`, "xugu");
expect(rangeSqlTexts(ranges)).toEqual([sql, "SELECT 1"]);
expect(ranges[0].sql.trimEnd()).toMatch(/END(?:\s+\w+)?;$/);
}
});
it("splits Xugu package specification without a slash before following SQL", () => {
const packageSpec = `CREATE OR REPLACE PACKAGE pkg_utils AS
FUNCTION get_version RETURN VARCHAR2;
PROCEDURE log_message(msg VARCHAR2);
END pkg_utils;`;
const forcePackageSpec = `CREATE OR REPLACE FORCE PACKAGE pkg_utils AS
PROCEDURE ping;
END pkg_utils;`;
const packageSpecWithoutReplace = `CREATE PACKAGE pkg_utils_without_replace AS
PROCEDURE ping;
END pkg_utils_without_replace;`;
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpec}\nSELECT 1;`, "xugu"))).toEqual([packageSpec, "SELECT 1"]);
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpec}\n/\nSELECT 1;`, "xugu"))).toEqual([packageSpec, "SELECT 1"]);
expect(rangeSqlTexts(splitSqlStatementRanges(`${forcePackageSpec}\nSELECT 1;`, "xugu"))).toEqual([forcePackageSpec, "SELECT 1"]);
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpecWithoutReplace}\nSELECT 1;`, "xugu"))).toEqual([packageSpecWithoutReplace, "SELECT 1"]);
});
it("splits plain CREATE TYPE AS OBJECT on semicolon without waiting for END", () => {
const sql = "CREATE OR REPLACE TYPE address_t AS OBJECT (id INT);\nSELECT 1;";
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "xugu"))).toEqual(["CREATE OR REPLACE TYPE address_t AS OBJECT (id INT)", "SELECT 1"]);
expect(rangeSqlTexts(splitSqlStatementRanges("CREATE TYPE address_t_without_replace AS OBJECT (id INT);\nSELECT 1;", "xugu"))).toEqual(["CREATE TYPE address_t_without_replace AS OBJECT (id INT)", "SELECT 1"]);
});
it("keeps Oracle-style CASE expressions inside Xugu and Oracle routines", () => {
const routine = `CREATE OR REPLACE FUNCTION dbx_case_expr RETURN NUMBER AS
BEGIN
RETURN CASE WHEN 1 = 1 THEN CASE WHEN 2 = 2 THEN 1 ELSE 2 END ELSE 0 END;
END;`;
const caseStatementRoutine = `CREATE OR REPLACE PROCEDURE dbx_case_statement AS
BEGIN
CASE WHEN 1 = 1 THEN NULL; ELSE NULL; END CASE;
END;`;
for (const database of ["xugu", "oracle"] as const) {
expect(rangeSqlTexts(splitSqlStatementRanges(`${routine}\nSELECT 1;`, database))).toEqual([routine, "SELECT 1"]);
expect(rangeSqlTexts(splitSqlStatementRanges(`${caseStatementRoutine}\nSELECT 1;`, database))).toEqual([caseStatementRoutine, "SELECT 1"]);
}
});
it("keeps SAP HANA DO blocks together", () => {
const ranges = splitSqlStatementRanges(sapHanaDoBlockFixture, "saphana");
+68 -10
View File
@@ -249,14 +249,16 @@ const ALTER_BODY_KEYWORDS = new Set(["ADD", "ALTER", "COMMENT", "DROP", "MODIFY"
const CLICKHOUSE_ALTER_TABLE_HEADER = /^ALTER\s+TABLE\s+(?:(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+")\s*\.\s*)?(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+")(?:\s+ON\s+CLUSTER\s+(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+"|'(?:''|[^'])+'))?\s*$/i;
const SET_OPERATION_KEYWORDS = new Set(["UNION", "INTERSECT", "EXCEPT", "MINUS"]);
const SET_OPERATION_MODIFIER_KEYWORDS = new Set(["ALL", "DISTINCT"]);
const ORACLE_LIKE_PL_SQL_DATABASES: ReadonlySet<DatabaseType> = new Set(["oracle", "dameng", "gaussdb", "yashandb", "oscar", "oceanbase-oracle"]);
const ORACLE_LIKE_PL_SQL_DATABASES: ReadonlySet<DatabaseType> = new Set(["oracle", "dameng", "gaussdb", "yashandb", "oscar", "oceanbase-oracle", "xugu"]);
const MYSQL_ROUTINE_BLOCK_DATABASES: ReadonlySet<DatabaseType> = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb"]);
const MYSQL_CREATE_TABLE_OPTION_DATABASES: ReadonlySet<DatabaseType> = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb", "gbase"]);
const MYSQL_ROUTINE_OBJECT_TYPES = new Set(["PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]);
const MYSQL_NON_ROUTINE_CREATE_TYPES = new Set(["DATABASE", "INDEX", "LOGFILE", "ROLE", "SCHEMA", "SERVER", "SPATIAL", "TABLE", "TEMPORARY", "UNIQUE", "USER", "VIEW"]);
const MYSQL_CONTROL_BLOCK_SUFFIXES = new Set(["IF", "LOOP", "CASE", "REPEAT", "WHILE"]);
const ORACLE_PL_SQL_BLOCK_STARTERS = new Set(["DECLARE", "BEGIN"]);
const ORACLE_PL_SQL_CREATE_OBJECT_TYPES = new Set(["FUNCTION", "PROCEDURE", "TRIGGER", "PACKAGE", "PACKAGE BODY", "TYPE", "TYPE BODY"]);
// Plain CREATE TYPE ... AS OBJECT (...); ends with ");" and is not a PL/SQL block.
// Only PACKAGE (spec), PACKAGE/TYPE BODY, and routine/trigger objects are PL/SQL blocks.
const ORACLE_PL_SQL_CREATE_OBJECT_TYPES = new Set(["FUNCTION", "PROCEDURE", "TRIGGER", "PACKAGE"]);
const ORACLE_PL_SQL_TERMINATORS = new Set(["IF", "LOOP", "CASE"]);
const SAP_HANA_SCRIPT_BLOCK_TERMINATORS = new Set(["IF", "FOR", "WHILE"]);
@@ -1614,15 +1616,32 @@ function startsWithOraclePlSqlBlock(sql: string): boolean {
if (ORACLE_PL_SQL_BLOCK_STARTERS.has(first)) return first !== "BEGIN" || words[1] !== "TRANSACTION";
if (first !== "CREATE") return false;
let index = 1;
while (["OR", "REPLACE", "EDITIONABLE", "NONEDITIONABLE"].includes(words[index] ?? "")) {
index += 1;
}
const index = skipOraclePlSqlCreateModifiers(words, 1);
// PACKAGE BODY / TYPE BODY are programmable blocks with an outer END.
if (words[index] === "PACKAGE" && words[index + 1] === "BODY") return true;
if (words[index] === "TYPE" && words[index + 1] === "BODY") return true;
// Plain CREATE TYPE ... AS OBJECT (...); is ordinary SQL terminated by ';'.
if (words[index] === "TYPE") return false;
return ORACLE_PL_SQL_CREATE_OBJECT_TYPES.has(words[index] ?? "");
}
/** Skip OR REPLACE / FORCE / NOFORCE / EDITIONABLE modifiers after CREATE. */
function skipOraclePlSqlCreateModifiers(words: readonly string[], startIndex: number): number {
let index = startIndex;
while (index < words.length) {
if (words[index] === "OR" && words[index + 1] === "REPLACE") {
index += 2;
continue;
}
if (["FORCE", "NOFORCE", "EDITIONABLE", "NONEDITIONABLE"].includes(words[index] ?? "")) {
index += 1;
continue;
}
break;
}
return index;
}
function startsWithSapHanaScriptBlock(sql: string): boolean {
return oraclePlSqlWords(sql)[0] === "DO";
}
@@ -1663,7 +1682,11 @@ function oraclePlSqlBlockIsComplete(sql: string): boolean {
const tokens = oraclePlSqlTokens(sql);
if (!startsWithOraclePlSqlBlock(sql)) return false;
const stack: string[] = [];
// Package/type specifications have no BEGIN — only declarations closed by
// END [name];. Bodies also own an outer END beyond nested routine END pairs.
const objectKind = oraclePlSqlCreateObjectKind(sql);
const stack: string[] = objectKind === "body" ? ["OBJECT_BODY"] : objectKind === "spec" ? ["OBJECT_SPEC"] : [];
let sawBegin = false;
for (let index = 0; index < tokens.length; index += 1) {
const token = tokens[index];
if (token.kind !== "word") continue;
@@ -1676,6 +1699,7 @@ function oraclePlSqlBlockIsComplete(sql: string): boolean {
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";
else stack.push("BLOCK");
continue;
@@ -1690,19 +1714,53 @@ function oraclePlSqlBlockIsComplete(sql: string): boolean {
continue;
}
if (token.value === "CASE") {
// Both CASE statements and CASE expressions own an END. The CASE token
// following END CASE is ignored below, so it cannot start a new scope.
if (previousWordToken(tokens, index) !== "END") stack.push("CASE");
continue;
}
if (token.value === "END") {
const next = nextWordToken(tokens, index);
const target = ORACLE_PL_SQL_TERMINATORS.has(next ?? "") ? next : "BLOCK";
const top = stack[stack.length - 1];
// CASE expressions close as END; while CASE statements close as END CASE;.
// In either form, this END belongs to CASE rather than the surrounding block.
if (top === "CASE") {
stack.pop();
continue;
}
const next = nextWordToken(tokens, index);
const target = ORACLE_PL_SQL_TERMINATORS.has(next ?? "") ? next : top === "OBJECT_BODY" || top === "OBJECT_SPEC" ? top : "BLOCK";
if (top === target || (target === "BLOCK" && top === "BLOCK")) stack.pop();
continue;
}
}
return stack.length === 0 && tokens[tokens.length - 1]?.kind === "semicolon";
const endsWithSemicolon = tokens[tokens.length - 1]?.kind === "semicolon";
if (objectKind === "spec") {
// Specs complete on outer END [name]; without requiring a BEGIN block.
return stack.length === 0 && endsWithSemicolon;
}
return sawBegin && stack.length === 0 && endsWithSemicolon;
}
/**
* Classify CREATE programmable objects:
* - body: PACKAGE BODY / TYPE BODY (outer END beyond nested routines)
* - spec: PACKAGE specification only (declarations + END, no BEGIN)
* - null: ordinary SQL / other objects (including plain CREATE TYPE ... AS OBJECT)
*/
function oraclePlSqlCreateObjectKind(sql: string): "body" | "spec" | null {
const words = oraclePlSqlWords(sql);
if (words[0] !== "CREATE") return null;
const index = skipOraclePlSqlCreateModifiers(words, 1);
if ((words[index] === "PACKAGE" || words[index] === "TYPE") && words[index + 1] === "BODY") {
return "body";
}
// Only PACKAGE specs lack BEGIN; plain TYPE objects end with ");".
if (words[index] === "PACKAGE") {
return "spec";
}
return null;
}
function oraclePlSqlWords(sql: string): string[] {