use dever_runtime::{ adapter::Document, database::{Database, Error, Kind, Options, Parameter, Settings}, task::{Limits, Runtime}, }; use std::{ path::PathBuf, sync::atomic::{AtomicU64, Ordering}, }; static NEXT: AtomicU64 = AtomicU64::new(0); mod performance; mod scalar; mod schema; struct Directory(PathBuf); impl Directory { fn new() -> Self { let path = std::env::temp_dir().join(format!( "dever-database-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed) )); std::fs::create_dir(&path).unwrap(); Self(path) } } impl Drop for Directory { fn drop(&mut self) { std::fs::remove_dir_all(&self.0).unwrap(); } } fn options() -> Options { Options { driver: "sqlite".into(), path: "data/case.db".into(), url: None, tls: None, tenant_directory: None, tenant_database_prefix: None, min_connections: 1, max_connections: 1, max_page_size: 20, wait_timeout_ms: 100, io_timeout_ms: 1000, } } fn runtime() -> Runtime { Runtime::new(Limits { threads: 2, workers: 2, tasks: 16, }) .unwrap() } const CREATE: &str = "CREATE TABLE account (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE);"; #[test] fn pool_and_checked_out_transaction_drop_outside_runtime_in_any_order() { for order in 0..3 { let directory = Directory::new(); let runtime = runtime(); let (database, executor) = runtime .run(async |scope| { let database = Database::open(&directory.0, options(), scope) .await .unwrap(); let mut executor = database.executor().await.unwrap(); executor.initialize(scope, CREATE).await.unwrap(); executor.begin(scope).await.unwrap(); executor .execute( scope, "INSERT INTO account(name) VALUES ('uncommitted')", vec![], ) .await .unwrap(); (database, executor) }) .unwrap(); match order { 0 => { drop(executor); database.close(); drop(database); drop(runtime); } 1 => { drop(database); drop(executor); drop(runtime); } _ => { drop(runtime); drop(database); drop(executor); } } self::runtime() .run(async |scope| { let database = Database::open(&directory.0, options(), scope) .await .unwrap(); let count = database .executor() .await .unwrap() .query(scope, "SELECT COUNT(*) FROM account", vec![], 1, |row| { row.get::<_, i64>(0) }) .await .unwrap(); assert_eq!(count, [0]); }) .unwrap(); } } #[test] fn transaction_commit_rollback_and_typed_rows() { 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.initialize(scope, CREATE).await.unwrap(); let owner = executor.begin(scope).await.unwrap(); assert!(owner); executor .execute( scope, "INSERT INTO account(name) VALUES (?1)", vec![Parameter::Text("Dever 中文".into())], ) .await .unwrap(); let nested = executor.begin(scope).await.unwrap(); assert!(!nested); executor.finish(scope, nested, true).await.unwrap(); executor.finish(scope, owner, true).await.unwrap(); let owner = executor.begin(scope).await.unwrap(); executor .execute( scope, "INSERT INTO account(name) VALUES (?1)", vec![Parameter::Text("rollback".into())], ) .await .unwrap(); executor.finish(scope, owner, false).await.unwrap(); let rows = executor .query( scope, "SELECT id, name FROM account ORDER BY id", vec![], 20, |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)), ) .await .unwrap(); assert_eq!(rows, vec![(1, "Dever 中文".into())]); drop(executor); drop(database.executor().await.unwrap()); database.close(); }) .unwrap(); } #[test] fn nested_failure_prevents_outer_commit_and_preserves_pool() { 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.initialize(scope, CREATE).await.unwrap(); let outer = executor.begin(scope).await.unwrap(); executor .execute( scope, "INSERT INTO account(name) VALUES ('temporary')", vec![], ) .await .unwrap(); let inner = executor.begin(scope).await.unwrap(); executor.finish(scope, inner, false).await.unwrap(); assert_eq!( executor.finish(scope, outer, true).await.unwrap_err().kind, Kind::Database ); drop(executor); let mut executor = database.executor().await.unwrap(); let count = executor .query(scope, "SELECT COUNT(*) FROM account", vec![], 1, |row| { row.get::<_, i64>(0) }) .await .unwrap(); assert_eq!(count, vec![0]); database.close(); }) .unwrap(); } #[test] fn binds_errors_and_result_bounds_are_real_database_checks() { 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.initialize(scope, CREATE).await.unwrap(); let input = "'); DROP TABLE account; --"; executor .execute( scope, "INSERT INTO account(name) VALUES (?1)", vec![Parameter::Text(input.into())], ) .await .unwrap(); let error = executor .execute( scope, "INSERT INTO account(name) VALUES (?1)", vec![Parameter::Text(input.into())], ) .await .unwrap_err(); assert_eq!(error.kind, Kind::Constraint); assert!(!error.to_string().contains(input)); let value = executor .query(scope, "SELECT name FROM account", vec![], 1, |row| { row.get::<_, String>(0) }) .await .unwrap(); assert_eq!(value, vec![input]); let invalid = executor .query(scope, "SELECT name FROM account", vec![], 1, |row| { row.get::<_, i64>(0) }) .await .unwrap_err(); assert_eq!(invalid.kind, Kind::InvalidData); let overflow = executor .query(scope, "SELECT 1 UNION ALL SELECT 2", vec![], 1, |row| { row.get::<_, i64>(0) }) .await .unwrap_err(); assert_eq!(overflow.kind, Kind::InvalidData); let exhausted = database.executor().await.err().unwrap(); assert_eq!(exhausted.kind, Kind::PoolExhausted); database.close(); }) .unwrap(); } #[test] fn timeout_discards_transaction_before_next_borrow() { let directory = Directory::new(); runtime().run(async |scope| { let mut config = options(); config.io_timeout_ms = 50; let database = Database::open(&directory.0, config, scope).await.unwrap(); let mut executor = database.executor().await.unwrap(); executor.initialize(scope, CREATE).await.unwrap(); executor.begin(scope).await.unwrap(); executor.execute(scope, "INSERT INTO account(name) VALUES ('must rollback')", vec![]).await.unwrap(); let result = executor.query(scope, "WITH RECURSIVE numbers(value) AS (VALUES(1) UNION ALL SELECT value + 1 FROM numbers WHERE value < 1000000000) SELECT SUM(value) FROM numbers", vec![], 1, |row| row.get::<_, i64>(0)).await; assert_eq!(result.unwrap_err().kind, Kind::Timeout); drop(executor); let mut replacement = database.executor().await.unwrap(); let count = replacement.query(scope, "SELECT COUNT(*) FROM account", vec![], 1, |row| row.get::<_, i64>(0)).await.unwrap(); assert_eq!(count, vec![0]); database.close(); }).unwrap(); } #[test] fn configuration_and_rollback_cause_do_not_expose_secrets() { for tenant in ["{}", "null"] { let document = Document::parse(format!(r#"{{"tenant":{tenant},"database":{{"default":{{"type":"sqlite","path":"data/app.db"}}}}}}"#)).unwrap(); assert!(Settings::load(&document).is_err()); } let settings = Settings::load( &Document::parse( r#"{"database":{"default":{"type":"sqlite","path":"data/app.db"}}}"#.into(), ) .unwrap(), ) .unwrap(); assert_eq!(settings.select("demo", None).unwrap().max_connections, 1); assert!(settings.select("demo", Some("missing")).is_err()); for path in ["/tmp/escape", "../escape", "data/../escape", ""] { let mut value = options(); value.path = path.into(); assert!(value.validate().is_err()); } let error = Error::new(Kind::Constraint, "business failure") .with_cause(Error::new(Kind::Connection, "rollback failed")) .with_cause(Error::new(Kind::Pool, "pool close failed")); assert_eq!(error.kind, Kind::Constraint); assert!(error.to_string().contains("rollback failed")); assert!(error.to_string().contains("pool close failed")); let rollback = error.cause.as_ref().unwrap(); assert_eq!(rollback.kind, Kind::Connection); assert_eq!(rollback.cause.as_ref().unwrap().kind, Kind::Pool); } #[test] fn schema_drift_fails_before_touching_data_or_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(); const SCHEMA: &str = "CREATE TABLE account (id INTEGER PRIMARY KEY, name TEXT NOT NULL); CREATE INDEX account_name ON account(name);"; executor.schema(scope, "account", SCHEMA).await.unwrap(); executor.schema(scope, "account", SCHEMA).await.unwrap(); executor.execute(scope, "INSERT INTO account(name) VALUES ('preserved')", vec![]).await.unwrap(); executor.initialize(scope, "DROP INDEX account_name;").await.unwrap(); assert_eq!(executor.schema(scope, "account", SCHEMA).await.unwrap_err().kind, Kind::Migration); let values = executor.query(scope, "SELECT name FROM account", vec![], 1, |row| row.get::<_, String>(0)).await.unwrap(); assert_eq!(values, vec!["preserved"]); database.close(); }).unwrap(); } #[test] fn stopping_a_running_query_rolls_back_and_releases_its_worker() { let directory = Directory::new(); runtime().run(async |scope| { let database = Database::open(&directory.0, options(), scope).await.unwrap(); let mut setup = database.executor().await.unwrap(); setup.initialize(scope, CREATE).await.unwrap(); drop(setup); let copy = database.clone(); let (started, ready) = tokio::sync::oneshot::channel(); let task = scope.spawn(move |child| async move { let mut executor = copy.executor().await.unwrap(); executor.begin(&child).await.unwrap(); executor.execute(&child, "INSERT INTO account(name) VALUES ('cancelled')", vec![]).await.unwrap(); started.send(()).unwrap(); executor.query(&child, "WITH RECURSIVE numbers(value) AS (VALUES(1) UNION ALL SELECT value + 1 FROM numbers WHERE value < 1000000000) SELECT SUM(value) FROM numbers", vec![], 1, |row| row.get::<_, i64>(0)).await }).unwrap(); ready.await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(10)).await; assert!(task.stop().await.unwrap().is_none()); let mut executor = database.executor().await.unwrap(); let count = executor.query(scope, "SELECT COUNT(*) FROM account", vec![], 1, |row| row.get::<_, i64>(0)).await.unwrap(); assert_eq!(count, vec![0]); database.close(); }).unwrap(); }