Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,366 @@
|
||||
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();
|
||||
}
|
||||
Reference in new issue
Block a user