Files
core/test/rust/runtime/database/registry.rs
T
2026-11-03 03:32:07 +00:00

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();
}