367 lines
14 KiB
Rust
367 lines
14 KiB
Rust
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();
|
|
}
|