feat(mysql): edit foreign keys and triggers
This commit is contained in:
1 parent
1d2d348dd8
commit
a858a6bd18
23 files changed
+838
-22
No files matched your search
@@ -1,7 +1,9 @@
|
||||
use super::column_format::{column_data_type, column_extra_clause};
|
||||
use super::comments::{build_sqlserver_column_comment_sql, build_sqlserver_table_comment_sql};
|
||||
use super::dialect::{capabilities_for, database_label, StructureDialect};
|
||||
use super::foreign_keys::build_foreign_key_sql;
|
||||
use super::indexes::build_create_index_statements;
|
||||
use super::triggers::build_trigger_sql;
|
||||
use super::types::{TableStructureSqlOptions, TableStructureSqlResult};
|
||||
use super::util::{clean, format_default_for_sql, normalize_default, qualified_table, quote_ident, quote_string};
|
||||
use super::validation::validate_columns;
|
||||
@@ -136,5 +138,8 @@ pub fn build_create_table_sql(options: TableStructureSqlOptions) -> TableStructu
|
||||
));
|
||||
}
|
||||
|
||||
statements.extend(build_foreign_key_sql(&options, &mut warnings));
|
||||
statements.extend(build_trigger_sql(&options, &mut warnings));
|
||||
|
||||
TableStructureSqlResult { statements, warnings }
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use super::dialect::{database_label, StructureDialect};
|
||||
use super::types::{EditableStructureForeignKey, ForeignKeyInfo, TableStructureSqlOptions};
|
||||
use super::util::{clean, qualified_table, quote_ident};
|
||||
|
||||
pub(super) fn build_foreign_key_sql(options: &TableStructureSqlOptions, warnings: &mut Vec<String>) -> Vec<String> {
|
||||
if options.foreign_keys.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let database_label = database_label(options.database_type);
|
||||
if super::dialect::capabilities_for(options.database_type).dialect != StructureDialect::Mysql {
|
||||
warnings.push(format!("Editing foreign keys is not supported for {database_label} from this editor."));
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let table = qualified_table(StructureDialect::Mysql, options.schema.as_deref(), &options.table_name);
|
||||
let mut statements = Vec::new();
|
||||
|
||||
for foreign_key in &options.foreign_keys {
|
||||
if foreign_key.marked_for_drop {
|
||||
if let Some(original) = &foreign_key.original {
|
||||
statements.push(drop_foreign_key_sql(&table, &original.name));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(original) = &foreign_key.original {
|
||||
if !has_foreign_key_change(foreign_key, original) {
|
||||
continue;
|
||||
}
|
||||
statements.push(drop_foreign_key_sql(&table, &original.name));
|
||||
}
|
||||
|
||||
if let Some(sql) = create_foreign_key_sql(&table, foreign_key, warnings) {
|
||||
statements.push(sql);
|
||||
}
|
||||
}
|
||||
|
||||
statements
|
||||
}
|
||||
|
||||
fn has_foreign_key_change(foreign_key: &EditableStructureForeignKey, original: &ForeignKeyInfo) -> bool {
|
||||
clean(&foreign_key.name) != clean(&original.name)
|
||||
|| clean(&foreign_key.column) != clean(&original.column)
|
||||
|| clean(&foreign_key.ref_schema) != clean(original.ref_schema.as_deref().unwrap_or(""))
|
||||
|| clean(&foreign_key.ref_table) != clean(&original.ref_table)
|
||||
|| clean(&foreign_key.ref_column) != clean(&original.ref_column)
|
||||
|| normalize_action(&foreign_key.on_update) != normalize_action(original.on_update.as_deref().unwrap_or(""))
|
||||
|| normalize_action(&foreign_key.on_delete) != normalize_action(original.on_delete.as_deref().unwrap_or(""))
|
||||
}
|
||||
|
||||
fn drop_foreign_key_sql(table: &str, name: &str) -> String {
|
||||
format!("ALTER TABLE {table} DROP FOREIGN KEY {};", quote_ident(StructureDialect::Mysql, name))
|
||||
}
|
||||
|
||||
fn create_foreign_key_sql(
|
||||
table: &str,
|
||||
foreign_key: &EditableStructureForeignKey,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> Option<String> {
|
||||
let name = clean(&foreign_key.name);
|
||||
let columns = identifier_list(&foreign_key.column);
|
||||
let ref_table = clean(&foreign_key.ref_table);
|
||||
let ref_columns = identifier_list(&foreign_key.ref_column);
|
||||
if name.is_empty() || columns.is_empty() || ref_table.is_empty() || ref_columns.is_empty() {
|
||||
warnings.push("Foreign key name, column, referenced table, and referenced column are required.".to_string());
|
||||
return None;
|
||||
}
|
||||
if columns.len() != ref_columns.len() {
|
||||
warnings.push(format!("Foreign key \"{name}\" must have the same number of local and referenced columns."));
|
||||
return None;
|
||||
}
|
||||
|
||||
let ref_target = if clean(&foreign_key.ref_schema).is_empty() {
|
||||
quote_ident(StructureDialect::Mysql, &ref_table)
|
||||
} else {
|
||||
format!(
|
||||
"{}.{}",
|
||||
quote_ident(StructureDialect::Mysql, &foreign_key.ref_schema),
|
||||
quote_ident(StructureDialect::Mysql, &ref_table)
|
||||
)
|
||||
};
|
||||
|
||||
let mut sql = format!(
|
||||
"ALTER TABLE {table} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {ref_target} ({}",
|
||||
quote_ident(StructureDialect::Mysql, &name),
|
||||
columns.iter().map(|column| quote_ident(StructureDialect::Mysql, column)).collect::<Vec<_>>().join(", "),
|
||||
ref_columns.iter().map(|column| quote_ident(StructureDialect::Mysql, column)).collect::<Vec<_>>().join(", ")
|
||||
);
|
||||
sql.push(')');
|
||||
|
||||
if let Some(action) = action_clause("ON DELETE", &foreign_key.on_delete, warnings) {
|
||||
sql.push(' ');
|
||||
sql.push_str(&action);
|
||||
}
|
||||
if let Some(action) = action_clause("ON UPDATE", &foreign_key.on_update, warnings) {
|
||||
sql.push(' ');
|
||||
sql.push_str(&action);
|
||||
}
|
||||
sql.push(';');
|
||||
Some(sql)
|
||||
}
|
||||
|
||||
fn action_clause(prefix: &str, value: &str, warnings: &mut Vec<String>) -> Option<String> {
|
||||
let action = normalize_action(value);
|
||||
if action.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match action.as_str() {
|
||||
"CASCADE" | "SET NULL" | "RESTRICT" | "NO ACTION" => Some(format!("{prefix} {action}")),
|
||||
_ => {
|
||||
warnings.push(format!("Unsupported foreign key action \"{}\".", clean(value)));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_action(value: &str) -> String {
|
||||
clean(value).split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_uppercase()
|
||||
}
|
||||
|
||||
fn identifier_list(value: &str) -> Vec<String> {
|
||||
value.split(',').map(clean).filter(|value| !value.is_empty()).collect()
|
||||
}
|
||||
@@ -33,6 +33,33 @@ fn index(name: &str, columns: &[&str]) -> EditableStructureIndex {
|
||||
}
|
||||
}
|
||||
|
||||
fn foreign_key(name: &str, column: &str, ref_table: &str, ref_column: &str) -> EditableStructureForeignKey {
|
||||
EditableStructureForeignKey {
|
||||
id: name.to_string(),
|
||||
name: name.to_string(),
|
||||
column: column.to_string(),
|
||||
ref_schema: String::new(),
|
||||
ref_table: ref_table.to_string(),
|
||||
ref_column: ref_column.to_string(),
|
||||
on_update: String::new(),
|
||||
on_delete: String::new(),
|
||||
original: None,
|
||||
marked_for_drop: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn trigger(name: &str, timing: &str, event: &str, statement: &str) -> EditableStructureTrigger {
|
||||
EditableStructureTrigger {
|
||||
id: name.to_string(),
|
||||
name: name.to_string(),
|
||||
timing: timing.to_string(),
|
||||
event: event.to_string(),
|
||||
statement: statement.to_string(),
|
||||
original: None,
|
||||
marked_for_drop: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_mysql_column_and_index_changes() {
|
||||
let mut renamed = column("display_name");
|
||||
@@ -72,6 +99,8 @@ fn builds_mysql_column_and_index_changes() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![renamed, email],
|
||||
indexes: vec![old_index, email_index],
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -100,6 +129,8 @@ fn mysql_add_timestamp_column_drops_invalid_precision() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![created_at],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -123,6 +154,8 @@ fn mysql_add_timestamp_column_preserves_valid_precision() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![created_at],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -153,6 +186,8 @@ fn builds_postgres_create_table_with_comments_and_index() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![id, name],
|
||||
indexes: vec![idx],
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -189,6 +224,8 @@ fn warns_for_sqlite_unsafe_column_changes() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -214,6 +251,8 @@ fn builds_rqlite_changes_with_sqlite_dialect() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![email],
|
||||
indexes: vec![email_index],
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -277,6 +316,8 @@ fn builds_mysql_column_reorder_statements() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![id, email, name],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -303,6 +344,8 @@ fn builds_sql_server_quoted_column_and_index_statements() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![email],
|
||||
indexes: vec![index("idx_users_email", &["email"])],
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -333,6 +376,8 @@ fn builds_duckdb_create_table_statements() {
|
||||
table_name: "events".to_string(),
|
||||
columns: vec![name, created_at],
|
||||
indexes: vec![index("idx_events_name", &["name"])],
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -373,6 +418,8 @@ fn builds_clickhouse_nullable_comment_and_reorder_statements() {
|
||||
table_name: "events".to_string(),
|
||||
columns: vec![source, status],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -414,6 +461,8 @@ fn builds_h2_schema_qualified_existing_column_statements() {
|
||||
table_name: "USERS".to_string(),
|
||||
columns: vec![name],
|
||||
indexes: vec![index("IDX_USERS_DISPLAY_NAME", &["DISPLAY_NAME"])],
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -454,6 +503,8 @@ fn builds_postgres_alter_table_add_primary_key() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![id],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -484,6 +535,8 @@ fn builds_postgres_alter_table_drop_primary_key() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![id],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -530,6 +583,8 @@ fn builds_mysql_alter_table_change_primary_key() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![old_pk, new_pk],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -563,6 +618,8 @@ fn builds_no_statements_when_primary_key_unchanged() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![id],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -593,6 +650,8 @@ fn warns_sqlite_cannot_alter_primary_key() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![id],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -616,6 +675,8 @@ fn mysql_create_table_with_auto_increment() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -639,6 +700,8 @@ fn mysql_create_table_with_on_update_current_timestamp() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -664,6 +727,8 @@ fn postgres_create_table_with_identity() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -689,6 +754,8 @@ fn sqlserver_create_table_with_identity() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -709,6 +776,8 @@ fn mysql_quotes_datetime_literal_default() {
|
||||
table_name: "events".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -729,6 +798,8 @@ fn mysql_does_not_quote_current_timestamp() {
|
||||
table_name: "events".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -750,6 +821,8 @@ fn mysql_does_not_quote_temporal_function_with_parens() {
|
||||
table_name: "events".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -770,6 +843,8 @@ fn mysql_date_literal_default_is_quoted() {
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -790,6 +865,8 @@ fn mysql_time_literal_default_is_quoted() {
|
||||
table_name: "shifts".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -810,6 +887,8 @@ fn non_temporal_types_are_not_quoted() {
|
||||
table_name: "games".to_string(),
|
||||
columns: vec![col],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
@@ -868,3 +947,109 @@ fn mysql_single_column_alter_quotes_datetime_literal() {
|
||||
|
||||
assert!(result.statements.iter().any(|s| s.contains("DEFAULT '2024-01-01 00:00:00'")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_mysql_foreign_key_changes() {
|
||||
let mut existing = foreign_key("fk_orders_users", "user_id", "users", "id");
|
||||
existing.on_delete = "CASCADE".to_string();
|
||||
existing.original = Some(ForeignKeyInfo {
|
||||
name: "fk_orders_users_old".to_string(),
|
||||
column: "customer_id".to_string(),
|
||||
ref_schema: None,
|
||||
ref_table: "customers".to_string(),
|
||||
ref_column: "id".to_string(),
|
||||
on_update: None,
|
||||
on_delete: Some("RESTRICT".to_string()),
|
||||
});
|
||||
|
||||
let mut dropped = foreign_key("fk_orders_accounts", "account_id", "accounts", "id");
|
||||
dropped.marked_for_drop = true;
|
||||
dropped.original = Some(ForeignKeyInfo {
|
||||
name: "fk_orders_accounts".to_string(),
|
||||
column: "account_id".to_string(),
|
||||
ref_schema: None,
|
||||
ref_table: "accounts".to_string(),
|
||||
ref_column: "id".to_string(),
|
||||
on_update: None,
|
||||
on_delete: None,
|
||||
});
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
schema: None,
|
||||
table_name: "orders".to_string(),
|
||||
columns: Vec::new(),
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: vec![existing, dropped],
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"ALTER TABLE `orders` DROP FOREIGN KEY `fk_orders_users_old`;",
|
||||
"ALTER TABLE `orders` ADD CONSTRAINT `fk_orders_users` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;",
|
||||
"ALTER TABLE `orders` DROP FOREIGN KEY `fk_orders_accounts`;",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_mysql_composite_foreign_key() {
|
||||
let composite = foreign_key("fk_order_items_product", "tenant_id, product_id", "products", "tenant_id, id");
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
schema: None,
|
||||
table_name: "order_items".to_string(),
|
||||
columns: Vec::new(),
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: vec![composite],
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"ALTER TABLE `order_items` ADD CONSTRAINT `fk_order_items_product` FOREIGN KEY (`tenant_id`, `product_id`) REFERENCES `products` (`tenant_id`, `id`);",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_mysql_trigger_changes() {
|
||||
let mut existing = trigger("orders_bu", "BEFORE", "UPDATE", "BEGIN\n SET NEW.updated_at = NOW();\nEND");
|
||||
existing.original = Some(TriggerInfo {
|
||||
name: "orders_bu".to_string(),
|
||||
event: "UPDATE".to_string(),
|
||||
timing: "BEFORE".to_string(),
|
||||
statement: Some("SET NEW.updated_at = CURRENT_TIMESTAMP".to_string()),
|
||||
});
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
schema: None,
|
||||
table_name: "orders".to_string(),
|
||||
columns: Vec::new(),
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: vec![existing],
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"DROP TRIGGER `orders_bu`;",
|
||||
"CREATE TRIGGER `orders_bu` BEFORE UPDATE ON `orders` FOR EACH ROW\nBEGIN\n SET NEW.updated_at = NOW();\nEND;",
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use super::dialect::{database_label, StructureDialect};
|
||||
use super::types::{EditableStructureTrigger, TableStructureSqlOptions, TriggerInfo};
|
||||
use super::util::{clean, qualified_table, quote_ident};
|
||||
|
||||
pub(super) fn build_trigger_sql(options: &TableStructureSqlOptions, warnings: &mut Vec<String>) -> Vec<String> {
|
||||
if options.triggers.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let database_label = database_label(options.database_type);
|
||||
if super::dialect::capabilities_for(options.database_type).dialect != StructureDialect::Mysql {
|
||||
warnings.push(format!("Editing triggers is not supported for {database_label} from this editor."));
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let table = qualified_table(StructureDialect::Mysql, options.schema.as_deref(), &options.table_name);
|
||||
let mut statements = Vec::new();
|
||||
|
||||
for trigger in &options.triggers {
|
||||
if trigger.marked_for_drop {
|
||||
if let Some(original) = &trigger.original {
|
||||
statements.push(drop_trigger_sql(options.schema.as_deref(), &original.name));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(original) = &trigger.original {
|
||||
if !has_trigger_change(trigger, original) {
|
||||
continue;
|
||||
}
|
||||
statements.push(drop_trigger_sql(options.schema.as_deref(), &original.name));
|
||||
}
|
||||
|
||||
if let Some(sql) = create_trigger_sql(&table, trigger, warnings) {
|
||||
statements.push(sql);
|
||||
}
|
||||
}
|
||||
|
||||
statements
|
||||
}
|
||||
|
||||
fn has_trigger_change(trigger: &EditableStructureTrigger, original: &TriggerInfo) -> bool {
|
||||
clean(&trigger.name) != clean(&original.name)
|
||||
|| normalize_keyword(&trigger.timing) != normalize_keyword(&original.timing)
|
||||
|| normalize_keyword(&trigger.event) != normalize_keyword(&original.event)
|
||||
|| normalize_statement(&trigger.statement) != normalize_statement(original.statement.as_deref().unwrap_or(""))
|
||||
}
|
||||
|
||||
fn drop_trigger_sql(schema: Option<&str>, name: &str) -> String {
|
||||
let qualified_name = if schema.is_some_and(|schema| !schema.trim().is_empty()) {
|
||||
format!(
|
||||
"{}.{}",
|
||||
quote_ident(StructureDialect::Mysql, schema.unwrap()),
|
||||
quote_ident(StructureDialect::Mysql, name)
|
||||
)
|
||||
} else {
|
||||
quote_ident(StructureDialect::Mysql, name)
|
||||
};
|
||||
format!("DROP TRIGGER {qualified_name};")
|
||||
}
|
||||
|
||||
fn create_trigger_sql(table: &str, trigger: &EditableStructureTrigger, warnings: &mut Vec<String>) -> Option<String> {
|
||||
let name = clean(&trigger.name);
|
||||
let timing = normalize_keyword(&trigger.timing);
|
||||
let event = normalize_keyword(&trigger.event);
|
||||
let statement = clean(&trigger.statement);
|
||||
|
||||
if name.is_empty() || timing.is_empty() || event.is_empty() || statement.is_empty() {
|
||||
warnings.push("Trigger name, timing, event, and statement are required.".to_string());
|
||||
return None;
|
||||
}
|
||||
if !matches!(timing.as_str(), "BEFORE" | "AFTER") {
|
||||
warnings.push(format!("Unsupported trigger timing \"{}\".", clean(&trigger.timing)));
|
||||
return None;
|
||||
}
|
||||
if !matches!(event.as_str(), "INSERT" | "UPDATE" | "DELETE") {
|
||||
warnings.push(format!("Unsupported trigger event \"{}\".", clean(&trigger.event)));
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(format!(
|
||||
"CREATE TRIGGER {} {timing} {event} ON {table} FOR EACH ROW\n{};",
|
||||
quote_ident(StructureDialect::Mysql, &name),
|
||||
statement.trim_end_matches(';').trim_end()
|
||||
))
|
||||
}
|
||||
|
||||
fn normalize_keyword(value: &str) -> String {
|
||||
clean(value).to_ascii_uppercase()
|
||||
}
|
||||
|
||||
fn normalize_statement(value: &str) -> String {
|
||||
clean(value).trim_end_matches(';').trim().to_string()
|
||||
}
|
||||
@@ -105,6 +105,69 @@ pub struct IndexInfo {
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditableStructureForeignKey {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub column: String,
|
||||
#[serde(default)]
|
||||
pub ref_schema: String,
|
||||
#[serde(default)]
|
||||
pub ref_table: String,
|
||||
#[serde(default)]
|
||||
pub ref_column: String,
|
||||
#[serde(default)]
|
||||
pub on_update: String,
|
||||
#[serde(default)]
|
||||
pub on_delete: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub original: Option<ForeignKeyInfo>,
|
||||
#[serde(default)]
|
||||
pub marked_for_drop: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ForeignKeyInfo {
|
||||
pub name: String,
|
||||
pub column: String,
|
||||
#[serde(default)]
|
||||
pub ref_schema: Option<String>,
|
||||
pub ref_table: String,
|
||||
pub ref_column: String,
|
||||
#[serde(default)]
|
||||
pub on_update: Option<String>,
|
||||
#[serde(default)]
|
||||
pub on_delete: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditableStructureTrigger {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub timing: String,
|
||||
#[serde(default)]
|
||||
pub event: String,
|
||||
#[serde(default)]
|
||||
pub statement: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub original: Option<TriggerInfo>,
|
||||
#[serde(default)]
|
||||
pub marked_for_drop: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TriggerInfo {
|
||||
pub name: String,
|
||||
pub event: String,
|
||||
pub timing: String,
|
||||
#[serde(default)]
|
||||
pub statement: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TableStructureSqlOptions {
|
||||
@@ -117,6 +180,10 @@ pub struct TableStructureSqlOptions {
|
||||
pub columns: Vec<EditableStructureColumn>,
|
||||
#[serde(default)]
|
||||
pub indexes: Vec<EditableStructureIndex>,
|
||||
#[serde(default)]
|
||||
pub foreign_keys: Vec<EditableStructureForeignKey>,
|
||||
#[serde(default)]
|
||||
pub triggers: Vec<EditableStructureTrigger>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub table_comment: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
||||
Reference in new issue
Block a user