use crate::{ adapter::Document, database::{Binding, Context, Parameter, Plan, Registry, Settings, Sql, Statement, Storage}, task::{Limits, Runtime}, }; use std::{ path::PathBuf, sync::atomic::{AtomicU64, Ordering}, }; static NEXT: AtomicU64 = AtomicU64::new(0); struct Directory(PathBuf); impl Directory { fn new() -> Self { let directory = Self(std::env::temp_dir().join(format!( "dever-registry-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed) ))); std::fs::create_dir(&directory.0).unwrap(); directory } } impl Drop for Directory { fn drop(&mut self) { std::fs::remove_dir_all(&self.0).unwrap(); } } fn runtime() -> Runtime { Runtime::new(Limits { threads: 2, workers: 2, tasks: 16, }) .unwrap() } const PARENT: &str = "CREATE TABLE z_parent(id INTEGER PRIMARY KEY);"; const CHILD: &str = "CREATE TABLE a_child(id INTEGER PRIMARY KEY, parent_id INTEGER NOT NULL REFERENCES z_parent(id));"; const PARENT_PLAN: Plan = Plan { create: Sql::same(PARENT), indexes: Sql::same(""), postgres_columns: &[], renames: &[], migrations: &[], seed_revision: "parent-v1", seeds: &[Statement { sql: Sql::same("INSERT INTO z_parent VALUES (?1)"), parameters: || Ok(vec![Parameter::Integer(7)]), }], }; const CHILD_PLAN: Plan = Plan { create: Sql::same(CHILD), seed_revision: "child-v1", seeds: &[Statement { sql: Sql::same("INSERT INTO a_child VALUES (?1,?2)"), parameters: || Ok(vec![Parameter::Integer(1), Parameter::Integer(7)]), }], ..PARENT_PLAN }; fn binding( table: &'static str, schema: &'static str, plan: Option<&'static Plan>, references: &'static [usize], ) -> Binding { Binding { storage: Storage::Global, component: "demo", database: None, table, schema: Sql::same(schema), plan, references, } } fn settings() -> Settings { Settings::load(&Document::parse(r#"{"database":{"default":{"type":"sqlite","path":"registry.db","max_connections":1}}}"#.into()).unwrap()).unwrap() } #[test] fn shared_internal_bindings_initialize_once_and_reject_conflicting_schema() { let directory = Directory::new(); let shared = binding( "queue", "CREATE TABLE queue(id INTEGER PRIMARY KEY);", None, &[], ); let mut bindings = [shared.clone(), shared]; runtime() .run(async |scope| { for expected in [1, 2] { let registry = Registry::prepare(&directory.0, settings(), &bindings, scope) .await .unwrap(); assert!(registry.same_pool(0, 1)); let mut context = Context::new(registry.clone()); context .on(0, scope) .await .unwrap() .execute( scope, "INSERT INTO queue VALUES (?1)", vec![Parameter::Integer(expected)], ) .await .unwrap(); let count = context .on(1, scope) .await .unwrap() .query(scope, "SELECT COUNT(*) FROM queue", vec![], 1, |row| { row.get::<_, i64>(0) }) .await .unwrap(); assert_eq!(count, [expected]); drop(context); registry.close().await; } bindings[1].schema = Sql::same("CREATE TABLE queue(id INTEGER PRIMARY KEY, extra TEXT);"); let error = Registry::prepare(&directory.0, settings(), &bindings, scope) .await .err() .expect("conflicting shared schema accepted"); assert_eq!(error.kind, crate::database::Kind::Migration); }) .unwrap(); } #[test] fn sqlite_registry_seeds_forward_references_after_all_tables_exist() { let directory = Directory::new(); let bindings = [ binding("a_child", CHILD, Some(&CHILD_PLAN), &[1]), binding("z_parent", PARENT, Some(&PARENT_PLAN), &[]), ]; runtime() .run(async |scope| { for _ in 0..2 { let registry = Registry::prepare(&directory.0, settings(), &bindings, scope) .await .unwrap(); let mut context = Context::new(registry.clone()); let executor = context.on(0, scope).await.unwrap(); assert_eq!( executor .query(scope, "SELECT parent_id FROM a_child", vec![], 1, |row| row .get::<_, i64>(0)) .await .unwrap(), [7] ); assert!( executor .execute(scope, "INSERT INTO a_child VALUES (2,999)", vec![]) .await .is_err() ); drop(context); registry.close().await; } }) .unwrap(); } #[test] fn sqlite_registry_preserves_cyclic_and_self_references_with_and_without_seeds() { const LEFT: &str = "CREATE TABLE cycle_left(id INTEGER PRIMARY KEY, right_id INTEGER REFERENCES cycle_right(id));"; const RIGHT: &str = "CREATE TABLE cycle_right(id INTEGER PRIMARY KEY, left_id INTEGER REFERENCES cycle_left(id));"; const SELF: &str = "CREATE TABLE self_node(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES self_node(id));"; const LEFT_PLAN: Plan = Plan { create: Sql::same(LEFT), seeds: &[Statement { sql: Sql::same("INSERT INTO cycle_left VALUES (1,2)"), parameters: || Ok(vec![]), }], ..PARENT_PLAN }; const RIGHT_PLAN: Plan = Plan { create: Sql::same(RIGHT), seeds: &[Statement { sql: Sql::same("INSERT INTO cycle_right VALUES (2,1)"), parameters: || Ok(vec![]), }], ..PARENT_PLAN }; const SELF_PLAN: Plan = Plan { create: Sql::same(SELF), seeds: &[Statement { sql: Sql::same("INSERT INTO self_node VALUES (1,1)"), parameters: || Ok(vec![]), }], ..PARENT_PLAN }; const EMPTY_LEFT: Plan = Plan { seeds: &[], ..LEFT_PLAN }; const EMPTY_RIGHT: Plan = Plan { seeds: &[], ..RIGHT_PLAN }; const EMPTY_SELF: Plan = Plan { seeds: &[], ..SELF_PLAN }; for seeded in [false, true] { let directory = Directory::new(); let bindings = [ binding( "cycle_left", LEFT, Some(if seeded { &LEFT_PLAN } else { &EMPTY_LEFT }), &[1], ), binding( "cycle_right", RIGHT, Some(if seeded { &RIGHT_PLAN } else { &EMPTY_RIGHT }), &[0], ), binding( "self_node", SELF, Some(if seeded { &SELF_PLAN } else { &EMPTY_SELF }), &[2], ), ]; runtime() .run(async |scope| { for _ in 0..2 { let registry = Registry::prepare(&directory.0, settings(), &bindings, scope) .await .unwrap(); let mut context = Context::new(registry.clone()); let executor = context.on(0, scope).await.unwrap(); assert_eq!( executor .query(scope, "SELECT count(*) FROM cycle_left", vec![], 1, |row| { row.get::<_, i64>(0) }) .await .unwrap(), [i64::from(seeded)] ); assert!( executor .execute(scope, "INSERT INTO cycle_left VALUES (3,999)", vec![]) .await .is_err() ); assert!( executor .execute(scope, "INSERT INTO self_node VALUES (3,999)", vec![]) .await .is_err() ); drop(context); registry.close().await; } }) .unwrap(); } } #[test] fn sqlite_registry_invalid_seed_rolls_back_the_physical_group() { const INVALID: Plan = Plan { seeds: &[Statement { sql: Sql::same("INSERT INTO a_child VALUES (1,999)"), parameters: || Ok(vec![]), }], ..CHILD_PLAN }; let directory = Directory::new(); runtime() .run(async |scope| { let bindings = [ binding("a_child", CHILD, Some(&INVALID), &[1]), binding("z_parent", PARENT, Some(&PARENT_PLAN), &[]), ]; let error = Registry::prepare(&directory.0, settings(), &bindings, scope) .await .err() .unwrap(); assert_eq!(error.kind, crate::database::Kind::Migration); let options = settings().select("", None).unwrap().clone(); let database = crate::database::Database::open(&directory.0, options, scope) .await .unwrap(); let mut executor = database.executor().await.unwrap(); assert_eq!( executor .query( scope, "SELECT count(*) FROM sqlite_schema WHERE type='table'", vec![], 1, |row| row.get::<_, i64>(0) ) .await .unwrap(), [0] ); drop(executor); database.close(); let bindings = [ binding("a_child", CHILD, Some(&CHILD_PLAN), &[1]), bindings[1].clone(), ]; let registry = Registry::prepare(&directory.0, settings(), &bindings, scope) .await .unwrap(); registry.close().await; }) .unwrap(); } #[test] fn sqlite_tenant_migration_batches_seed_dependencies_in_each_physical_database() { let directory = Directory::new(); let mut config = settings(); config.0.get_mut("default").unwrap().tenant_directory = Some("tenants".into()); config.1 = Some(crate::database::TenantOptions { database: "default".into(), max_pools: 2, idle_timeout_ms: 100, }); let mut bindings = [ binding("a_child", CHILD, Some(&CHILD_PLAN), &[1]), binding("z_parent", PARENT, Some(&PARENT_PLAN), &[]), ]; for binding in &mut bindings { binding.storage = Storage::Tenant; } runtime() .run(async |scope| { let registry = Registry::prepare(&directory.0, config, &bindings, scope) .await .unwrap(); for tenant in [1, 2, 1] { registry.migrate_tenant(scope, tenant).await.unwrap(); let mut context = Context::new(registry.clone()); context.select_tenant(scope, tenant).await.unwrap(); assert_eq!( context .on(0, scope) .await .unwrap() .query(scope, "SELECT parent_id FROM a_child", vec![], 1, |row| row .get::<_, i64>(0)) .await .unwrap(), [7] ); } registry.close().await; }) .unwrap(); }