fix(sql): preserve Oracle-style routine bodies for postgres-family

This commit is contained in:
zipg authored and GitHub committed 2026-09-22 11:09:45 +08:00
1 parent 32189c7271
commit 74f1704d09
9 files changed
+418 -16

No files matched your search

@@ -210,6 +210,31 @@ WHEN NOT MATCHED THEN
INSERT (user_id, restyp_code, res_id, appcde)
VALUES (b.userid, b.restyp_code, b.res_id, b.appcde);`;
// Issue #8979: a GaussDB/openGauss instance in Oracle (A) compatibility mode reached through a
// PostgreSQL/openGauss connection. The routine body is Oracle style and the definition is closed
// by a standalone `/` line, so splitting at the body semicolons sent a truncated procedure and the
// server answered `ERROR: subprogram body is not ended correctly at end of input`.
const postgresFamilyIssue8979ProcedureScript = `CREATE OR REPLACE PROCEDURE sync_yxdyurl_probe() AS
DECLARE
BEGIN
update test_xm_20260913 set a = '23' where a = '1';
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
/
SELECT 1 AS after_procedure;`;
const postgresFamilyDollarQuotedFunctionScript = `CREATE OR REPLACE FUNCTION dbx_issue_8979_pg() RETURNS int AS $$
BEGIN
RETURN 1;
END;
$$ LANGUAGE plpgsql;
/
SELECT dbx_issue_8979_pg();`;
const xuguProgrammableObjectFixtures = [
`CREATE OR REPLACE PROCEDURE dbx_xugu_procedure AS
v_value INTEGER;
@@ -694,6 +719,46 @@ END pkg_utils_without_replace;`;
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpecWithoutReplace}\nSELECT 1;`, "xugu"))).toEqual([packageSpecWithoutReplace, "SELECT 1"]);
});
it("keeps an Oracle-style procedure body together for PostgreSQL-family connections", () => {
const procedureBody = postgresFamilyIssue8979ProcedureScript.slice(0, postgresFamilyIssue8979ProcedureScript.indexOf("\n/"));
const cases = [
{ databaseType: "postgres" as const, options: undefined },
{ databaseType: "opengauss" as const, options: { compatibilityMode: "PG" } },
];
for (const { databaseType, options } of cases) {
expect(rangeSqlTexts(splitSqlStatementRanges(postgresFamilyIssue8979ProcedureScript, databaseType, options))).toEqual([procedureBody, "SELECT 1 AS after_procedure"]);
expect(statementRangeAtCursor(postgresFamilyIssue8979ProcedureScript, indexOf(postgresFamilyIssue8979ProcedureScript, "ROLLBACK"), databaseType, options)?.sql.trim()).toBe(procedureBody);
expect(statementRangeAtCursor(postgresFamilyIssue8979ProcedureScript, postgresFamilyIssue8979ProcedureScript.length - 1, databaseType, options)?.sql.trim()).toBe("SELECT 1 AS after_procedure");
}
});
it("does not treat PostgreSQL dollar-quoted routines as Oracle-style bodies", () => {
const functionSql = `CREATE OR REPLACE FUNCTION dbx_issue_8979_pg() RETURNS int AS $$
BEGIN
RETURN 1;
END;
$$ LANGUAGE plpgsql`;
for (const databaseType of ["postgres", "opengauss"] as const) {
expect(rangeSqlTexts(splitSqlStatementRanges(postgresFamilyDollarQuotedFunctionScript, databaseType))).toEqual([functionSql, "SELECT dbx_issue_8979_pg()"]);
expect(statementRangeAtCursor(postgresFamilyDollarQuotedFunctionScript, indexOf(postgresFamilyDollarQuotedFunctionScript, "RETURN 1"), databaseType)?.sql.trim()).toBe(functionSql);
}
});
it("still splits ordinary PostgreSQL statements around a bare slash line", () => {
const script = `BEGIN;
SELECT 1;
COMMIT;
/
CREATE FUNCTION dbx_issue_8979_sql() RETURNS int AS 'SELECT 1' LANGUAGE sql;
CREATE TRIGGER dbx_issue_8979_trg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f();
SELECT 2;`;
expect(rangeSqlTexts(splitSqlStatementRanges(script, "postgres"))).toEqual(["BEGIN", "SELECT 1", "COMMIT", "CREATE FUNCTION dbx_issue_8979_sql() RETURNS int AS 'SELECT 1' LANGUAGE sql", "CREATE TRIGGER dbx_issue_8979_trg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()", "SELECT 2"]);
});
it("keeps openGauss packages together while compatibility metadata is unknown", () => {
const packageSpec = `CREATE OR REPLACE PACKAGE pkg_utils AS
FUNCTION get_version RETURN VARCHAR2;
@@ -1,6 +1,6 @@
import type { SqlCompletionColumn, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
import { getSqlCompletionContext, isOracleSystemValueName } from "@/lib/sql/sqlCompletion";
import { executableStatementRanges, isOraclePlSqlStatement, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import { executableStatementRanges, keepsOracleStyleBlockTogether, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import { DBX_TDENGINE_TBNAME_COLUMN, isTdengineStableTableType } from "@/lib/table/tableEditing";
import type { DatabaseType, SqlColumnReference, SqlReferenceAnalysis, SqlReferenceScope, SqlTableReference, SqlTextSpan } from "@/types/database";
@@ -31,7 +31,7 @@ export function sqlSemanticDiagnosticRangesForViewport(sql: string, visibleRange
const selected: SqlTextRange[] = [];
const seen = new Set<string>();
for (const statement of statements) {
if (isOraclePlSqlStatement(statement.sql, databaseType, parameterOptions)) continue;
if (keepsOracleStyleBlockTogether(statement.sql, databaseType, parameterOptions)) continue;
if (!visibleRanges.some((visibleRange) => rangesIntersect(statement, visibleRange))) continue;
const key = `${statement.from}:${statement.to}`;
if (seen.has(key)) continue;
+81 -3
View File
@@ -295,6 +295,11 @@ const SET_OPERATION_MODIFIER_KEYWORDS = new Set(["ALL", "DISTINCT"]);
// ranges must stay whole instead of splitting at every body semicolon.
const ORACLE_LIKE_PL_SQL_DATABASES: ReadonlySet<DatabaseType> = new Set(["oracle", "dameng", "gaussdb", "yashandb", "oscar", "oceanbase-oracle", "xugu", "argo"]);
const MYSQL_ROUTINE_BLOCK_DATABASES: ReadonlySet<DatabaseType> = new Set(["mysql", "doris", "starrocks", "manticoresearch", "goldendb"]);
// PostgreSQL/openGauss are also the connection types users pick for GaussDB/openGauss instances
// running in Oracle (A) compatibility mode, where a routine body is written in Oracle style
// (`CREATE PROCEDURE p AS DECLARE ... BEGIN ... END;`) and closed by a standalone `/` line.
// Mirrors SqlDialectProfile::postgres_family in dbx-sql — keep both in sync.
const POSTGRES_FAMILY_DATABASES: ReadonlySet<DatabaseType> = new Set(["postgres", "opengauss"]);
// Backslash escaping inside '...'/"..." strings is a MySQL-family extension; in standard SQL '\'
// is a complete one-char string and quotes are escaped by doubling (''). Treating backslash as an
// escape unconditionally makes ESCAPE '\' swallow its closing quote and the following statement
@@ -517,7 +522,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
continue;
}
}
if (isOracleLikeDatabase(databaseType, parameterOptions) && isAtLineStart(sql, i) && isSlashLine(sql, i)) {
if ((isOracleLikeDatabase(databaseType, parameterOptions) || isPostgresFamilyDatabase(databaseType)) && isAtLineStart(sql, i) && isSlashLine(sql, i)) {
const lineEnd = findLineEnd(sql, i);
flush(i);
i = nextLineStart(sql, lineEnd);
@@ -627,7 +632,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
// Internal semicolons remain part of the routine body.
flush();
} else {
if (oraclePlSqlStatementEnd === undefined && isOracleLikeDatabase(databaseType, parameterOptions) && !postgresDollarQuotedRoutine && statementStart !== -1) {
if (oraclePlSqlStatementEnd === undefined && !postgresDollarQuotedRoutine && statementStart !== -1 && keepsOracleStyleBlockTogether(sql.slice(statementStart, i), databaseType, parameterOptions)) {
const statementSoFar = sql.slice(statementStart, i);
oraclePlSqlStatementEnd = startsWithOraclePlSqlBlock(statementSoFar) ? statementStart + (oraclePlSqlBlockEnd(sql.slice(statementStart)) ?? sql.length - statementStart) : null;
}
@@ -766,7 +771,7 @@ function rangeForCursorInSoftRanges(sql: string, ranges: RawStatement[], pos: nu
}
function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): RawStatement[] {
if (isOraclePlSqlStatement(statement.sql, databaseType, parameterOptions)) return [statement];
if (keepsOracleStyleBlockTogether(statement.sql, databaseType, parameterOptions)) return [statement];
if (isSapHanaScriptBlockStatement(statement.sql, databaseType)) return [statement];
// Routine bodies contain top-level-looking SET/INSERT/SELECT lines that are not independent statements.
if (isMysqlRoutineBlockDatabase(databaseType) && startsWithMysqlRoutineBlock(statement.sql, parameterOptions)) return [statement];
@@ -1647,6 +1652,20 @@ export function isOracleLikeDatabase(databaseType?: DatabaseType, options?: SqlP
return !!databaseType && (ORACLE_LIKE_PL_SQL_DATABASES.has(databaseType) || isOpenGaussOracleCompatibility(databaseType, options));
}
function isPostgresFamilyDatabase(databaseType?: DatabaseType): boolean {
return !!databaseType && POSTGRES_FAMILY_DATABASES.has(databaseType);
}
/**
* Whether a statement must stay a single statement instead of being cut at its inner semicolons:
* Oracle PL/SQL blocks on Oracle-like dialects, and the Oracle-style routine bodies that a
* PostgreSQL-family connection reaches on a GaussDB/openGauss Oracle-compatibility server.
*/
export function keepsOracleStyleBlockTogether(sql: string, databaseType?: DatabaseType, options?: SqlParameterOptions): boolean {
if (isOraclePlSqlStatement(sql, databaseType, options)) return true;
return isPostgresFamilyDatabase(databaseType) && startsWithOracleStyleRoutineBody(sql);
}
export function isOraclePlSqlStatement(sql: string, databaseType?: DatabaseType, options?: SqlParameterOptions): boolean {
return isOracleLikeDatabase(databaseType, options) && startsWithOraclePlSqlBlock(sql);
}
@@ -1916,6 +1935,65 @@ function startsWithOraclePlSqlBlockWords(words: readonly string[]): boolean {
return ORACLE_PL_SQL_CREATE_OBJECT_TYPES.has(words[index] ?? "");
}
/**
* Whether the statement is a routine whose body is written in Oracle PL/SQL syntax
* (`CREATE [OR REPLACE] PROCEDURE|FUNCTION ... { AS | IS } { DECLARE | BEGIN }`, plus the
* Oracle-only `PACKAGE BODY` / `TYPE BODY` forms).
*
* This is deliberately narrower than `startsWithOraclePlSqlBlock`: a PostgreSQL connection only
* sees this shape when the server is a GaussDB/openGauss instance in Oracle (A) compatibility
* mode, because PostgreSQL itself requires a dollar-quoted or string body (`AS $$ ... $$`,
* `AS 'body'`) and has no `PACKAGE`/`TYPE BODY`.
*/
function startsWithOracleStyleRoutineBody(sql: string): boolean {
const words = oraclePlSqlWords(sql);
if (words[0] !== "CREATE") return false;
const index = skipOraclePlSqlCreateModifiers(words, 1);
if ((words[index] === "PACKAGE" || words[index] === "TYPE") && words[index + 1] === "BODY") return true;
if (words[index] !== "FUNCTION" && words[index] !== "PROCEDURE") return false;
return routineBodyStartsAtPlsqlKeyword(sql);
}
/**
* The body introducer of an Oracle-style routine has to be the DECLARE/BEGIN keyword itself.
* Declarations may follow `AS`/`IS` with or without the optional `DECLARE`, while a PostgreSQL
* body opens with `$$`, `$tag$` or a quoted string there, so only a bare identifier matches.
*/
function routineBodyStartsAtPlsqlKeyword(sql: string): boolean {
const tokens = oraclePlSqlTokens(sql);
for (const token of tokens) {
if (token.kind !== "word" || (token.value !== "AS" && token.value !== "IS")) continue;
return nextWordAtPlsqlBodyKeyword(sql, token.to);
}
return false;
}
function nextWordAtPlsqlBodyKeyword(sql: string, from: number): boolean {
let i = from;
while (i < sql.length) {
const ch = sql[i];
if (isSqlWhitespace(ch)) {
i += 1;
continue;
}
if (ch === "-" && sql[i + 1] === "-") {
const newline = sql.indexOf("\n", i);
i = newline === -1 ? sql.length : newline + 1;
continue;
}
if (ch === "/" && sql[i + 1] === "*") {
const close = sql.indexOf("*/", i + 2);
i = close === -1 ? sql.length : close + 2;
continue;
}
break;
}
const word = /^[A-Za-z_][A-Za-z0-9_$]*/.exec(sql.slice(i, i + 16))?.[0];
const upper = word?.toUpperCase();
return upper === "DECLARE" || upper === "BEGIN";
}
function startsWithPostgresDollarQuotedRoutinePrefix(sql: string): boolean {
const words = oraclePlSqlWords(sql);
if (words[0] !== "CREATE") return false;