mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(sql): keep WITH FUNCTION inline declarations with their main query
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user