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