fix(sql): keep WITH FUNCTION inline declarations with their main query

This commit is contained in:
弈秋
2026-10-13 23:48:19 +08:00
committed by GitHub
parent 6fd96a5e50
commit e78244f4d3
3 changed files with 193 additions and 0 deletions
@@ -678,6 +678,46 @@ describe("splitSqlStatementRanges", () => {
expect(rangeSqlTexts(splitSqlStatementRanges(`${cases[0]}\nSELECT 1 FROM dual;`, "oracle"))).toEqual([cases[0], "SELECT 1 FROM dual"]);
});
it("keeps Dameng WITH FUNCTION declaration and its main query together (#11275)", () => {
const sql = `WITH FUNCTION f RETURN NUMBER IS
v_start TIMESTAMP := SYSTIMESTAMP;
v_delay_seconds NUMBER := MOD(TO_NUMBER(TO_CHAR(SYSDATE, 'SSSSS')), 10) + 1;
BEGIN
WHILE (SYSTIMESTAMP - v_start) < (v_delay_seconds * INTERVAL '1' SECOND) LOOP
NULL;
END LOOP;
RETURN 1;
END;
SELECT f FROM dual;`;
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "dameng"))).toEqual([sql]);
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "oracle"))).toEqual([sql]);
expect(rangeSqlTexts(splitSqlStatementRanges(`${sql}\nSELECT 2 FROM dual;`, "dameng"))).toEqual([sql, "SELECT 2 FROM dual"]);
// The statement after the WITH query keeps splitting at its own terminator.
expect(rangeSqlTexts(splitSqlStatementRanges(`${sql}\nSELECT 2 FROM dual;`, "oracle"))).toEqual([sql, "SELECT 2 FROM dual"]);
// A plain CTE without PL/SQL declarations is ordinary SQL: the trailing `;` stays a delimiter.
expect(rangeSqlTexts(splitSqlStatementRanges("WITH t AS (SELECT 1 FROM dual) SELECT * FROM t;", "dameng"))).toEqual(["WITH t AS (SELECT 1 FROM dual) SELECT * FROM t"]);
// Cursor anywhere inside the declaration section targets the whole statement.
const range = statementRangeAtCursor(sql, sql.indexOf("SYSTIMESTAMP;"), "dameng");
expect(range?.sql).toBe(sql);
});
it("keeps multiple WITH FUNCTION declarations in one statement until the main query (#11275)", () => {
const sql = `WITH FUNCTION f1 RETURN NUMBER IS
BEGIN
RETURN 1;
END;
FUNCTION f2 RETURN NUMBER IS
v NUMBER := 0;
BEGIN
v := 2;
RETURN v;
END;
SELECT f1 + f2 FROM dual;`;
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "dameng"))).toEqual([sql]);
expect(rangeSqlTexts(splitSqlStatementRanges(`${sql}\nSELECT 3 FROM dual;`, "dameng"))).toEqual([sql, "SELECT 3 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);
@@ -328,6 +328,9 @@ const ORACLE_PL_SQL_BLOCK_STARTERS = new Set(["DECLARE", "BEGIN"]);
// 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"]);
// The main query that follows Oracle's `WITH [FUNCTION|PROCEDURE ... END;]` inline PL/SQL
// declaration section. Only a top-level `;` once one of these starts is the statement terminator.
const ORACLE_WITH_FUNCTION_MAIN_QUERY_KEYWORDS = new Set(["SELECT", "INSERT", "UPDATE", "DELETE", "MERGE", "WITH"]);
const SAP_HANA_SCRIPT_BLOCK_TERMINATORS = new Set(["IF", "FOR", "WHILE"]);
/**
@@ -2160,10 +2163,17 @@ function startsWithOraclePlSqlBlock(sql: string): boolean {
return startsWithOraclePlSqlBlockWords(oraclePlSqlWords(sql));
}
/** Whether the statement opens with Oracle's `WITH [FUNCTION|PROCEDURE ...]` inline PL/SQL
* declaration section (Oracle 12c `plsql_declarations`, also accepted by DM8 — issue #11275). */
function isOracleWithFunctionDeclaration(words: readonly string[]): boolean {
return words[0] === "WITH" && (words[1] === "FUNCTION" || words[1] === "PROCEDURE");
}
function startsWithOraclePlSqlBlockWords(words: readonly string[]): boolean {
const first = words[0];
if (!first) return false;
if (ORACLE_PL_SQL_BLOCK_STARTERS.has(first)) return first !== "BEGIN" || words[1] !== "TRANSACTION";
if (first === "WITH") return isOracleWithFunctionDeclaration(words);
if (first !== "CREATE") return false;
const index = skipOraclePlSqlCreateModifiers(words, 1);
@@ -2307,15 +2317,24 @@ function oraclePlSqlBlockEnd(sql: string): number | null {
const tokens = oraclePlSqlTokens(sql);
const words = oraclePlSqlWordValues(tokens);
if (!startsWithOraclePlSqlBlockWords(words)) return null;
const withFunctionDeclaration = isOracleWithFunctionDeclaration(words);
// 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(words);
const stack: string[] = objectKind === "body" ? ["OBJECT_BODY"] : objectKind === "spec" ? ["OBJECT_SPEC"] : [];
let sawBegin = false;
let inMainQuery = false;
for (let index = 0; index < tokens.length; index += 1) {
const token = tokens[index];
if (token.kind === "semicolon") {
// Inside a `WITH FUNCTION ...` declaration section every top-level `;` — declaration
// assignments, nested block ends, and the `END;` closing each declaration — stays part
// of the statement. Only the main query's own terminator ends it (#11275).
if (withFunctionDeclaration) {
if (inMainQuery && stack.length === 0) return token.to;
continue;
}
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;
@@ -2323,6 +2342,10 @@ function oraclePlSqlBlockEnd(sql: string): number | null {
}
if (token.kind !== "word") continue;
if (withFunctionDeclaration && stack.length === 0 && index > 0 && ORACLE_WITH_FUNCTION_MAIN_QUERY_KEYWORDS.has(token.value)) {
inMainQuery = true;
}
if (token.value === "DECLARE") {
if (stack[stack.length - 1] !== "DECLARATION") stack.push("DECLARATION");
continue;
+130
View File
@@ -3094,6 +3094,11 @@ impl OraclePlSqlBlock {
match self.tokens.as_slice() {
[first, ..] if first.is_word("DECLARE") => true,
[first, second, ..] if first.is_word("BEGIN") && !Self::is_transaction_begin_tail(second) => true,
// Oracle 12c `WITH [FUNCTION|PROCEDURE ... END;] <main query>` inline PL/SQL
// declarations (also accepted by DM8 — issue #11275). The declaration section is
// PL/SQL, but the statement is not over at its `END;` — the main query's own
// terminator ends it.
[first, second, ..] if first.is_word("WITH") && second.is_any_word(&["FUNCTION", "PROCEDURE"]) => true,
[first, rest @ ..] if first.is_word("CREATE") => Self::starts_create_plsql_object(rest),
_ => false,
}
@@ -3146,6 +3151,13 @@ impl OraclePlSqlBlock {
return false;
}
// `WITH [FUNCTION|PROCEDURE ... END;] <main query>;` follows its own completion
// rule (see with_function_is_complete); the classic block walk below would fire
// at the declaration's `END;` and cut the main query off the statement.
if self.starts_with_function_declaration() {
return self.with_function_is_complete();
}
// Package/type specifications have declarations and an outer END with
// no BEGIN. Bodies also own an outer END beyond nested routine END
// pairs so an inner END cannot finish the object.
@@ -3223,6 +3235,68 @@ impl OraclePlSqlBlock {
}
}
fn starts_with_function_declaration(&self) -> bool {
matches!(
self.tokens.as_slice(),
[first, second, ..] if first.is_word("WITH") && second.is_any_word(&["FUNCTION", "PROCEDURE"])
)
}
/// Completion rule for `WITH [FUNCTION|PROCEDURE ... END;] <main query>;` (issue #11275):
/// no depth-0 `;` may end the statement while the WITH declaration section is open —
/// declaration assignments, nested block ends and the `END;` closing each declaration all
/// stay inside. Once the main query starts at an empty scope, its own terminator
/// completes the statement.
fn with_function_is_complete(&self) -> bool {
if !self.tokens.last().is_some_and(OraclePlSqlToken::is_semicolon) {
return false;
}
let mut scopes: Vec<OraclePlSqlScope> = Vec::new();
let mut in_main_query = false;
for (index, token) in self.tokens.iter().enumerate() {
if token.is_semicolon() {
if in_main_query && scopes.is_empty() {
return true;
}
continue;
}
let Some(word) = token.as_word() else {
continue;
};
// The statement-initial WITH token sits at index 0 and must not open the query.
if index > 0
&& scopes.is_empty()
&& matches!(word, "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "MERGE" | "WITH")
{
in_main_query = true;
continue;
}
match word {
"BEGIN" => scopes.push(OraclePlSqlScope::Block),
"CASE" if previous_word_token(&self.tokens, index) != Some("END") => {
scopes.push(OraclePlSqlScope::Case);
}
"END" => {
let next = self.tokens.get(index + 1).and_then(OraclePlSqlToken::as_word);
if matches!(next, Some("IF" | "LOOP")) {
continue;
}
if matches!(next, Some("CASE")) && !matches!(scopes.last(), Some(OraclePlSqlScope::Case)) {
continue;
}
scopes.pop();
}
_ => {}
}
}
false
}
fn starts_create_plsql_object(tokens: &[OraclePlSqlToken]) -> bool {
let tokens = Self::skip_create_modifiers(tokens);
match tokens {
@@ -5167,6 +5241,62 @@ END;";
);
}
#[test]
fn oracle_like_split_keeps_with_function_declaration_and_main_query_together_per_issue_11275() {
// DM8 accepts Oracle's `WITH [FUNCTION ...] <main query>` form and executes it as ONE
// statement (the reporter's own client returned f = 1). Splitting at the first
// declaration `;` sent the truncated `WITH FUNCTION ... v_start TIMESTAMP := SYSTIMESTAMP`
// fragment, and DM rejected it with 42000 / vendorCode -2007
// ("第 2 行, 第 39 列[]附近出现错误: 语法分析出错").
let sql = "\
WITH FUNCTION f RETURN NUMBER IS
v_start TIMESTAMP := SYSTIMESTAMP;
v_delay_seconds NUMBER := MOD(TO_NUMBER(TO_CHAR(SYSDATE, 'SSSSS')), 10) + 1;
BEGIN
WHILE (SYSTIMESTAMP - v_start) < (v_delay_seconds * INTERVAL '1' SECOND) LOOP
NULL;
END LOOP;
RETURN 1;
END;
SELECT f FROM dual;";
for db_type in [DatabaseType::Dameng, DatabaseType::Oracle, DatabaseType::Gaussdb, DatabaseType::Xugu] {
assert_eq!(split_sql_statements_for_database(sql, db_type), vec![sql.to_string()], "{db_type:?}");
}
// The statement after the WITH query still splits at its own terminator.
let script = format!("{sql}\nSELECT 2 FROM dual;");
assert_eq!(
split_sql_statements_for_database(&script, DatabaseType::Dameng),
vec![sql.to_string(), "SELECT 2 FROM dual".to_string()]
);
// Multiple declarations stay in one statement until the main query's terminator.
let two_declarations = "\
WITH FUNCTION f1 RETURN NUMBER IS
BEGIN
RETURN 1;
END;
FUNCTION f2 RETURN NUMBER IS
v NUMBER := 0;
BEGIN
v := 2;
RETURN v;
END;
SELECT f1 + f2 FROM dual;";
assert_eq!(
split_sql_statements_for_database(two_declarations, DatabaseType::Dameng),
vec![two_declarations.to_string()]
);
// A plain CTE without PL/SQL declarations is ordinary SQL: the trailing `;` stays a
// delimiter and the statement is emitted without it.
let cte = "WITH t AS (SELECT 1 FROM dual) SELECT * FROM t;";
assert_eq!(
split_sql_statements_for_database(cte, DatabaseType::Dameng),
vec!["WITH t AS (SELECT 1 FROM dual) SELECT * FROM t".to_string()]
);
}
#[test]
fn argo_split_keeps_create_procedure_body_together() {
// ArgoDB (Transwarp) accepts PL/SQL-style procedure definitions whose body contains