use regex::Regex; use serde::{Deserialize, Serialize}; use crate::models::connection::DatabaseType; use crate::types::ObjectSourceKind; #[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, 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, pub 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, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub name: String, pub source: 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, 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)) } pub fn build_routine_rename_object_source_statements( input: RoutineRenameObjectSourceInput, ) -> Result, 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(); 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) { 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 build_executable_object_source_statements(input: EditableObjectSourceSqlInput) -> Result, String> { 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 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.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) && matches!(input.object_type, ObjectSourceKind::Function | ObjectSourceKind::Procedure) { return Ok(vec![executable_oracle_routine_ddl(source)]); } if input.database_type == DatabaseType::Informix && input.object_type == ObjectSourceKind::View { return Ok(executable_informix_view_statements(input.schema.as_deref(), &input.name, source)); } if input.database_type == DatabaseType::Mysql && input.object_type == ObjectSourceKind::View { return Ok(vec![executable_mysql_view_ddl(source)]); } if is_mysql_like(input.database_type) && matches!(input.object_type, ObjectSourceKind::Function | ObjectSourceKind::Procedure) { return Ok(executable_mysql_routine_statements(&input, source)); } 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 build_executable_object_source_sql(input: EditableObjectSourceSqlInput) -> Result { Ok(build_executable_object_source_statements(input)?.join("\n")) } /// 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 input.database_type == DatabaseType::Informix && 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()); } 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 Regex::new(r"(?i)^(?:CREATE|ALTER)\s+").unwrap().is_match(source) { return ensure_semicolon(source); } let qualified_name = if matches!(input.database_type, Some(DatabaseType::Mysql | DatabaseType::Goldendb)) { 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{}", ensure_semicolon(source)); } format!("CREATE VIEW {qualified_name} AS\n{}", ensure_semicolon(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(); } if is_mysql_like(database_type) && matches!(object_type, ObjectSourceKind::Procedure | ObjectSourceKind::Function) { 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 { 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 )) } 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::Sequence => "SEQUENCE", ObjectSourceKind::Synonym => "SYNONYM", 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('`', "``")) } 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_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 { 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(); if Regex::new(r"(?i)^CREATE\s+OR\s+REPLACE\s+").unwrap().is_match(trimmed) || source_starts_with_alter(trimmed) { 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(trimmed) { let replaced = create_view.replace(trimmed, "CREATE OR REPLACE $1"); return ensure_semicolon(replaced.as_ref()); } 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)\b").unwrap().is_match(trimmed) { return ensure_semicolon(&format!("CREATE OR REPLACE {trimmed}")); } ensure_semicolon(trimmed) } 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 { 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::>() .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) -> Vec { 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 mut statements = Vec::with_capacity(3); // MySQL has no cross-version CREATE OR REPLACE for stored routines; DBeaver also // replaces them by dropping the target routine before executing the CREATE body. statements.push(mysql_drop_routine_if_exists(input.object_type.clone(), input.schema.as_deref(), create_name)); statements.push(ensure_semicolon(source)); if declaration.as_ref().is_some_and(|declaration| routine_name_changed(&declaration.name, &input.name)) { 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 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 { 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 { 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 { 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::>() .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::>() .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 { 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 { 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 { 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 { 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 mysql_routine_declaration(source: &str) -> Option { 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 { 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 { 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(source) { return ensure_semicolon(&existing_view_statement.replace(source, "ALTER VIEW ")); } 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 { 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("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 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 { 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 { 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 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 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 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(), }); 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(), }); assert_eq!(sql, "CREATE ALGORITHM=UNDEFINED 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::Mysql), schema: Some("reporting".to_string()), name: "active_users".to_string(), source: "SELECT id FROM users".to_string(), }); assert_eq!(sql, "CREATE VIEW `reporting`.`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 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(input( DatabaseType::Oracle, ObjectSourceKind::Procedure, source, )) .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("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 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: "CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache_v2`(IN mode_name varchar(20)) BEGIN SELECT 1; END" .to_string(), }) .unwrap(); assert_eq!( statements, vec![ "DROP PROCEDURE IF EXISTS `app`.`refresh_cache_v2`;", "CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache_v2`(IN mode_name varchar(20)) BEGIN SELECT 1; END;", "DROP PROCEDURE IF EXISTS `app`.`refresh_cache`;", ] ); } #[test] fn mysql_procedure_save_replaces_existing_routine() { 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: "CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`() BEGIN SELECT 1; END".to_string(), }) .unwrap(); assert_eq!( statements, vec![ "DROP PROCEDURE IF EXISTS `app`.`refresh_cache`;", "CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`() BEGIN SELECT 1; END;", ] ); } #[test] fn mysql_function_save_replaces_existing_routine() { 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: "CREATE DEFINER=CURRENT_USER FUNCTION `active_count`() RETURNS INT RETURN 1".to_string(), }) .unwrap(); assert_eq!( statements, vec![ "DROP FUNCTION IF EXISTS `app`.`active_count`;", "CREATE DEFINER=CURRENT_USER FUNCTION `active_count`() RETURNS INT RETURN 1;", ] ); } #[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 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(), }; assert_eq!(build_editable_object_source(input.clone()), expected); assert_eq!(build_executable_object_source_sql(input).unwrap(), expected); } #[test] fn mysql_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 */\nALTER 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_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\";", ] ); } }