Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
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();
}