use super::*; #[test] fn database_migrations_preserve_data_and_reject_loss_or_changed_history() { let directory = Directory::new(); defaults::fixture_defaults(&directory.0); let source = directory.0.join("module/demo/account"); let original_model = std::fs::read_to_string(source.join("model.dever")) .unwrap() .replace( "type Account {\n", "type Account {\nlegacy: Text default \"old\"\n", ); std::fs::write(source.join("model.dever"), &original_model).unwrap(); let original = compile(&directory.0, "original"); settings(&directory.0, "data/account.db"); let run = |binary: &Path, name: &str, input: &str| { let result = execute(binary, &format!("demo.account.{name}"), input); assert!( result.status.success(), "{name}: {}", String::from_utf8_lossy(&result.stderr) ); serde_json::from_slice::(&result.stdout).unwrap()["data"].clone() }; let first = run(&original, "create", "{}"); assert_eq!(first["name"], "O'Reilly"); let target = original_model .replace("legacy: Text default \"old\"\n", "") .replace("name: Text(1, 20)", "display_name: Text(1, 8)") .replace( "default \"O'Reilly\"", "default \"O'Reilly\" from name unique", ) .replace("Decimal(5, 2)", "Decimal(7, 3)") + UPGRADE; std::fs::write(source.join("model.dever"), &target).unwrap(); let app = std::fs::read_to_string(source.join("app.dever")) .unwrap() .replace("row.name", "row.display_name"); std::fs::write(source.join("app.dever"), app).unwrap(); let upgraded = compile(&directory.0, "upgraded"); let next = run(&upgraded, "get", r#"{"key":1}"#); assert_eq!(next["name"], "Final"); assert_eq!(next["uid"], first["uid"]); assert_eq!(next["amount"], first["amount"]); assert_eq!(run(&upgraded, "count", "{}"), 2); assert_eq!(run(&upgraded, "get", r#"{"key":2}"#)["name"], "Seed"); assert_eq!(run(&upgraded, "count", "{}"), 2); assert!( !execute(&original, "demo.account.count", "{}") .status .success() ); std::fs::write( source.join("model.dever"), target.replace("[\"Final\",\"Editor\"]", "[\"Changed\",\"Editor\"]"), ) .unwrap(); let changed = compile(&directory.0, "changed"); let rejected = execute(&changed, "demo.account.count", "{}"); assert!(!rejected.status.success()); assert!( String::from_utf8_lossy(&rejected.stderr) .contains("applied migration content or order changed") ); assert_eq!(run(&upgraded, "get", r#"{"key":1}"#), next); let narrow = target .replace("Text(1, 8)", "Text(1, 4)") .replace("default \"O'Reilly\"", "default \"Ok\""); std::fs::write(source.join("model.dever"), narrow).unwrap(); let narrowed = compile(&directory.0, "narrowed"); assert!( !execute(&narrowed, "demo.account.count", "{}") .status .success() ); assert_eq!(run(&upgraded, "get", r#"{"key":1}"#), next); assert_eq!(run(&upgraded, "count", "{}"), 2); settings(&directory.0, "data/fresh.db"); // 空库的最终表没有历史 name/legacy;执行旧数据 before 会失败,因此验证确实跳过。 assert_eq!(run(&upgraded, "count", "{}"), 1); assert_eq!(run(&upgraded, "get", r#"{"key":1}"#)["name"], "Seed"); } fn compile(root: &Path, name: &str) -> std::path::PathBuf { let project = Project::load(root).unwrap(); let program = project.check().unwrap(); build::build(&program, &project.sources, "main", &root.join(name)).unwrap() } fn settings(root: &Path, path: &str) { std::fs::create_dir_all(root.join("config")).unwrap(); std::fs::write(root.join("config/setting.json"),format!(r#"{{"database":{{"default":{{"type":"sqlite","path":"{path}","max_connections":1,"max_page_size":20}}}}}}"#)).unwrap(); } const UPGRADE: &str = r#"index(display_name, created_at) migrate normalize { drop legacy before { sqlite = "UPDATE demo_account_model SET name = ?1 WHERE name = ?2" postgres = "UPDATE demo_account_model SET name = $1 WHERE name = $2" parameters = ["Editor","O'Reilly"] } after { sqlite = "UPDATE demo_account_model SET display_name = ?1 WHERE display_name = ?2" postgres = "UPDATE demo_account_model SET display_name = $1 WHERE display_name = $2" parameters = ["Final","Editor"] } } seed { { display_name = "Seed", amount = 1 } } "#;