fix(sql): preserve Oracle-style routine bodies for postgres-family
This commit is contained in:
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+270
-11
@@ -132,6 +132,7 @@ pub struct SqlFileImportStatement {
|
||||
struct SqlDialectProfile {
|
||||
supports_hash_line_comments: bool,
|
||||
supports_oracle_plsql_blocks: bool,
|
||||
supports_oracle_style_routine_bodies: bool,
|
||||
supports_slash_line_block_delimiter: bool,
|
||||
supports_custom_delimiter_commands: bool,
|
||||
supports_mysql_routine_blocks: bool,
|
||||
@@ -150,6 +151,7 @@ impl Default for SqlDialectProfile {
|
||||
Self {
|
||||
supports_hash_line_comments: false,
|
||||
supports_oracle_plsql_blocks: false,
|
||||
supports_oracle_style_routine_bodies: false,
|
||||
supports_slash_line_block_delimiter: false,
|
||||
supports_custom_delimiter_commands: true,
|
||||
supports_mysql_routine_blocks: false,
|
||||
@@ -180,6 +182,10 @@ impl SqlDialectProfile {
|
||||
return Self::gaussdb();
|
||||
}
|
||||
|
||||
if matches!(db_type, DatabaseType::Postgres | DatabaseType::OpenGauss) {
|
||||
return Self::postgres_family();
|
||||
}
|
||||
|
||||
if Self::is_oracle_like_database(db_type) {
|
||||
return Self::oracle_like();
|
||||
}
|
||||
@@ -212,6 +218,20 @@ impl SqlDialectProfile {
|
||||
Self { supports_oracle_plsql_blocks: true, supports_slash_line_block_delimiter: true, ..Self::default() }
|
||||
}
|
||||
|
||||
/// PostgreSQL and openGauss, which are also the connection types users pick for a
|
||||
/// GaussDB/openGauss instance running in Oracle (A) compatibility mode. GaussDB PL/SQL
|
||||
/// bodies are Oracle-style (`CREATE PROCEDURE p AS DECLARE ... BEGIN ... END;`) and are
|
||||
/// terminated by a standalone `/` line, while the surrounding script syntax is still
|
||||
/// PostgreSQL, so the profile keeps those constructs whole without enabling the full
|
||||
/// Oracle PL/SQL dialect.
|
||||
fn postgres_family() -> Self {
|
||||
Self {
|
||||
supports_oracle_style_routine_bodies: true,
|
||||
supports_slash_line_block_delimiter: true,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn gaussdb() -> Self {
|
||||
Self {
|
||||
supports_postgres_dollar_quoted_routines: true,
|
||||
@@ -556,9 +576,8 @@ impl SqlStatementSplitter {
|
||||
} else {
|
||||
self.buffer.push(ch);
|
||||
}
|
||||
} else if self.options.profile.supports_oracle_plsql_blocks
|
||||
&& !self.postgres_dollar_quoted_routine
|
||||
&& starts_with_oracle_plsql_block(&self.buffer)
|
||||
} else if !self.postgres_dollar_quoted_routine
|
||||
&& keeps_oracle_style_block_together(self.options, &self.buffer)
|
||||
{
|
||||
self.buffer.push(ch);
|
||||
if oracle_plsql_block_is_complete(&self.buffer) {
|
||||
@@ -884,7 +903,7 @@ fn split_statement_range_at_blank_lines(
|
||||
statement: &SqlStatementRange,
|
||||
options: SqlParsingOptions,
|
||||
) -> Vec<SqlStatementRange> {
|
||||
if options.profile.supports_oracle_plsql_blocks && starts_with_oracle_plsql_block(&statement.text) {
|
||||
if keeps_oracle_style_block_together(options, &statement.text) {
|
||||
return vec![statement.clone()];
|
||||
}
|
||||
if options.profile.supports_hana_do_blocks && starts_with_hana_do_block(&statement.text) {
|
||||
@@ -1100,9 +1119,8 @@ fn split_sql_statement_ranges_with_options(sql: &str, options: SqlParsingOptions
|
||||
}
|
||||
push_statement_range(&mut ranges, sql, start, i, options);
|
||||
} else {
|
||||
let is_oracle_plsql = options.profile.supports_oracle_plsql_blocks
|
||||
&& !postgres_dollar_quoted_routine
|
||||
&& starts_with_oracle_plsql_block(&sql[start..i]);
|
||||
let is_oracle_plsql =
|
||||
!postgres_dollar_quoted_routine && keeps_oracle_style_block_together(options, &sql[start..i]);
|
||||
if is_oracle_plsql {
|
||||
if !oracle_plsql_block_is_complete(&sql[start..i + ch.len_utf8()]) {
|
||||
i += ch.len_utf8();
|
||||
@@ -2412,6 +2430,20 @@ fn starts_with_oracle_plsql_block(sql: &str) -> bool {
|
||||
OraclePlSqlBlock::parse(sql).starts_block()
|
||||
}
|
||||
|
||||
/// Whether a statement must stay a single statement instead of being cut at its inner
|
||||
/// semicolons: Oracle PL/SQL blocks for Oracle-like dialects, and the Oracle-style routine bodies
|
||||
/// that PostgreSQL-family connections reach on a GaussDB/openGauss Oracle-compatibility server.
|
||||
fn keeps_oracle_style_block_together(options: SqlParsingOptions, statement_head: &str) -> bool {
|
||||
if options.profile.supports_oracle_plsql_blocks {
|
||||
return starts_with_oracle_plsql_block(statement_head);
|
||||
}
|
||||
options.profile.supports_oracle_style_routine_bodies && starts_with_oracle_style_routine_body(statement_head)
|
||||
}
|
||||
|
||||
fn starts_with_oracle_style_routine_body(sql: &str) -> bool {
|
||||
OraclePlSqlBlock::parse(sql).starts_oracle_style_routine_body()
|
||||
}
|
||||
|
||||
fn starts_with_postgres_dollar_quoted_routine_prefix(sql: &str) -> bool {
|
||||
let block = OraclePlSqlBlock::parse(sql);
|
||||
let Some(first) = block.tokens.first() else {
|
||||
@@ -2515,6 +2547,48 @@ impl OraclePlSqlBlock {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 [`Self::starts_block`]. 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`. Keeping the narrow test at
|
||||
/// the entry point lets [`Self::is_complete`] end such a body at its outer `END;` while
|
||||
/// ordinary PostgreSQL routines, `CREATE TRIGGER ... EXECUTE FUNCTION` statements and
|
||||
/// transaction `BEGIN;` blocks keep splitting at their own statement terminator.
|
||||
fn starts_oracle_style_routine_body(&self) -> bool {
|
||||
let [first, rest @ ..] = self.tokens.as_slice() else {
|
||||
return false;
|
||||
};
|
||||
if !first.is_word("CREATE") {
|
||||
return false;
|
||||
}
|
||||
let rest = Self::skip_create_modifiers(rest);
|
||||
match rest {
|
||||
[object, body, ..] if object.is_any_word(&["PACKAGE", "TYPE"]) && body.is_word("BODY") => true,
|
||||
[object, ..] if object.is_any_word(&["FUNCTION", "PROCEDURE"]) => {
|
||||
Self::routine_body_starts_with_plsql_keyword(rest)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The body introducer of an Oracle-style routine. Declarations may follow `AS`/`IS` either
|
||||
/// directly or after the optional `DECLARE` keyword, so only the `DECLARE`/`BEGIN` forms are
|
||||
/// recognized; a dollar-quoted or string body is never a word token, so PostgreSQL routines
|
||||
/// cannot match here.
|
||||
fn routine_body_starts_with_plsql_keyword(tokens: &[OraclePlSqlToken]) -> bool {
|
||||
for (index, token) in tokens.iter().enumerate() {
|
||||
if !token.is_any_word(&["AS", "IS"]) {
|
||||
continue;
|
||||
}
|
||||
return tokens.get(index + 1).is_some_and(|next| next.is_any_word(&["DECLARE", "BEGIN"]));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_complete(&self) -> bool {
|
||||
if !self.starts_block() {
|
||||
return false;
|
||||
@@ -2924,8 +2998,8 @@ mod tests {
|
||||
find_statement_at_cursor_for_database, fuzzy_filter_enabled, fuzzy_like_pattern_with_escape,
|
||||
fuzzy_subsequence_match, optimize_sql_file_import_statements, prepare_sql_file_statement, split_sql_script,
|
||||
split_sql_statement_ranges_with_options, split_sql_statements_for_database, starts_with_executable_sql_keyword,
|
||||
starts_with_executable_sql_keyword_for_database, SqlDialectProfile, SqlFileStatementAction, SqlParsingOptions,
|
||||
SqlStatementSplitter,
|
||||
starts_with_executable_sql_keyword_for_database, starts_with_oracle_style_routine_body, SqlDialectProfile,
|
||||
SqlFileStatementAction, SqlParsingOptions, SqlStatementSplitter,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -3772,19 +3846,27 @@ SELECT 2;";
|
||||
|
||||
#[test]
|
||||
fn sql_dialect_profiles_map_database_types_to_parser_capabilities() {
|
||||
let default = SqlDialectProfile::for_database_type(DatabaseType::Postgres);
|
||||
assert_eq!(default, SqlDialectProfile::default());
|
||||
let default = SqlDialectProfile::default();
|
||||
assert!(default.supports_custom_delimiter_commands);
|
||||
assert!(default.supports_dollar_quoted_strings);
|
||||
assert!(!default.supports_hash_line_comments);
|
||||
assert!(!default.supports_mysql_routine_blocks);
|
||||
assert!(!default.supports_oracle_plsql_blocks);
|
||||
assert!(!default.supports_oracle_style_routine_bodies);
|
||||
assert!(!default.supports_hana_do_blocks);
|
||||
assert!(!default.supports_slash_line_block_delimiter);
|
||||
assert!(!default.supports_go_batch_separator);
|
||||
assert!(!default.keeps_sqlserver_module_batch_at_cursor);
|
||||
assert!(!default.preserves_tdsql_leading_directives);
|
||||
|
||||
for db_type in [DatabaseType::Postgres, DatabaseType::OpenGauss] {
|
||||
let profile = SqlDialectProfile::for_database_type(db_type);
|
||||
assert_eq!(profile, SqlDialectProfile::postgres_family());
|
||||
assert!(!profile.supports_oracle_plsql_blocks);
|
||||
assert!(profile.supports_oracle_style_routine_bodies);
|
||||
assert!(profile.supports_slash_line_block_delimiter);
|
||||
}
|
||||
|
||||
let mysql = SqlDialectProfile::for_database_type(DatabaseType::Mysql);
|
||||
assert!(mysql.supports_hash_line_comments);
|
||||
assert!(mysql.supports_mysql_routine_blocks);
|
||||
@@ -4220,6 +4302,183 @@ SELECT 2 AS final_statement;";
|
||||
assert_eq!(statements[2], "SELECT 2 AS final_statement");
|
||||
}
|
||||
|
||||
const POSTGRES_FAMILY_PROCEDURE_SCRIPT: &str = "\
|
||||
CREATE OR REPLACE PROCEDURE sync_yxdyurl_and_flag()
|
||||
AS DECLARE
|
||||
BEGIN
|
||||
|
||||
update test_xm_20260913 set a = '23' where a = '1';
|
||||
|
||||
COMMIT;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
RAISE;
|
||||
END;
|
||||
/
|
||||
SELECT 1;";
|
||||
|
||||
const POSTGRES_FAMILY_PROCEDURE_BODY: &str = "\
|
||||
CREATE OR REPLACE PROCEDURE sync_yxdyurl_and_flag()
|
||||
AS DECLARE
|
||||
BEGIN
|
||||
|
||||
update test_xm_20260913 set a = '23' where a = '1';
|
||||
|
||||
COMMIT;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
ROLLBACK;
|
||||
RAISE;
|
||||
END;";
|
||||
|
||||
/// Issue #8979: a GaussDB/openGauss Oracle-compatibility server reached through the plain
|
||||
/// PostgreSQL connection type must not split the PL/SQL body at its inner semicolons, or the
|
||||
/// server only receives an incomplete routine and answers with
|
||||
/// `subprogram body is not ended correctly at end of input`.
|
||||
#[test]
|
||||
fn postgres_family_keeps_oracle_style_procedure_body_in_one_statement() {
|
||||
for db_type in [DatabaseType::Postgres, DatabaseType::OpenGauss] {
|
||||
assert_eq!(
|
||||
split_sql_statements_for_database(POSTGRES_FAMILY_PROCEDURE_SCRIPT, db_type),
|
||||
vec![POSTGRES_FAMILY_PROCEDURE_BODY.to_string(), "SELECT 1".to_string()],
|
||||
"{db_type:?} must keep the procedure body whole and drop the slash terminator"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_family_current_statement_keeps_oracle_style_procedure_body_together() {
|
||||
let sql = POSTGRES_FAMILY_PROCEDURE_SCRIPT;
|
||||
let body_cursors =
|
||||
[sql.find("update test_xm").unwrap(), sql.find("COMMIT").unwrap(), sql.find("RAISE").unwrap()];
|
||||
|
||||
for db_type in [DatabaseType::Postgres, DatabaseType::OpenGauss] {
|
||||
for cursor in body_cursors {
|
||||
assert_eq!(
|
||||
find_statement_at_cursor_for_database(sql, cursor, db_type),
|
||||
POSTGRES_FAMILY_PROCEDURE_BODY,
|
||||
"{db_type:?} must resolve the cursor inside the body to the whole procedure"
|
||||
);
|
||||
}
|
||||
assert_eq!(find_statement_at_cursor_for_database(sql, sql.len(), db_type), "SELECT 1");
|
||||
}
|
||||
}
|
||||
|
||||
/// The reported scripts end with a bare `/` on the last line without a trailing newline, so the
|
||||
/// terminator has to be recognized at end of input too.
|
||||
#[test]
|
||||
fn postgres_family_drops_trailing_slash_line_without_trailing_newline() {
|
||||
for suffix in ["", "\n"] {
|
||||
let sql = format!("{POSTGRES_FAMILY_PROCEDURE_BODY}\n/{suffix}");
|
||||
for db_type in [DatabaseType::Postgres, DatabaseType::OpenGauss] {
|
||||
assert_eq!(
|
||||
split_sql_statements_for_database(&sql, db_type),
|
||||
vec![POSTGRES_FAMILY_PROCEDURE_BODY.to_string()],
|
||||
"{db_type:?} must drop the trailing slash line (suffix {suffix:?})"
|
||||
);
|
||||
assert_eq!(
|
||||
find_statement_at_cursor_for_database(&sql, sql.len(), db_type),
|
||||
POSTGRES_FAMILY_PROCEDURE_BODY
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_family_keeps_slash_terminated_routine_body_whole() {
|
||||
let sql = "\
|
||||
CREATE OR REPLACE FUNCTION f_pg() RETURNS int AS $$
|
||||
BEGIN
|
||||
RETURN 1;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
/
|
||||
SELECT f_pg();";
|
||||
|
||||
for db_type in [DatabaseType::Postgres, DatabaseType::OpenGauss] {
|
||||
assert_eq!(
|
||||
split_sql_statements_for_database(sql, db_type),
|
||||
vec![
|
||||
"CREATE OR REPLACE FUNCTION f_pg() RETURNS int AS $$
|
||||
BEGIN
|
||||
RETURN 1;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql",
|
||||
"SELECT f_pg()",
|
||||
],
|
||||
"{db_type:?} must treat the standalone slash line as a statement terminator"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_family_current_statement_keeps_dollar_quoted_routine_whole() {
|
||||
let sql = "\
|
||||
CREATE OR REPLACE FUNCTION f_pg() RETURNS int AS $$
|
||||
BEGIN
|
||||
RETURN 1;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
/
|
||||
SELECT f_pg();";
|
||||
|
||||
for db_type in [DatabaseType::Postgres, DatabaseType::OpenGauss] {
|
||||
let statement = find_statement_at_cursor_for_database(sql, sql.find("RETURN 1").unwrap(), db_type);
|
||||
assert!(statement.contains("LANGUAGE plpgsql"), "{db_type:?} resolved {statement:?}");
|
||||
assert!(!statement.contains("SELECT f_pg()"), "{db_type:?} resolved {statement:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_family_still_splits_ordinary_postgres_statements() {
|
||||
let sql = "\
|
||||
CREATE OR REPLACE FUNCTION f_pg() RETURNS int AS $$
|
||||
BEGIN
|
||||
RETURN 1;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
CREATE FUNCTION f_sql() RETURNS int AS 'SELECT 1' LANGUAGE sql;
|
||||
CREATE TRIGGER t_pg AFTER INSERT ON t_pg FOR EACH ROW EXECUTE FUNCTION f_pg();
|
||||
BEGIN;
|
||||
SELECT f_pg();
|
||||
COMMIT;";
|
||||
|
||||
assert_eq!(
|
||||
split_sql_statements_for_database(sql, DatabaseType::Postgres),
|
||||
vec![
|
||||
"CREATE OR REPLACE FUNCTION f_pg() RETURNS int AS $$
|
||||
BEGIN
|
||||
RETURN 1;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql",
|
||||
"CREATE FUNCTION f_sql() RETURNS int AS 'SELECT 1' LANGUAGE sql",
|
||||
"CREATE TRIGGER t_pg AFTER INSERT ON t_pg FOR EACH ROW EXECUTE FUNCTION f_pg()",
|
||||
"BEGIN",
|
||||
"SELECT f_pg()",
|
||||
"COMMIT",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_family_does_not_enable_full_oracle_plsql_blocks() {
|
||||
assert!(!starts_with_oracle_style_routine_body("CREATE PROCEDURE p() LANGUAGE sql AS 'SELECT 1';"));
|
||||
assert!(!starts_with_oracle_style_routine_body(
|
||||
"CREATE TRIGGER t AFTER INSERT ON x FOR EACH ROW EXECUTE FUNCTION f();"
|
||||
));
|
||||
assert!(!starts_with_oracle_style_routine_body("DECLARE c CURSOR FOR SELECT 1;"));
|
||||
assert!(!starts_with_oracle_style_routine_body(
|
||||
"CREATE FUNCTION f_pg() RETURNS int AS $$\nBEGIN\n RETURN 1;\nEND;\n$$ LANGUAGE plpgsql;"
|
||||
));
|
||||
assert!(!starts_with_oracle_style_routine_body(
|
||||
"CREATE OR REPLACE FUNCTION f_tag() RETURNS int AS $body$ BEGIN RETURN 1; END; $body$ LANGUAGE plpgsql"
|
||||
));
|
||||
assert!(starts_with_oracle_style_routine_body("CREATE OR REPLACE PROCEDURE p()\nAS DECLARE\nBEGIN"));
|
||||
assert!(starts_with_oracle_style_routine_body("CREATE PROCEDURE p() IS BEGIN"));
|
||||
assert!(starts_with_oracle_style_routine_body("CREATE OR REPLACE PACKAGE BODY pkg AS"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_like_split_keeps_issue_2405_anonymous_plsql_block_together() {
|
||||
let sql = "\
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Reference in new issue
Block a user