use super::*; use dever_runtime::database::{Migration, Plan, Sql, Statement}; const BASE: &str = "CREATE TABLE \"account\" (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE CHECK(_dever_length(name) <= 24), legacy TEXT, amount TEXT(5,2) NOT NULL);"; const NEXT: &str = "CREATE TABLE \"account\" (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT NOT NULL UNIQUE CHECK(_dever_length(label) <= 8), amount TEXT(7,3) NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0,1)));"; const BEFORE: Statement = Statement { sql: Sql::same("UPDATE account SET name = ?1 WHERE name = ?2"), parameters: || { Ok(vec![ Parameter::Text("Editor".into()), Parameter::Text("Legacy".into()), ]) }, }; const AFTER: Statement = Statement { sql: Sql::same("UPDATE account SET label = ?1 WHERE label = ?2"), parameters: || { Ok(vec![ Parameter::Text("Final".into()), Parameter::Text("Editor".into()), ]) }, }; const SEED: Statement = Statement { sql: Sql::same( "INSERT INTO account(label,amount) SELECT ?1,?2 WHERE NOT EXISTS(SELECT 1 FROM account WHERE label = ?1)", ), parameters: || { Ok(vec![ Parameter::Text("Seed".into()), Parameter::Text("10001000".into()), ]) }, }; const STEP: Migration = Migration { name: "normalize", revision: "normalize-v1", drops: &["legacy"], before: &[BEFORE], after: &[AFTER], }; const INITIAL: Plan = Plan { create: Sql::same(BASE), indexes: Sql::same(""), postgres_columns: &[], renames: &[], migrations: &[], seeds: &[], seed_revision: "empty", }; const UPGRADE: Plan = Plan { create: Sql::same(NEXT), indexes: Sql::same(""), postgres_columns: &[], renames: &[("label", "name")], migrations: &[STEP], seeds: &[SEED], seed_revision: "seed-v1", }; #[test] fn schema_upgrade_preserves_rows_decimal_foreign_keys_and_id_sequence() { let directory = Directory::new(); runtime().run(async |scope| { let database = Database::open(&directory.0,options(),scope).await.unwrap(); let mut executor = database.executor().await.unwrap(); executor.migrate(scope,"account",BASE,Some(&INITIAL)).await.unwrap(); executor.initialize(scope,"INSERT INTO account(id,name,legacy,amount) VALUES (1,'Legacy','old','101230'),(20,'Deleted','old','100000'); DELETE FROM account WHERE id=20; CREATE TABLE dependent(account_id INTEGER REFERENCES account(id) ON DELETE CASCADE); INSERT INTO dependent VALUES (1);").await.unwrap(); executor.migrate(scope,"account",NEXT,Some(&UPGRADE)).await.unwrap(); let rows = executor.query(scope,"SELECT id,label,amount,enabled FROM account ORDER BY id",vec![],20,|row|Ok((row.get::<_,i64>(0)?,row.get::<_,String>(1)?,dever_runtime::database::scalar::decimal(row,2,7,3)?.to_string(),row.get::<_,bool>(3)?))).await.unwrap(); assert_eq!(rows,[(1,"Final".into(),"12.3".into(),true),(21,"Seed".into(),"1".into(),true)]); let child = executor.query(scope,"SELECT account_id FROM dependent",vec![],20,|row|row.get::<_,i64>(0)).await.unwrap(); assert_eq!(child,[1]); assert!(executor.execute(scope,"INSERT INTO dependent VALUES (999)",vec![]).await.is_err()); executor.migrate(scope,"account",NEXT,Some(&UPGRADE)).await.unwrap(); assert_eq!(executor.query(scope,"SELECT COUNT(*) FROM account",vec![],1,|row|row.get::<_,i64>(0)).await.unwrap(),[2]); let old = executor.migrate(scope,"account",BASE,Some(&INITIAL)).await.unwrap_err(); assert_eq!(old.kind,Kind::Migration); const ALTERED: Plan = Plan { migrations:&[Migration { revision:"changed", ..STEP }], ..UPGRADE }; assert_eq!(executor.migrate(scope,"account",NEXT,Some(&ALTERED)).await.unwrap_err().kind,Kind::Migration); }).unwrap(); } #[test] fn failed_after_step_or_narrowing_rolls_back_data_ddl_seed_and_history() { let directory = Directory::new(); runtime().run(async |scope| { let database = Database::open(&directory.0,options(),scope).await.unwrap(); let mut executor = database.executor().await.unwrap(); executor.migrate(scope,"account",BASE,Some(&INITIAL)).await.unwrap(); executor.initialize(scope,"INSERT INTO account(name,legacy,amount) VALUES ('Legacy','old','101230');").await.unwrap(); const FAIL: Plan = Plan { migrations:&[Migration { after:&[Statement { sql:Sql::same("UPDATE absent_table SET value = ?1"),parameters:||Ok(vec![Parameter::Integer(1)]) }], ..STEP }],..UPGRADE }; assert!(executor.migrate(scope,"account",NEXT,Some(&FAIL)).await.is_err()); let rows = executor.query(scope,"SELECT name,legacy,amount FROM account",vec![],20,|row|Ok((row.get::<_,String>(0)?,row.get::<_,String>(1)?,row.get::<_,String>(2)?))).await.unwrap(); assert_eq!(rows,[("Legacy".into(),"old".into(),"101230".into())]); assert_eq!(executor.query(scope,"SELECT COUNT(*) FROM _dever_migration",vec![],1,|row|row.get::<_,i64>(0)).await.unwrap(),[0]); assert_eq!(executor.query(scope,"SELECT definition FROM _dever_history WHERE name='account'",vec![],1,|row|row.get::<_,String>(0)).await.unwrap(),[BASE]); executor.migrate(scope,"account",NEXT,Some(&UPGRADE)).await.unwrap(); const TIGHT: &str = "CREATE TABLE \"account\" (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT NOT NULL UNIQUE CHECK(_dever_length(label) <= 4), amount TEXT(7,3) NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0,1)));"; const TIGHT_PLAN: Plan = Plan {create:Sql::same(TIGHT),..UPGRADE}; assert_eq!(executor.migrate(scope,"account",TIGHT,Some(&TIGHT_PLAN)).await.unwrap_err().kind,Kind::Constraint); executor.migrate(scope,"account",NEXT,Some(&UPGRADE)).await.unwrap(); assert_eq!(executor.query(scope,"SELECT label FROM account ORDER BY id",vec![],20,|row|row.get::<_,String>(0)).await.unwrap(),["Final","Seed"]); }).unwrap(); } #[test] fn seed_conflict_rolls_back_prior_seed_rows_and_revision() { let directory = Directory::new(); runtime().run(async |scope| { let database = Database::open(&directory.0,options(),scope).await.unwrap(); let mut executor = database.executor().await.unwrap(); const SQL: &str = "CREATE TABLE \"account\" (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE);"; const FIRST: Statement = Statement { sql:Sql::same("INSERT INTO account(name,email) SELECT ?1,?2 WHERE NOT EXISTS(SELECT 1 FROM account WHERE name=?1)"), parameters:||Ok(vec![Parameter::Text("first".into()),Parameter::Text("one".into())]) }; const START: Plan = Plan {create:Sql::same(SQL),indexes:Sql::same(""),postgres_columns:&[],renames:&[],migrations:&[],seeds:&[FIRST],seed_revision:"one"}; executor.migrate(scope,"account",SQL,Some(&START)).await.unwrap(); const MORE: Plan = Plan {seeds:&[FIRST, Statement {sql:FIRST.sql,parameters:||Ok(vec![Parameter::Text("second".into()),Parameter::Text("two".into())])}, Statement {sql:FIRST.sql,parameters:||Ok(vec![Parameter::Text("third".into()),Parameter::Text("one".into())])}],seed_revision:"more",..START}; assert_eq!(executor.migrate(scope,"account",SQL,Some(&MORE)).await.unwrap_err().kind,Kind::Constraint); assert_eq!(executor.query(scope,"SELECT name FROM account",vec![],20,|row|row.get::<_,String>(0)).await.unwrap(),["first"]); assert_eq!(executor.query(scope,"SELECT revision FROM _dever_seed WHERE model='account'",vec![],1,|row|row.get::<_,String>(0)).await.unwrap(),["one"]); }).unwrap(); } #[test] fn empty_rebuild_keeps_the_last_allocated_id() { let directory = Directory::new(); runtime().run(async |scope| { let database = Database::open(&directory.0,options(),scope).await.unwrap(); let mut executor = database.executor().await.unwrap(); executor.migrate(scope,"account",BASE,Some(&INITIAL)).await.unwrap(); executor.initialize(scope,"INSERT INTO account(id,name,amount) VALUES (20,'gone','100000'); DELETE FROM account;").await.unwrap(); const EMPTY: Plan = Plan { seeds:&[],seed_revision:"empty",..UPGRADE }; executor.migrate(scope,"account",NEXT,Some(&EMPTY)).await.unwrap(); executor.execute(scope,"INSERT INTO account(label,amount) VALUES ('new','10000000')",vec![]).await.unwrap(); assert_eq!(executor.query(scope,"SELECT id FROM account",vec![],1,|row|row.get::<_,i64>(0)).await.unwrap(),[21]); }).unwrap(); } #[test] fn foreign_seed_failure_preserves_earlier_models_and_rolls_back_its_model() { let directory = Directory::new(); runtime().run(async |scope| { let database = Database::open(&directory.0,options(),scope).await.unwrap(); let mut executor = database.executor().await.unwrap(); const CHILD: &str = "CREATE TABLE \"child\" (id INTEGER PRIMARY KEY, parent_id INTEGER NOT NULL REFERENCES parent(id));"; const EMPTY: Plan = Plan {create:Sql::same(CHILD),indexes:Sql::same(""),postgres_columns:&[],renames:&[],migrations:&[],seeds:&[],seed_revision:"empty"}; // 没有记录时可以先建外键表;真正的 Seed 必须引用已存在的目标记录。 executor.migrate(scope,"child",CHILD,Some(&EMPTY)).await.unwrap(); const PARENT: &str = "CREATE TABLE \"parent\" (id INTEGER PRIMARY KEY, name TEXT NOT NULL);"; executor.schema(scope,"parent",PARENT).await.unwrap(); executor.execute(scope,"INSERT INTO parent VALUES (1,'preserved')",vec![]).await.unwrap(); const INVALID: Plan = Plan {seeds:&[Statement {sql:Sql::same("INSERT INTO child VALUES (1,999)"),parameters:||Ok(vec![])}],seed_revision:"invalid",..EMPTY}; assert!(executor.migrate(scope,"child",CHILD,Some(&INVALID)).await.is_err()); assert_eq!(executor.query(scope,"SELECT name FROM parent",vec![],1,|row|row.get::<_,String>(0)).await.unwrap(),["preserved"]); assert_eq!(executor.query(scope,"SELECT COUNT(*) FROM child",vec![],1,|row|row.get::<_,i64>(0)).await.unwrap(),[0]); assert_eq!(executor.query(scope,"SELECT revision FROM _dever_seed WHERE model='child'",vec![],1,|row|row.get::<_,String>(0)).await.unwrap(),["empty"]); assert!(executor.execute(scope,"INSERT INTO child VALUES (1,999)",vec![]).await.is_err()); const VALID: Plan = Plan {seeds:&[Statement {sql:Sql::same("INSERT INTO child VALUES (1,1)"),parameters:||Ok(vec![])}],seed_revision:"valid",..EMPTY}; executor.migrate(scope,"child",CHILD,Some(&VALID)).await.unwrap(); }).unwrap(); }