use rust_ef::entity::IEntityType;
use rust_ef::migration::{MigrationDialect, MigrationEngine, MigrationStore};
use rust_ef::prelude::*;
#[derive(Debug, Clone, EntityType)]
#[table("fk_cascade_blogs")]
#[allow(
dead_code,
reason = "struct exists for #[derive(EntityType)]-generated entity_meta(); fields are never instantiated in tests"
)]
struct FkCascadeBlog {
#[primary_key]
#[auto_increment]
id: i32,
#[navigation]
posts: HasMany<FkCascadePost>,
}
#[derive(Debug, Clone, EntityType)]
#[table("fk_cascade_posts")]
#[allow(
dead_code,
reason = "struct exists for #[derive(EntityType)]-generated entity_meta(); fields are never instantiated in tests"
)]
struct FkCascadePost {
#[primary_key]
#[auto_increment]
id: i32,
#[foreign_key(FkCascadeBlog)]
blog_id: i32,
#[navigation]
blog: BelongsTo<FkCascadeBlog>,
}
#[derive(Debug, Clone, EntityType)]
#[table("fk_optional_blogs")]
#[allow(
dead_code,
reason = "struct exists for #[derive(EntityType)]-generated entity_meta(); fields are never instantiated in tests"
)]
struct FkOptionalBlog {
#[primary_key]
#[auto_increment]
id: i32,
#[navigation]
posts: HasMany<FkOptionalPost>,
}
#[derive(Debug, Clone, EntityType)]
#[table("fk_optional_posts")]
#[allow(
dead_code,
reason = "struct exists for #[derive(EntityType)]-generated entity_meta(); fields are never instantiated in tests"
)]
struct FkOptionalPost {
#[primary_key]
#[auto_increment]
id: i32,
#[foreign_key(FkOptionalBlog)]
blog_id: Option<i32>,
#[navigation]
blog: BelongsTo<FkOptionalBlog>,
}
#[derive(Debug, Clone, EntityType)]
#[table("fk_setnull_blogs")]
#[allow(
dead_code,
reason = "struct exists for #[derive(EntityType)]-generated entity_meta(); fields are never instantiated in tests"
)]
struct FkSetNullBlog {
#[primary_key]
#[auto_increment]
id: i32,
#[navigation]
#[on_delete(SetNull)]
posts: HasMany<FkSetNullPost>,
}
#[derive(Debug, Clone, EntityType)]
#[table("fk_setnull_posts")]
#[allow(
dead_code,
reason = "struct exists for #[derive(EntityType)]-generated entity_meta(); fields are never instantiated in tests"
)]
struct FkSetNullPost {
#[primary_key]
#[auto_increment]
id: i32,
#[foreign_key(FkSetNullBlog)]
blog_id: Option<i32>,
#[navigation]
blog: BelongsTo<FkSetNullBlog>,
}
#[derive(Debug, Clone, EntityType)]
#[table("fk_noaction_blogs")]
#[allow(
dead_code,
reason = "struct exists for #[derive(EntityType)]-generated entity_meta(); fields are never instantiated in tests"
)]
struct FkNoActionBlog {
#[primary_key]
#[auto_increment]
id: i32,
#[navigation]
#[on_delete(NoAction)]
posts: HasMany<FkNoActionPost>,
}
#[derive(Debug, Clone, EntityType)]
#[table("fk_noaction_posts")]
#[allow(
dead_code,
reason = "struct exists for #[derive(EntityType)]-generated entity_meta(); fields are never instantiated in tests"
)]
struct FkNoActionPost {
#[primary_key]
#[auto_increment]
id: i32,
#[foreign_key(FkNoActionBlog)]
blog_id: i32,
#[navigation]
blog: BelongsTo<FkNoActionBlog>,
}
fn assert_fk_on_delete(up_sql: &str, expected_clause: &str, label: &str) {
assert!(
up_sql.contains(&format!("ON DELETE {}", expected_clause)),
"{}: up_sql should contain 'ON DELETE {}':\n{}",
label,
expected_clause,
up_sql
);
}
#[test]
fn test_required_fk_generates_cascade() {
let engine = MigrationEngine::new(MigrationDialect::Sqlite);
let metas = vec![FkCascadeBlog::entity_meta(), FkCascadePost::entity_meta()];
let migration = engine.generate("InitCascade", &metas, &None).unwrap();
assert_fk_on_delete(&migration.up_sql, "CASCADE", "required FK");
assert!(
migration.up_sql.contains("\"fk_cascade_posts\""),
"up_sql should reference fk_cascade_posts table"
);
assert!(
migration.up_sql.contains("\"blog_id\""),
"up_sql should reference blog_id column"
);
}
#[test]
fn test_optional_fk_generates_restrict() {
let engine = MigrationEngine::new(MigrationDialect::Sqlite);
let metas = vec![FkOptionalBlog::entity_meta(), FkOptionalPost::entity_meta()];
let migration = engine.generate("InitRestrict", &metas, &None).unwrap();
assert_fk_on_delete(&migration.up_sql, "RESTRICT", "optional FK");
}
#[test]
fn test_explicit_set_null() {
let engine = MigrationEngine::new(MigrationDialect::Sqlite);
let metas = vec![FkSetNullBlog::entity_meta(), FkSetNullPost::entity_meta()];
let migration = engine.generate("InitSetNull", &metas, &None).unwrap();
assert_fk_on_delete(&migration.up_sql, "SET NULL", "explicit SetNull");
}
#[test]
fn test_explicit_no_action() {
let engine = MigrationEngine::new(MigrationDialect::Sqlite);
let metas = vec![FkNoActionBlog::entity_meta(), FkNoActionPost::entity_meta()];
let migration = engine.generate("InitNoAction", &metas, &None).unwrap();
assert_fk_on_delete(&migration.up_sql, "NO ACTION", "explicit NoAction");
}
#[test]
fn test_on_delete_change_triggers_fk_rebuild() {
let engine = MigrationEngine::new(MigrationDialect::Sqlite);
let current_metas = vec![FkCascadeBlog::entity_meta(), FkCascadePost::entity_meta()];
let current_snapshot = engine.create_snapshot("current", ¤t_metas);
let mut prev_snapshot = current_snapshot.clone();
prev_snapshot.migration_id = "previous".to_string();
for et in &mut prev_snapshot.entity_types {
if et.table_name == "fk_cascade_posts" {
for col in &mut et.columns {
if col.column_name == "blog_id" {
col.fk_on_delete = Some("SET NULL".to_string());
}
}
}
}
let migration = engine
.generate("ChangeOnDelete", ¤t_metas, &Some(prev_snapshot))
.unwrap();
assert!(
migration.up_sql.contains("DROP CONSTRAINT")
|| migration.up_sql.contains("DROP FOREIGN KEY"),
"up_sql should contain DROP CONSTRAINT/FOREIGN KEY:\n{}",
migration.up_sql
);
assert!(
migration.up_sql.contains("ADD CONSTRAINT"),
"up_sql should contain ADD CONSTRAINT:\n{}",
migration.up_sql
);
assert_fk_on_delete(&migration.up_sql, "CASCADE", "ON DELETE change");
}
#[test]
fn test_snapshot_json_roundtrip() {
let engine = MigrationEngine::new(MigrationDialect::Sqlite);
let metas = vec![FkCascadeBlog::entity_meta(), FkCascadePost::entity_meta()];
let snapshot = engine.create_snapshot("roundtrip", &metas);
let posts_et = snapshot
.entity_types
.iter()
.find(|et| et.table_name == "fk_cascade_posts")
.expect("fk_cascade_posts should be in snapshot");
let blog_id_col = posts_et
.columns
.iter()
.find(|c| c.column_name == "blog_id")
.expect("blog_id column should exist");
assert_eq!(
blog_id_col.fk_on_delete.as_deref(),
Some("CASCADE"),
"blog_id should have fk_on_delete = CASCADE"
);
let tmp = std::env::temp_dir().join("fk_on_delete_roundtrip_test");
let store = MigrationStore::new(&tmp);
store.save_snapshot(&snapshot).unwrap();
let json_text = std::fs::read_to_string(tmp.join("model_snapshot.json")).unwrap();
assert!(
json_text.contains("\"fk_on_delete\":\"CASCADE\""),
"JSON should contain fk_on_delete field:\n{}",
json_text
);
let loaded = store
.load_snapshot()
.unwrap()
.expect("snapshot should load");
let loaded_posts = loaded
.entity_types
.iter()
.find(|et| et.table_name == "fk_cascade_posts")
.expect("fk_cascade_posts should be in loaded snapshot");
let loaded_blog_id = loaded_posts
.columns
.iter()
.find(|c| c.column_name == "blog_id")
.expect("blog_id column should exist in loaded snapshot");
assert_eq!(
loaded_blog_id.fk_on_delete.as_deref(),
Some("CASCADE"),
"loaded blog_id should have fk_on_delete = CASCADE"
);
let _ = std::fs::remove_dir_all(&tmp);
}