mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
fix(sql): split GaussDB dollar-quoted routines
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = "\
|
||||
|
||||
Reference in New Issue
Block a user