mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-15 14:20:43 +08:00
* fix(oceanbase): support viewing package body implementation in oracle mode (#13628) * fix(oceanbase): handle UTF-8 object source prefixes safely --------- Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
4519 lines
193 KiB
Rust
4519 lines
193 KiB
Rust
use regex::Regex;
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlparser::dialect::OracleDialect;
|
|
use sqlparser::keywords::Keyword;
|
|
use sqlparser::tokenizer::{Token, Tokenizer, Whitespace};
|
|
|
|
use crate::models::connection::DatabaseType;
|
|
use crate::sql::{oracle_plsql_block_is_complete, split_sql_statements_for_database};
|
|
use crate::types::ObjectSourceKind;
|
|
|
|
mod oracle_indexes;
|
|
mod oracle_trigger;
|
|
pub use oracle_trigger::{
|
|
prepare_oracle_trigger_sql, OracleTriggerSqlBinding, OracleTriggerSqlHeader, OracleTriggerSqlInput,
|
|
OracleTriggerSqlPurpose, PreparedOracleTriggerSql,
|
|
};
|
|
mod oracle_routines;
|
|
pub use oracle_indexes::{
|
|
extract_oracle_table_triggers, oracle_index_ddl_sources, oracle_table_created_index_identities,
|
|
oracle_trigger_ddl_sources, oracle_trigger_definitions_match, oracle_trigger_enabled,
|
|
oracle_trigger_metadata_matches, oracle_trigger_source_with_create, order_oracle_trigger_sources,
|
|
parse_oracle_table_ddl, rebase_oracle_table_ddl, rebase_oracle_trigger_ddl, separate_oracle_supporting_indexes,
|
|
separate_oracle_supporting_indexes_with_retained, split_oracle_table_ddl, split_oracle_table_ddl_with_retained,
|
|
OracleDdlNameMapping, OracleIndexDdlSource, OracleSupportingIndexDdl, OracleSupportingIndexIdentity,
|
|
OracleTableConstraintSource, OracleTableDdlParts, OracleTableDdlSource, OracleTriggerDdlSource,
|
|
};
|
|
pub use oracle_routines::{
|
|
analyze_oracle_package_body_draft, analyze_oracle_routine_draft, compact_oracle_ddl,
|
|
comparable_oracle_routine_definition, prepare_oracle_routine_definition, validate_oracle_routine_target_definition,
|
|
OracleDraftIssue, OracleDraftName, OracleDraftPackageCall, OracleDraftPackageMember, OracleDraftPackageMemberKind,
|
|
OracleDraftParameter, OracleDraftQuery, OracleDraftReference, OracleDraftReferenceRole,
|
|
OracleRoutineDefinitionInput, OracleRoutineDraftAnalysis,
|
|
};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EditableObjectSourceSqlInput {
|
|
pub database_type: DatabaseType,
|
|
pub object_type: ObjectSourceKind,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub schema: Option<String>,
|
|
pub name: String,
|
|
pub source: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct OracleObjectSourceValidationInput {
|
|
pub database_type: DatabaseType,
|
|
pub object_type: ObjectSourceKind,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub schema: Option<String>,
|
|
pub name: String,
|
|
pub source: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RoutineRenameObjectSourceInput {
|
|
pub database_type: DatabaseType,
|
|
pub object_type: ObjectSourceKind,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub schema: Option<String>,
|
|
pub name: String,
|
|
pub new_name: String,
|
|
pub source: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct OracleRoutineReferenceRewriteInput {
|
|
pub database_type: DatabaseType,
|
|
pub caller_type: ObjectSourceKind,
|
|
pub target_type: ObjectSourceKind,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub schema: Option<String>,
|
|
pub old_name: String,
|
|
pub new_name: String,
|
|
pub source: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct OracleTableReferenceRewriteInput {
|
|
pub database_type: DatabaseType,
|
|
pub object_type: ObjectSourceKind,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub schema: Option<String>,
|
|
pub old_name: String,
|
|
pub new_name: String,
|
|
pub source: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct BuildViewDdlInput {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub database_type: Option<DatabaseType>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub schema: Option<String>,
|
|
pub name: String,
|
|
pub source: String,
|
|
/// Driver-reported identifier quote (e.g. `` ` `` for Kingbase MySQL
|
|
/// compatibility mode). When set, overrides the database_type-based quote
|
|
/// selection so hyphenated schemas render as valid identifiers.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub identifier_quote: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum ObjectSourceSaveExecutionMode {
|
|
#[serde(rename = "single")]
|
|
Single,
|
|
#[serde(rename = "script")]
|
|
Script,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct RoutineDeclaration {
|
|
kind: ObjectSourceKind,
|
|
name: String,
|
|
signature: String,
|
|
}
|
|
|
|
pub fn supports_source_backed_routine_rename(
|
|
database_type: Option<DatabaseType>,
|
|
object_type: ObjectSourceKind,
|
|
) -> bool {
|
|
if !matches!(object_type, ObjectSourceKind::Function | ObjectSourceKind::Procedure) {
|
|
return false;
|
|
}
|
|
let Some(database_type) = database_type else {
|
|
return false;
|
|
};
|
|
database_type != DatabaseType::SqlServer
|
|
&& (is_mysql_like(database_type)
|
|
|| is_postgres_like(database_type)
|
|
|| is_oracle_like(database_type)
|
|
|| database_type == DatabaseType::OceanbaseOracle)
|
|
}
|
|
|
|
pub fn build_routine_rename_object_source_statements(
|
|
input: RoutineRenameObjectSourceInput,
|
|
) -> Result<Vec<String>, String> {
|
|
if !supports_source_backed_routine_rename(Some(input.database_type), input.object_type.clone()) {
|
|
return Err(format!(
|
|
"Renaming {:?} from source is not supported for {:?}.",
|
|
input.object_type, input.database_type
|
|
));
|
|
}
|
|
|
|
let source = input.source.trim();
|
|
if input.database_type == DatabaseType::Oracle || input.database_type == DatabaseType::OceanbaseOracle {
|
|
validate_oracle_object_source(
|
|
input.database_type,
|
|
&input.object_type,
|
|
input.schema.as_deref(),
|
|
&input.name,
|
|
source,
|
|
)?;
|
|
}
|
|
let declaration = if is_mysql_like(input.database_type) {
|
|
mysql_routine_declaration(source)
|
|
} else {
|
|
routine_declaration(source)
|
|
};
|
|
let Some(declaration) = declaration else {
|
|
return Err(format!("Cannot find a CREATE {:?} declaration in the object source.", input.object_type));
|
|
};
|
|
if declaration.kind != input.object_type {
|
|
return Err(format!("Cannot find a CREATE {:?} declaration in the object source.", input.object_type));
|
|
}
|
|
|
|
let renamed_source = if is_mysql_like(input.database_type) {
|
|
replace_mysql_routine_declaration_name(source, &input.new_name)
|
|
} else {
|
|
replace_sql_routine_declaration_name(source, input.schema.as_deref(), &input.new_name)
|
|
};
|
|
let Some(renamed_source) = renamed_source else {
|
|
return Err(format!("Cannot rewrite the {:?} name in the object source.", input.object_type));
|
|
};
|
|
|
|
if is_oracle_like(input.database_type) || input.database_type == DatabaseType::OceanbaseOracle {
|
|
let renamed_source = rewrite_oracle_routine_self_references(
|
|
&renamed_source,
|
|
&input.name,
|
|
&input.new_name,
|
|
input.schema.as_deref(),
|
|
input.object_type.clone(),
|
|
)?;
|
|
return Ok(vec![
|
|
ensure_semicolon(&renamed_source),
|
|
format!(
|
|
"DROP {} {};",
|
|
object_type_keyword(&input.object_type),
|
|
postgres_qualified_name(input.schema.as_deref(), &input.name)
|
|
),
|
|
]);
|
|
}
|
|
|
|
build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
|
database_type: input.database_type,
|
|
object_type: input.object_type,
|
|
schema: input.schema,
|
|
name: input.name,
|
|
source: renamed_source,
|
|
})
|
|
}
|
|
|
|
pub fn rewrite_oracle_routine_references(input: OracleRoutineReferenceRewriteInput) -> Result<String, String> {
|
|
if !(is_oracle_like(input.database_type) || input.database_type == DatabaseType::OceanbaseOracle)
|
|
|| !matches!(
|
|
input.caller_type,
|
|
ObjectSourceKind::View | ObjectSourceKind::Procedure | ObjectSourceKind::Function
|
|
)
|
|
{
|
|
return Err("Static routine reference rewriting is supported only for Oracle-family views and routines.".into());
|
|
}
|
|
if input.old_name == input.new_name {
|
|
return Ok(input.source);
|
|
}
|
|
let tokens = Tokenizer::new(&OracleDialect {}, &input.source)
|
|
.tokenize_with_location()
|
|
.map_err(|error| format!("Cannot safely tokenize the dependent source: {error}"))?;
|
|
let significant = tokens
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(index, token)| (!matches!(token.token, Token::Whitespace(_))).then_some(index))
|
|
.collect::<Vec<_>>();
|
|
if matches!(input.caller_type, ObjectSourceKind::Procedure | ObjectSourceKind::Function) {
|
|
if let Some(execute_index) =
|
|
find_oracle_execute_immediate(significant.iter().copied().map(|index| (index, &tokens[index].token)))
|
|
{
|
|
let location = tokens[execute_index].span.start;
|
|
return Err(format!(
|
|
"Dynamic SQL must be rewritten as a verifiable static call at line {}, column {}.",
|
|
location.line, location.column
|
|
));
|
|
}
|
|
}
|
|
if significant.windows(2).any(|pair| {
|
|
(token_word_is(&tokens[pair[0]].token, "FUNCTION") || token_word_is(&tokens[pair[0]].token, "PROCEDURE"))
|
|
&& (token_matches_identifier(&tokens[pair[1]].token, &input.old_name)
|
|
|| token_matches_identifier(&tokens[pair[1]].token, &input.new_name))
|
|
}) {
|
|
return Err(format!(
|
|
"A local routine named {} or {} may shadow the dependent reference.",
|
|
input.old_name, input.new_name
|
|
));
|
|
}
|
|
let mut local_bindings = Vec::new();
|
|
if matches!(input.caller_type, ObjectSourceKind::Procedure | ObjectSourceKind::Function) {
|
|
for body_start in 0..significant.len() {
|
|
if !token_word_is(&tokens[significant[body_start]].token, "AS")
|
|
&& !token_word_is(&tokens[significant[body_start]].token, "IS")
|
|
{
|
|
continue;
|
|
}
|
|
if let Some(body_end) = (body_start + 1..significant.len())
|
|
.find(|position| token_word_is(&tokens[significant[*position]].token, "BEGIN"))
|
|
{
|
|
local_bindings.extend(significant[body_start + 1..body_end].iter().map(|index| &tokens[*index].token));
|
|
}
|
|
|
|
// Formal parameters are declared before AS/IS and can shadow the
|
|
// dependent name just like declarations in the block body.
|
|
if let Some(close) =
|
|
(0..body_start).rev().find(|position| tokens[significant[*position]].token == Token::RParen)
|
|
{
|
|
let mut nesting = 0usize;
|
|
let mut open = None;
|
|
for position in (0..=close).rev() {
|
|
match tokens[significant[position]].token {
|
|
Token::RParen => nesting += 1,
|
|
Token::LParen => {
|
|
nesting -= 1;
|
|
if nesting == 0 {
|
|
open = Some(position);
|
|
break;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if let Some(open) = open {
|
|
local_bindings.extend(significant[open + 1..close].iter().map(|index| &tokens[*index].token));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Nested anonymous blocks can declare a collection or record after
|
|
// the outer BEGIN. Reject a possibly shadowed rewrite there too.
|
|
for declaration in 0..significant.len() {
|
|
if !token_word_is(&tokens[significant[declaration]].token, "DECLARE") {
|
|
continue;
|
|
}
|
|
if let Some(block_start) = (declaration + 1..significant.len())
|
|
.find(|position| token_word_is(&tokens[significant[*position]].token, "BEGIN"))
|
|
{
|
|
local_bindings
|
|
.extend(significant[declaration + 1..block_start].iter().map(|index| &tokens[*index].token));
|
|
}
|
|
}
|
|
}
|
|
let mut edits = Vec::new();
|
|
for (position, token_index) in significant.iter().copied().enumerate() {
|
|
let token = &tokens[token_index];
|
|
if !token_matches_identifier(&token.token, &input.old_name) {
|
|
continue;
|
|
}
|
|
let previous = position.checked_sub(1).map(|index| &tokens[significant[index]].token);
|
|
let previous_previous = position.checked_sub(2).map(|index| &tokens[significant[index]].token);
|
|
if previous == Some(&Token::Period) {
|
|
let Some(Token::Word(owner)) = previous_previous else {
|
|
return Err(format!("Cannot determine which routine reference to rename: {}.", input.old_name));
|
|
};
|
|
if !input.schema.as_deref().is_some_and(|schema| identifier_matches(owner, schema)) {
|
|
continue;
|
|
}
|
|
if local_bindings
|
|
.iter()
|
|
.any(|binding| token_matches_identifier(binding, input.schema.as_deref().unwrap_or_default()))
|
|
{
|
|
return Err(format!(
|
|
"The schema qualifier may be shadowed by a local declaration while rewriting {}.",
|
|
input.old_name
|
|
));
|
|
}
|
|
} else if local_bindings.iter().any(|binding| {
|
|
token_matches_identifier(binding, &input.old_name) || token_matches_identifier(binding, &input.new_name)
|
|
}) {
|
|
return Err(format!(
|
|
"A local declaration may shadow the {} or {} reference.",
|
|
input.old_name, input.new_name
|
|
));
|
|
}
|
|
let next = significant.get(position + 1).map(|index| &tokens[*index].token);
|
|
let routine_caller = matches!(input.caller_type, ObjectSourceKind::Procedure | ObjectSourceKind::Function);
|
|
let parenthesized_call = (input.target_type == ObjectSourceKind::Function
|
|
|| (input.target_type == ObjectSourceKind::Procedure && routine_caller))
|
|
&& matches!(next, Some(Token::LParen));
|
|
let procedure_call = input.target_type == ObjectSourceKind::Procedure
|
|
&& matches!(input.caller_type, ObjectSourceKind::Procedure | ObjectSourceKind::Function)
|
|
&& matches!(next, Some(Token::SemiColon));
|
|
if parenthesized_call || procedure_call {
|
|
edits.push((token.span, quote_postgres_identifier(&input.new_name)));
|
|
} else if matches!(next, Some(Token::Period)) {
|
|
return Err(format!("The dependent reference {} is qualified in an unsupported form.", input.old_name));
|
|
}
|
|
}
|
|
if edits.is_empty() {
|
|
return Err(format!("The dependent source does not contain a verifiable static call to {}.", input.old_name));
|
|
}
|
|
let mut result = input.source;
|
|
for (span, replacement) in edits.into_iter().rev() {
|
|
let start = routine_location_to_byte_offset(&result, span.start)
|
|
.ok_or_else(|| "Cannot map routine token positions to the dependent source text.".to_string())?;
|
|
let end = routine_location_to_byte_offset(&result, span.end)
|
|
.ok_or_else(|| "Cannot map routine token positions to the dependent source text.".to_string())?;
|
|
if start >= end || end > result.len() {
|
|
return Err("Routine token positions overlap or exceed the dependent source text.".into());
|
|
}
|
|
result.replace_range(start..end, &replacement);
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
pub fn rewrite_oracle_table_references(input: OracleTableReferenceRewriteInput) -> Result<String, String> {
|
|
if !(is_oracle_like(input.database_type) || input.database_type == DatabaseType::OceanbaseOracle)
|
|
|| !matches!(
|
|
input.object_type,
|
|
ObjectSourceKind::Procedure | ObjectSourceKind::Function | ObjectSourceKind::Trigger
|
|
)
|
|
{
|
|
return Err(
|
|
"Static table reference rewriting supports only Oracle-family procedures, functions, and triggers.".into(),
|
|
);
|
|
}
|
|
let tokens = Tokenizer::new(&OracleDialect {}, &input.source)
|
|
.tokenize_with_location()
|
|
.map_err(|error| format!("Cannot safely tokenize the dependent source: {error}"))?;
|
|
let significant = tokens
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(index, token)| (!matches!(token.token, Token::Whitespace(_))).then_some(index))
|
|
.collect::<Vec<_>>();
|
|
if let Some(execute_index) =
|
|
find_oracle_execute_immediate(significant.iter().copied().map(|index| (index, &tokens[index].token)))
|
|
{
|
|
let location = tokens[execute_index].span.start;
|
|
return Err(format!(
|
|
"Dynamic SQL is outside the supported table-reference subset at line {}, column {}.",
|
|
location.line, location.column
|
|
));
|
|
}
|
|
|
|
let mut edits = Vec::new();
|
|
for (position, token_index) in significant.iter().copied().enumerate() {
|
|
let token = &tokens[token_index];
|
|
if !token_matches_identifier(&token.token, &input.old_name) {
|
|
continue;
|
|
}
|
|
let previous = position.checked_sub(1).map(|index| &tokens[significant[index]].token);
|
|
let previous_previous = position.checked_sub(2).map(|index| &tokens[significant[index]].token);
|
|
let next = significant.get(position + 1).map(|index| &tokens[*index].token);
|
|
let next_next = significant.get(position + 2).map(|index| &tokens[*index].token);
|
|
|
|
let owner_qualified = previous == Some(&Token::Period);
|
|
if owner_qualified {
|
|
let Some(Token::Word(owner)) = previous_previous else {
|
|
return Err(format!("Cannot determine which table reference to rename: {}.", input.old_name));
|
|
};
|
|
if !input.schema.as_deref().is_some_and(|schema| identifier_matches(owner, schema)) {
|
|
continue;
|
|
}
|
|
if position >= 3 && matches!(tokens[significant[position - 3]].token, Token::Period) {
|
|
return Err(format!("Three-part table reference {} is outside the supported subset.", input.old_name));
|
|
}
|
|
}
|
|
let context_position = if owner_qualified { position - 2 } else { position };
|
|
let context_previous = context_position.checked_sub(1).map(|index| &tokens[significant[index]].token);
|
|
let context_previous_previous = context_position.checked_sub(2).map(|index| &tokens[significant[index]].token);
|
|
|
|
let type_reference = next == Some(&Token::Period)
|
|
&& next_next.is_some_and(|column| matches!(column, Token::Word(_)))
|
|
&& significant
|
|
.get(position + 3)
|
|
.is_some_and(|index| matches!(tokens[*index].token, Token::Mod | Token::Char('%')))
|
|
&& significant.get(position + 4).is_some_and(|index| token_word_is(&tokens[*index].token, "TYPE"));
|
|
let rowtype_reference = next.is_some_and(|value| matches!(value, Token::Mod | Token::Char('%')))
|
|
&& next_next.is_some_and(|value| token_word_is(value, "ROWTYPE"));
|
|
let relation_reference = context_previous.is_some_and(|value| {
|
|
token_word_is(value, "FROM")
|
|
|| token_word_is(value, "UPDATE")
|
|
|| (input.object_type == ObjectSourceKind::Trigger && token_word_is(value, "ON"))
|
|
}) || (context_previous.is_some_and(|value| token_word_is(value, "INTO"))
|
|
&& context_previous_previous
|
|
.is_some_and(|value| token_word_is(value, "INSERT") || token_word_is(value, "MERGE")));
|
|
|
|
if relation_reference && context_previous.is_some_and(|value| token_word_is(value, "FROM")) {
|
|
let multiple_relations = significant
|
|
.iter()
|
|
.skip(position + 1)
|
|
.map(|index| &tokens[*index].token)
|
|
.take_while(|candidate| {
|
|
![
|
|
"WHERE",
|
|
"GROUP",
|
|
"ORDER",
|
|
"HAVING",
|
|
"CONNECT",
|
|
"START",
|
|
"UNION",
|
|
"MINUS",
|
|
"INTERSECT",
|
|
"FETCH",
|
|
"FOR",
|
|
]
|
|
.iter()
|
|
.any(|clause| token_word_is(candidate, clause))
|
|
&& !matches!(candidate, Token::SemiColon)
|
|
})
|
|
.any(|candidate| matches!(candidate, Token::Comma) || token_word_is(candidate, "JOIN"));
|
|
if multiple_relations {
|
|
return Err(format!(
|
|
"Multiple relations in one FROM clause are outside the supported subset at line {}, column {}.",
|
|
token.span.start.line, token.span.start.column
|
|
));
|
|
}
|
|
}
|
|
|
|
if !(type_reference || rowtype_reference || relation_reference) {
|
|
return Err(format!(
|
|
"Cannot safely identify the table reference at line {}, column {}.",
|
|
token.span.start.line, token.span.start.column
|
|
));
|
|
}
|
|
edits.push((token.span, quote_postgres_identifier(&input.new_name)));
|
|
}
|
|
if edits.is_empty() {
|
|
return Err(format!("The dependent source has no verifiable static reference to table {}.", input.old_name));
|
|
}
|
|
let mut result = input.source;
|
|
for (span, replacement) in edits.into_iter().rev() {
|
|
let start = routine_location_to_byte_offset(&result, span.start)
|
|
.ok_or_else(|| "Cannot map table-reference token positions to the dependent source text.".to_string())?;
|
|
let end = routine_location_to_byte_offset(&result, span.end)
|
|
.ok_or_else(|| "Cannot map table-reference token positions to the dependent source text.".to_string())?;
|
|
if start >= end || end > result.len() {
|
|
return Err("Table-reference token positions overlap or exceed the dependent source text.".into());
|
|
}
|
|
result.replace_range(start..end, &replacement);
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
pub fn build_executable_object_source_statements(input: EditableObjectSourceSqlInput) -> Result<Vec<String>, String> {
|
|
build_executable_object_source_statements_with_mysql_validation_name(input, None, true)
|
|
}
|
|
|
|
/// Builds executable MySQL routine replacement statements using a caller-owned
|
|
/// validation name. Transfer callers pass a per-run name and preflight its
|
|
/// absence, so validation never drops an unrelated user routine.
|
|
pub fn build_mysql_routine_replacement_statements(
|
|
input: EditableObjectSourceSqlInput,
|
|
validation_name: &str,
|
|
) -> Result<Vec<String>, String> {
|
|
if !is_mysql_like(input.database_type)
|
|
|| !matches!(input.object_type, ObjectSourceKind::Procedure | ObjectSourceKind::Function)
|
|
{
|
|
return Err("MySQL routine replacement is supported only for MySQL procedures and functions.".to_string());
|
|
}
|
|
if validation_name.trim().is_empty() {
|
|
return Err("MySQL routine validation name must not be empty.".to_string());
|
|
}
|
|
build_executable_object_source_statements_with_mysql_validation_name(input, Some(validation_name), false)
|
|
}
|
|
|
|
fn build_executable_object_source_statements_with_mysql_validation_name(
|
|
input: EditableObjectSourceSqlInput,
|
|
mysql_validation_name: Option<&str>,
|
|
drop_mysql_validation_before_create: bool,
|
|
) -> Result<Vec<String>, String> {
|
|
if input.database_type == DatabaseType::OceanbaseOracle && input.object_type == ObjectSourceKind::Sequence {
|
|
return Ok(vec![oceanbase_sequence_edit_sql(&input)?]);
|
|
}
|
|
let source = input.source.trim();
|
|
let source = if is_opengauss_like(input.database_type) && input.object_type == ObjectSourceKind::Procedure {
|
|
strip_standalone_trailing_slash(source)
|
|
} else {
|
|
source
|
|
};
|
|
if matches!(input.database_type, DatabaseType::Oracle | DatabaseType::OceanbaseOracle | DatabaseType::Dameng)
|
|
&& matches!(input.object_type, ObjectSourceKind::Type | ObjectSourceKind::TypeBody)
|
|
{
|
|
validate_oracle_object_source(
|
|
input.database_type,
|
|
&input.object_type,
|
|
input.schema.as_deref(),
|
|
&input.name,
|
|
source,
|
|
)?;
|
|
return Ok(vec![oracle_management_ddl(source, input.schema.as_deref(), &input.object_type)?]);
|
|
}
|
|
if (input.database_type == DatabaseType::Oracle || input.database_type == DatabaseType::OceanbaseOracle)
|
|
&& matches!(
|
|
input.object_type,
|
|
ObjectSourceKind::Procedure
|
|
| ObjectSourceKind::Function
|
|
| ObjectSourceKind::Package
|
|
| ObjectSourceKind::PackageBody
|
|
)
|
|
&& source_starts_with_create(source)
|
|
{
|
|
validate_oracle_object_source(
|
|
input.database_type,
|
|
&input.object_type,
|
|
input.schema.as_deref(),
|
|
&input.name,
|
|
source,
|
|
)?;
|
|
}
|
|
if input.database_type == DatabaseType::SqlServer {
|
|
if input.object_type == ObjectSourceKind::View {
|
|
return Ok(vec![build_sqlserver_alter_view_sql(input.schema.as_deref(), &input.name, source)]);
|
|
}
|
|
// CREATE OR ALTER requires SQL Server 2016 SP1+, while ALTER keeps existing routines executable on older servers.
|
|
return Ok(vec![replace_sqlserver_create_with_alter(source)]);
|
|
}
|
|
|
|
if input.database_type == DatabaseType::DuckDb && input.object_type == ObjectSourceKind::View {
|
|
return Ok(vec![executable_duckdb_view_ddl(input.schema.as_deref(), &input.name, source)]);
|
|
}
|
|
|
|
if is_postgres_like(input.database_type) && input.object_type == ObjectSourceKind::View {
|
|
if let Some(sql) = executable_postgres_view_ddl(source) {
|
|
return Ok(vec![sql]);
|
|
}
|
|
if source_starts_with_alter(source) {
|
|
// ALTER VIEW is already executable DDL, but it is not a view body.
|
|
return Ok(vec![ensure_semicolon(source)]);
|
|
}
|
|
return Ok(vec![format!(
|
|
"CREATE OR REPLACE VIEW {} AS\n{}",
|
|
postgres_qualified_name(input.schema.as_deref(), &input.name),
|
|
ensure_semicolon(source)
|
|
)]);
|
|
}
|
|
|
|
if (is_oracle_like(input.database_type) || input.database_type == DatabaseType::OceanbaseOracle)
|
|
&& input.object_type == ObjectSourceKind::View
|
|
{
|
|
return Ok(vec![executable_oracle_view_ddl(input.schema.as_deref(), &input.name, source)]);
|
|
}
|
|
|
|
if (is_oracle_like(input.database_type) || input.database_type == DatabaseType::OceanbaseOracle)
|
|
&& matches!(
|
|
input.object_type,
|
|
ObjectSourceKind::Function
|
|
| ObjectSourceKind::Procedure
|
|
| ObjectSourceKind::Package
|
|
| ObjectSourceKind::PackageBody
|
|
| ObjectSourceKind::Type
|
|
| ObjectSourceKind::TypeBody
|
|
)
|
|
{
|
|
return Ok(vec![executable_oracle_routine_ddl(source)]);
|
|
}
|
|
|
|
if matches!(input.database_type, DatabaseType::Informix | DatabaseType::Gbase8s)
|
|
&& input.object_type == ObjectSourceKind::View
|
|
{
|
|
return Ok(executable_informix_view_statements(input.schema.as_deref(), &input.name, source));
|
|
}
|
|
|
|
if is_mysql_like(input.database_type)
|
|
&& matches!(input.object_type, ObjectSourceKind::Function | ObjectSourceKind::Procedure)
|
|
{
|
|
return Ok(executable_mysql_routine_statements(
|
|
&input,
|
|
source,
|
|
mysql_validation_name,
|
|
drop_mysql_validation_before_create,
|
|
));
|
|
}
|
|
|
|
let create_statement = ensure_semicolon(source);
|
|
let cleanup = build_routine_rename_cleanup(&input, source);
|
|
Ok(if let Some(cleanup) = cleanup { vec![create_statement, cleanup] } else { vec![create_statement] })
|
|
}
|
|
|
|
pub fn validate_oracle_executable_object_source(input: OracleObjectSourceValidationInput) -> Result<(), String> {
|
|
if input.object_type == ObjectSourceKind::Trigger {
|
|
return oracle_trigger::validate(input);
|
|
}
|
|
if !matches!(input.database_type, DatabaseType::Oracle | DatabaseType::OceanbaseOracle)
|
|
|| !matches!(
|
|
input.object_type,
|
|
ObjectSourceKind::Procedure
|
|
| ObjectSourceKind::Function
|
|
| ObjectSourceKind::Package
|
|
| ObjectSourceKind::PackageBody
|
|
)
|
|
{
|
|
return Err("Strict object-source validation is supported only for Oracle routines and packages.".to_string());
|
|
}
|
|
validate_oracle_object_source(
|
|
input.database_type,
|
|
&input.object_type,
|
|
input.schema.as_deref(),
|
|
&input.name,
|
|
&input.source,
|
|
)
|
|
}
|
|
|
|
pub fn fingerprint_oracle_routine_object_source(input: EditableObjectSourceSqlInput) -> Result<String, String> {
|
|
if !matches!(input.database_type, DatabaseType::Oracle | DatabaseType::OceanbaseOracle)
|
|
|| !matches!(
|
|
input.object_type,
|
|
ObjectSourceKind::Procedure
|
|
| ObjectSourceKind::Function
|
|
| ObjectSourceKind::Package
|
|
| ObjectSourceKind::PackageBody
|
|
)
|
|
{
|
|
return Err("Routine source fingerprinting is supported only for Oracle routines and packages.".into());
|
|
}
|
|
validate_oracle_object_source(
|
|
input.database_type,
|
|
&input.object_type,
|
|
input.schema.as_deref(),
|
|
&input.name,
|
|
&input.source,
|
|
)?;
|
|
|
|
let tokens = Tokenizer::new(&OracleDialect {}, &input.source)
|
|
.tokenize_with_location()
|
|
.map_err(|error| format!("Cannot tokenize Oracle routine source: {error}"))?;
|
|
let mut tokens = tokens
|
|
.iter()
|
|
.filter(|token| {
|
|
!matches!(
|
|
&token.token,
|
|
Token::Whitespace(Whitespace::Space | Whitespace::Tab | Whitespace::Newline) | Token::EOF
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let last_statement_token = tokens.iter().rposition(|token| {
|
|
!matches!(
|
|
token.token,
|
|
Token::Whitespace(Whitespace::SingleLineComment { .. } | Whitespace::MultiLineComment(_))
|
|
)
|
|
});
|
|
if last_statement_token.is_some_and(|index| tokens[index].token == Token::SemiColon) {
|
|
tokens.remove(last_statement_token.unwrap());
|
|
}
|
|
let token_fingerprints = tokens
|
|
.iter()
|
|
.map(|token| {
|
|
let start = routine_location_to_byte_offset(&input.source, token.span.start)
|
|
.ok_or_else(|| "Cannot map Oracle routine token positions to the source text.".to_string())?;
|
|
let end = routine_location_to_byte_offset(&input.source, token.span.end)
|
|
.ok_or_else(|| "Cannot map Oracle routine token positions to the source text.".to_string())?;
|
|
if start >= end || end > input.source.len() {
|
|
return Err("Oracle routine token positions overlap or exceed the source text.".to_string());
|
|
}
|
|
Ok(input.source[start..end].to_string())
|
|
})
|
|
.collect::<Result<Vec<_>, String>>()?;
|
|
serde_json::to_string(&token_fingerprints)
|
|
.map_err(|error| format!("Cannot encode Oracle routine fingerprint: {error}"))
|
|
}
|
|
|
|
pub fn build_executable_object_source_sql(input: EditableObjectSourceSqlInput) -> Result<String, String> {
|
|
Ok(build_executable_object_source_statements(input)?.join("\n"))
|
|
}
|
|
|
|
fn validate_oracle_object_source(
|
|
database_type: DatabaseType,
|
|
object_type: &ObjectSourceKind,
|
|
schema: Option<&str>,
|
|
name: &str,
|
|
source: &str,
|
|
) -> Result<(), String> {
|
|
let is_type = matches!(object_type, ObjectSourceKind::Type | ObjectSourceKind::TypeBody);
|
|
let keyword_is = |token: &Token, keyword: &str| {
|
|
if is_type {
|
|
unquoted_keyword_is(token, keyword)
|
|
} else {
|
|
token_word_is(token, keyword)
|
|
}
|
|
};
|
|
let identifier_is = |token: &Token, expected: &str| {
|
|
if is_type {
|
|
matches!(token, Token::Word(word) if if word.quote_style.is_some() { word.value == expected } else { word.value.to_ascii_uppercase() == expected })
|
|
} else {
|
|
oracle_identifier_matches(token, expected)
|
|
}
|
|
};
|
|
let initial_tokens =
|
|
Tokenizer::new(&OracleDialect {}, source).tokenize_with_location().map_err(|error| error.to_string())?;
|
|
let first_significant = initial_tokens.iter().find(|token| !matches!(token.token, Token::Whitespace(_)));
|
|
let source = if first_significant.is_some_and(|token| keyword_is(&token.token, "CREATE")) {
|
|
source.to_string()
|
|
} else if matches!(
|
|
object_type,
|
|
ObjectSourceKind::Procedure
|
|
| ObjectSourceKind::Function
|
|
| ObjectSourceKind::Package
|
|
| ObjectSourceKind::PackageBody
|
|
| ObjectSourceKind::Type
|
|
| ObjectSourceKind::TypeBody
|
|
) {
|
|
format!("CREATE OR REPLACE {}", source.trim())
|
|
} else {
|
|
format!("CREATE {}", source.trim())
|
|
};
|
|
let statements = split_sql_statements_for_database(&source, database_type);
|
|
if statements.len() != 1 {
|
|
return Err("Oracle object source must contain exactly one complete object definition.".to_string());
|
|
}
|
|
|
|
let tokens =
|
|
Tokenizer::new(&OracleDialect {}, &source).tokenize_with_location().map_err(|error| error.to_string())?;
|
|
let significant = tokens
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(index, token)| (!matches!(token.token, Token::Whitespace(_))).then_some(index))
|
|
.collect::<Vec<_>>();
|
|
let token_at = |position: usize| significant.get(position).map(|index| &tokens[*index].token);
|
|
let mut position = 0;
|
|
if !token_at(position).is_some_and(|token| keyword_is(token, "CREATE")) {
|
|
return Err("Oracle object source must begin with CREATE.".to_string());
|
|
}
|
|
position += 1;
|
|
if token_at(position).is_some_and(|token| keyword_is(token, "OR")) {
|
|
if !token_at(position + 1).is_some_and(|token| keyword_is(token, "REPLACE")) {
|
|
return Err("Oracle object source has an invalid CREATE prefix.".to_string());
|
|
}
|
|
position += 2;
|
|
}
|
|
if token_at(position).is_some_and(|token| keyword_is(token, "EDITIONABLE") || keyword_is(token, "NONEDITIONABLE")) {
|
|
position += 1;
|
|
}
|
|
|
|
let kind_matches = match object_type {
|
|
ObjectSourceKind::Procedure => token_at(position).is_some_and(|token| token_word_is(token, "PROCEDURE")),
|
|
ObjectSourceKind::Function => token_at(position).is_some_and(|token| token_word_is(token, "FUNCTION")),
|
|
ObjectSourceKind::Package => {
|
|
token_at(position).is_some_and(|token| token_word_is(token, "PACKAGE"))
|
|
&& !token_at(position + 1).is_some_and(|token| token_word_is(token, "BODY"))
|
|
}
|
|
ObjectSourceKind::PackageBody => {
|
|
token_at(position).is_some_and(|token| token_word_is(token, "PACKAGE"))
|
|
&& token_at(position + 1).is_some_and(|token| token_word_is(token, "BODY"))
|
|
}
|
|
ObjectSourceKind::Type => {
|
|
token_at(position).is_some_and(|token| unquoted_keyword_is(token, "TYPE"))
|
|
&& !token_at(position + 1).is_some_and(|token| unquoted_keyword_is(token, "BODY"))
|
|
}
|
|
ObjectSourceKind::TypeBody => {
|
|
token_at(position).is_some_and(|token| unquoted_keyword_is(token, "TYPE"))
|
|
&& token_at(position + 1).is_some_and(|token| unquoted_keyword_is(token, "BODY"))
|
|
}
|
|
ObjectSourceKind::View => token_at(position).is_some_and(|token| unquoted_keyword_is(token, "VIEW")),
|
|
ObjectSourceKind::Trigger => token_at(position).is_some_and(|token| unquoted_keyword_is(token, "TRIGGER")),
|
|
_ => false,
|
|
};
|
|
if !kind_matches {
|
|
return Err(format!("Oracle object source declaration does not match {object_type:?}."));
|
|
}
|
|
position += 1;
|
|
if *object_type == ObjectSourceKind::PackageBody || *object_type == ObjectSourceKind::TypeBody {
|
|
position += 1;
|
|
}
|
|
|
|
let first_name = token_at(position).ok_or_else(|| "Oracle object source has no declaration name.".to_string())?;
|
|
let (owner, declared_name, consumed) = if token_at(position + 1) == Some(&Token::Period) {
|
|
let declared_name = token_at(position + 2)
|
|
.ok_or_else(|| "Oracle object source has an incomplete qualified name.".to_string())?;
|
|
(Some(first_name), declared_name, 3)
|
|
} else {
|
|
(None, first_name, 1)
|
|
};
|
|
if !identifier_is(declared_name, name) {
|
|
return Err(format!("Oracle source declares a different object than {name}."));
|
|
}
|
|
if let Some(Token::Word(owner)) = owner {
|
|
if schema.is_some_and(|schema| !identifier_is(&Token::Word(owner.clone()), schema)) {
|
|
return Err("Oracle object source owner does not match the selected schema.".to_string());
|
|
}
|
|
} else if owner.is_some() {
|
|
return Err("Oracle object source has an invalid owner identifier.".to_string());
|
|
}
|
|
if token_at(position + consumed).is_some_and(|token| token == &Token::Period) {
|
|
return Err("Three-part Oracle object names are not supported.".to_string());
|
|
}
|
|
if matches!(object_type, ObjectSourceKind::Type | ObjectSourceKind::TypeBody)
|
|
&& significant.iter().enumerate().any(|(index, token_index)| {
|
|
unquoted_keyword_is(&tokens[*token_index].token, "FORCE")
|
|
&& significant[..index].iter().all(|earlier| {
|
|
!unquoted_keyword_is(&tokens[*earlier].token, "AS")
|
|
&& !unquoted_keyword_is(&tokens[*earlier].token, "IS")
|
|
})
|
|
})
|
|
{
|
|
return Err("FORCE is not supported when editing an Oracle-family type.".to_string());
|
|
}
|
|
if matches!(object_type, ObjectSourceKind::Type | ObjectSourceKind::TypeBody) {
|
|
let mut brackets = Vec::new();
|
|
for index in &significant {
|
|
match &tokens[*index].token {
|
|
Token::LParen => brackets.push(Token::RParen),
|
|
Token::LBracket => brackets.push(Token::RBracket),
|
|
Token::RParen | Token::RBracket if brackets.pop().as_ref() != Some(&tokens[*index].token) => {
|
|
return Err("TYPE source contains an unmatched closing bracket.".into());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if !brackets.is_empty() {
|
|
return Err("TYPE source contains an incomplete declaration.".into());
|
|
}
|
|
if !significant[position + consumed..].iter().any(|index| {
|
|
["AS", "IS", "UNDER"].iter().any(|keyword| unquoted_keyword_is(&tokens[*index].token, keyword))
|
|
}) {
|
|
return Err("TYPE source has no complete definition after its name.".into());
|
|
}
|
|
if *object_type == ObjectSourceKind::TypeBody && !oracle_plsql_block_is_complete(&source) {
|
|
return Err("TYPE BODY source is missing its outer END or contains an incomplete block.".into());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn oracle_management_ddl(source: &str, schema: Option<&str>, object_type: &ObjectSourceKind) -> Result<String, String> {
|
|
let mut source = strip_standalone_trailing_slash(source).to_string();
|
|
let tokenize = |source: &str| {
|
|
Tokenizer::new(&OracleDialect {}, source)
|
|
.tokenize_with_location()
|
|
.map(|tokens| {
|
|
tokens.into_iter().filter(|token| !matches!(token.token, Token::Whitespace(_))).collect::<Vec<_>>()
|
|
})
|
|
.map_err(|error| error.to_string())
|
|
};
|
|
let tokens = tokenize(&source)?;
|
|
let first = tokens.first().ok_or_else(|| "TYPE source is empty.".to_string())?;
|
|
if unquoted_keyword_is(&first.token, "CREATE") {
|
|
if !tokens.get(1).is_some_and(|token| unquoted_keyword_is(&token.token, "OR")) {
|
|
let offset =
|
|
routine_location_to_byte_offset(&source, first.span.end).ok_or("Cannot locate TYPE CREATE prefix")?;
|
|
source.insert_str(offset, " OR REPLACE");
|
|
}
|
|
} else {
|
|
let offset =
|
|
routine_location_to_byte_offset(&source, first.span.start).ok_or("Cannot locate TYPE declaration")?;
|
|
source.insert_str(offset, "CREATE OR REPLACE ");
|
|
}
|
|
if let Some(owner) = schema.filter(|owner| !owner.is_empty()) {
|
|
let tokens = tokenize(&source)?;
|
|
let keyword = match object_type {
|
|
ObjectSourceKind::Type | ObjectSourceKind::TypeBody => "TYPE",
|
|
ObjectSourceKind::Package | ObjectSourceKind::PackageBody => "PACKAGE",
|
|
ObjectSourceKind::Procedure => "PROCEDURE",
|
|
ObjectSourceKind::Function => "FUNCTION",
|
|
ObjectSourceKind::View => "VIEW",
|
|
ObjectSourceKind::Trigger => "TRIGGER",
|
|
_ => return Err("This object cannot be rebuilt by the TYPE manager.".into()),
|
|
};
|
|
let type_index = tokens
|
|
.iter()
|
|
.position(|token| unquoted_keyword_is(&token.token, keyword))
|
|
.ok_or("Cannot locate the object declaration keyword")?;
|
|
let name_index = type_index
|
|
+ if matches!(object_type, ObjectSourceKind::TypeBody | ObjectSourceKind::PackageBody) { 2 } else { 1 };
|
|
if tokens.get(name_index + 1).is_none_or(|token| token.token != Token::Period) {
|
|
let token = tokens.get(name_index).ok_or("TYPE declaration has no name")?;
|
|
let offset = routine_location_to_byte_offset(&source, token.span.start).ok_or("Cannot locate TYPE name")?;
|
|
source.insert_str(offset, &format!("\"{}\".", owner.replace('"', "\"\"")));
|
|
}
|
|
}
|
|
Ok(ensure_semicolon(&source))
|
|
}
|
|
|
|
/// Build a source-backed step and its comparison value for TYPE management.
|
|
/// This deliberately does not change the ordinary object-source save contract.
|
|
pub fn build_oracle_type_management_source(input: EditableObjectSourceSqlInput) -> Result<(String, String), String> {
|
|
if !matches!(input.database_type, DatabaseType::Oracle | DatabaseType::OceanbaseOracle | DatabaseType::Dameng)
|
|
|| !matches!(
|
|
input.object_type,
|
|
ObjectSourceKind::Type
|
|
| ObjectSourceKind::TypeBody
|
|
| ObjectSourceKind::Procedure
|
|
| ObjectSourceKind::Function
|
|
| ObjectSourceKind::Package
|
|
| ObjectSourceKind::PackageBody
|
|
| ObjectSourceKind::View
|
|
| ObjectSourceKind::Trigger
|
|
)
|
|
{
|
|
return Err("TYPE management supports only Oracle-family source-backed dependencies.".into());
|
|
}
|
|
validate_oracle_object_source(
|
|
input.database_type,
|
|
&input.object_type,
|
|
input.schema.as_deref(),
|
|
&input.name,
|
|
&input.source,
|
|
)?;
|
|
let sql = oracle_management_ddl(&input.source, input.schema.as_deref(), &input.object_type)?;
|
|
let tokens = Tokenizer::new(&OracleDialect {}, &sql).tokenize().map_err(|error| error.to_string())?;
|
|
let significant = tokens
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(index, token)| (!matches!(token, Token::Whitespace(_))).then_some(index))
|
|
.collect::<Vec<_>>();
|
|
let keyword = match input.object_type {
|
|
ObjectSourceKind::Type | ObjectSourceKind::TypeBody => "TYPE",
|
|
ObjectSourceKind::Package | ObjectSourceKind::PackageBody => "PACKAGE",
|
|
ObjectSourceKind::Procedure => "PROCEDURE",
|
|
ObjectSourceKind::Function => "FUNCTION",
|
|
ObjectSourceKind::View => "VIEW",
|
|
ObjectSourceKind::Trigger => "TRIGGER",
|
|
_ => unreachable!(),
|
|
};
|
|
let kind_position = significant
|
|
.iter()
|
|
.position(|index| unquoted_keyword_is(&tokens[*index], keyword))
|
|
.ok_or("Missing management declaration")?;
|
|
let first_name = kind_position
|
|
+ if matches!(input.object_type, ObjectSourceKind::TypeBody | ObjectSourceKind::PackageBody) { 2 } else { 1 };
|
|
let qualified = significant.get(first_name + 1).is_some_and(|index| tokens[*index] == Token::Period);
|
|
let name_index =
|
|
*significant.get(first_name + if qualified { 2 } else { 0 }).ok_or("Missing management object name")?;
|
|
let owner_index = qualified.then(|| significant[first_name]);
|
|
let fingerprint = tokens
|
|
.into_iter()
|
|
.enumerate()
|
|
.filter_map(|(index, token)| {
|
|
if index == name_index {
|
|
Some(format!("target-name:{}", input.name))
|
|
} else if Some(index) == owner_index {
|
|
let owner = input.schema.clone().unwrap_or_else(|| match &token {
|
|
Token::Word(word) if word.quote_style.is_none() => word.value.to_ascii_uppercase(),
|
|
Token::Word(word) => word.value.clone(),
|
|
_ => token.to_string(),
|
|
});
|
|
Some(format!("target-owner:{owner}"))
|
|
} else {
|
|
match token {
|
|
Token::Whitespace(Whitespace::Space | Whitespace::Tab | Whitespace::Newline) | Token::EOF => None,
|
|
Token::Word(word) if word.quote_style.is_none() => {
|
|
Some(format!("word:{}", word.value.to_ascii_uppercase()))
|
|
}
|
|
token => Some(format!("token:{token}")),
|
|
}
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let fingerprint = serde_json::to_string(&fingerprint).map_err(|error| error.to_string())?;
|
|
Ok((sql, fingerprint))
|
|
}
|
|
|
|
/// Rebuild directly in the original firing state, never ENABLE then DISABLE.
|
|
pub fn build_oracle_type_management_trigger_source(
|
|
mut input: EditableObjectSourceSqlInput,
|
|
enabled: bool,
|
|
) -> Result<(String, String), String> {
|
|
if input.object_type != ObjectSourceKind::Trigger {
|
|
return Err("A trigger firing state applies only to a TRIGGER definition.".into());
|
|
}
|
|
let (mut sql, fingerprint) = build_oracle_type_management_source(input.clone())?;
|
|
if input.database_type == DatabaseType::Dameng {
|
|
return if enabled {
|
|
Ok((sql, fingerprint))
|
|
} else {
|
|
Err("DM cannot yet rebuild a disabled trigger without a verified disabled-at-create syntax. Keep its definition unchanged for COMPILE, or prepare that dependency separately before starting this TYPE plan.".into())
|
|
};
|
|
}
|
|
let tokens = Tokenizer::new(&OracleDialect {}, &sql).tokenize_with_location().map_err(|e| e.to_string())?;
|
|
let tokens: Vec<_> = tokens.iter().filter(|t| !matches!(t.token, Token::Whitespace(_))).collect();
|
|
let declaration =
|
|
tokens.iter().position(|t| unquoted_keyword_is(&t.token, "TRIGGER")).ok_or("Missing TRIGGER declaration")?;
|
|
let name = declaration + 1;
|
|
let after_name = name + if tokens.get(name + 1).is_some_and(|t| t.token == Token::Period) { 3 } else { 1 };
|
|
let body = tokens
|
|
.iter()
|
|
.enumerate()
|
|
.skip(after_name)
|
|
.find(|(_, t)| {
|
|
["WHEN", "DECLARE", "BEGIN", "CALL", "COMPOUND"].iter().any(|word| unquoted_keyword_is(&t.token, word))
|
|
})
|
|
.map(|(i, _)| i)
|
|
.ok_or(
|
|
"Cannot locate a supported trigger body or WHEN clause; keep the draft and prepare this trigger separately",
|
|
)?;
|
|
let state = if enabled { "ENABLE" } else { "DISABLE" };
|
|
let previous =
|
|
tokens.get(body - 1).filter(|t| ["ENABLE", "DISABLE"].iter().any(|word| unquoted_keyword_is(&t.token, word)));
|
|
let (start, end, replacement) = if let Some(previous) = previous {
|
|
(previous.span.start, previous.span.end, state.to_string())
|
|
} else {
|
|
(tokens[body].span.start, tokens[body].span.start, format!("{state} "))
|
|
};
|
|
let start = routine_location_to_byte_offset(&sql, start).ok_or("Cannot locate the trigger state")?;
|
|
let end = routine_location_to_byte_offset(&sql, end).ok_or("Cannot locate the trigger state")?;
|
|
sql.replace_range(start..end, &replacement);
|
|
input.source = sql;
|
|
build_oracle_type_management_source(input)
|
|
}
|
|
|
|
fn oracle_identifier_matches(token: &Token, expected: &str) -> bool {
|
|
matches!(token, Token::Word(word) if identifier_matches(word, expected))
|
|
}
|
|
|
|
fn unquoted_keyword_is(token: &Token, expected: &str) -> bool {
|
|
matches!(token, Token::Word(word) if word.quote_style.is_none() && word.value.eq_ignore_ascii_case(expected))
|
|
}
|
|
|
|
/// Convert a raw database object source into a form suitable for the source editor.
|
|
///
|
|
/// This is the *editable* presentation shown to the user when they open a view,
|
|
/// procedure, or function for editing. For SQL Server the raw `CREATE VIEW` /
|
|
/// `CREATE PROCEDURE` is rewritten to `ALTER` so the user doesn't see a
|
|
/// mismatched CREATE statement for an already-existing object. Callers that
|
|
/// only need the first statement should use this instead of calling
|
|
/// `build_executable_object_source_statements` and discarding rename-cleanup
|
|
/// statements.
|
|
pub fn build_editable_object_source(input: EditableObjectSourceSqlInput) -> String {
|
|
let source = input.source.clone();
|
|
if is_postgres_like(input.database_type)
|
|
&& input.object_type == ObjectSourceKind::View
|
|
&& source_starts_with_create_or_alter(&source)
|
|
{
|
|
// Some providers return full view DDL instead of a bare SELECT body.
|
|
return ensure_semicolon(source.trim());
|
|
}
|
|
if matches!(input.database_type, DatabaseType::Informix | DatabaseType::Gbase8s)
|
|
&& input.object_type == ObjectSourceKind::View
|
|
{
|
|
return editable_informix_view_ddl(input.schema.as_deref(), &input.name, &source);
|
|
}
|
|
if is_mysql_like(input.database_type)
|
|
&& matches!(input.object_type, ObjectSourceKind::Function | ObjectSourceKind::Procedure)
|
|
{
|
|
return ensure_semicolon(source.trim());
|
|
}
|
|
if input.database_type == DatabaseType::Mysql && input.object_type == ObjectSourceKind::View {
|
|
// Existing view DDL opens as ALTER, while save must preserve CREATE for new views and TiDB.
|
|
return executable_mysql_view_ddl(&source);
|
|
}
|
|
match build_executable_object_source_statements(input) {
|
|
Ok(statements) => statements.into_iter().next().unwrap_or_default(),
|
|
Err(_) => ensure_semicolon(source.trim()),
|
|
}
|
|
}
|
|
|
|
pub fn build_view_ddl_sql(input: BuildViewDdlInput) -> String {
|
|
let source = input.source.trim();
|
|
if input.database_type.is_some_and(is_mysql_like) {
|
|
return display_mysql_view_ddl(source, "e_mysql_identifier(&input.name));
|
|
}
|
|
let terminated_source = if matches!(
|
|
input.database_type,
|
|
Some(DatabaseType::Oracle | DatabaseType::OceanbaseOracle | DatabaseType::Dameng)
|
|
) {
|
|
ensure_oracle_ddl_terminated(source)
|
|
} else {
|
|
ensure_semicolon(source)
|
|
};
|
|
if Regex::new(r"(?i)^(?:CREATE|ALTER)\s+").unwrap().is_match(source) {
|
|
return terminated_source;
|
|
}
|
|
|
|
// Backtick-quoting is used for MySQL-family engines and for connections
|
|
// whose driver reports a backtick identifier quote (Kingbase MySQL
|
|
// compatibility mode); everything else keeps the PostgreSQL double quote.
|
|
let use_backtick = input.identifier_quote.as_deref() == Some("`")
|
|
|| (input.identifier_quote.is_none()
|
|
&& matches!(input.database_type, Some(DatabaseType::Mysql | DatabaseType::Goldendb)));
|
|
let qualified_name = if use_backtick {
|
|
mysql_qualified_name(input.schema.as_deref(), &input.name)
|
|
} else {
|
|
postgres_qualified_name(input.schema.as_deref(), &input.name)
|
|
};
|
|
|
|
if input.database_type.is_none()
|
|
|| input.database_type.is_some_and(|database_type| {
|
|
is_postgres_like(database_type)
|
|
|| database_type == DatabaseType::OpenGauss
|
|
|| database_type == DatabaseType::Questdb
|
|
})
|
|
{
|
|
return format!("CREATE OR REPLACE VIEW {qualified_name} AS\n{terminated_source}");
|
|
}
|
|
|
|
format!("CREATE VIEW {qualified_name} AS\n{terminated_source}")
|
|
}
|
|
|
|
pub fn build_export_object_source_sql(
|
|
database_type: DatabaseType,
|
|
object_type: ObjectSourceKind,
|
|
source: &str,
|
|
) -> String {
|
|
let source = source.trim();
|
|
let source = if is_opengauss_like(database_type) && object_type == ObjectSourceKind::Procedure {
|
|
strip_standalone_trailing_slash(source)
|
|
} else {
|
|
source
|
|
};
|
|
if source.is_empty() {
|
|
return String::new();
|
|
}
|
|
// MySQL routine bodies, trigger bodies and event bodies may contain `;`, so the
|
|
// exported statements need a client-side delimiter the same way `mysqldump` emits one.
|
|
if is_mysql_like(database_type)
|
|
&& matches!(
|
|
object_type,
|
|
ObjectSourceKind::Procedure
|
|
| ObjectSourceKind::Function
|
|
| ObjectSourceKind::Trigger
|
|
| ObjectSourceKind::Event
|
|
)
|
|
{
|
|
return mysql_delimited_routine_source(source);
|
|
}
|
|
ensure_semicolon(source)
|
|
}
|
|
|
|
pub fn object_source_save_execution_mode(_database_type: DatabaseType) -> ObjectSourceSaveExecutionMode {
|
|
ObjectSourceSaveExecutionMode::Single
|
|
}
|
|
|
|
fn build_routine_rename_cleanup(input: &EditableObjectSourceSqlInput, source: &str) -> Option<String> {
|
|
if !matches!(input.object_type, ObjectSourceKind::Function | ObjectSourceKind::Procedure) {
|
|
return None;
|
|
}
|
|
|
|
if is_mysql_like(input.database_type) {
|
|
let declaration = mysql_routine_declaration(source)?;
|
|
if declaration.kind != input.object_type || !routine_name_changed(&declaration.name, &input.name) {
|
|
return None;
|
|
}
|
|
return Some(format!(
|
|
"DROP {} IF EXISTS {};",
|
|
object_type_keyword(&input.object_type),
|
|
mysql_qualified_name(input.schema.as_deref(), &input.name)
|
|
));
|
|
}
|
|
|
|
if !is_postgres_like(input.database_type) {
|
|
return None;
|
|
}
|
|
|
|
let declaration = routine_declaration(source)?;
|
|
if declaration.kind != input.object_type || !routine_name_changed(&declaration.name, &input.name) {
|
|
return None;
|
|
}
|
|
|
|
Some(format!(
|
|
"DROP {} IF EXISTS {}{};",
|
|
object_type_keyword(&input.object_type),
|
|
postgres_qualified_name(input.schema.as_deref(), &input.name),
|
|
declaration.signature
|
|
))
|
|
}
|
|
|
|
/// Alter sequence options without replaying START WITH or dropping the existing sequence.
|
|
fn oceanbase_sequence_edit_sql(input: &EditableObjectSourceSqlInput) -> Result<String, String> {
|
|
let invalid = || {
|
|
"Expected CREATE/ALTER SEQUENCE for the selected object with supported sequence options; restart and additional statements are not allowed.".to_string()
|
|
};
|
|
let mut tokens: Vec<Token> = Tokenizer::new(&OracleDialect {}, input.source.trim())
|
|
.tokenize()
|
|
.map_err(|_| invalid())?
|
|
.into_iter()
|
|
.filter(|token| !matches!(token, Token::Whitespace(_)))
|
|
.collect();
|
|
if matches!(tokens.last(), Some(Token::SemiColon)) {
|
|
tokens.pop();
|
|
}
|
|
let word = |index: usize, expected: &str| matches!(tokens.get(index), Some(Token::Word(w)) if w.quote_style.is_none() && w.value.eq_ignore_ascii_case(expected));
|
|
let creating = word(0, "CREATE");
|
|
if (!creating && !word(0, "ALTER")) || !word(1, "SEQUENCE") {
|
|
return Err(invalid());
|
|
}
|
|
let mut index = 2;
|
|
let mut names = Vec::new();
|
|
loop {
|
|
let Some(Token::Word(name)) = tokens.get(index) else {
|
|
return Err(invalid());
|
|
};
|
|
names.push(if name.quote_style.is_some() { name.value.clone() } else { name.value.to_uppercase() });
|
|
index += 1;
|
|
if !matches!(tokens.get(index), Some(Token::Period)) {
|
|
break;
|
|
}
|
|
index += 1;
|
|
}
|
|
let expected = match input.schema.as_deref().filter(|schema| !schema.is_empty()) {
|
|
Some(schema) => vec![schema.to_string(), input.name.clone()],
|
|
None => vec![input.name.clone()],
|
|
};
|
|
if names != expected && names != vec![input.name.clone()] {
|
|
return Err(invalid());
|
|
}
|
|
let mut options = Vec::new();
|
|
while index < tokens.len() {
|
|
let start = index;
|
|
let skip_start = creating && word(index, "START");
|
|
let numeric = if word(index, "INCREMENT") || skip_start {
|
|
let next = if skip_start { "WITH" } else { "BY" };
|
|
if !word(index + 1, next) {
|
|
return Err(invalid());
|
|
}
|
|
index += 2;
|
|
true
|
|
} else if ["MINVALUE", "MAXVALUE", "CACHE"].iter().any(|option| word(index, option)) {
|
|
index += 1;
|
|
true
|
|
} else if ["NOMINVALUE", "NOMAXVALUE", "NOCACHE", "CYCLE", "NOCYCLE", "ORDER", "NOORDER"]
|
|
.iter()
|
|
.any(|option| word(index, option))
|
|
{
|
|
index += 1;
|
|
false
|
|
} else {
|
|
return Err(invalid());
|
|
};
|
|
if numeric {
|
|
if matches!(tokens.get(index), Some(Token::Minus | Token::Plus)) {
|
|
index += 1;
|
|
}
|
|
if !matches!(tokens.get(index), Some(Token::Number(value, false)) if !value.is_empty() && value.bytes().all(|c| c.is_ascii_digit()))
|
|
{
|
|
return Err(invalid());
|
|
}
|
|
index += 1;
|
|
}
|
|
if !skip_start {
|
|
options.push(tokens[start..index].iter().map(ToString::to_string).collect::<Vec<_>>().join(" "));
|
|
}
|
|
}
|
|
if options.is_empty() {
|
|
return Err(invalid());
|
|
}
|
|
Ok(format!(
|
|
"ALTER SEQUENCE {}\n {};",
|
|
postgres_qualified_name(input.schema.as_deref(), &input.name),
|
|
options.join("\n ")
|
|
))
|
|
}
|
|
|
|
fn is_postgres_like(database_type: DatabaseType) -> bool {
|
|
matches!(
|
|
database_type,
|
|
DatabaseType::Postgres
|
|
| DatabaseType::Redshift
|
|
| DatabaseType::Gaussdb
|
|
| DatabaseType::Kwdb
|
|
| DatabaseType::OpenGauss
|
|
| DatabaseType::Questdb
|
|
| DatabaseType::Kingbase
|
|
| DatabaseType::Highgo
|
|
| DatabaseType::Uxdb
|
|
| DatabaseType::Vastbase
|
|
)
|
|
}
|
|
|
|
fn is_opengauss_like(database_type: DatabaseType) -> bool {
|
|
matches!(database_type, DatabaseType::Gaussdb | DatabaseType::OpenGauss)
|
|
}
|
|
|
|
fn is_mysql_like(database_type: DatabaseType) -> bool {
|
|
matches!(database_type, DatabaseType::Mysql | DatabaseType::Goldendb)
|
|
}
|
|
|
|
fn is_oracle_like(database_type: DatabaseType) -> bool {
|
|
matches!(database_type, DatabaseType::Oracle | DatabaseType::Dameng)
|
|
}
|
|
|
|
fn object_type_keyword(object_type: &ObjectSourceKind) -> &'static str {
|
|
match object_type {
|
|
ObjectSourceKind::View => "VIEW",
|
|
ObjectSourceKind::MaterializedView => "MATERIALIZED_VIEW",
|
|
ObjectSourceKind::Procedure => "PROCEDURE",
|
|
ObjectSourceKind::Function => "FUNCTION",
|
|
ObjectSourceKind::Trigger => "TRIGGER",
|
|
ObjectSourceKind::Event => "EVENT",
|
|
ObjectSourceKind::Sequence => "SEQUENCE",
|
|
ObjectSourceKind::Synonym => "SYNONYM",
|
|
ObjectSourceKind::Job => "JOB",
|
|
ObjectSourceKind::Package => "PACKAGE",
|
|
ObjectSourceKind::PackageBody => "PACKAGE BODY",
|
|
ObjectSourceKind::Type => "TYPE",
|
|
ObjectSourceKind::TypeBody => "TYPE BODY",
|
|
}
|
|
}
|
|
|
|
fn quote_postgres_identifier(value: &str) -> String {
|
|
format!("\"{}\"", value.replace('"', "\"\""))
|
|
}
|
|
|
|
fn quote_mysql_identifier(value: &str) -> String {
|
|
format!("`{}`", value.replace('`', "``"))
|
|
}
|
|
|
|
pub fn ensure_oracle_ddl_terminated(sql: &str) -> String {
|
|
let trimmed = sql.trim_end();
|
|
if trimmed.trim().is_empty() {
|
|
return String::new();
|
|
}
|
|
if let Ok(tokens) = Tokenizer::new(&OracleDialect {}, trimmed).tokenize() {
|
|
let last_code = tokens.iter().rev().find(|token| !matches!(token, Token::Whitespace(_)));
|
|
if matches!(last_code, Some(Token::SemiColon | Token::Div)) {
|
|
return trimmed.to_string();
|
|
}
|
|
if !matches!(tokens.last(), Some(Token::Whitespace(Whitespace::SingleLineComment { .. }))) {
|
|
return format!("{trimmed};");
|
|
}
|
|
}
|
|
format!("{trimmed}\n;")
|
|
}
|
|
|
|
fn ensure_semicolon(sql: &str) -> String {
|
|
let trimmed = sql.trim();
|
|
if trimmed.ends_with(';') {
|
|
trimmed.to_string()
|
|
} else {
|
|
format!("{trimmed};")
|
|
}
|
|
}
|
|
|
|
fn strip_standalone_trailing_slash(sql: &str) -> &str {
|
|
let trimmed = sql.trim();
|
|
let Some(without_slash) = trimmed.strip_suffix('/') else {
|
|
return trimmed;
|
|
};
|
|
if without_slash.ends_with('\n') || without_slash.ends_with('\r') {
|
|
without_slash.trim_end()
|
|
} else {
|
|
trimmed
|
|
}
|
|
}
|
|
|
|
fn mysql_delimited_routine_source(source: &str) -> String {
|
|
let trimmed = source.trim();
|
|
if Regex::new(r"(?i)^\s*DELIMITER\b").unwrap().is_match(trimmed) {
|
|
return trimmed.to_string();
|
|
}
|
|
let body = trimmed.trim_end_matches(';').trim_end();
|
|
let delimiter = mysql_routine_script_delimiter(body);
|
|
format!("DELIMITER {delimiter}\n{body}{delimiter}\nDELIMITER ;")
|
|
}
|
|
|
|
fn mysql_routine_script_delimiter(source: &str) -> &'static str {
|
|
["//", "$$", ";;", "__DBX_DELIMITER__"]
|
|
.into_iter()
|
|
.find(|delimiter| !source.contains(delimiter))
|
|
.unwrap_or("__DBX_DELIMITER__")
|
|
}
|
|
|
|
fn source_starts_with_create_or_alter(source: &str) -> bool {
|
|
let executable = &source[leading_sql_statement_start(source)..];
|
|
Regex::new(r"(?i)^(?:CREATE|ALTER)\s+").unwrap().is_match(executable)
|
|
}
|
|
|
|
fn source_starts_with_create(source: &str) -> bool {
|
|
let executable = &source[leading_sql_statement_start(source)..];
|
|
Regex::new(r"(?i)^CREATE\s+").unwrap().is_match(executable)
|
|
}
|
|
|
|
fn source_starts_with_alter(source: &str) -> bool {
|
|
let executable = &source[leading_sql_statement_start(source)..];
|
|
Regex::new(r"(?i)^ALTER\s+").unwrap().is_match(executable)
|
|
}
|
|
|
|
fn executable_postgres_view_ddl(source: &str) -> Option<String> {
|
|
let trimmed = source.trim();
|
|
let statement_start = leading_sql_statement_start(trimmed);
|
|
let executable = &trimmed[statement_start..];
|
|
if Regex::new(r"(?i)^CREATE\s+OR\s+REPLACE\s+").unwrap().is_match(executable) {
|
|
return Some(ensure_semicolon(trimmed));
|
|
}
|
|
|
|
// Kingbase extends PostgreSQL's prefix with FORCE after the optional RECURSIVE modifier.
|
|
let create_view =
|
|
Regex::new(r"(?i)^CREATE\s+((?:(?:TEMP|TEMPORARY)\s+)?(?:RECURSIVE\s+)?(?:FORCE\s+)?VIEW\s+)").unwrap();
|
|
if create_view.is_match(executable) {
|
|
let replaced = create_view.replace(executable, "CREATE OR REPLACE $1");
|
|
return Some(ensure_semicolon(&format!("{}{}", &trimmed[..statement_start], replaced)));
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn leading_sql_statement_start(source: &str) -> usize {
|
|
let mut index = 0;
|
|
loop {
|
|
index = skip_sql_whitespace(source, index);
|
|
if let Some(end) = sql_line_comment_end(source, index) {
|
|
index = end;
|
|
continue;
|
|
}
|
|
if let Some(end) = sql_block_comment_end(source, index) {
|
|
index = end;
|
|
continue;
|
|
}
|
|
return index;
|
|
}
|
|
}
|
|
|
|
fn executable_oracle_view_ddl(schema: Option<&str>, name: &str, source: &str) -> String {
|
|
let trimmed = source.trim();
|
|
let statement_start = leading_sql_statement_start(trimmed);
|
|
let executable = &trimmed[statement_start..];
|
|
if Regex::new(r"(?i)^CREATE\s+OR\s+REPLACE\s+").unwrap().is_match(executable)
|
|
|| source_starts_with_alter(executable)
|
|
{
|
|
return ensure_semicolon(trimmed);
|
|
}
|
|
|
|
let create_view = Regex::new(r"(?i)^CREATE\s+((?:(?:NO)?FORCE\s+)?(?:(?:NON)?EDITIONABLE\s+)?VIEW\s+)").unwrap();
|
|
if create_view.is_match(executable) {
|
|
let replaced = create_view.replace(executable, "CREATE OR REPLACE $1");
|
|
return ensure_semicolon(&format!("{}{}", &trimmed[..statement_start], replaced));
|
|
}
|
|
|
|
format!("CREATE OR REPLACE VIEW {} AS\n{}", postgres_qualified_name(schema, name), ensure_semicolon(trimmed))
|
|
}
|
|
|
|
fn executable_oracle_routine_ddl(source: &str) -> String {
|
|
let trimmed = source.trim();
|
|
if Regex::new(r"(?i)^(?:PROCEDURE|FUNCTION|PACKAGE(?:\s+BODY)?|TYPE(?:\s+BODY)?)\b").unwrap().is_match(trimmed) {
|
|
return ensure_semicolon(&format!("CREATE OR REPLACE {trimmed}"));
|
|
}
|
|
ensure_semicolon(trimmed)
|
|
}
|
|
|
|
/// View DDL shown in Edit Structure. MySQL returns `CREATE VIEW`, which the
|
|
/// structure editor used to present like a table script. Rewrite that to
|
|
/// `CREATE OR REPLACE VIEW` and keep the view name unqualified.
|
|
fn display_mysql_view_ddl(source: &str, view_name: &str) -> String {
|
|
let trimmed = source.trim();
|
|
if let Some(statement) = rewrite_mysql_view_statement(trimmed) {
|
|
return statement;
|
|
}
|
|
if Regex::new(r"(?i)^(?:CREATE|ALTER)\s+").unwrap().is_match(trimmed) {
|
|
return ensure_semicolon(trimmed);
|
|
}
|
|
format!("CREATE OR REPLACE VIEW {view_name} AS\n{}", ensure_semicolon(trimmed))
|
|
}
|
|
|
|
fn rewrite_mysql_view_statement(source: &str) -> Option<String> {
|
|
let start = leading_sql_statement_start(source);
|
|
let executable = source[start..].trim_end();
|
|
let create_view = Regex::new(
|
|
r"(?is)^(?:CREATE|ALTER)\s+(?:OR\s+REPLACE\s+)?(?P<options>(?:ALGORITHM\s*=\s*(?:UNDEFINED|MERGE|TEMPTABLE)\s+)?(?:DEFINER\s*=\s*(?:(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s*@\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)|CURRENT_USER(?:\(\))?)\s+)?(?:SQL\s+SECURITY\s+(?:DEFINER|INVOKER)\s+)?)VIEW\s+(?:(?:`(?:``|[^`])+`|[A-Za-z0-9_$]+)\s*\.\s*)?(?P<rest>.*)$",
|
|
)
|
|
.unwrap();
|
|
let captures = create_view.captures(executable)?;
|
|
let options = captures.name("options").map(|value| value.as_str()).unwrap_or("");
|
|
let rest = captures.name("rest")?.as_str();
|
|
let statement = format!("CREATE OR REPLACE {options}VIEW {rest}");
|
|
Some(ensure_semicolon(&format!("{}{}", &source[..start], statement.trim_start())))
|
|
}
|
|
|
|
fn executable_mysql_view_ddl(source: &str) -> String {
|
|
let trimmed = source.trim();
|
|
let statement_start = leading_sql_statement_start(trimmed);
|
|
let executable = &trimmed[statement_start..];
|
|
let create_view = Regex::new(
|
|
r"(?is)^CREATE\s+(?:OR\s+REPLACE\s+)?(?:ALGORITHM\s*=\s*(?:UNDEFINED|MERGE|TEMPTABLE)\s+)?(?:DEFINER\s*=\s*(?:(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s*@\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)|CURRENT_USER(?:\(\))?)\s+)?(?:SQL\s+SECURITY\s+(?:DEFINER|INVOKER)\s+)?VIEW\s+",
|
|
)
|
|
.unwrap();
|
|
if create_view.is_match(executable) {
|
|
let create = Regex::new(r"(?i)^CREATE\s+(?:OR\s+REPLACE\s+)?").unwrap();
|
|
let replaced = create.replace(executable, "ALTER ");
|
|
return ensure_semicolon(&format!("{}{}", &trimmed[..statement_start], replaced));
|
|
}
|
|
|
|
ensure_semicolon(trimmed)
|
|
}
|
|
|
|
fn executable_informix_view_statements(schema: Option<&str>, name: &str, source: &str) -> Vec<String> {
|
|
let (target_name, create_tail) = informix_view_definition(schema, name, source);
|
|
if source_starts_with_alter(source.trim()) {
|
|
return vec![ensure_semicolon(source.trim())];
|
|
}
|
|
|
|
let validation_name = informix_validation_view_name(&target_name);
|
|
let mut statements = vec![
|
|
drop_informix_view_if_exists(&validation_name),
|
|
create_informix_view(&validation_name, &create_tail),
|
|
drop_informix_view_if_exists(&validation_name),
|
|
];
|
|
|
|
let original_name = informix_identifier(name);
|
|
if !target_name.eq_ignore_ascii_case(&original_name) {
|
|
statements.push(drop_informix_view_if_exists(&original_name));
|
|
}
|
|
statements.push(drop_informix_view_if_exists(&target_name));
|
|
statements.push(create_informix_view(&target_name, &create_tail));
|
|
statements
|
|
}
|
|
|
|
fn editable_informix_view_ddl(schema: Option<&str>, name: &str, source: &str) -> String {
|
|
if source_starts_with_alter(source.trim()) {
|
|
return ensure_semicolon(source.trim());
|
|
}
|
|
let (target_name, create_tail) = informix_view_definition(schema, name, source);
|
|
create_informix_view(&target_name, &create_tail)
|
|
}
|
|
|
|
fn informix_view_definition(schema: Option<&str>, name: &str, source: &str) -> (String, String) {
|
|
let trimmed = source.trim();
|
|
let create_view = Regex::new(
|
|
r#"(?is)^\s*CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)"#,
|
|
)
|
|
.unwrap();
|
|
if let Some(captures) = create_view.captures(trimmed) {
|
|
let view_name = captures.get(1).unwrap();
|
|
let target_name = strip_informix_owner_qualifiers(view_name.as_str(), schema);
|
|
let body = strip_informix_owner_qualifiers(&trimmed[view_name.end()..], schema);
|
|
(target_name.trim().to_string(), body)
|
|
} else {
|
|
let body = strip_informix_owner_qualifiers(trimmed, schema);
|
|
(informix_identifier(name), format!(" AS\n{body}"))
|
|
}
|
|
}
|
|
|
|
fn postgres_qualified_name(schema: Option<&str>, name: &str) -> String {
|
|
schema
|
|
.into_iter()
|
|
.chain(std::iter::once(name))
|
|
.filter(|part| !part.is_empty())
|
|
.map(quote_postgres_identifier)
|
|
.collect::<Vec<_>>()
|
|
.join(".")
|
|
}
|
|
|
|
fn informix_identifier(name: &str) -> String {
|
|
if is_simple_informix_identifier(name) {
|
|
name.to_string()
|
|
} else {
|
|
quote_postgres_identifier(name)
|
|
}
|
|
}
|
|
|
|
fn create_informix_view(name: &str, create_tail: &str) -> String {
|
|
ensure_semicolon(&format!("CREATE VIEW {}{}", name.trim(), create_tail))
|
|
}
|
|
|
|
fn drop_informix_view_if_exists(name: &str) -> String {
|
|
format!("DROP VIEW IF EXISTS {};", name.trim())
|
|
}
|
|
|
|
fn executable_mysql_routine_statements(
|
|
input: &EditableObjectSourceSqlInput,
|
|
source: &str,
|
|
custom_validation_name: Option<&str>,
|
|
drop_validation_before_create: bool,
|
|
) -> Vec<String> {
|
|
if !mysql_source_starts_with_create_routine(source) {
|
|
return vec![ensure_semicolon(source)];
|
|
}
|
|
|
|
let declaration = mysql_routine_declaration(source).filter(|declaration| declaration.kind == input.object_type);
|
|
let create_name = declaration.as_ref().map(|declaration| declaration.name.as_str()).unwrap_or(&input.name);
|
|
let is_rename =
|
|
declaration.as_ref().is_some_and(|declaration| routine_name_changed(&declaration.name, &input.name));
|
|
let mut statements = Vec::with_capacity(6);
|
|
|
|
// MySQL has no cross-version CREATE OR REPLACE for stored routines. Validate the CREATE
|
|
// body under a temporary name first (same idea as Informix view saves) so a syntax error
|
|
// cannot leave the original routine deleted after DROP.
|
|
let owned_validation_name;
|
|
let validation_name = if let Some(validation_name) = custom_validation_name {
|
|
validation_name
|
|
} else {
|
|
owned_validation_name = mysql_validation_routine_name(create_name);
|
|
&owned_validation_name
|
|
};
|
|
if let Some(validation_source) = replace_mysql_routine_declaration_name(source, validation_name) {
|
|
if drop_validation_before_create {
|
|
statements.push(mysql_drop_routine_if_exists(
|
|
input.object_type.clone(),
|
|
input.schema.as_deref(),
|
|
validation_name,
|
|
));
|
|
}
|
|
statements.push(ensure_semicolon(&validation_source));
|
|
statements.push(mysql_drop_routine_if_exists(
|
|
input.object_type.clone(),
|
|
input.schema.as_deref(),
|
|
validation_name,
|
|
));
|
|
}
|
|
|
|
statements.push(mysql_drop_routine_if_exists(input.object_type.clone(), input.schema.as_deref(), create_name));
|
|
statements.push(ensure_semicolon(source));
|
|
|
|
if is_rename {
|
|
statements.push(mysql_drop_routine_if_exists(input.object_type.clone(), input.schema.as_deref(), &input.name));
|
|
}
|
|
|
|
statements
|
|
}
|
|
|
|
fn mysql_drop_routine_if_exists(object_type: ObjectSourceKind, schema: Option<&str>, name: &str) -> String {
|
|
format!("DROP {} IF EXISTS {};", object_type_keyword(&object_type), mysql_qualified_name(schema, name))
|
|
}
|
|
|
|
fn mysql_source_starts_with_create_routine(source: &str) -> bool {
|
|
Regex::new(r"(?is)^\s*CREATE\s+(?:DEFINER\s*=.+?\s+)?(?:FUNCTION|PROCEDURE)\b").unwrap().is_match(source)
|
|
}
|
|
|
|
fn mysql_validation_routine_name(target_name: &str) -> String {
|
|
let mut hash = 0x811c9dc5u32;
|
|
for byte in target_name.bytes() {
|
|
hash ^= byte as u32;
|
|
hash = hash.wrapping_mul(0x01000193);
|
|
}
|
|
format!("dbx_routine_check_{hash:08x}")
|
|
}
|
|
|
|
fn informix_validation_view_name(target_name: &str) -> String {
|
|
let mut hash = 0x811c9dc5u32;
|
|
for byte in target_name.bytes() {
|
|
hash ^= byte as u32;
|
|
hash = hash.wrapping_mul(0x01000193);
|
|
}
|
|
format!("dbx_view_check_{hash:08x}")
|
|
}
|
|
|
|
fn strip_informix_owner_qualifiers(source: &str, schema: Option<&str>) -> String {
|
|
let Some(schema) = schema.map(str::trim).filter(|schema| !schema.is_empty()) else {
|
|
return source.to_string();
|
|
};
|
|
|
|
let mut result = String::with_capacity(source.len());
|
|
let mut index = 0;
|
|
while index < source.len() {
|
|
if let Some(end) = sql_single_quoted_literal_end(source, index) {
|
|
result.push_str(&source[index..end]);
|
|
index = end;
|
|
continue;
|
|
}
|
|
if let Some(end) = sql_line_comment_end(source, index) {
|
|
result.push_str(&source[index..end]);
|
|
index = end;
|
|
continue;
|
|
}
|
|
if let Some(end) = sql_block_comment_end(source, index) {
|
|
result.push_str(&source[index..end]);
|
|
index = end;
|
|
continue;
|
|
}
|
|
if let Some((end, replacement)) = informix_owner_qualifier_replacement(source, index, schema) {
|
|
result.push_str(replacement);
|
|
index = end;
|
|
continue;
|
|
}
|
|
let ch = source[index..].chars().next().unwrap();
|
|
result.push(ch);
|
|
index += ch.len_utf8();
|
|
}
|
|
result
|
|
}
|
|
|
|
fn informix_owner_qualifier_replacement<'a>(source: &'a str, start: usize, schema: &str) -> Option<(usize, &'a str)> {
|
|
if let Some((owner_end, owner)) = read_quoted_sql_identifier(source, start) {
|
|
if owner.eq_ignore_ascii_case(schema) {
|
|
let dot = skip_sql_whitespace(source, owner_end);
|
|
if source[dot..].starts_with('.') {
|
|
let ident_start = skip_sql_whitespace(source, dot + 1);
|
|
if let Some((ident_end, ident_text)) = read_informix_identifier_text(source, ident_start) {
|
|
return Some((ident_end, ident_text));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !is_simple_informix_identifier(schema) || !starts_with_ignore_ascii_case_at(source, start, schema) {
|
|
return None;
|
|
}
|
|
if source[..start].chars().next_back().is_some_and(is_informix_identifier_part) {
|
|
return None;
|
|
}
|
|
let owner_end = start + schema.len();
|
|
if source[owner_end..].chars().next().is_some_and(is_informix_identifier_part) {
|
|
return None;
|
|
}
|
|
let dot = skip_sql_whitespace(source, owner_end);
|
|
if !source[dot..].starts_with('.') {
|
|
return None;
|
|
}
|
|
let ident_start = skip_sql_whitespace(source, dot + 1);
|
|
read_informix_identifier_text(source, ident_start)
|
|
}
|
|
|
|
fn sql_single_quoted_literal_end(source: &str, start: usize) -> Option<usize> {
|
|
if !source[start..].starts_with('\'') {
|
|
return None;
|
|
}
|
|
let mut index = start + 1;
|
|
while index < source.len() {
|
|
let ch = source[index..].chars().next().unwrap();
|
|
index += ch.len_utf8();
|
|
if ch == '\'' {
|
|
if source[index..].starts_with('\'') {
|
|
index += 1;
|
|
} else {
|
|
return Some(index);
|
|
}
|
|
}
|
|
}
|
|
Some(source.len())
|
|
}
|
|
|
|
fn sql_line_comment_end(source: &str, start: usize) -> Option<usize> {
|
|
if !source[start..].starts_with("--") {
|
|
return None;
|
|
}
|
|
let rest = &source[start..];
|
|
Some(start + rest.find('\n').map(|index| index + 1).unwrap_or(rest.len()))
|
|
}
|
|
|
|
fn sql_block_comment_end(source: &str, start: usize) -> Option<usize> {
|
|
if !source[start..].starts_with("/*") {
|
|
return None;
|
|
}
|
|
|
|
// PostgreSQL and Kingbase default to nested SQL block comments, so match the outer terminator.
|
|
let mut depth = 1;
|
|
let mut index = start + 2;
|
|
while index < source.len() {
|
|
if source[index..].starts_with("/*") {
|
|
depth += 1;
|
|
index += 2;
|
|
} else if source[index..].starts_with("*/") {
|
|
depth -= 1;
|
|
index += 2;
|
|
if depth == 0 {
|
|
return Some(index);
|
|
}
|
|
} else {
|
|
index += source[index..].chars().next().unwrap().len_utf8();
|
|
}
|
|
}
|
|
|
|
// Preserve the existing safe behavior: an unclosed leading comment consumes the remaining source.
|
|
Some(source.len())
|
|
}
|
|
|
|
fn read_quoted_sql_identifier(source: &str, start: usize) -> Option<(usize, String)> {
|
|
if !source[start..].starts_with('"') {
|
|
return None;
|
|
}
|
|
let mut value = String::new();
|
|
let mut index = start + 1;
|
|
while index < source.len() {
|
|
let ch = source[index..].chars().next().unwrap();
|
|
index += ch.len_utf8();
|
|
if ch == '"' {
|
|
if source[index..].starts_with('"') {
|
|
value.push('"');
|
|
index += 1;
|
|
} else {
|
|
return Some((index, value));
|
|
}
|
|
} else {
|
|
value.push(ch);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn read_informix_identifier_text(source: &str, start: usize) -> Option<(usize, &str)> {
|
|
if let Some((end, _)) = read_quoted_sql_identifier(source, start) {
|
|
return Some((end, &source[start..end]));
|
|
}
|
|
let first = source[start..].chars().next()?;
|
|
if !is_informix_identifier_start(first) {
|
|
return None;
|
|
}
|
|
let mut end = start + first.len_utf8();
|
|
while end < source.len() {
|
|
let ch = source[end..].chars().next().unwrap();
|
|
if !is_informix_identifier_part(ch) {
|
|
break;
|
|
}
|
|
end += ch.len_utf8();
|
|
}
|
|
Some((end, &source[start..end]))
|
|
}
|
|
|
|
fn skip_sql_whitespace(source: &str, mut index: usize) -> usize {
|
|
while index < source.len() {
|
|
let ch = source[index..].chars().next().unwrap();
|
|
if !ch.is_whitespace() {
|
|
break;
|
|
}
|
|
index += ch.len_utf8();
|
|
}
|
|
index
|
|
}
|
|
|
|
fn starts_with_ignore_ascii_case_at(source: &str, start: usize, needle: &str) -> bool {
|
|
source.get(start..start + needle.len()).is_some_and(|value| value.eq_ignore_ascii_case(needle))
|
|
}
|
|
|
|
fn is_informix_identifier_start(ch: char) -> bool {
|
|
ch == '_' || ch.is_ascii_alphabetic()
|
|
}
|
|
|
|
fn is_informix_identifier_part(ch: char) -> bool {
|
|
ch == '_' || ch == '$' || ch.is_ascii_alphanumeric()
|
|
}
|
|
|
|
fn is_simple_informix_identifier(name: &str) -> bool {
|
|
Regex::new(r"^[A-Za-z_][A-Za-z0-9_$]*$").unwrap().is_match(name)
|
|
}
|
|
|
|
fn mysql_qualified_name(schema: Option<&str>, name: &str) -> String {
|
|
schema
|
|
.into_iter()
|
|
.chain(std::iter::once(name))
|
|
.filter(|part| !part.is_empty())
|
|
.map(quote_mysql_identifier)
|
|
.collect::<Vec<_>>()
|
|
.join(".")
|
|
}
|
|
|
|
fn quote_sqlserver_identifier(value: &str) -> String {
|
|
format!("[{}]", value.replace(']', "]]"))
|
|
}
|
|
|
|
fn sqlserver_qualified_name(schema: Option<&str>, name: &str) -> String {
|
|
schema
|
|
.into_iter()
|
|
.chain(std::iter::once(name))
|
|
.filter(|part| !part.is_empty())
|
|
.map(quote_sqlserver_identifier)
|
|
.collect::<Vec<_>>()
|
|
.join(".")
|
|
}
|
|
|
|
fn unquote_postgres_identifier(value: &str) -> String {
|
|
let trimmed = value.trim();
|
|
if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
|
|
trimmed[1..trimmed.len() - 1].replace("\"\"", "\"")
|
|
} else {
|
|
trimmed.to_string()
|
|
}
|
|
}
|
|
|
|
fn split_qualified_routine_name(value: &str) -> Vec<String> {
|
|
Regex::new(r#""(?:""|[^"])+"|[A-Za-z_][\w$]*"#)
|
|
.unwrap()
|
|
.find_iter(value)
|
|
.map(|part| unquote_postgres_identifier(part.as_str()))
|
|
.collect()
|
|
}
|
|
|
|
fn unquote_mysql_identifier(value: &str) -> String {
|
|
let trimmed = value.trim();
|
|
if trimmed.starts_with('`') && trimmed.ends_with('`') && trimmed.len() >= 2 {
|
|
trimmed[1..trimmed.len() - 1].replace("``", "`")
|
|
} else {
|
|
trimmed.to_string()
|
|
}
|
|
}
|
|
|
|
fn split_mysql_qualified_routine_name(value: &str) -> Vec<String> {
|
|
Regex::new(r"`(?:``|[^`])+`|[A-Za-z_][\w$]*")
|
|
.unwrap()
|
|
.find_iter(value)
|
|
.map(|part| unquote_mysql_identifier(part.as_str()))
|
|
.collect()
|
|
}
|
|
|
|
fn routine_declaration(source: &str) -> Option<RoutineDeclaration> {
|
|
let re = Regex::new(
|
|
r#"(?is)^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:NON)?EDITIONABLE\s+)?(FUNCTION|PROCEDURE)\s+((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)\s*(\(.*?\))?"#,
|
|
)
|
|
.unwrap();
|
|
let captures = re.captures(source)?;
|
|
let kind = parse_object_source_kind(captures.get(1)?.as_str())?;
|
|
let name_parts = split_qualified_routine_name(captures.get(2)?.as_str());
|
|
let name = name_parts.last()?.clone();
|
|
let signature = captures.get(3).map(|value| value.as_str().trim().to_string()).unwrap_or_default();
|
|
Some(RoutineDeclaration { kind, name, signature })
|
|
}
|
|
|
|
fn replace_sql_routine_declaration_name(source: &str, schema: Option<&str>, new_name: &str) -> Option<String> {
|
|
let re = Regex::new(
|
|
r#"(?is)^(\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:NON)?EDITIONABLE\s+)?(?:FUNCTION|PROCEDURE)\s+)((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)"#,
|
|
)
|
|
.unwrap();
|
|
let captures = re.captures(source)?;
|
|
let full = captures.get(0)?;
|
|
let prefix = captures.get(1)?.as_str();
|
|
let existing_name = captures.get(2)?.as_str();
|
|
let existing_parts = split_qualified_routine_name(existing_name);
|
|
let schema_name =
|
|
schema.or_else(|| existing_parts.first().filter(|_| existing_parts.len() > 1).map(String::as_str));
|
|
let replacement = if let Some(schema_name) = schema_name {
|
|
format!("{}.{}", quote_postgres_identifier(schema_name), quote_postgres_identifier(new_name))
|
|
} else {
|
|
quote_postgres_identifier(new_name)
|
|
};
|
|
Some(format!("{}{}{}{}", &source[..full.start()], prefix, replacement, &source[full.end()..]))
|
|
}
|
|
|
|
fn rewrite_oracle_routine_self_references(
|
|
source: &str,
|
|
old_name: &str,
|
|
new_name: &str,
|
|
schema: Option<&str>,
|
|
object_type: ObjectSourceKind,
|
|
) -> Result<String, String> {
|
|
let tokens = Tokenizer::new(&OracleDialect {}, source)
|
|
.tokenize_with_location()
|
|
.map_err(|error| format!("Cannot safely tokenize the routine source: {error}"))?;
|
|
let significant = tokens
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(index, token)| (!matches!(token.token, Token::Whitespace(_))).then_some(index))
|
|
.collect::<Vec<_>>();
|
|
if old_name != new_name {
|
|
if let Some(execute_index) =
|
|
find_oracle_execute_immediate(significant.iter().copied().map(|index| (index, &tokens[index].token)))
|
|
{
|
|
let location = tokens[execute_index].span.start;
|
|
return Err(format!(
|
|
"Dynamic SQL must be rewritten as a verifiable static call at line {}, column {}.",
|
|
location.line, location.column
|
|
));
|
|
}
|
|
}
|
|
let routine_kind_position = significant
|
|
.iter()
|
|
.position(|index| {
|
|
token_word_is(&tokens[*index].token, "FUNCTION") || token_word_is(&tokens[*index].token, "PROCEDURE")
|
|
})
|
|
.ok_or_else(|| "Cannot locate the routine declaration.".to_string())?;
|
|
let mut declaration_name_position = routine_kind_position + 1;
|
|
if significant.get(declaration_name_position + 1).is_some_and(|index| tokens[*index].token == Token::Period) {
|
|
declaration_name_position += 2;
|
|
}
|
|
let body_start = (declaration_name_position + 1..significant.len())
|
|
.find(|position| {
|
|
token_word_is(&tokens[significant[*position]].token, "AS")
|
|
|| token_word_is(&tokens[significant[*position]].token, "IS")
|
|
})
|
|
.ok_or_else(|| "Cannot locate the routine body.".to_string())?;
|
|
let declaration_end = (body_start + 1..significant.len())
|
|
.find(|position| token_word_is(&tokens[significant[*position]].token, "BEGIN"))
|
|
.unwrap_or(significant.len());
|
|
if significant[body_start + 1..declaration_end]
|
|
.iter()
|
|
.any(|index| token_matches_identifier(&tokens[*index].token, old_name))
|
|
{
|
|
return Err(format!("A local declaration named {old_name} may shadow self-recursive calls."));
|
|
}
|
|
if (body_start + 1..significant.len()).any(|position| {
|
|
(token_word_is(&tokens[significant[position]].token, "FUNCTION")
|
|
|| token_word_is(&tokens[significant[position]].token, "PROCEDURE"))
|
|
&& significant
|
|
.get(position + 1)
|
|
.is_some_and(|index| token_matches_identifier(&tokens[*index].token, old_name))
|
|
}) {
|
|
return Err(format!("A local routine named {old_name} may shadow self-recursive calls."));
|
|
}
|
|
if old_name != new_name
|
|
&& significant[body_start + 1..].iter().any(|index| token_matches_identifier(&tokens[*index].token, new_name))
|
|
{
|
|
return Err(format!(
|
|
"The new routine name {new_name} already occurs in the body and could capture an existing reference."
|
|
));
|
|
}
|
|
let mut end = significant.len();
|
|
if significant.last().is_some_and(|index| tokens[*index].token == Token::SemiColon) {
|
|
end -= 1;
|
|
}
|
|
let outer_end = (end >= body_start + 2
|
|
&& token_word_is(&tokens[significant[end - 2]].token, "END")
|
|
&& token_matches_identifier(&tokens[significant[end - 1]].token, old_name))
|
|
.then_some(significant[end - 1]);
|
|
|
|
let mut edits = Vec::new();
|
|
for position in body_start..significant.len() {
|
|
let token_index = significant[position];
|
|
let token = &tokens[token_index];
|
|
if !token_matches_identifier(&token.token, old_name) {
|
|
continue;
|
|
}
|
|
if Some(token_index) == outer_end {
|
|
edits.push((token.span, quote_postgres_identifier(new_name)));
|
|
continue;
|
|
}
|
|
let previous = position.checked_sub(1).map(|index| &tokens[significant[index]].token);
|
|
let previous_previous = position.checked_sub(2).map(|index| &tokens[significant[index]].token);
|
|
let qualified_owner = if previous == Some(&Token::Period) {
|
|
let Some(Token::Word(owner)) = previous_previous else {
|
|
return Err(format!("Cannot determine which routine reference to rename: {old_name}."));
|
|
};
|
|
if schema.is_some_and(|schema| identifier_matches(owner, schema)) {
|
|
true
|
|
} else {
|
|
continue;
|
|
}
|
|
} else {
|
|
false
|
|
};
|
|
let next = significant.get(position + 1).map(|index| &tokens[*index].token);
|
|
let is_call = matches!(next, Some(Token::LParen))
|
|
|| (object_type == ObjectSourceKind::Procedure && matches!(next, Some(Token::SemiColon)));
|
|
if is_call && (!matches!(previous, Some(Token::Period)) || qualified_owner) {
|
|
edits.push((token.span, quote_postgres_identifier(new_name)));
|
|
} else {
|
|
return Err(format!(
|
|
"Cannot safely determine whether {old_name} is a self-reference or another identifier."
|
|
));
|
|
}
|
|
}
|
|
|
|
let mut result = source.to_string();
|
|
for (span, replacement) in edits.into_iter().rev() {
|
|
let start = routine_location_to_byte_offset(source, span.start)
|
|
.ok_or_else(|| "Cannot map routine token positions to the source text.".to_string())?;
|
|
let end = routine_location_to_byte_offset(source, span.end)
|
|
.ok_or_else(|| "Cannot map routine token positions to the source text.".to_string())?;
|
|
if start >= end || end > result.len() {
|
|
return Err("Routine token positions overlap or exceed the source text.".to_string());
|
|
}
|
|
result.replace_range(start..end, &replacement);
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
fn routine_location_to_byte_offset(source: &str, location: sqlparser::tokenizer::Location) -> Option<usize> {
|
|
let mut line = 1;
|
|
let mut column = 1;
|
|
for (offset, character) in source.char_indices() {
|
|
if line == location.line && column == location.column {
|
|
return Some(offset);
|
|
}
|
|
if character == '\n' {
|
|
line += 1;
|
|
column = 1;
|
|
} else {
|
|
column += 1;
|
|
}
|
|
}
|
|
(line == location.line && column == location.column).then_some(source.len())
|
|
}
|
|
|
|
fn token_word_is(token: &Token, expected: &str) -> bool {
|
|
matches!(token, Token::Word(word) if word.value.eq_ignore_ascii_case(expected))
|
|
}
|
|
|
|
fn find_oracle_execute_immediate<'a>(tokens: impl IntoIterator<Item = (usize, &'a Token)>) -> Option<usize> {
|
|
let mut previous: Option<(usize, &'a Token)> = None;
|
|
for (index, token) in tokens {
|
|
if let Some((previous_index, previous_token)) = previous {
|
|
if token_unquoted_word_is(previous_token, "EXECUTE") && token_unquoted_word_is(token, "IMMEDIATE") {
|
|
return Some(previous_index);
|
|
}
|
|
}
|
|
previous = Some((index, token));
|
|
}
|
|
None
|
|
}
|
|
|
|
fn token_unquoted_word_is(token: &Token, expected: &str) -> bool {
|
|
matches!(token, Token::Word(word) if word.quote_style.is_none() && word.value.eq_ignore_ascii_case(expected))
|
|
}
|
|
|
|
fn token_matches_identifier(token: &Token, expected: &str) -> bool {
|
|
matches!(token, Token::Word(word) if identifier_matches(word, expected))
|
|
}
|
|
|
|
fn identifier_matches(identifier: &sqlparser::tokenizer::Word, expected: &str) -> bool {
|
|
if identifier.quote_style.is_some() {
|
|
identifier.value == expected
|
|
} else if identifier.keyword != Keyword::NoKeyword {
|
|
false
|
|
} else {
|
|
identifier.value.to_ascii_uppercase() == expected
|
|
}
|
|
}
|
|
|
|
fn mysql_routine_declaration(source: &str) -> Option<RoutineDeclaration> {
|
|
let re = Regex::new(
|
|
r"(?is)^\s*CREATE\s+(?:DEFINER\s*=\s*(?:(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s*@\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)|CURRENT_USER(?:\(\))?)\s+)?(FUNCTION|PROCEDURE)\s+(?:IF\s+NOT\s+EXISTS\s+)?((?:`(?:``|[^`])+`|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:`(?:``|[^`])+`|[A-Za-z_][\w$]*))?)",
|
|
)
|
|
.unwrap();
|
|
let captures = re.captures(source)?;
|
|
let kind = parse_object_source_kind(captures.get(1)?.as_str())?;
|
|
let name_parts = split_mysql_qualified_routine_name(captures.get(2)?.as_str());
|
|
let name = name_parts.last()?.clone();
|
|
Some(RoutineDeclaration { kind, name, signature: String::new() })
|
|
}
|
|
|
|
fn replace_mysql_routine_declaration_name(source: &str, new_name: &str) -> Option<String> {
|
|
let re = Regex::new(
|
|
r"(?is)^(\s*CREATE\s+(?:DEFINER\s*=\s*(?:(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s*@\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)|CURRENT_USER(?:\(\))?)\s+)?(?:FUNCTION|PROCEDURE)\s+(?:IF\s+NOT\s+EXISTS\s+)?)((?:`(?:``|[^`])+`|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:`(?:``|[^`])+`|[A-Za-z_][\w$]*))?)",
|
|
)
|
|
.unwrap();
|
|
let captures = re.captures(source)?;
|
|
let full = captures.get(0)?;
|
|
let prefix = captures.get(1)?.as_str();
|
|
Some(format!("{}{}{}{}", &source[..full.start()], prefix, quote_mysql_identifier(new_name), &source[full.end()..]))
|
|
}
|
|
|
|
fn routine_name_changed(source_name: &str, saved_name: &str) -> bool {
|
|
!source_name.eq_ignore_ascii_case(saved_name)
|
|
}
|
|
|
|
fn replace_sqlserver_create_with_alter(source: &str) -> String {
|
|
let statement_start = leading_sql_statement_start(source);
|
|
let executable = &source[statement_start..];
|
|
let create = Regex::new(r"(?i)^CREATE\s+(?:OR\s+ALTER\s+)?").unwrap();
|
|
format!("{}{}", &source[..statement_start], create.replace(executable, "ALTER "))
|
|
}
|
|
|
|
fn build_sqlserver_alter_view_sql(schema: Option<&str>, name: &str, source: &str) -> String {
|
|
// A view definition fetched from SQL Server carries the same header comment
|
|
// block a routine does. Matching the CREATE prefix at offset 0 made the
|
|
// rewrite miss on those, so the body kept its own `CREATE VIEW` underneath a
|
|
// freshly prepended `ALTER VIEW ... AS` header — two conflicting statements
|
|
// in one save (issue #13004). Mirror the routine path: locate the first
|
|
// statement start, match there, and keep the leading prefix untouched.
|
|
let statement_start = leading_sql_statement_start(source);
|
|
let executable = &source[statement_start..];
|
|
let existing_view_statement = Regex::new(r"(?i)^CREATE\s+(?:OR\s+ALTER\s+)?VIEW\s+|^ALTER\s+VIEW\s+").unwrap();
|
|
if existing_view_statement.is_match(executable) {
|
|
let replaced = existing_view_statement.replace(executable, "ALTER VIEW ");
|
|
return ensure_semicolon(&format!("{}{}", &source[..statement_start], replaced));
|
|
}
|
|
|
|
format!("ALTER VIEW {} AS\n{}", sqlserver_qualified_name(schema, name), ensure_semicolon(source))
|
|
}
|
|
|
|
fn executable_duckdb_view_ddl(schema: Option<&str>, name: &str, source: &str) -> String {
|
|
let existing_view_statement =
|
|
Regex::new(r"(?i)^CREATE\s+(?:OR\s+REPLACE\s+)?VIEW(?:\s+IF\s+NOT\s+EXISTS)?\s+").unwrap();
|
|
if existing_view_statement.is_match(source) {
|
|
return ensure_semicolon(&existing_view_statement.replace(source, "CREATE OR REPLACE VIEW "));
|
|
}
|
|
|
|
format!("CREATE OR REPLACE VIEW {} AS\n{}", postgres_qualified_name(schema, name), ensure_semicolon(source))
|
|
}
|
|
|
|
fn parse_object_source_kind(value: &str) -> Option<ObjectSourceKind> {
|
|
if value.eq_ignore_ascii_case("VIEW") {
|
|
Some(ObjectSourceKind::View)
|
|
} else if value.eq_ignore_ascii_case("MATERIALIZED VIEW") || value.eq_ignore_ascii_case("MATERIALIZED_VIEW") {
|
|
Some(ObjectSourceKind::MaterializedView)
|
|
} else if value.eq_ignore_ascii_case("PROCEDURE") {
|
|
Some(ObjectSourceKind::Procedure)
|
|
} else if value.eq_ignore_ascii_case("FUNCTION") {
|
|
Some(ObjectSourceKind::Function)
|
|
} else if value.eq_ignore_ascii_case("TRIGGER") {
|
|
Some(ObjectSourceKind::Trigger)
|
|
} else if value.eq_ignore_ascii_case("SEQUENCE") {
|
|
Some(ObjectSourceKind::Sequence)
|
|
} else if value.eq_ignore_ascii_case("SYNONYM") {
|
|
Some(ObjectSourceKind::Synonym)
|
|
} else if value.eq_ignore_ascii_case("JOB") {
|
|
Some(ObjectSourceKind::Job)
|
|
} else if value.eq_ignore_ascii_case("PACKAGE") {
|
|
Some(ObjectSourceKind::Package)
|
|
} else if value.eq_ignore_ascii_case("PACKAGE BODY") || value.eq_ignore_ascii_case("PACKAGE_BODY") {
|
|
Some(ObjectSourceKind::PackageBody)
|
|
} else if value.eq_ignore_ascii_case("TYPE") {
|
|
Some(ObjectSourceKind::Type)
|
|
} else if value.eq_ignore_ascii_case("TYPE BODY") || value.eq_ignore_ascii_case("TYPE_BODY") {
|
|
Some(ObjectSourceKind::TypeBody)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn oracle_routine_fingerprint(source: &str) -> Result<String, String> {
|
|
fingerprint_oracle_routine_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
name: "CALLER_1".into(),
|
|
source: source.into(),
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_source_fingerprint_ignores_only_whitespace_and_one_final_semicolon() {
|
|
let submitted = "CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN APP.P_NEW; APP.P_NEW; END;";
|
|
let readback = "CREATE OR REPLACE PROCEDURE APP.CALLER_1\n IS\nBEGIN APP.P_NEW; APP.P_NEW; END";
|
|
|
|
assert_eq!(oracle_routine_fingerprint(submitted).unwrap(), oracle_routine_fingerprint(readback).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_source_fingerprint_preserves_literals_comments_hints_and_identifiers() {
|
|
let base = "CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN DBMS_OUTPUT.PUT_LINE('a b'); APP.P_NEW; END;";
|
|
for changed in [
|
|
"CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN DBMS_OUTPUT.PUT_LINE('a b'); APP.P_NEW; END;",
|
|
"CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN /* changed */ DBMS_OUTPUT.PUT_LINE('a b'); APP.P_NEW; END;",
|
|
"CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN /*+ changed */ DBMS_OUTPUT.PUT_LINE('a b'); APP.P_NEW; END;",
|
|
"CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN DBMS_OUTPUT.PUT_LINE(q'{a b}'); APP.P_NEW; END;",
|
|
"CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN DBMS_OUTPUT.PUT_LINE(q'{a b}'); \"APP\".P_NEW; END;",
|
|
"CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN DBMS_OUTPUT.PUT_LINE('a b'); APP.P_OTHER; END;",
|
|
] {
|
|
assert_ne!(oracle_routine_fingerprint(base).unwrap(), oracle_routine_fingerprint(changed).unwrap());
|
|
}
|
|
let q_literal = "CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN DBMS_OUTPUT.PUT_LINE(q'{a b}'); END;";
|
|
let changed_q_literal =
|
|
"CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN DBMS_OUTPUT.PUT_LINE(q'{a b}'); END;";
|
|
let nq_literal = "CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN DBMS_OUTPUT.PUT_LINE(nq'{a b}'); END;";
|
|
let changed_nq_literal =
|
|
"CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN DBMS_OUTPUT.PUT_LINE(nq'{a b}'); END;";
|
|
assert_ne!(
|
|
oracle_routine_fingerprint(q_literal).unwrap(),
|
|
oracle_routine_fingerprint(changed_q_literal).unwrap()
|
|
);
|
|
assert_ne!(
|
|
oracle_routine_fingerprint(nq_literal).unwrap(),
|
|
oracle_routine_fingerprint(changed_nq_literal).unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_source_fingerprint_rejects_wrong_identity_and_multiple_statements() {
|
|
for source in [
|
|
"CREATE OR REPLACE PROCEDURE OTHER.CALLER_1 IS BEGIN APP.P_NEW; END;",
|
|
"CREATE OR REPLACE PROCEDURE APP.OTHER IS BEGIN APP.P_NEW; END;",
|
|
"CREATE OR REPLACE PROCEDURE APP.CALLER_1 IS BEGIN APP.P_NEW; END; SELECT 1",
|
|
] {
|
|
assert!(oracle_routine_fingerprint(source).is_err(), "accepted invalid source: {source}");
|
|
}
|
|
}
|
|
|
|
fn type_management_source(
|
|
database_type: DatabaseType,
|
|
object_type: ObjectSourceKind,
|
|
name: &str,
|
|
source: &str,
|
|
) -> EditableObjectSourceSqlInput {
|
|
EditableObjectSourceSqlInput {
|
|
database_type,
|
|
object_type,
|
|
schema: Some("Exact Owner".to_string()),
|
|
name: name.to_string(),
|
|
source: source.to_string(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_type_management_accepts_one_exact_type_or_body_definition() {
|
|
let type_source = "CREATE OR REPLACE TYPE \"Exact Owner\".\"Address Type\" AS OBJECT (id NUMBER);";
|
|
let body_source = "CREATE OR REPLACE TYPE BODY \"Exact Owner\".\"Address Type\" AS MEMBER FUNCTION score RETURN NUMBER IS BEGIN RETURN 1; END; END;\n/";
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::OceanbaseOracle, DatabaseType::Dameng] {
|
|
for (object_type, source) in
|
|
[(ObjectSourceKind::Type, type_source), (ObjectSourceKind::TypeBody, body_source)]
|
|
{
|
|
let statements = build_executable_object_source_statements(type_management_source(
|
|
database_type,
|
|
object_type.clone(),
|
|
"Address Type",
|
|
source,
|
|
))
|
|
.unwrap();
|
|
assert_eq!(statements.len(), 1, "{database_type:?} {object_type:?}");
|
|
assert!(statements[0].contains("Address Type"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_type_management_rejects_wrong_name_kind_and_extra_ddl() {
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::OceanbaseOracle, DatabaseType::Dameng] {
|
|
for (object_type, name, source) in [
|
|
(
|
|
ObjectSourceKind::Type,
|
|
"Address Type",
|
|
"CREATE TYPE \"Exact Owner\".\"Other Type\" AS OBJECT (id NUMBER);",
|
|
),
|
|
(ObjectSourceKind::Type, "Address Type", "CREATE TYPE BODY \"Exact Owner\".\"Address Type\" AS END;"),
|
|
(
|
|
ObjectSourceKind::TypeBody,
|
|
"Address Type",
|
|
"CREATE TYPE \"Exact Owner\".\"Address Type\" AS OBJECT (id NUMBER);",
|
|
),
|
|
(
|
|
ObjectSourceKind::Type,
|
|
"Address Type",
|
|
"CREATE TYPE \"Exact Owner\".\"Address Type\" AS OBJECT (id NUMBER); DROP TABLE \"Exact Owner\".T;",
|
|
),
|
|
(
|
|
ObjectSourceKind::Type,
|
|
"Address Type",
|
|
"CREATE TYPE \"Other Owner\".\"Address Type\" AS OBJECT (id NUMBER);",
|
|
),
|
|
] {
|
|
assert!(
|
|
build_executable_object_source_statements(type_management_source(
|
|
database_type,
|
|
object_type.clone(),
|
|
name,
|
|
source
|
|
))
|
|
.is_err(),
|
|
"accepted {database_type:?} {object_type:?}: {source}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_type_management_requires_complete_blocks_and_rejects_getddl_mixtures() {
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::OceanbaseOracle, DatabaseType::Dameng] {
|
|
for (kind, source) in [
|
|
(ObjectSourceKind::Type, "CREATE TYPE \"Exact Owner\".\"Address Type\" AS OBJECT (ID NUMBER"),
|
|
(ObjectSourceKind::TypeBody, "CREATE TYPE BODY \"Exact Owner\".\"Address Type\" AS MEMBER FUNCTION F RETURN NUMBER IS BEGIN RETURN 1; END;"),
|
|
(ObjectSourceKind::Type, "CREATE TYPE \"Exact Owner\".\"Address Type\" AS OBJECT(ID NUMBER); CREATE TYPE BODY \"Exact Owner\".\"Address Type\" AS MEMBER FUNCTION F RETURN NUMBER IS BEGIN RETURN 1; END; END;"),
|
|
(ObjectSourceKind::Type, "CREATE TYPE \"Exact Owner\".\"Address Type\" FORCE AS OBJECT(ID NUMBER);"),
|
|
] {
|
|
assert!(build_executable_object_source_statements(type_management_source(database_type, kind.clone(), "Address Type", source)).is_err(), "accepted {database_type:?} {kind:?}: {source}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_type_management_qualifies_dictionary_source_and_preserves_comments_and_literals() {
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::OceanbaseOracle, DatabaseType::Dameng] {
|
|
for source in [
|
|
"-- source header\nTYPE \"Address Type\" AS OBJECT(ID NUMBER)",
|
|
"CREATE /* retained */ TYPE \"Address Type\" AS OBJECT(ID NUMBER)",
|
|
"TYPE BODY \"Address Type\" AS MEMBER FUNCTION F RETURN NUMBER IS V VARCHAR2(80):=q'[END; DROP TABLE literal_only;]'; BEGIN IF 1=1 THEN RETURN 1; END IF; RETURN 0; END; END;\n/",
|
|
] {
|
|
let kind = if source.contains("TYPE BODY") { ObjectSourceKind::TypeBody } else { ObjectSourceKind::Type };
|
|
let sql = build_executable_object_source_statements(type_management_source(database_type, kind, "Address Type", source)).unwrap().join("\n");
|
|
assert!(sql.contains("\"Exact Owner\".\"Address Type\""), "target owner not bound: {sql}");
|
|
if source.contains("retained") { assert!(sql.contains("/* retained */")); }
|
|
if source.contains("literal_only") { assert!(sql.contains("q'[END; DROP TABLE literal_only;]'")); }
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_type_management_accepts_vendor_forms_and_quoted_keyword_names() {
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::OceanbaseOracle, DatabaseType::Dameng] {
|
|
for name in ["BODY", "FORCE", "TYPE"] {
|
|
for qualifier in ["\"Exact Owner\".", ""] {
|
|
let source = format!("CREATE TYPE {qualifier}\"{name}\" AS OBJECT(ID NUMBER);");
|
|
assert!(
|
|
build_executable_object_source_statements(type_management_source(
|
|
database_type,
|
|
ObjectSourceKind::Type,
|
|
name,
|
|
&source
|
|
))
|
|
.is_ok(),
|
|
"rejected quoted {name} for {database_type:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
for source in [
|
|
"TYPE \"Address Type\" IS RECORD (ID NUMBER, LABEL VARCHAR(40));",
|
|
"TYPE \"Address Type\" IS ARRAY INTEGER[3];",
|
|
"TYPE \"Address Type\" AS TABLE OF NUMBER INDEX BY VARCHAR(40);",
|
|
"TYPE \"Address Type\" AS VARRAY(20) OF NUMBER;",
|
|
"TYPE \"Address Type\" UNDER \"Parent Type\" (LABEL VARCHAR(40)) NOT FINAL;",
|
|
] {
|
|
assert!(
|
|
build_executable_object_source_statements(type_management_source(
|
|
DatabaseType::Dameng,
|
|
ObjectSourceKind::Type,
|
|
"Address Type",
|
|
source
|
|
))
|
|
.is_ok(),
|
|
"rejected DM type form: {source}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_type_management_quoted_keywords_do_not_hide_force() {
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::OceanbaseOracle, DatabaseType::Dameng] {
|
|
let source = "CREATE OR REPLACE TYPE \"Exact Owner\".\"AS\" FORCE AS OBJECT(ID NUMBER);";
|
|
assert!(
|
|
build_executable_object_source_statements(type_management_source(
|
|
database_type,
|
|
ObjectSourceKind::Type,
|
|
"AS",
|
|
source
|
|
))
|
|
.is_err(),
|
|
"quoted AS name bypassed FORCE rejection for {database_type:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn type_management_trigger_rebuild_has_the_requested_state_in_the_create_statement() {
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::OceanbaseOracle] {
|
|
for header in [
|
|
"BEFORE INSERT ON APP.T FOR EACH ROW",
|
|
"BEFORE INSERT ON APP.T FOR EACH ROW ENABLE WHEN (NEW.ID > 0)",
|
|
"FOR INSERT ON APP.T",
|
|
] {
|
|
for enabled in [false, true] {
|
|
let body = if header.starts_with("FOR ") {
|
|
"COMPOUND TRIGGER BEFORE STATEMENT IS BEGIN NULL; END BEFORE STATEMENT; END;"
|
|
} else {
|
|
"BEGIN NULL; END;"
|
|
};
|
|
let input = type_management_source(
|
|
database_type,
|
|
ObjectSourceKind::Trigger,
|
|
"Address Type",
|
|
&format!("TRIGGER \"Address Type\" {header} {body}"),
|
|
);
|
|
let (sql, expected) = build_oracle_type_management_trigger_source(input.clone(), enabled).unwrap();
|
|
let state = if enabled { "ENABLE" } else { "DISABLE" };
|
|
assert!(
|
|
sql.contains(&format!("{state} WHEN"))
|
|
|| sql.contains(&format!("{state} BEGIN"))
|
|
|| sql.contains(&format!("{state} COMPOUND")),
|
|
"{sql}"
|
|
);
|
|
assert_eq!(split_sql_statements_for_database(&sql, database_type).len(), 1);
|
|
assert_eq!(
|
|
build_oracle_type_management_source(EditableObjectSourceSqlInput { source: sql, ..input })
|
|
.unwrap()
|
|
.1,
|
|
expected
|
|
);
|
|
}
|
|
}
|
|
}
|
|
let input = type_management_source(
|
|
DatabaseType::Dameng,
|
|
ObjectSourceKind::Trigger,
|
|
"Address Type",
|
|
"TRIGGER \"Address Type\" BEFORE INSERT ON APP.T BEGIN NULL; END;",
|
|
);
|
|
assert!(build_oracle_type_management_trigger_source(input.clone(), false).is_err());
|
|
assert!(build_oracle_type_management_trigger_source(input, true).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn type_management_dependency_builder_binds_callers_and_compares_normalized_source() {
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::OceanbaseOracle, DatabaseType::Dameng] {
|
|
for (kind, source) in [
|
|
(ObjectSourceKind::Procedure, "PROCEDURE \"Address Type\" AS BEGIN NULL; END;"),
|
|
(ObjectSourceKind::Function, "FUNCTION \"Address Type\" RETURN NUMBER AS BEGIN RETURN 1; END;"),
|
|
(ObjectSourceKind::View, "CREATE VIEW \"Address Type\" AS SELECT 1 AS ID FROM DUAL;"),
|
|
(ObjectSourceKind::Trigger, "CREATE TRIGGER \"Address Type\" BEFORE INSERT ON T BEGIN NULL; END;"),
|
|
(ObjectSourceKind::Package, "CREATE PACKAGE \"Address Type\" AS PROCEDURE P; END;"),
|
|
(
|
|
ObjectSourceKind::PackageBody,
|
|
"CREATE PACKAGE BODY \"Address Type\" AS PROCEDURE P IS BEGIN NULL; END; END;",
|
|
),
|
|
] {
|
|
let input = type_management_source(database_type, kind.clone(), "Address Type", source);
|
|
let (sql, fingerprint) = build_oracle_type_management_source(input.clone()).unwrap();
|
|
assert!(sql.contains("\"Exact Owner\".\"Address Type\""));
|
|
let (_, readback) =
|
|
build_oracle_type_management_source(EditableObjectSourceSqlInput { source: sql, ..input }).unwrap();
|
|
assert_eq!(fingerprint, readback);
|
|
let input = type_management_source(database_type, kind, "Different Type", source);
|
|
assert!(build_oracle_type_management_source(input).is_err());
|
|
}
|
|
}
|
|
let (first, second) = (
|
|
"procedure \"Address Type\" as begin null; end;",
|
|
"CREATE OR REPLACE PROCEDURE \"Exact Owner\".\"Address Type\" AS BEGIN NULL; END;",
|
|
);
|
|
let make = |source| {
|
|
build_oracle_type_management_source(type_management_source(
|
|
DatabaseType::Oracle,
|
|
ObjectSourceKind::Procedure,
|
|
"Address Type",
|
|
source,
|
|
))
|
|
.unwrap()
|
|
.1
|
|
};
|
|
assert_eq!(make(first), make(second));
|
|
assert_ne!(make(second), make("CREATE PROCEDURE \"Address Type\" AS BEGIN NULL; /* preserved */ END;"));
|
|
}
|
|
|
|
#[test]
|
|
fn type_management_readback_compares_the_validated_target_not_catalogue_quoting() {
|
|
let make = |source: &str| {
|
|
build_oracle_type_management_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
object_type: ObjectSourceKind::Type,
|
|
schema: Some("APP".into()),
|
|
name: "ROOT".into(),
|
|
source: source.into(),
|
|
})
|
|
.map(|(_, fingerprint)| fingerprint)
|
|
};
|
|
let expected = make("CREATE OR REPLACE TYPE APP.ROOT AS OBJECT (ID NUMBER);").unwrap();
|
|
assert_eq!(expected, make("TYPE ROOT AS OBJECT (ID NUMBER);").unwrap());
|
|
assert_eq!(expected, make("CREATE TYPE \"APP\".\"ROOT\" AS OBJECT (ID NUMBER);").unwrap());
|
|
assert!(make("\"CREATE\" TYPE APP.ROOT AS OBJECT (ID NUMBER);").is_err());
|
|
assert!(make("CREATE \"OR\" REPLACE TYPE APP.ROOT AS OBJECT (ID NUMBER);").is_err());
|
|
}
|
|
|
|
fn ob_sequence(source: &str, schema: Option<&str>, name: &str) -> EditableObjectSourceSqlInput {
|
|
EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
object_type: ObjectSourceKind::Sequence,
|
|
schema: schema.map(str::to_string),
|
|
name: name.to_string(),
|
|
source: source.to_string(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oceanbase_sequence_edit_preserves_counter_and_object_identity() {
|
|
let source = "CREATE SEQUENCE \"App\".\"Seq\" MINVALUE -999 MAXVALUE 9999999999999999999999999999 INCREMENT BY -2 START WITH 40 CACHE 20 NOCYCLE NOORDER;";
|
|
let input = ob_sequence(source, Some("App"), "Seq");
|
|
let expected = "ALTER SEQUENCE \"App\".\"Seq\"\n MINVALUE - 999\n MAXVALUE 9999999999999999999999999999\n INCREMENT BY - 2\n CACHE 20\n NOCYCLE\n NOORDER;";
|
|
assert_eq!(build_editable_object_source(input.clone()), expected);
|
|
assert_eq!(build_executable_object_source_statements(input).unwrap(), vec![expected]);
|
|
let alter = ob_sequence(expected, Some("App"), "Seq");
|
|
assert_eq!(build_executable_object_source_statements(alter).unwrap(), vec![expected]);
|
|
}
|
|
|
|
#[test]
|
|
fn oceanbase_sequence_edit_handles_unqualified_and_quoted_names() {
|
|
for (schema, name, source, expected_name) in [
|
|
(Some("APP"), "SEQ", "alter sequence seq increment by 3 nocache", "\"APP\".\"SEQ\""),
|
|
(None, "SEQ", "create sequence seq start with 1 increment by 1", "\"SEQ\""),
|
|
(
|
|
Some("A.B"),
|
|
"S\"Q",
|
|
"CREATE SEQUENCE \"A.B\".\"S\"\"Q\" START WITH 1 -- no reset\n INCREMENT BY 2",
|
|
"\"A.B\".\"S\"\"Q\"",
|
|
),
|
|
] {
|
|
let sql = build_executable_object_source_sql(ob_sequence(source, schema, name)).unwrap();
|
|
assert!(sql.starts_with(&format!("ALTER SEQUENCE {expected_name}")));
|
|
assert!(!sql.contains("START WITH"));
|
|
assert!(!sql.contains("DROP"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oceanbase_sequence_edit_rejects_restart_wrong_targets_and_extra_statements() {
|
|
for source in [
|
|
"ALTER SEQUENCE APP.SEQ RESTART START WITH 1",
|
|
"ALTER SEQUENCE APP.OTHER INCREMENT BY 1",
|
|
"ALTER SEQUENCE OTHER.SEQ INCREMENT BY 1",
|
|
"ALTER SEQUENCE \"app\".SEQ INCREMENT BY 1",
|
|
"ALTER SEQUENCE APP.SEQ INCREMENT BY 1; DROP TABLE APP.T",
|
|
"DROP SEQUENCE APP.SEQ",
|
|
"CREATE OR REPLACE SEQUENCE APP.SEQ INCREMENT BY 1",
|
|
"CREATE SEQUENCE APP.SEQ START WITH 1",
|
|
"ALTER SEQUENCE APP.SEQ CACHE 1.5",
|
|
"ALTER SEQUENCE APP.SEQ CACHE 1e3",
|
|
"ALTER SEQUENCE APP.SEQ INCREMENT BY",
|
|
"ALTER SEQUENCE APP.SEQ START WITH 1",
|
|
"CREATE SEQUENCE APP.SEQ START WITH 1 INCREMENT BY 1; SELECT 1 FROM DUAL",
|
|
] {
|
|
assert!(
|
|
build_executable_object_source_statements(ob_sequence(source, Some("APP"), "SEQ")).is_err(),
|
|
"{source}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_ddl_terminator_respects_quoted_tokens_and_slash_delimiters() {
|
|
for (source, expected) in [
|
|
("SELECT q'[owner's --;]' FROM DUAL -- tail;", "SELECT q'[owner's --;]' FROM DUAL -- tail;\n;"),
|
|
("SELECT q'[owner's --;]' FROM DUAL; -- tail;", "SELECT q'[owner's --;]' FROM DUAL; -- tail;"),
|
|
("SELECT 1 AS \"名--称\" FROM DUAL -- tail;", "SELECT 1 AS \"名--称\" FROM DUAL -- tail;\n;"),
|
|
("SELECT 1 FROM DUAL\n/", "SELECT 1 FROM DUAL\n/"),
|
|
("SELECT 1 FROM DUAL;\n/", "SELECT 1 FROM DUAL;\n/"),
|
|
("SELECT 1 FROM DUAL\n/\n-- tail;", "SELECT 1 FROM DUAL\n/\n-- tail;"),
|
|
(" CREATE TABLE t (id NUMBER);\n", " CREATE TABLE t (id NUMBER);"),
|
|
(" \n", ""),
|
|
] {
|
|
assert_eq!(ensure_oracle_ddl_terminated(source), expected);
|
|
}
|
|
}
|
|
|
|
fn input(database_type: DatabaseType, object_type: ObjectSourceKind, source: &str) -> EditableObjectSourceSqlInput {
|
|
EditableObjectSourceSqlInput {
|
|
database_type,
|
|
object_type,
|
|
schema: Some("public".to_string()),
|
|
name: "refresh_cache".to_string(),
|
|
source: source.to_string(),
|
|
}
|
|
}
|
|
|
|
fn assert_sqlserver_editable_and_executable(object_type: ObjectSourceKind, source: &str, expected: &str) {
|
|
let input = EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type,
|
|
schema: Some("dbo".to_string()),
|
|
name: "usp_demo".to_string(),
|
|
source: source.to_string(),
|
|
};
|
|
|
|
assert_eq!(build_editable_object_source(input.clone()), expected);
|
|
assert_eq!(build_executable_object_source_sql(input).unwrap(), expected);
|
|
}
|
|
|
|
fn informix_view_statements(name: &str, source: &str) -> Vec<String> {
|
|
build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Informix,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("gbasedbt".to_string()),
|
|
name: name.to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
fn expected_informix_view_replace_statements(
|
|
original_name: &str,
|
|
target_name: &str,
|
|
create_tail: &str,
|
|
) -> Vec<String> {
|
|
let validation_name = informix_validation_view_name(target_name);
|
|
let mut statements = vec![
|
|
drop_informix_view_if_exists(&validation_name),
|
|
create_informix_view(&validation_name, create_tail),
|
|
drop_informix_view_if_exists(&validation_name),
|
|
];
|
|
if !target_name.eq_ignore_ascii_case(original_name) {
|
|
statements.push(drop_informix_view_if_exists(original_name));
|
|
}
|
|
statements.push(drop_informix_view_if_exists(target_name));
|
|
statements.push(create_informix_view(target_name, create_tail));
|
|
statements
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_edited_source_saves_as_alter() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("dbo".to_string()),
|
|
name: "usp_demo".to_string(),
|
|
source: "CREATE PROCEDURE dbo.usp_demo AS SELECT 1;".to_string(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(sql, "ALTER PROCEDURE dbo.usp_demo AS SELECT 1;");
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_alter_source_saves_as_alter() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("dbo".to_string()),
|
|
name: "usp_demo".to_string(),
|
|
source: "ALTER PROCEDURE dbo.usp_demo AS SELECT 1;".to_string(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(sql, "ALTER PROCEDURE dbo.usp_demo AS SELECT 1;");
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_create_function_source_saves_as_alter() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type: ObjectSourceKind::Function,
|
|
schema: Some("dbo".to_string()),
|
|
name: "fn_demo".to_string(),
|
|
source: "CREATE FUNCTION dbo.fn_demo() RETURNS INT AS BEGIN RETURN 1 END;".to_string(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(sql, "ALTER FUNCTION dbo.fn_demo() RETURNS INT AS BEGIN RETURN 1 END;");
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_create_or_alter_function_source_saves_as_alter() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type: ObjectSourceKind::Function,
|
|
schema: Some("dbo".to_string()),
|
|
name: "fn_demo".to_string(),
|
|
source: "CREATE OR ALTER FUNCTION dbo.fn_demo() RETURNS INT AS BEGIN RETURN 1 END;".to_string(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(sql, "ALTER FUNCTION dbo.fn_demo() RETURNS INT AS BEGIN RETURN 1 END;");
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_commented_procedure_source_preserves_comments_and_uses_alter() {
|
|
let source = "-- =============================================\n-- Author: DBX\n-- Description: issue 2269 reproduction\n-- =============================================\n\nCREATE PROCEDURE [dbo].[usp_demo]\nAS\nBEGIN\n SELECT 1;\nEND;";
|
|
let expected = "-- =============================================\n-- Author: DBX\n-- Description: issue 2269 reproduction\n-- =============================================\n\nALTER PROCEDURE [dbo].[usp_demo]\nAS\nBEGIN\n SELECT 1;\nEND;";
|
|
|
|
assert_sqlserver_editable_and_executable(ObjectSourceKind::Procedure, source, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_block_commented_function_source_preserves_comment_and_uses_alter() {
|
|
let source = "/* keep this function header */\nCREATE OR ALTER FUNCTION dbo.fn_demo() RETURNS INT AS BEGIN RETURN 1 END;";
|
|
let expected =
|
|
"/* keep this function header */\nALTER FUNCTION dbo.fn_demo() RETURNS INT AS BEGIN RETURN 1 END;";
|
|
|
|
assert_sqlserver_editable_and_executable(ObjectSourceKind::Function, source, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_commented_alter_source_remains_unchanged() {
|
|
let source = "-- keep this note\nALTER PROCEDURE dbo.usp_demo AS SELECT 1;";
|
|
|
|
assert_sqlserver_editable_and_executable(ObjectSourceKind::Procedure, source, source);
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_unclosed_block_comment_source_remains_unchanged() {
|
|
let source = "/* unclosed header\nCREATE PROCEDURE dbo.usp_demo AS SELECT 1;";
|
|
|
|
assert_sqlserver_editable_and_executable(ObjectSourceKind::Procedure, source, source);
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_leading_whitespace_before_procedure_uses_alter() {
|
|
let source = "\n \tCREATE PROCEDURE dbo.usp_demo AS SELECT 1;\n";
|
|
|
|
assert_sqlserver_editable_and_executable(
|
|
ObjectSourceKind::Procedure,
|
|
source,
|
|
"ALTER PROCEDURE dbo.usp_demo AS SELECT 1;",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_view_save_rewrites_create_to_alter_view() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dbo".to_string()),
|
|
name: "new_view".to_string(),
|
|
source: "CREATE VIEW dbo.new_view AS SELECT * FROM AppInfo".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "ALTER VIEW dbo.new_view AS SELECT * FROM AppInfo;");
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_view_save_rewrites_create_or_alter_to_alter_view() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dbo".to_string()),
|
|
name: "new_view".to_string(),
|
|
source: "CREATE OR ALTER VIEW dbo.new_view AS SELECT * FROM AppInfo;".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "ALTER VIEW dbo.new_view AS SELECT * FROM AppInfo;");
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_view_body_saves_as_alter_view() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dbo".to_string()),
|
|
name: "new_view".to_string(),
|
|
source: "SELECT\n *\nFROM AppInfo".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "ALTER VIEW [dbo].[new_view] AS\nSELECT\n *\nFROM AppInfo;");
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_commented_view_source_preserves_comments_and_uses_alter_view() {
|
|
// SQL Server returns view definitions with the same header comment block
|
|
// a routine carries. The view rewrite used to match its CREATE prefix at
|
|
// offset 0, so the comment made it miss and the saved script became
|
|
// `ALTER VIEW ... AS` followed by the untouched `CREATE VIEW ...` body —
|
|
// two conflicting statements (issue #13004).
|
|
let source = "-- =============================================\n-- Author: DBX\n-- =============================================\n\nCREATE VIEW [dbo].[v_demo]\nAS\nSELECT id FROM users;";
|
|
let expected = "-- =============================================\n-- Author: DBX\n-- =============================================\n\nALTER VIEW [dbo].[v_demo]\nAS\nSELECT id FROM users;";
|
|
|
|
assert_sqlserver_editable_and_executable(ObjectSourceKind::View, source, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_block_commented_view_source_preserves_comment_and_uses_alter_view() {
|
|
let source = "/* keep this view header */\nCREATE OR ALTER VIEW dbo.v_demo AS SELECT id FROM users;";
|
|
let expected = "/* keep this view header */\nALTER VIEW dbo.v_demo AS SELECT id FROM users;";
|
|
|
|
assert_sqlserver_editable_and_executable(ObjectSourceKind::View, source, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_commented_alter_view_source_remains_unchanged() {
|
|
let source = "-- keep this note\nALTER VIEW dbo.v_demo AS SELECT id FROM users;";
|
|
|
|
assert_sqlserver_editable_and_executable(ObjectSourceKind::View, source, source);
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_unclosed_block_commented_view_source_is_not_rewritten() {
|
|
// An unclosed block comment has no statement start, so nothing is
|
|
// rewritten: the whole payload stays the view body and only gets the
|
|
// ALTER VIEW header, exactly like a bare SELECT body does.
|
|
let source = "/* unclosed header\nCREATE VIEW dbo.v_demo AS SELECT id FROM users;";
|
|
|
|
assert_sqlserver_editable_and_executable(
|
|
ObjectSourceKind::View,
|
|
source,
|
|
"ALTER VIEW [dbo].[usp_demo] AS\n/* unclosed header\nCREATE VIEW dbo.v_demo AS SELECT id FROM users;",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn duckdb_view_create_source_saves_as_create_or_replace_view() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::DuckDb,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("main".to_string()),
|
|
name: "active_orders".to_string(),
|
|
source: "CREATE VIEW active_orders AS SELECT id FROM orders WHERE active".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "CREATE OR REPLACE VIEW active_orders AS SELECT id FROM orders WHERE active;");
|
|
}
|
|
|
|
#[test]
|
|
fn duckdb_view_body_saves_as_qualified_create_or_replace_view() {
|
|
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::DuckDb,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("reporting".to_string()),
|
|
name: "active orders".to_string(),
|
|
source: "SELECT id FROM orders WHERE active".to_string(),
|
|
});
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"CREATE OR REPLACE VIEW \"reporting\".\"active orders\" AS\nSELECT id FROM orders WHERE active;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn postgres_view_body_opens_as_create_or_replace_view() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Postgres,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("public".to_string()),
|
|
name: "active users".to_string(),
|
|
source: " SELECT id, name FROM users WHERE active ".to_string(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(
|
|
sql,
|
|
"CREATE OR REPLACE VIEW \"public\".\"active users\" AS\nSELECT id, name FROM users WHERE active;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn postgres_view_create_source_opens_without_rewrapping_or_reformatting() {
|
|
let source = "CREATE OR REPLACE VIEW public.active_users AS SELECT id, name FROM users WHERE active = true";
|
|
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Postgres,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("public".to_string()),
|
|
name: "active_users".to_string(),
|
|
source: source.to_string(),
|
|
});
|
|
|
|
assert_eq!(sql, format!("{source};"));
|
|
}
|
|
|
|
#[test]
|
|
fn postgres_view_create_source_saves_as_create_or_replace_view() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Postgres,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("public".to_string()),
|
|
name: "active_users".to_string(),
|
|
source: "CREATE VIEW public.active_users AS SELECT id, name FROM users WHERE active = true".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"CREATE OR REPLACE VIEW public.active_users AS SELECT id, name FROM users WHERE active = true;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn postgres_view_create_or_replace_source_saves_without_rewrapping() {
|
|
let source = "CREATE OR REPLACE VIEW public.active_users AS SELECT id, name FROM users WHERE active = true";
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Postgres,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("public".to_string()),
|
|
name: "active_users".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, format!("{source};"));
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_body_opens_as_create_or_replace_view() {
|
|
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dbx_issue3895_repro".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source: "SELECT id, label FROM source_rows WHERE id >= 1".to_string(),
|
|
});
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"CREATE OR REPLACE VIEW \"dbx_issue3895_repro\".\"edited_view\" AS\nSELECT id, label FROM source_rows WHERE id >= 1;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_create_source_saves_as_create_or_replace_view() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dbx_issue3895_repro".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source: "CREATE VIEW dbx_issue3895_repro.edited_view AS SELECT id, label FROM source_rows".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "CREATE OR REPLACE VIEW dbx_issue3895_repro.edited_view AS SELECT id, label FROM source_rows;");
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_create_force_source_saves_as_create_or_replace_force_view() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("app".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source: "CREATE FORCE VIEW app.edited_view AS SELECT id FROM source_rows".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "CREATE OR REPLACE FORCE VIEW app.edited_view AS SELECT id FROM source_rows;");
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_create_recursive_force_source_preserves_modifier_order() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("app".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source: "CREATE RECURSIVE FORCE VIEW app.edited_view AS SELECT id FROM source_rows".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "CREATE OR REPLACE RECURSIVE FORCE VIEW app.edited_view AS SELECT id FROM source_rows;");
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_create_temporary_recursive_force_source_preserves_legal_modifiers() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("app".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source: "CREATE TEMPORARY RECURSIVE FORCE VIEW app.edited_view AS SELECT id FROM source_rows".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"CREATE OR REPLACE TEMPORARY RECURSIVE FORCE VIEW app.edited_view AS SELECT id FROM source_rows;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_create_or_replace_force_source_saves_without_rewrapping() {
|
|
let source = "CREATE OR REPLACE FORCE VIEW app.edited_view AS SELECT id FROM source_rows";
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("app".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, format!("{source};"));
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_create_source_preserves_leading_line_comment() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("app".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source: "-- keep this note\nCREATE VIEW app.edited_view AS SELECT id FROM source_rows".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "-- keep this note\nCREATE OR REPLACE VIEW app.edited_view AS SELECT id FROM source_rows;");
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_create_source_preserves_leading_block_comment() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("app".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source: "/* CREATE VIEW in this comment is ignored */\nCREATE VIEW app.edited_view AS SELECT id FROM source_rows"
|
|
.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"/* CREATE VIEW in this comment is ignored */\nCREATE OR REPLACE VIEW app.edited_view AS SELECT id FROM source_rows;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_create_source_preserves_leading_nested_block_comment() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("app".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source: "/* outer /* nested */ comment */\nCREATE FORCE VIEW app.edited_view AS SELECT id FROM source_rows"
|
|
.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"/* outer /* nested */ comment */\nCREATE OR REPLACE FORCE VIEW app.edited_view AS SELECT id FROM source_rows;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_create_source_preserves_leading_multi_level_block_comment() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("app".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source:
|
|
"/* level 1 /* level 2 /* level 3 */ level 2 */ level 1 */\nCREATE VIEW app.edited_view AS SELECT id FROM source_rows"
|
|
.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"/* level 1 /* level 2 /* level 3 */ level 2 */ level 1 */\nCREATE OR REPLACE VIEW app.edited_view AS SELECT id FROM source_rows;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn kingbase_view_unclosed_leading_block_comment_keeps_safe_body_fallback() {
|
|
let source = "/* unclosed comment\nCREATE FORCE VIEW app.edited_view AS SELECT id FROM source_rows";
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Kingbase,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("app".to_string()),
|
|
name: "edited_view".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, format!("CREATE OR REPLACE VIEW \"app\".\"edited_view\" AS\n{source};"));
|
|
}
|
|
|
|
#[test]
|
|
fn postgres_view_alter_source_saves_without_body_wrapping() {
|
|
let source = "ALTER VIEW public.active_users SET (security_barrier = true)";
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Postgres,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("public".to_string()),
|
|
name: "active_users".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, format!("{source};"));
|
|
}
|
|
|
|
#[test]
|
|
fn postgres_compatible_view_body_opens_as_create_or_replace_view() {
|
|
for database_type in [DatabaseType::OpenGauss, DatabaseType::Kwdb] {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("public".to_string()),
|
|
name: "active users".to_string(),
|
|
source: " SELECT id, name FROM users WHERE active ".to_string(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(
|
|
sql,
|
|
"CREATE OR REPLACE VIEW \"public\".\"active users\" AS\nSELECT id, name FROM users WHERE active;"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_view_body_saves_as_create_or_replace_view() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("DBX_TEST".to_string()),
|
|
name: "V_ACTIVE_USERS".to_string(),
|
|
source: "SELECT id, name FROM users WHERE active = 1".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"CREATE OR REPLACE VIEW \"DBX_TEST\".\"V_ACTIVE_USERS\" AS\nSELECT id, name FROM users WHERE active = 1;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn oceanbase_oracle_view_source_builds_executable_replace_ddl() {
|
|
for (source, expected) in [
|
|
(
|
|
"SELECT 2 AS N FROM DUAL",
|
|
"CREATE OR REPLACE VIEW \"SYS\".\"DBX_VIEW_SAVE_PROBE\" AS\nSELECT 2 AS N FROM DUAL;",
|
|
),
|
|
(
|
|
"CREATE VIEW SYS.DBX_VIEW_SAVE_PROBE AS SELECT 3 AS N FROM DUAL",
|
|
"CREATE OR REPLACE VIEW SYS.DBX_VIEW_SAVE_PROBE AS SELECT 3 AS N FROM DUAL;",
|
|
),
|
|
] {
|
|
let input = EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("SYS".to_string()),
|
|
name: "DBX_VIEW_SAVE_PROBE".to_string(),
|
|
source: source.to_string(),
|
|
};
|
|
|
|
assert_eq!(build_editable_object_source(input.clone()), expected);
|
|
assert_eq!(build_executable_object_source_sql(input).unwrap(), expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oceanbase_oracle_view_full_ddl_keeps_leading_comments() {
|
|
for (source, expected) in [
|
|
(
|
|
"/* Keep this note */\nCREATE VIEW SYS.DBX_VIEW_SAVE_PROBE AS SELECT 2 AS N FROM DUAL",
|
|
"/* Keep this note */\nCREATE OR REPLACE VIEW SYS.DBX_VIEW_SAVE_PROBE AS SELECT 2 AS N FROM DUAL;",
|
|
),
|
|
(
|
|
"-- Keep this note\nCREATE OR REPLACE VIEW SYS.DBX_VIEW_SAVE_PROBE AS SELECT 3 AS N FROM DUAL",
|
|
"-- Keep this note\nCREATE OR REPLACE VIEW SYS.DBX_VIEW_SAVE_PROBE AS SELECT 3 AS N FROM DUAL;",
|
|
),
|
|
] {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("SYS".to_string()),
|
|
name: "DBX_VIEW_SAVE_PROBE".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_view_create_source_saves_as_create_or_replace_view() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("DBX_TEST".to_string()),
|
|
name: "V_ACTIVE_USERS".to_string(),
|
|
source: "CREATE FORCE EDITIONABLE VIEW DBX_TEST.V_ACTIVE_USERS AS SELECT id FROM users".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "CREATE OR REPLACE FORCE EDITIONABLE VIEW DBX_TEST.V_ACTIVE_USERS AS SELECT id FROM users;");
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_view_source_opened_for_editing_shows_create_or_replace_view() {
|
|
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("DBX_TEST".to_string()),
|
|
name: "V_ACTIVE_USERS".to_string(),
|
|
source: "SELECT id, name FROM users WHERE active = 1".to_string(),
|
|
});
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"CREATE OR REPLACE VIEW \"DBX_TEST\".\"V_ACTIVE_USERS\" AS\nSELECT id, name FROM users WHERE active = 1;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn informix_view_body_saves_with_validate_drop_create() {
|
|
let statements = informix_view_statements("demo_view", "SELECT id, name FROM users");
|
|
|
|
assert_eq!(
|
|
statements,
|
|
expected_informix_view_replace_statements("demo_view", "demo_view", " AS\nSELECT id, name FROM users")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn informix_view_create_source_strips_owner_qualifier_before_save() {
|
|
let statements =
|
|
informix_view_statements("demo_view", "create view \"gbasedbt\".demo_view (id) as select id from users");
|
|
|
|
assert_eq!(
|
|
statements,
|
|
expected_informix_view_replace_statements("demo_view", "demo_view", " (id) as select id from users")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn informix_view_create_source_preserves_sql_target_name() {
|
|
let statements = informix_view_statements("new_view", "create view codex_created_view as select id from users");
|
|
|
|
assert_eq!(
|
|
statements,
|
|
expected_informix_view_replace_statements("new_view", "codex_created_view", " as select id from users")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn informix_view_create_source_strips_same_owner_table_references() {
|
|
let statements = informix_view_statements(
|
|
"dba_db_links",
|
|
"create view \"gbasedbt\".dba_db_links as select x0.db_link from \"gbasedbt\".user_db_links x0",
|
|
);
|
|
|
|
assert_eq!(
|
|
statements,
|
|
expected_informix_view_replace_statements(
|
|
"dba_db_links",
|
|
"dba_db_links",
|
|
" as select x0.db_link from user_db_links x0",
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn informix_view_body_strips_same_owner_table_references() {
|
|
let statements = informix_view_statements("demo_view", "SELECT id FROM gbasedbt.users");
|
|
|
|
assert_eq!(
|
|
statements,
|
|
expected_informix_view_replace_statements("demo_view", "demo_view", " AS\nSELECT id FROM users")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn informix_owner_qualifier_rewrite_skips_strings_and_comments() {
|
|
let statements = informix_view_statements(
|
|
"demo_view",
|
|
"SELECT 'gbasedbt.users' AS literal, id FROM gbasedbt.users -- gbasedbt.audit\n/* gbasedbt.logs */",
|
|
);
|
|
|
|
assert_eq!(
|
|
statements,
|
|
expected_informix_view_replace_statements(
|
|
"demo_view",
|
|
"demo_view",
|
|
" AS\nSELECT 'gbasedbt.users' AS literal, id FROM users -- gbasedbt.audit\n/* gbasedbt.logs */",
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn informix_view_source_opened_for_editing_shows_unqualified_create_view() {
|
|
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Informix,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("gbasedbt".to_string()),
|
|
name: "demo_view".to_string(),
|
|
source: "create view \"gbasedbt\".demo_view as select id from users".to_string(),
|
|
});
|
|
|
|
assert_eq!(sql, "CREATE VIEW demo_view as select id from users;");
|
|
}
|
|
|
|
#[test]
|
|
fn gbase8s_view_source_opened_for_editing_shows_unqualified_create_view() {
|
|
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Gbase8s,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("gbasedbt".to_string()),
|
|
name: "v_orders".to_string(),
|
|
source: "create view \"gbasedbt\".v_orders as select id from orders".to_string(),
|
|
});
|
|
|
|
assert_eq!(sql, "CREATE VIEW v_orders as select id from orders;");
|
|
}
|
|
|
|
#[test]
|
|
fn view_ddl_wraps_postgres_body_as_create_or_replace_view() {
|
|
let sql = build_view_ddl_sql(BuildViewDdlInput {
|
|
database_type: Some(DatabaseType::Postgres),
|
|
schema: Some("public".to_string()),
|
|
name: "active users".to_string(),
|
|
source: " SELECT id, name FROM users WHERE active ".to_string(),
|
|
identifier_quote: None,
|
|
});
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"CREATE OR REPLACE VIEW \"public\".\"active users\" AS\nSELECT id, name FROM users WHERE active;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn view_ddl_keeps_existing_create_view_statement() {
|
|
let sql = build_view_ddl_sql(BuildViewDdlInput {
|
|
database_type: Some(DatabaseType::Mysql),
|
|
schema: Some("reporting".to_string()),
|
|
name: "active_users".to_string(),
|
|
source: "CREATE ALGORITHM=UNDEFINED VIEW `active_users` AS SELECT `id` FROM `users`".to_string(),
|
|
identifier_quote: None,
|
|
});
|
|
|
|
assert_eq!(sql, "CREATE OR REPLACE ALGORITHM=UNDEFINED VIEW `active_users` AS SELECT `id` FROM `users`;");
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_view_ddl_uses_create_or_replace_without_database_name() {
|
|
let sql = build_view_ddl_sql(BuildViewDdlInput {
|
|
database_type: Some(DatabaseType::Mysql),
|
|
schema: Some("app".to_string()),
|
|
name: "active_users".to_string(),
|
|
source: "SELECT id FROM users".to_string(),
|
|
identifier_quote: None,
|
|
});
|
|
|
|
assert_eq!(sql, "CREATE OR REPLACE VIEW `active_users` AS\nSELECT id FROM users;");
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_show_create_view_drops_database_qualifier() {
|
|
let sql = build_view_ddl_sql(BuildViewDdlInput {
|
|
database_type: Some(DatabaseType::Mysql),
|
|
schema: Some("app".to_string()),
|
|
name: "active_users".to_string(),
|
|
source: "CREATE ALGORITHM=UNDEFINED DEFINER=`hr`@`%` SQL SECURITY DEFINER VIEW `app`.`active_users` AS select `id` from `users`"
|
|
.to_string(),
|
|
identifier_quote: None,
|
|
});
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"CREATE OR REPLACE ALGORITHM=UNDEFINED DEFINER=`hr`@`%` SQL SECURITY DEFINER VIEW `active_users` AS select `id` from `users`;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn view_ddl_uses_create_view_for_non_postgres_like_databases() {
|
|
let sql = build_view_ddl_sql(BuildViewDdlInput {
|
|
database_type: Some(DatabaseType::Sqlite),
|
|
schema: Some("main".to_string()),
|
|
name: "active_users".to_string(),
|
|
source: "SELECT id FROM users".to_string(),
|
|
identifier_quote: None,
|
|
});
|
|
|
|
assert_eq!(sql, "CREATE VIEW \"main\".\"active_users\" AS\nSELECT id FROM users;");
|
|
}
|
|
|
|
#[test]
|
|
fn view_ddl_uses_backtick_quote_when_identifier_quote_reports_mysql_compat() {
|
|
let sql = build_view_ddl_sql(BuildViewDdlInput {
|
|
database_type: Some(DatabaseType::Kingbase),
|
|
schema: Some("audit-schema".to_string()),
|
|
name: "active_users".to_string(),
|
|
source: "SELECT id FROM users".to_string(),
|
|
identifier_quote: Some("`".to_string()),
|
|
});
|
|
|
|
assert_eq!(sql, "CREATE OR REPLACE VIEW `audit-schema`.`active_users` AS\nSELECT id FROM users;");
|
|
}
|
|
|
|
#[test]
|
|
fn view_ddl_keeps_double_quote_without_mysql_compat_identifier_quote() {
|
|
let sql = build_view_ddl_sql(BuildViewDdlInput {
|
|
database_type: Some(DatabaseType::Kingbase),
|
|
schema: Some("audit-schema".to_string()),
|
|
name: "active_users".to_string(),
|
|
source: "SELECT id FROM users".to_string(),
|
|
identifier_quote: None,
|
|
});
|
|
|
|
assert_eq!(sql, "CREATE OR REPLACE VIEW \"audit-schema\".\"active_users\" AS\nSELECT id FROM users;");
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_package_source_saves_as_single_create_or_replace_statement() {
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::PackageBody,
|
|
schema: Some("HR".to_string()),
|
|
name: "PAYROLL".to_string(),
|
|
source: "CREATE OR REPLACE PACKAGE BODY PAYROLL AS\nEND PAYROLL;".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "CREATE OR REPLACE PACKAGE BODY PAYROLL AS\nEND PAYROLL;");
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_like_bare_procedure_source_saves_as_create_or_replace() {
|
|
let source = "PROCEDURE refresh_cache AS\nBEGIN\n DELETE FROM cache_entries;\n COMMIT;\nEND;";
|
|
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::Dameng] {
|
|
let statements =
|
|
build_executable_object_source_statements(input(database_type, ObjectSourceKind::Procedure, source))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
statements,
|
|
vec![
|
|
"CREATE OR REPLACE PROCEDURE refresh_cache AS\nBEGIN\n DELETE FROM cache_entries;\n COMMIT;\nEND;"
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_like_bare_function_source_saves_as_create_or_replace() {
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::Dameng] {
|
|
let statements = build_executable_object_source_statements(input(
|
|
database_type,
|
|
ObjectSourceKind::Function,
|
|
"FUNCTION refresh_cache RETURN NUMBER AS\nBEGIN\n RETURN 1;\nEND;",
|
|
))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
statements,
|
|
vec!["CREATE OR REPLACE FUNCTION refresh_cache RETURN NUMBER AS\nBEGIN\n RETURN 1;\nEND;"]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oceanbase_oracle_bare_package_body_source_saves_as_create_or_replace() {
|
|
let source = "PACKAGE BODY PAYROLL AS\n PROCEDURE PROCESS IS BEGIN NULL; END;\nEND PAYROLL;";
|
|
|
|
for database_type in [DatabaseType::Oracle, DatabaseType::OceanbaseOracle, DatabaseType::Dameng] {
|
|
let statements =
|
|
build_executable_object_source_statements(input(database_type, ObjectSourceKind::PackageBody, source))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
statements,
|
|
vec![
|
|
"CREATE OR REPLACE PACKAGE BODY PAYROLL AS\n PROCEDURE PROCESS IS BEGIN NULL; END;\nEND PAYROLL;"
|
|
]
|
|
);
|
|
|
|
let editable = build_editable_object_source(input(database_type, ObjectSourceKind::PackageBody, source));
|
|
assert_eq!(
|
|
editable,
|
|
"CREATE OR REPLACE PACKAGE BODY PAYROLL AS\n PROCEDURE PROCESS IS BEGIN NULL; END;\nEND PAYROLL;"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_create_prefix_is_not_duplicated() {
|
|
for source in [
|
|
"CREATE PROCEDURE refresh_cache AS BEGIN NULL; END;",
|
|
"CREATE OR REPLACE PROCEDURE refresh_cache AS BEGIN NULL; END;",
|
|
] {
|
|
let statements = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("public".to_string()),
|
|
name: "REFRESH_CACHE".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(statements, vec![source]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn non_oracle_bare_routine_source_is_unchanged() {
|
|
let source = "PROCEDURE refresh_cache AS BEGIN NULL; END;";
|
|
let statements = build_executable_object_source_statements(input(
|
|
DatabaseType::Postgres,
|
|
ObjectSourceKind::Procedure,
|
|
source,
|
|
))
|
|
.unwrap();
|
|
|
|
assert_eq!(statements, vec![source]);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_programmable_metadata_object_kinds() {
|
|
assert_eq!(parse_object_source_kind("TRIGGER"), Some(ObjectSourceKind::Trigger));
|
|
assert_eq!(parse_object_source_kind("SYNONYM"), Some(ObjectSourceKind::Synonym));
|
|
assert_eq!(parse_object_source_kind("JOB"), Some(ObjectSourceKind::Job));
|
|
assert_eq!(parse_object_source_kind("TYPE"), Some(ObjectSourceKind::Type));
|
|
assert_eq!(parse_object_source_kind("TYPE_BODY"), Some(ObjectSourceKind::TypeBody));
|
|
assert_eq!(parse_object_source_kind("PACKAGE BODY"), Some(ObjectSourceKind::PackageBody));
|
|
}
|
|
|
|
#[test]
|
|
fn postgres_procedure_rename_adds_drop_cleanup() {
|
|
let statements = build_executable_object_source_statements(input(
|
|
DatabaseType::Postgres,
|
|
ObjectSourceKind::Procedure,
|
|
"CREATE OR REPLACE PROCEDURE \"public\".\"refresh_cache_v2\"(mode text)\nLANGUAGE SQL\nAS $$ SELECT 1 $$;",
|
|
))
|
|
.unwrap();
|
|
assert_eq!(
|
|
statements,
|
|
vec![
|
|
"CREATE OR REPLACE PROCEDURE \"public\".\"refresh_cache_v2\"(mode text)\nLANGUAGE SQL\nAS $$ SELECT 1 $$;",
|
|
"DROP PROCEDURE IF EXISTS \"public\".\"refresh_cache\"(mode text);",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_routine_rename_adds_drop_cleanup() {
|
|
let source =
|
|
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache_v2`(IN mode_name varchar(20)) BEGIN SELECT 1; END";
|
|
let statements = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("app".to_string()),
|
|
name: "refresh_cache".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
let validation_name = mysql_validation_routine_name("refresh_cache_v2");
|
|
let validation_source = replace_mysql_routine_declaration_name(source, &validation_name).unwrap();
|
|
assert_eq!(
|
|
statements,
|
|
vec![
|
|
format!("DROP PROCEDURE IF EXISTS `app`.`{validation_name}`;"),
|
|
ensure_semicolon(&validation_source),
|
|
format!("DROP PROCEDURE IF EXISTS `app`.`{validation_name}`;"),
|
|
"DROP PROCEDURE IF EXISTS `app`.`refresh_cache_v2`;".to_string(),
|
|
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache_v2`(IN mode_name varchar(20)) BEGIN SELECT 1; END;"
|
|
.to_string(),
|
|
"DROP PROCEDURE IF EXISTS `app`.`refresh_cache`;".to_string(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_procedure_save_replaces_existing_routine() {
|
|
let source = "CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`() BEGIN SELECT 1; END";
|
|
let statements = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("app".to_string()),
|
|
name: "refresh_cache".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
let validation_name = mysql_validation_routine_name("refresh_cache");
|
|
let validation_source = replace_mysql_routine_declaration_name(source, &validation_name).unwrap();
|
|
|
|
assert_eq!(
|
|
statements,
|
|
vec![
|
|
format!("DROP PROCEDURE IF EXISTS `app`.`{validation_name}`;"),
|
|
ensure_semicolon(&validation_source),
|
|
format!("DROP PROCEDURE IF EXISTS `app`.`{validation_name}`;"),
|
|
"DROP PROCEDURE IF EXISTS `app`.`refresh_cache`;".to_string(),
|
|
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`() BEGIN SELECT 1; END;".to_string(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_routine_replace_sequence_keeps_the_create_last_for_the_desktop_run_path() {
|
|
// The desktop Run path of a routine source tab (issue #12855) executes every statement before the last one
|
|
// as the replace preparation and then runs the tab's own CREATE through the normal query pipeline. That only
|
|
// holds while the CREATE stays last; a rename ends with the cleanup DROP instead and is never prepared.
|
|
for (object_type, source) in [
|
|
(ObjectSourceKind::Procedure, "CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`() BEGIN SELECT 1; END"),
|
|
(ObjectSourceKind::Function, "CREATE FUNCTION `refresh_cache`() RETURNS INT RETURN 1"),
|
|
] {
|
|
let keyword = object_type_keyword(&object_type);
|
|
let input = |name: &str| EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: object_type.clone(),
|
|
schema: Some("app".to_string()),
|
|
name: name.to_string(),
|
|
source: source.to_string(),
|
|
};
|
|
|
|
let statements = build_executable_object_source_statements(input("refresh_cache")).unwrap();
|
|
let (create, preparation) = statements.split_last().unwrap();
|
|
assert_eq!(create, &format!("{source};"));
|
|
assert_eq!(preparation.len(), 4);
|
|
assert!(preparation[0].starts_with(&format!("DROP {keyword} IF EXISTS `app`.`dbx_routine_check_")));
|
|
assert!(preparation[1].starts_with("CREATE") && preparation[1].contains("`dbx_routine_check_"));
|
|
assert_eq!(preparation[2], preparation[0]);
|
|
assert_eq!(preparation[3], format!("DROP {keyword} IF EXISTS `app`.`refresh_cache`;"));
|
|
|
|
let renamed = build_executable_object_source_statements(input("refresh_cache_old")).unwrap();
|
|
assert_eq!(renamed.last().unwrap(), &format!("DROP {keyword} IF EXISTS `app`.`refresh_cache_old`;"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_transfer_routine_replacement_uses_scoped_validation_name() {
|
|
let statements = build_mysql_routine_replacement_statements(
|
|
EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("app".to_string()),
|
|
name: "refresh_cache".to_string(),
|
|
source: "CREATE PROCEDURE `refresh_cache`() BEGIN SELECT 1; END".to_string(),
|
|
},
|
|
"dbx_transfer_check_0123456789abcdef",
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(statements.len(), 4);
|
|
assert!(statements[0].contains("CREATE PROCEDURE `dbx_transfer_check_0123456789abcdef`"));
|
|
assert!(statements[1].contains("DROP PROCEDURE IF EXISTS `app`.`dbx_transfer_check_0123456789abcdef`"));
|
|
assert!(statements[2].contains("DROP PROCEDURE IF EXISTS `app`.`refresh_cache`"));
|
|
assert!(!statements[0].contains("DROP"));
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_function_save_replaces_existing_routine() {
|
|
let source = "CREATE DEFINER=CURRENT_USER FUNCTION `active_count`() RETURNS INT RETURN 1";
|
|
let statements = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: ObjectSourceKind::Function,
|
|
schema: Some("app".to_string()),
|
|
name: "active_count".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
let validation_name = mysql_validation_routine_name("active_count");
|
|
let validation_source = replace_mysql_routine_declaration_name(source, &validation_name).unwrap();
|
|
|
|
assert_eq!(
|
|
statements,
|
|
vec![
|
|
format!("DROP FUNCTION IF EXISTS `app`.`{validation_name}`;"),
|
|
ensure_semicolon(&validation_source),
|
|
format!("DROP FUNCTION IF EXISTS `app`.`{validation_name}`;"),
|
|
"DROP FUNCTION IF EXISTS `app`.`active_count`;".to_string(),
|
|
"CREATE DEFINER=CURRENT_USER FUNCTION `active_count`() RETURNS INT RETURN 1;".to_string(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_alter_routine_source_saves_without_dropping() {
|
|
let statements = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("app".to_string()),
|
|
name: "refresh_cache".to_string(),
|
|
source: "ALTER PROCEDURE `refresh_cache` COMMENT 'refreshes cache'".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(statements, vec!["ALTER PROCEDURE `refresh_cache` COMMENT 'refreshes cache';"]);
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_routine_source_opened_for_editing_keeps_create_statement() {
|
|
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("app".to_string()),
|
|
name: "refresh_cache".to_string(),
|
|
source: "CREATE PROCEDURE `refresh_cache`() BEGIN SELECT 1; END".to_string(),
|
|
});
|
|
|
|
assert_eq!(sql, "CREATE PROCEDURE `refresh_cache`() BEGIN SELECT 1; END;");
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_view_source_opened_for_editing_uses_alter_view() {
|
|
let source = "CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `new_view` AS select `base_plugins`.`id` AS `id` from `base_plugins`";
|
|
let expected = "ALTER ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `new_view` AS select `base_plugins`.`id` AS `id` from `base_plugins`;";
|
|
let mut input = EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dol_test".to_string()),
|
|
name: "new_view".to_string(),
|
|
source: source.to_string(),
|
|
};
|
|
|
|
let editable = build_editable_object_source(input.clone());
|
|
assert_eq!(editable, expected);
|
|
input.source = editable;
|
|
assert_eq!(build_executable_object_source_sql(input).unwrap(), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_new_view_create_source_remains_create() {
|
|
let source = "CREATE VIEW `new_view` AS SELECT 1 AS `id`";
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dol_test".to_string()),
|
|
name: "new_view".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, "CREATE VIEW `new_view` AS SELECT 1 AS `id`;");
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_new_view_create_source_preserves_leading_comments() {
|
|
let source =
|
|
"-- keep this view note\n/* and this block */\nCREATE OR REPLACE VIEW `new_view` AS SELECT 1 AS `id`";
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dol_test".to_string()),
|
|
name: "new_view".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"-- keep this view note\n/* and this block */\nCREATE OR REPLACE VIEW `new_view` AS SELECT 1 AS `id`;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_view_alter_source_remains_unchanged() {
|
|
let source = "ALTER ALGORITHM=MERGE VIEW `new_view` AS SELECT 2 AS `id`;";
|
|
let sql = build_executable_object_source_sql(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Mysql,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dol_test".to_string()),
|
|
name: "new_view".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(sql, source);
|
|
}
|
|
|
|
#[test]
|
|
fn opengauss_procedure_source_omits_gsql_trailing_slash() {
|
|
let source = "CREATE OR REPLACE PROCEDURE public.refresh_cache()\nAS DECLARE BEGIN\n NULL;\nEND;\n/";
|
|
let expected = "CREATE OR REPLACE PROCEDURE public.refresh_cache()\nAS DECLARE BEGIN\n NULL;\nEND;";
|
|
|
|
for database_type in [DatabaseType::Gaussdb, DatabaseType::OpenGauss] {
|
|
let editable = build_editable_object_source(input(database_type, ObjectSourceKind::Procedure, source));
|
|
assert_eq!(editable, expected);
|
|
|
|
let exported = build_export_object_source_sql(database_type, ObjectSourceKind::Procedure, source);
|
|
assert_eq!(exported, expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_routine_export_uses_delimiter_script() {
|
|
let sql = build_export_object_source_sql(
|
|
DatabaseType::Mysql,
|
|
ObjectSourceKind::Procedure,
|
|
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`()\nBEGIN\n SELECT 1;\nEND",
|
|
);
|
|
|
|
assert_eq!(
|
|
sql,
|
|
"DELIMITER //\nCREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`()\nBEGIN\n SELECT 1;\nEND//\nDELIMITER ;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_trigger_and_event_export_use_delimiter_script() {
|
|
let trigger = build_export_object_source_sql(
|
|
DatabaseType::Mysql,
|
|
ObjectSourceKind::Trigger,
|
|
"CREATE DEFINER=`root`@`%` TRIGGER `trg_orders_ai` AFTER INSERT ON `orders` FOR EACH ROW\nBEGIN\n INSERT INTO audit_log(msg) VALUES ('x');\nEND",
|
|
);
|
|
|
|
assert_eq!(
|
|
trigger,
|
|
"DELIMITER //\nCREATE DEFINER=`root`@`%` TRIGGER `trg_orders_ai` AFTER INSERT ON `orders` FOR EACH ROW\nBEGIN\n INSERT INTO audit_log(msg) VALUES ('x');\nEND//\nDELIMITER ;"
|
|
);
|
|
|
|
// An event body already carries its schedule, so only the terminator changes.
|
|
let event = build_export_object_source_sql(
|
|
DatabaseType::Mysql,
|
|
ObjectSourceKind::Event,
|
|
"CREATE DEFINER=`root`@`%` EVENT `ev_purge` ON SCHEDULE EVERY 1 DAY DO DELETE FROM audit_log WHERE id < 0;",
|
|
);
|
|
|
|
assert_eq!(
|
|
event,
|
|
"DELIMITER //\nCREATE DEFINER=`root`@`%` EVENT `ev_purge` ON SCHEDULE EVERY 1 DAY DO DELETE FROM audit_log WHERE id < 0//\nDELIMITER ;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_routine_export_does_not_double_wrap_delimiter_script() {
|
|
let source = "DELIMITER //\nCREATE PROCEDURE `refresh_cache`()\nBEGIN\n SELECT 1;\nEND//\nDELIMITER ;";
|
|
|
|
let sql = build_export_object_source_sql(DatabaseType::Mysql, ObjectSourceKind::Procedure, source);
|
|
|
|
assert_eq!(sql, source);
|
|
}
|
|
|
|
#[test]
|
|
fn mysql_view_export_keeps_regular_statement_terminator() {
|
|
let sql = build_export_object_source_sql(
|
|
DatabaseType::Mysql,
|
|
ObjectSourceKind::View,
|
|
"CREATE VIEW `active_users` AS SELECT `id` FROM `users`",
|
|
);
|
|
|
|
assert_eq!(sql, "CREATE VIEW `active_users` AS SELECT `id` FROM `users`;");
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_view_source_opened_for_editing_shows_alter_view() {
|
|
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dbo".to_string()),
|
|
name: "v_active_users".to_string(),
|
|
source: "CREATE VIEW dbo.v_active_users AS SELECT id, name FROM users WHERE active = 1;".to_string(),
|
|
});
|
|
assert_eq!(sql, "ALTER VIEW dbo.v_active_users AS SELECT id, name FROM users WHERE active = 1;");
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_view_body_opened_for_editing_shows_alter_view() {
|
|
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type: ObjectSourceKind::View,
|
|
schema: Some("dbo".to_string()),
|
|
name: "new_view".to_string(),
|
|
source: "SELECT\n *\nFROM AppInfo".to_string(),
|
|
});
|
|
assert_eq!(sql, "ALTER VIEW [dbo].[new_view] AS\nSELECT\n *\nFROM AppInfo;");
|
|
}
|
|
|
|
#[test]
|
|
fn sqlserver_procedure_source_opened_for_editing_shows_alter() {
|
|
let sql = build_editable_object_source(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::SqlServer,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("dbo".to_string()),
|
|
name: "usp_demo".to_string(),
|
|
source: "CREATE PROCEDURE dbo.usp_demo AS SELECT 1;".to_string(),
|
|
});
|
|
assert_eq!(sql, "ALTER PROCEDURE dbo.usp_demo AS SELECT 1;");
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_routine_rename_rewrites_source_and_drops_original() {
|
|
let statements = build_routine_rename_object_source_statements(RoutineRenameObjectSourceInput {
|
|
database_type: DatabaseType::Dameng,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("SYSDBA".to_string()),
|
|
name: "SP_TAB_BAKSET_REMOVE_BATCH".to_string(),
|
|
new_name: "SP_TAB_BAKSET_REMOVE_BATCH_2".to_string(),
|
|
source:
|
|
"CREATE OR REPLACE PROCEDURE \"SYSDBA\".\"SP_TAB_BAKSET_REMOVE_BATCH\" AS\nBEGIN\n SELECT 1;\nEND;"
|
|
.to_string(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(
|
|
statements,
|
|
vec![
|
|
"CREATE OR REPLACE PROCEDURE \"SYSDBA\".\"SP_TAB_BAKSET_REMOVE_BATCH_2\" AS\nBEGIN\n SELECT 1;\nEND;",
|
|
"DROP PROCEDURE \"SYSDBA\".\"SP_TAB_BAKSET_REMOVE_BATCH\";",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn oceanbase_routine_source_builder_rewrites_self_reference_for_planned_migration() {
|
|
let statements = build_routine_rename_object_source_statements(RoutineRenameObjectSourceInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
name: "P_OLD".into(),
|
|
new_name: "P_NEW".into(),
|
|
source: "CREATE OR REPLACE PROCEDURE APP.P_OLD AS BEGIN P_OLD; END P_OLD;".into(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(statements[0], "CREATE OR REPLACE PROCEDURE \"APP\".\"P_NEW\" AS BEGIN \"P_NEW\"; END \"P_NEW\";");
|
|
assert_eq!(statements[1], "DROP PROCEDURE \"APP\".\"P_OLD\";");
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_rename_rejects_mismatched_owner_name_and_extra_sql() {
|
|
for source in [
|
|
"CREATE OR REPLACE PROCEDURE APP.P_OTHER AS BEGIN APP.P_OLD; END;",
|
|
"CREATE OR REPLACE PROCEDURE OTHER.P_OLD AS BEGIN NULL; END;",
|
|
"CREATE OR REPLACE PROCEDURE APP.P_OLD AS BEGIN NULL; END; DROP TABLE T;",
|
|
] {
|
|
let result = build_routine_rename_object_source_statements(RoutineRenameObjectSourceInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
name: "P_OLD".into(),
|
|
new_name: "P_NEW".into(),
|
|
source: source.into(),
|
|
});
|
|
assert!(result.is_err(), "accepted unsafe source: {source}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_executable_routine_rejects_wrong_identity_and_create_or_replace() {
|
|
for source in [
|
|
"CREATE OR REPLACE PROCEDURE APP.P_OTHER AS BEGIN NULL; END;",
|
|
"CREATE PROCEDURE OTHER.P_OLD AS BEGIN NULL; END;",
|
|
"CREATE OR REPLACE PROCEDURE APP.P_OLD AS BEGIN NULL; END; DROP TABLE T;",
|
|
] {
|
|
let result = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
name: "P_OLD".into(),
|
|
source: source.into(),
|
|
});
|
|
assert!(result.is_err(), "accepted unsafe source: {source}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_executable_package_source_requires_exact_identity_and_one_unit() {
|
|
let valid = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Package,
|
|
schema: Some("APP".into()),
|
|
name: "P_NEW".into(),
|
|
source: "CREATE PACKAGE APP.P_NEW AS PROCEDURE RUN; END P_NEW;".into(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(valid.len(), 1);
|
|
|
|
for source in [
|
|
"CREATE PACKAGE APP.P_OTHER AS PROCEDURE RUN; END P_OTHER;",
|
|
"CREATE PACKAGE OTHER.P_NEW AS PROCEDURE RUN; END P_NEW;",
|
|
"CREATE PACKAGE APP.P_NEW AS PROCEDURE RUN; END P_NEW; BEGIN DROP TABLE T; END;",
|
|
] {
|
|
let result = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Package,
|
|
schema: Some("APP".into()),
|
|
name: "P_NEW".into(),
|
|
source: source.into(),
|
|
});
|
|
assert!(result.is_err(), "accepted unsafe package source: {source}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_executable_source_respects_quoted_mixed_case_identity() {
|
|
let input = |source: &str| EditableObjectSourceSqlInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Package,
|
|
schema: Some("MiXeD".into()),
|
|
name: "Pkg".into(),
|
|
source: source.into(),
|
|
};
|
|
|
|
assert!(build_executable_object_source_statements(input(
|
|
"CREATE PACKAGE \"MiXeD\".\"Pkg\" AS PROCEDURE RUN; END \"Pkg\";"
|
|
))
|
|
.is_ok());
|
|
for source in [
|
|
"CREATE PACKAGE MIXED.\"Pkg\" AS PROCEDURE RUN; END \"Pkg\";",
|
|
"CREATE PACKAGE \"MiXeD\".PKG AS PROCEDURE RUN; END PKG;",
|
|
] {
|
|
assert!(
|
|
build_executable_object_source_statements(input(source)).is_err(),
|
|
"accepted identity with incorrect case semantics: {source}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_strict_source_validation_checks_create_and_bare_definitions() {
|
|
let validate = |source: &str| {
|
|
validate_oracle_executable_object_source(OracleObjectSourceValidationInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
name: "P_OLD".into(),
|
|
source: source.into(),
|
|
})
|
|
};
|
|
|
|
for source in [
|
|
"CREATE OR REPLACE PROCEDURE APP.P_OTHER AS BEGIN APP.P_OLD; END;",
|
|
"PROCEDURE APP.P_OTHER AS BEGIN APP.P_OLD; END;",
|
|
"CREATE PROCEDURE OTHER.P_OLD AS BEGIN NULL; END;",
|
|
"CREATE/*comment*/ OR REPLACE PROCEDURE APP.P_OTHER AS BEGIN NULL; END;",
|
|
"CREATE OR REPLACE FUNCTION APP.P_OLD RETURN NUMBER AS BEGIN RETURN 1; END;",
|
|
"CREATE OR REPLACE PROCEDURE APP.P_OLD AS BEGIN NULL; END; DROP TABLE T;",
|
|
] {
|
|
assert!(validate(source).is_err(), "accepted unsafe source: {source}");
|
|
}
|
|
|
|
assert!(validate(
|
|
"CREATE/*prefix*/OR/*replace*/REPLACE/*kind*/PROCEDURE APP.P_OLD AS BEGIN IF 1 = 1 THEN NULL; END IF; EXECUTE IMMEDIATE 'BEGIN NULL; END;'; END;"
|
|
)
|
|
.is_ok());
|
|
assert!(validate(
|
|
"PROCEDURE APP.P_OLD AS BEGIN IF 1 = 1 THEN NULL; END IF; EXECUTE IMMEDIATE 'BEGIN NULL; END;'; END;"
|
|
)
|
|
.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_routine_rename_updates_self_recursion_and_outer_end_only() {
|
|
let source = r#"CREATE OR REPLACE FUNCTION "SYSDBA"."F_REC"(N NUMBER) RETURN NUMBER AS
|
|
BEGIN
|
|
-- 注释中的 F_REC 必须保持不变。
|
|
IF N <= 1 THEN RETURN 'F_REC'; END IF;
|
|
RETURN "SYSDBA"."F_REC"(N - 1);
|
|
END F_REC;"#
|
|
.replace('\n', "\r\n");
|
|
let statements = build_routine_rename_object_source_statements(RoutineRenameObjectSourceInput {
|
|
database_type: DatabaseType::Dameng,
|
|
object_type: ObjectSourceKind::Function,
|
|
schema: Some("SYSDBA".to_string()),
|
|
name: "F_REC".to_string(),
|
|
new_name: "F_REC_2".to_string(),
|
|
source: source.to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
statements[0],
|
|
r#"CREATE OR REPLACE FUNCTION "SYSDBA"."F_REC_2"(N NUMBER) RETURN NUMBER AS
|
|
BEGIN
|
|
-- 注释中的 F_REC 必须保持不变。
|
|
IF N <= 1 THEN RETURN 'F_REC'; END IF;
|
|
RETURN "SYSDBA"."F_REC_2"(N - 1);
|
|
END "F_REC_2";"#
|
|
.replace('\n', "\r\n")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_routine_rename_does_not_treat_matching_schema_as_a_self_reference() {
|
|
let statements = build_routine_rename_object_source_statements(RoutineRenameObjectSourceInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("F_REC".to_string()),
|
|
name: "F_REC".to_string(),
|
|
new_name: "F_REC_2".to_string(),
|
|
source: "CREATE OR REPLACE PROCEDURE \"F_REC\".\"F_REC\" AS BEGIN F_REC; END F_REC;".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
statements[0],
|
|
"CREATE OR REPLACE PROCEDURE \"F_REC\".\"F_REC_2\" AS BEGIN \"F_REC_2\"; END \"F_REC_2\";"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_routine_rename_rejects_ambiguous_old_name_use() {
|
|
let result = build_routine_rename_object_source_statements(RoutineRenameObjectSourceInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".to_string()),
|
|
name: "F_REC".to_string(),
|
|
new_name: "F_REC_2".to_string(),
|
|
source: "CREATE FUNCTION APP.F_REC(N NUMBER) RETURN NUMBER AS BEGIN SELECT F_REC INTO N FROM T; RETURN N; END F_REC;".to_string(),
|
|
});
|
|
|
|
assert!(result.unwrap_err().contains("Cannot safely determine whether F_REC"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_routine_rename_rejects_new_name_capture() {
|
|
let result = build_routine_rename_object_source_statements(RoutineRenameObjectSourceInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".to_string()),
|
|
name: "F_REC".to_string(),
|
|
new_name: "F_REC_2".to_string(),
|
|
source: "CREATE FUNCTION APP.F_REC(N NUMBER) RETURN NUMBER AS BEGIN RETURN F_REC_2(N - 1); END F_REC;"
|
|
.to_string(),
|
|
});
|
|
|
|
assert!(result.unwrap_err().contains("could capture an existing reference"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_routine_rename_rejects_local_routine_shadowing() {
|
|
let result = build_routine_rename_object_source_statements(RoutineRenameObjectSourceInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".to_string()),
|
|
name: "F_REC".to_string(),
|
|
new_name: "F_REC_2".to_string(),
|
|
source: "CREATE FUNCTION APP.F_REC(N NUMBER) RETURN NUMBER AS FUNCTION F_REC(X NUMBER) RETURN NUMBER IS BEGIN RETURN X; END F_REC; BEGIN RETURN F_REC(N - 1); END F_REC;".to_string(),
|
|
});
|
|
|
|
assert!(result.unwrap_err().contains("may shadow self-recursive calls"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_family_routine_rename_does_not_rewrite_keywords_matching_quoted_routine_name() {
|
|
let statements = build_routine_rename_object_source_statements(RoutineRenameObjectSourceInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".to_string()),
|
|
name: "IF".to_string(),
|
|
new_name: "F_IF".to_string(),
|
|
source: "CREATE FUNCTION APP.\"IF\"(N NUMBER) RETURN NUMBER AS BEGIN IF N = 0 THEN RETURN 0; END IF; RETURN \"IF\"(N - 1); END \"IF\";".to_string(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
statements[0],
|
|
"CREATE FUNCTION \"APP\".\"F_IF\"(N NUMBER) RETURN NUMBER AS BEGIN IF N = 0 THEN RETURN 0; END IF; RETURN \"F_IF\"(N - 1); END \"F_IF\";"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_caller_rewrite_changes_only_static_calls() {
|
|
let source = "CREATE OR REPLACE VIEW APP.V_CALL AS\r\nSELECT APP.F_OLD(ID), OTHER.F_OLD(ID), 'F_OLD()' AS NOTE FROM APP.T -- F_OLD()\r\n";
|
|
let rewritten = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
caller_type: ObjectSourceKind::View,
|
|
target_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".into()),
|
|
old_name: "F_OLD".into(),
|
|
new_name: "F_NEW".into(),
|
|
source: source.into(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
rewritten,
|
|
"CREATE OR REPLACE VIEW APP.V_CALL AS\r\nSELECT APP.\"F_NEW\"(ID), OTHER.F_OLD(ID), 'F_OLD()' AS NOTE FROM APP.T -- F_OLD()\r\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_caller_rewrite_preserves_case_sensitive_sibling_calls() {
|
|
let rewritten = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
caller_type: ObjectSourceKind::Function,
|
|
target_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".into()),
|
|
old_name: "f_old".into(),
|
|
new_name: "f_new".into(),
|
|
source: "CREATE FUNCTION APP.CALLER RETURN NUMBER AS BEGIN RETURN \"f_old\"(1) + F_OLD(1); END;".into(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert_eq!(rewritten, "CREATE FUNCTION APP.CALLER RETURN NUMBER AS BEGIN RETURN \"f_new\"(1) + F_OLD(1); END;");
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_caller_rewrite_rejects_new_name_capture_before_writing() {
|
|
let result = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::Function,
|
|
target_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".into()),
|
|
old_name: "F_OLD".into(),
|
|
new_name: "F_NEW".into(),
|
|
source: "CREATE FUNCTION APP.CALLER RETURN NUMBER AS FUNCTION F_NEW(X NUMBER) RETURN NUMBER IS BEGIN RETURN X; END; BEGIN RETURN F_OLD(1); END;".into(),
|
|
});
|
|
assert!(result.unwrap_err().contains("F_OLD or F_NEW"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_caller_rewrite_rejects_dynamic_old_name_even_with_static_call() {
|
|
let result = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::Procedure,
|
|
target_type: ObjectSourceKind::Procedure,
|
|
schema: Some("SYS".into()),
|
|
old_name: "DBX50_OLD_20261014_8F31".into(),
|
|
new_name: "DBX50_NEW_20261014_8F31".into(),
|
|
source: "CREATE OR REPLACE PROCEDURE \"SYS\".\"DBX50_CALL_20261014_8F31\" IS BEGIN DBX50_OLD_20261014_8F31; EXECUTE IMMEDIATE 'BEGIN DBX50_OLD_20261014_8F31; END;'; END".into(),
|
|
});
|
|
|
|
let error = result.unwrap_err();
|
|
assert!(error.contains("Dynamic SQL"));
|
|
assert!(error.contains("line 1") && error.contains("column"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_caller_rewrite_rejects_dynamic_sql_forms_and_comment_separated_keywords() {
|
|
for dynamic_sql in
|
|
["'BEGIN DBX50_OLD_20261014_8F31; END;'", "statement_text", "'BEGIN ' || 'DBX50_OLD_20261014_8F31; END;'"]
|
|
{
|
|
let source = format!(
|
|
"CREATE OR REPLACE PROCEDURE SYS.CALLER IS BEGIN DBX50_OLD_20261014_8F31; EXECUTE /* comment */ IMMEDIATE {dynamic_sql}; END;"
|
|
);
|
|
let result = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::Procedure,
|
|
target_type: ObjectSourceKind::Procedure,
|
|
schema: Some("SYS".into()),
|
|
old_name: "DBX50_OLD_20261014_8F31".into(),
|
|
new_name: "DBX50_NEW_20261014_8F31".into(),
|
|
source,
|
|
});
|
|
|
|
let error = result.unwrap_err();
|
|
assert!(error.contains("Dynamic SQL"), "accepted dynamic SQL: {dynamic_sql}");
|
|
assert!(error.contains("line 1") && error.contains("column"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_caller_rewrite_ignores_execute_immediate_in_literals_comments_and_quoted_words() {
|
|
let source = "CREATE OR REPLACE PROCEDURE SYS.CALLER IS v VARCHAR2(200); BEGIN DBX50_OLD_20261014_8F31; SELECT \"EXECUTE\" IMMEDIATE INTO v FROM DUAL; v := 'EXECUTE IMMEDIATE DBX50_OLD_20261014_8F31'; v := q'[EXECUTE IMMEDIATE DBX50_OLD_20261014_8F31]'; v := nq'[EXECUTE IMMEDIATE DBX50_OLD_20261014_8F31]'; -- EXECUTE IMMEDIATE DBX50_OLD_20261014_8F31\n DBMS_OUTPUT.PUT_LINE(\"EXECUTE\"); EXECUTE \"IMMEDIATE\"; END;";
|
|
let rewritten = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::Procedure,
|
|
target_type: ObjectSourceKind::Procedure,
|
|
schema: Some("SYS".into()),
|
|
old_name: "DBX50_OLD_20261014_8F31".into(),
|
|
new_name: "DBX50_NEW_20261014_8F31".into(),
|
|
source: source.into(),
|
|
})
|
|
.unwrap();
|
|
|
|
assert!(rewritten.contains("\"DBX50_NEW_20261014_8F31\";"));
|
|
assert!(rewritten.contains("'EXECUTE IMMEDIATE DBX50_OLD_20261014_8F31'"));
|
|
assert!(rewritten.contains("q'[EXECUTE IMMEDIATE DBX50_OLD_20261014_8F31]'"));
|
|
assert!(rewritten.contains("nq'[EXECUTE IMMEDIATE DBX50_OLD_20261014_8F31]'"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_self_reference_rewrite_rejects_dynamic_sql() {
|
|
let result = build_routine_rename_object_source_statements(RoutineRenameObjectSourceInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("SYS".into()),
|
|
name: "DBX50_OLD_20261014_8F31".into(),
|
|
new_name: "DBX50_NEW_20261014_8F31".into(),
|
|
source: "CREATE OR REPLACE PROCEDURE SYS.DBX50_OLD_20261014_8F31 IS BEGIN DBX50_OLD_20261014_8F31; EXECUTE IMMEDIATE 'BEGIN DBX50_OLD_20261014_8F31; END;'; END;".into(),
|
|
});
|
|
|
|
let error = result.unwrap_err();
|
|
assert!(error.contains("Dynamic SQL"));
|
|
assert!(error.contains("line 1") && error.contains("column"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_caller_rewrite_handles_unqualified_calls_and_rejects_ambiguous_forms() {
|
|
let rewritten = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::Procedure,
|
|
target_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
old_name: "P_OLD".into(),
|
|
new_name: "P_NEW".into(),
|
|
source: "CREATE PROCEDURE APP.CALLER AS BEGIN P_OLD; P_OLD(1); END;".into(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(rewritten, "CREATE PROCEDURE APP.CALLER AS BEGIN \"P_NEW\"; \"P_NEW\"(1); END;");
|
|
|
|
let result = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::Function,
|
|
target_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".into()),
|
|
old_name: "F_OLD".into(),
|
|
new_name: "F_NEW".into(),
|
|
source: "CREATE FUNCTION APP.CALLER RETURN NUMBER AS BEGIN RETURN F_OLD.VALUE; END;".into(),
|
|
});
|
|
assert!(result.unwrap_err().contains("unsupported form"));
|
|
|
|
let scalar_reference = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::Function,
|
|
target_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".into()),
|
|
old_name: "F_OLD".into(),
|
|
new_name: "F_NEW".into(),
|
|
source: "CREATE FUNCTION APP.CALLER RETURN NUMBER AS F_OLD NUMBER; BEGIN RETURN F_OLD; END;".into(),
|
|
});
|
|
assert!(scalar_reference.unwrap_err().contains("local declaration may shadow"));
|
|
|
|
let procedure_from_view = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::View,
|
|
target_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
old_name: "P_OLD".into(),
|
|
new_name: "P_NEW".into(),
|
|
source: "CREATE VIEW APP.V_CALL AS SELECT P_OLD(1) FROM DUAL".into(),
|
|
});
|
|
assert!(procedure_from_view.unwrap_err().contains("does not contain a verifiable static call"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_caller_rewrite_rejects_shadowed_collection_calls() {
|
|
let result = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::Function,
|
|
target_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".into()),
|
|
old_name: "F_OLD".into(),
|
|
new_name: "F_NEW".into(),
|
|
source: "CREATE FUNCTION APP.CALLER RETURN NUMBER AS TYPE numbers IS TABLE OF NUMBER; F_OLD numbers; BEGIN RETURN F_OLD(1) + APP.F_OLD(1); END;".into(),
|
|
});
|
|
|
|
assert!(result.unwrap_err().contains("local declaration may shadow"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_caller_rewrite_rejects_shadowed_nested_block_and_parameter_names() {
|
|
for source in [
|
|
"CREATE FUNCTION APP.CALLER RETURN NUMBER AS BEGIN DECLARE TYPE numbers IS TABLE OF NUMBER; F_OLD numbers; BEGIN RETURN F_OLD(1) + APP.F_OLD(1); END; END;",
|
|
"CREATE FUNCTION APP.CALLER(F_OLD NUMBER) RETURN NUMBER AS BEGIN RETURN APP.F_OLD(1); END;",
|
|
"CREATE PROCEDURE APP.CALLER AS PROCEDURE helper IS BEGIN NULL; END; PROCEDURE worker IS TYPE numbers IS TABLE OF NUMBER; F_OLD numbers; BEGIN F_OLD(1) := 1; APP.F_OLD(1); END; BEGIN worker; END;",
|
|
] {
|
|
let result = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::Function,
|
|
target_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".into()),
|
|
old_name: "F_OLD".into(),
|
|
new_name: "F_NEW".into(),
|
|
source: source.into(),
|
|
});
|
|
|
|
assert!(result.unwrap_err().contains("local declaration may shadow"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_routine_caller_rewrite_rejects_shadowed_schema_qualifiers() {
|
|
let result = rewrite_oracle_routine_references(OracleRoutineReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
caller_type: ObjectSourceKind::Function,
|
|
target_type: ObjectSourceKind::Function,
|
|
schema: Some("APP".into()),
|
|
old_name: "F_OLD".into(),
|
|
new_name: "F_NEW".into(),
|
|
source: "CREATE FUNCTION APP.CALLER RETURN NUMBER AS APP RECORD; BEGIN RETURN APP.F_OLD(1); END;".into(),
|
|
});
|
|
|
|
assert!(result.unwrap_err().contains("schema qualifier may be shadowed"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_table_reference_rewrite_updates_static_sql_and_record_types_only() {
|
|
let source = "CREATE OR REPLACE PROCEDURE APP.P AS r APP.T_OLD%ROWTYPE; v APP.T_OLD.V%TYPE; BEGIN SELECT COUNT(*) INTO v FROM APP.T_OLD; END;";
|
|
let rewritten = rewrite_oracle_table_references(OracleTableReferenceRewriteInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
old_name: "T_OLD".into(),
|
|
new_name: "T_NEW".into(),
|
|
source: source.into(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(
|
|
rewritten,
|
|
"CREATE OR REPLACE PROCEDURE APP.P AS r APP.\"T_NEW\"%ROWTYPE; v APP.\"T_NEW\".V%TYPE; BEGIN SELECT COUNT(*) INTO v FROM APP.\"T_NEW\"; END;"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_table_reference_rewrite_updates_trigger_binding_without_touching_literals_or_other_owner() {
|
|
let source = "CREATE OR REPLACE TRIGGER APP.TR_OLD BEFORE INSERT ON APP.T_OLD FOR EACH ROW BEGIN :NEW.V := 'T_OLD'; -- T_OLD\n NULL; END;";
|
|
let rewritten = rewrite_oracle_table_references(OracleTableReferenceRewriteInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
object_type: ObjectSourceKind::Trigger,
|
|
schema: Some("APP".into()),
|
|
old_name: "T_OLD".into(),
|
|
new_name: "T_NEW".into(),
|
|
source: source.into(),
|
|
})
|
|
.unwrap();
|
|
assert_eq!(
|
|
rewritten,
|
|
"CREATE OR REPLACE TRIGGER APP.TR_OLD BEFORE INSERT ON APP.\"T_NEW\" FOR EACH ROW BEGIN :NEW.V := 'T_OLD'; -- T_OLD\n NULL; END;"
|
|
);
|
|
|
|
let other_owner = rewrite_oracle_table_references(OracleTableReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
old_name: "T_OLD".into(),
|
|
new_name: "T_NEW".into(),
|
|
source: "CREATE PROCEDURE APP.P AS n NUMBER; BEGIN SELECT COUNT(*) INTO n FROM OTHER.T_OLD; END;".into(),
|
|
});
|
|
assert!(other_owner.unwrap_err().contains("no verifiable static reference"));
|
|
}
|
|
|
|
#[test]
|
|
fn oracle_table_reference_rewrite_rejects_ambiguous_and_dynamic_source() {
|
|
let ambiguous = rewrite_oracle_table_references(OracleTableReferenceRewriteInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
old_name: "T_OLD".into(),
|
|
new_name: "T_NEW".into(),
|
|
source: "CREATE PROCEDURE APP.P AS T_OLD NUMBER; BEGIN NULL; END;".into(),
|
|
});
|
|
assert!(ambiguous.unwrap_err().contains("Cannot safely identify"));
|
|
|
|
let dynamic = rewrite_oracle_table_references(OracleTableReferenceRewriteInput {
|
|
database_type: DatabaseType::Oracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
old_name: "T_OLD".into(),
|
|
new_name: "T_NEW".into(),
|
|
source: "CREATE PROCEDURE APP.P AS BEGIN EXECUTE IMMEDIATE 'SELECT * FROM T_OLD'; END;".into(),
|
|
});
|
|
let dynamic_error = dynamic.unwrap_err();
|
|
assert!(dynamic_error.contains("Dynamic SQL"));
|
|
assert!(dynamic_error.contains("line 1, column 33"));
|
|
|
|
for source in [
|
|
"CREATE PROCEDURE APP.P AS n NUMBER; BEGIN SELECT COUNT(*) INTO n FROM APP.T_OLD o JOIN APP.T_OTHER x ON x.ID=o.ID; END;",
|
|
"CREATE PROCEDURE APP.P AS n NUMBER; BEGIN SELECT COUNT(*) INTO n FROM APP.T_OLD o, APP.T_OTHER x; END;",
|
|
] {
|
|
let multiple_relations = rewrite_oracle_table_references(OracleTableReferenceRewriteInput {
|
|
database_type: DatabaseType::OceanbaseOracle,
|
|
object_type: ObjectSourceKind::Procedure,
|
|
schema: Some("APP".into()),
|
|
old_name: "T_OLD".into(),
|
|
new_name: "T_NEW".into(),
|
|
source: source.into(),
|
|
});
|
|
assert!(multiple_relations.is_err());
|
|
}
|
|
}
|
|
}
|