fix(sql): split GaussDB dollar-quoted routines

This commit is contained in:
zipg
2026-07-28 00:51:34 +08:00
committed by GitHub
parent 59f9d5b2a1
commit 09f794fd97
4 changed files with 112 additions and 4 deletions
@@ -80,6 +80,18 @@ BEGIN
NULL;
END;`;
const gaussDbDollarQuotedFunctionScript = `DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_md5_uuid;
CREATE OR REPLACE FUNCTION dbx_issue_4572_tmp_md5_uuid (v_str IN TEXT) RETURNS varchar(36) LANGUAGE PLPGSQL IMMUTABLE AS $function$
DECLARE
str1 TEXT;
BEGIN
str1 := md5(v_str);
RETURN CAST(str1 AS varchar(36));
END$function$;
DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_missing;`;
const xuguProgrammableObjectFixtures = [
`CREATE OR REPLACE PROCEDURE dbx_xugu_procedure AS
v_value INTEGER;
@@ -292,6 +304,15 @@ describe("splitSqlStatementRanges", () => {
expect(rangeSqlTexts(splitSqlStatementRanges(gaussDbNestedProcedure, "gaussdb"))).toEqual([gaussDbNestedProcedure]);
});
it("separates GaussDB dollar-quoted functions from surrounding statements", () => {
const ranges = splitSqlStatementRanges(gaussDbDollarQuotedFunctionScript, "gaussdb");
expect(rangeSqlTexts(ranges)).toEqual([
"DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_md5_uuid",
gaussDbDollarQuotedFunctionScript.slice(gaussDbDollarQuotedFunctionScript.indexOf("CREATE"), gaussDbDollarQuotedFunctionScript.lastIndexOf(";\n\nDROP")),
"DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_missing",
]);
});
it("keeps Xugu programmable object DDL together and retains its terminator", () => {
for (const sql of xuguProgrammableObjectFixtures) {
const ranges = splitSqlStatementRanges(`${sql}\nSELECT 1;`, "xugu");
+14 -1
View File
@@ -289,6 +289,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
let customDelimiter: string | null = null;
let state: QuoteState = "none";
let dollarTag = "";
let postgresDollarQuotedRoutine = false;
let i = 0;
const isWhitespace = (ch: string) => ch === " " || ch === "\t" || ch === "\r" || ch === "\n";
@@ -305,6 +306,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
if (statementStart === -1) {
statementEnd = -1;
pendingHintStart = -1;
postgresDollarQuotedRoutine = false;
return;
}
const trimmedTo = trimRangeEnd(sql, statementStart, to);
@@ -314,6 +316,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
statementStart = -1;
statementEnd = -1;
pendingHintStart = -1;
postgresDollarQuotedRoutine = false;
};
while (i < len) {
@@ -470,6 +473,9 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
const tagMatch = /^\$[A-Za-z_0-9]*\$/.exec(sql.slice(i));
if (tagMatch) {
markContent(i);
if (databaseType === "gaussdb" && statementStart !== -1 && startsWithPostgresDollarQuotedRoutinePrefix(sql.slice(statementStart, i))) {
postgresDollarQuotedRoutine = true;
}
dollarTag = tagMatch[0].slice(1, -1);
i += tagMatch[0].length;
state = "dollar";
@@ -497,7 +503,7 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
flush();
} else {
const statementSoFar = statementStart === -1 ? "" : sql.slice(statementStart, i);
const isOraclePlSql = isOracleLikeDatabase(databaseType) && statementStart !== -1 && startsWithOraclePlSqlBlock(statementSoFar);
const isOraclePlSql = isOracleLikeDatabase(databaseType) && !postgresDollarQuotedRoutine && statementStart !== -1 && startsWithOraclePlSqlBlock(statementSoFar);
const isSapHanaScriptBlock = isSapHanaScriptBlockDatabase(databaseType) && statementStart !== -1 && startsWithSapHanaScriptBlock(statementSoFar);
if (isOraclePlSql || isSapHanaScriptBlock) {
markContent(i);
@@ -1625,6 +1631,13 @@ function startsWithOraclePlSqlBlock(sql: string): boolean {
return ORACLE_PL_SQL_CREATE_OBJECT_TYPES.has(words[index] ?? "");
}
function startsWithPostgresDollarQuotedRoutinePrefix(sql: string): boolean {
const words = oraclePlSqlWords(sql);
if (words[0] !== "CREATE") return false;
const objectIndex = skipOraclePlSqlCreateModifiers(words, 1);
return (words[objectIndex] === "FUNCTION" || words[objectIndex] === "PROCEDURE") && words[words.length - 1] === "AS";
}
/** Skip OR REPLACE / FORCE / NOFORCE / EDITIONABLE modifiers after CREATE. */
function skipOraclePlSqlCreateModifiers(words: readonly string[], startIndex: number): number {
let index = startIndex;
@@ -93,6 +93,7 @@ describe("connectionStore metadata loading", () => {
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth,
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
listDatabases,
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
+76 -3
View File
@@ -113,6 +113,7 @@ struct SqlDialectProfile {
supports_custom_delimiter_commands: bool,
supports_mysql_routine_blocks: bool,
supports_dollar_quoted_strings: bool,
supports_postgres_dollar_quoted_routines: bool,
supports_hana_do_blocks: bool,
supports_go_batch_separator: bool,
keeps_sqlserver_module_batch_at_cursor: bool,
@@ -127,6 +128,7 @@ impl Default for SqlDialectProfile {
supports_custom_delimiter_commands: true,
supports_mysql_routine_blocks: false,
supports_dollar_quoted_strings: true,
supports_postgres_dollar_quoted_routines: false,
supports_hana_do_blocks: false,
supports_go_batch_separator: false,
keeps_sqlserver_module_batch_at_cursor: false,
@@ -136,6 +138,10 @@ impl Default for SqlDialectProfile {
impl SqlDialectProfile {
fn for_database_type(db_type: DatabaseType) -> Self {
if matches!(db_type, DatabaseType::Gaussdb) {
return Self::gaussdb();
}
if Self::is_oracle_like_database(db_type) {
return Self::oracle_like();
}
@@ -163,6 +169,10 @@ impl SqlDialectProfile {
Self { supports_oracle_plsql_blocks: true, supports_slash_line_block_delimiter: true, ..Self::default() }
}
fn gaussdb() -> Self {
Self { supports_postgres_dollar_quoted_routines: true, ..Self::oracle_like() }
}
fn sql_server() -> Self {
Self { supports_go_batch_separator: true, keeps_sqlserver_module_batch_at_cursor: true, ..Self::default() }
}
@@ -271,6 +281,7 @@ pub struct SqlStatementSplitter {
in_line_comment: bool,
in_block_comment: bool,
dollar_quote_tag: Option<String>,
postgres_dollar_quoted_routine: bool,
previous: Option<char>,
custom_delimiter: Option<String>,
options: SqlParsingOptions,
@@ -373,6 +384,11 @@ impl SqlStatementSplitter {
.flatten()
{
if self.custom_delimiter.is_none() && !self.on_delimiter_line() {
if self.options.profile.supports_postgres_dollar_quoted_routines
&& starts_with_postgres_dollar_quoted_routine_prefix(&self.buffer)
{
self.postgres_dollar_quoted_routine = true;
}
for tag_ch in tag.chars() {
self.buffer.push(tag_ch);
self.previous = Some(tag_ch);
@@ -416,6 +432,7 @@ impl SqlStatementSplitter {
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)
{
self.buffer.push(ch);
@@ -445,6 +462,7 @@ impl SqlStatementSplitter {
statements.push(before.to_string());
}
self.buffer.clear();
self.postgres_dollar_quoted_routine = false;
self.previous = None;
i += 1;
continue;
@@ -464,6 +482,7 @@ impl SqlStatementSplitter {
}
}
self.buffer.clear();
self.postgres_dollar_quoted_routine = false;
self.previous = None;
i += 1;
continue;
@@ -515,6 +534,7 @@ impl SqlStatementSplitter {
statements.push(statement.to_string());
}
self.buffer.clear();
self.postgres_dollar_quoted_routine = false;
self.previous = None;
}
@@ -726,6 +746,7 @@ fn split_sql_statement_ranges_with_options(sql: &str, options: SqlParsingOptions
let mut in_block_comment = false;
let mut dollar_quote_tag: Option<String> = None;
let mut custom_delimiter: Option<String> = None;
let mut postgres_dollar_quoted_routine = false;
while i < sql.len() {
if let Some(tag) = &dollar_quote_tag {
@@ -779,6 +800,11 @@ fn split_sql_statement_ranges_with_options(sql: &str, options: SqlParsingOptions
options.profile.supports_dollar_quoted_strings.then(|| dollar_quote_tag_at_str(sql, i)).flatten()
{
if custom_delimiter.is_none() && !is_on_delimiter_line(sql, start, i) {
if options.profile.supports_postgres_dollar_quoted_routines
&& starts_with_postgres_dollar_quoted_routine_prefix(&sql[start..i])
{
postgres_dollar_quoted_routine = true;
}
i += tag.len();
dollar_quote_tag = Some(tag);
continue;
@@ -790,6 +816,7 @@ fn split_sql_statement_ranges_with_options(sql: &str, options: SqlParsingOptions
if options.profile.supports_slash_line_block_delimiter && line == "/" {
push_statement_range(&mut ranges, sql, start, line_start, options);
start = i + ch.len_utf8();
postgres_dollar_quoted_routine = false;
i = start;
continue;
}
@@ -802,6 +829,7 @@ fn split_sql_statement_ranges_with_options(sql: &str, options: SqlParsingOptions
}
custom_delimiter = if new_delimiter == ";" { None } else { Some(new_delimiter.to_string()) };
start = i + ch.len_utf8();
postgres_dollar_quoted_routine = false;
i = start;
continue;
}
@@ -836,8 +864,9 @@ 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 && starts_with_oracle_plsql_block(&sql[start..i]);
let is_oracle_plsql = options.profile.supports_oracle_plsql_blocks
&& !postgres_dollar_quoted_routine
&& starts_with_oracle_plsql_block(&sql[start..i]);
if is_oracle_plsql {
if !oracle_plsql_block_is_complete(&sql[start..i + ch.len_utf8()]) {
i += ch.len_utf8();
@@ -856,6 +885,7 @@ fn split_sql_statement_ranges_with_options(sql: &str, options: SqlParsingOptions
}
i += ch.len_utf8();
start = i;
postgres_dollar_quoted_routine = false;
}
_ => {
i += ch.len_utf8();
@@ -865,6 +895,7 @@ fn split_sql_statement_ranges_with_options(sql: &str, options: SqlParsingOptions
let end = i - delimiter.len();
push_statement_range(&mut ranges, sql, start, end, options);
start = i;
postgres_dollar_quoted_routine = false;
}
}
}
@@ -1990,6 +2021,19 @@ fn starts_with_oracle_plsql_block(sql: &str) -> bool {
OraclePlSqlBlock::parse(sql).starts_block()
}
fn starts_with_postgres_dollar_quoted_routine_prefix(sql: &str) -> bool {
let block = OraclePlSqlBlock::parse(sql);
let Some(first) = block.tokens.first() else {
return false;
};
if !first.is_word("CREATE") {
return false;
}
let tokens = OraclePlSqlBlock::skip_create_modifiers(&block.tokens[1..]);
tokens.first().is_some_and(|token| token.is_any_word(&["FUNCTION", "PROCEDURE"]))
&& block.tokens.iter().rev().find_map(OraclePlSqlToken::as_word) == Some("AS")
}
fn oracle_plsql_block_is_complete(sql: &str) -> bool {
OraclePlSqlBlock::parse(sql).is_complete()
}
@@ -3072,7 +3116,6 @@ SELECT 2;";
for db_type in [
DatabaseType::Oracle,
DatabaseType::Dameng,
DatabaseType::Gaussdb,
DatabaseType::Yashandb,
DatabaseType::Oscar,
DatabaseType::OceanbaseOracle,
@@ -3081,8 +3124,15 @@ SELECT 2;";
assert_eq!(profile, SqlDialectProfile::oracle_like());
assert!(profile.supports_oracle_plsql_blocks);
assert!(profile.supports_slash_line_block_delimiter);
assert!(!profile.supports_postgres_dollar_quoted_routines);
}
let gaussdb = SqlDialectProfile::for_database_type(DatabaseType::Gaussdb);
assert_eq!(gaussdb, SqlDialectProfile::gaussdb());
assert!(gaussdb.supports_oracle_plsql_blocks);
assert!(gaussdb.supports_slash_line_block_delimiter);
assert!(gaussdb.supports_postgres_dollar_quoted_routines);
let sql_server = SqlDialectProfile::for_database_type(DatabaseType::SqlServer);
assert_eq!(sql_server, SqlDialectProfile::sql_server());
assert!(sql_server.supports_go_batch_separator);
@@ -3148,6 +3198,29 @@ END;";
assert_eq!(split_sql_statements_for_database(sql, DatabaseType::Xugu), vec![sql.to_string()]);
}
#[test]
fn gaussdb_split_separates_dollar_quoted_function_from_following_statements() {
let sql = "\
DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_md5_uuid;
CREATE OR REPLACE FUNCTION dbx_issue_4572_tmp_md5_uuid (v_str IN TEXT) RETURNS varchar(36) LANGUAGE PLPGSQL IMMUTABLE AS $function$
DECLARE
str1 TEXT;
BEGIN
str1 := md5(v_str);
RETURN CAST(str1 AS varchar(36));
END$function$;
DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_missing;";
let statements = split_sql_statements_for_database(sql, DatabaseType::Gaussdb);
assert_eq!(statements.len(), 3);
assert_eq!(statements[0], "DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_md5_uuid");
assert!(statements[1].starts_with("CREATE OR REPLACE FUNCTION dbx_issue_4572_tmp_md5_uuid"));
assert!(statements[1].ends_with("END$function$"));
assert_eq!(statements[2], "DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_missing");
}
#[test]
fn oracle_like_split_keeps_issue_2405_anonymous_plsql_block_together() {
let sql = "\