229 lines
7.3 KiB
Rust
229 lines
7.3 KiB
Rust
use super::*;
|
|
use crate::database::{Error, Options, Parameter};
|
|
use futures_util::FutureExt;
|
|
use std::panic::AssertUnwindSafe;
|
|
use tokio_postgres::Client;
|
|
|
|
mod cases;
|
|
mod codec;
|
|
mod constraints;
|
|
#[cfg(feature = "net")]
|
|
mod registry;
|
|
|
|
const ID: Column = Column {
|
|
constraints: &[("account_pkey", "p", "PRIMARY KEY (\"id\")")],
|
|
..column("id", "bigint")
|
|
};
|
|
const NAME: Column = Column {
|
|
constraints: &[("_dever_unique_account_1", "u", "UNIQUE (\"name\")")],
|
|
..column("name", "text")
|
|
};
|
|
const TITLE: Column = Column {
|
|
constraints: &[("_dever_unique_account_1", "u", "UNIQUE (\"title\")")],
|
|
..column("title", "text")
|
|
};
|
|
const UID: Column = Column {
|
|
generated: true,
|
|
constraints: &[("_dever_unique_account_3", "u", "UNIQUE (\"uid\")")],
|
|
..column("uid", "uuid")
|
|
};
|
|
|
|
const INITIAL: &str = "CREATE TABLE \"account\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL CONSTRAINT _dever_unique_account_1 UNIQUE, legacy bigint NOT NULL DEFAULT 4, amount numeric(6,2) NOT NULL DEFAULT 1.25);";
|
|
const FINAL: &str = "CREATE TABLE \"account\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, title text NOT NULL CONSTRAINT _dever_unique_account_1 UNIQUE, amount numeric(8,3) NOT NULL DEFAULT 1.25, uid uuid NOT NULL CONSTRAINT _dever_unique_account_3 UNIQUE);";
|
|
const INDEX: &str = "CREATE INDEX account_title ON account(title);";
|
|
const HISTORICAL: Migration = Migration {
|
|
name: "historical",
|
|
revision: "historical-one",
|
|
drops: &["ancient"],
|
|
before: &[statement("UPDATE account SET ancient = 1")],
|
|
after: &[statement("UPDATE account SET ancient = 2")],
|
|
};
|
|
const UPGRADE: Migration = Migration {
|
|
name: "rename",
|
|
revision: "rename-one",
|
|
drops: &["legacy"],
|
|
before: &[statement("UPDATE account SET name = name || '_before'")],
|
|
after: &[statement("UPDATE account SET title = title || '_after'")],
|
|
};
|
|
const INITIAL_PLAN: Plan = Plan {
|
|
create: Sql::same(INITIAL),
|
|
postgres_columns: &[
|
|
ID,
|
|
NAME,
|
|
Column {
|
|
default: Some("4"),
|
|
..column("legacy", "bigint")
|
|
},
|
|
Column {
|
|
default: Some("1.25"),
|
|
..column("amount", "numeric(6,2)")
|
|
},
|
|
],
|
|
migrations: &[HISTORICAL],
|
|
seeds: &[Statement {
|
|
sql: Sql::same("INSERT INTO account(name) VALUES ($1) ON CONFLICT(name) DO NOTHING"),
|
|
parameters: || Ok(vec![Parameter::Text("seed".into())]),
|
|
}],
|
|
seed_revision: "initial-seed",
|
|
..EMPTY
|
|
};
|
|
const FINAL_PLAN: Plan = Plan {
|
|
create: Sql::same(FINAL),
|
|
indexes: Sql::same(INDEX),
|
|
postgres_columns: &[
|
|
ID,
|
|
TITLE,
|
|
Column {
|
|
default: Some("1.25"),
|
|
..column("amount", "numeric(8,3)")
|
|
},
|
|
UID,
|
|
],
|
|
migrations: &[HISTORICAL, UPGRADE],
|
|
renames: &[("title", "name")],
|
|
seeds: &[Statement {
|
|
sql: Sql::same(
|
|
"INSERT INTO account(title,uid) VALUES ($1,$2::uuid) ON CONFLICT(title) DO NOTHING",
|
|
),
|
|
parameters: || {
|
|
Ok(vec![
|
|
Parameter::Text("new".into()),
|
|
Parameter::Uuid(*crate::database::scalar::generated_uuid()?.as_bytes()),
|
|
])
|
|
},
|
|
}],
|
|
seed_revision: "final-seed",
|
|
};
|
|
|
|
const fn statement(sql: &'static str) -> Statement {
|
|
Statement {
|
|
sql: Sql::same(sql),
|
|
parameters: || Ok(vec![]),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
|
|
fn schema_migration_seed_uuid_and_rollback() {
|
|
fixture(cases::migrations);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
|
|
fn catalog_drift_fails_before_data_or_history_mutation() {
|
|
fixture(cases::drift);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
|
|
fn new_database_records_final_history_without_old_transforms() {
|
|
fixture(cases::fresh);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
|
|
fn native_binary_codecs_validate_precision_and_parameter_types() {
|
|
fixture(codec::roundtrip);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
|
|
fn foreign_keys_and_tightening_validate_existing_data() {
|
|
fixture(constraints::foreign_keys);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
|
|
fn numeric_migrations_reject_precision_loss() {
|
|
fixture(constraints::numeric);
|
|
}
|
|
|
|
fn configuration() -> serde_json::Value {
|
|
let path =
|
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../config/setting.json");
|
|
let document =
|
|
crate::adapter::Document::load(&path).expect("cannot read PostgreSQL test settings");
|
|
let root: serde_json::Value = document.decode().expect("invalid PostgreSQL test settings");
|
|
root.get("database")
|
|
.and_then(|database| database.get("postgres_test"))
|
|
.expect("database.postgres_test must explicitly select an isolated test database")
|
|
.clone()
|
|
}
|
|
|
|
fn options() -> Options {
|
|
let options: Options = serde_json::from_value(configuration())
|
|
.unwrap_or_else(|_| panic!("invalid PostgreSQL test options"));
|
|
assert_eq!(options.driver, "postgres");
|
|
options
|
|
}
|
|
|
|
fn fixture(check: impl AsyncFnOnce(&mut Client)) {
|
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
runtime.block_on(isolated(options(), check));
|
|
}
|
|
|
|
#[cfg(feature = "net")]
|
|
fn registry_fixture(check: impl AsyncFnOnce(&mut Client, &crate::task::Scope<'_>)) {
|
|
let runtime = crate::task::Runtime::new(crate::task::Limits {
|
|
threads: 1,
|
|
workers: 1,
|
|
tasks: 16,
|
|
})
|
|
.unwrap();
|
|
runtime
|
|
.run(async |scope| isolated(options(), async |client| check(client, scope).await).await)
|
|
.unwrap();
|
|
}
|
|
|
|
async fn isolated(options: Options, check: impl AsyncFnOnce(&mut Client)) {
|
|
let database = super::super::Database::open(options).await.unwrap();
|
|
let mut client = database
|
|
.pool
|
|
.get()
|
|
.await
|
|
.unwrap_or_else(|_| panic!("cannot acquire PostgreSQL test connection"));
|
|
let name = format!(
|
|
"dever_test_{}",
|
|
crate::database::scalar::generated_uuid()
|
|
.unwrap()
|
|
.to_string()
|
|
.replace('-', "")
|
|
);
|
|
let quoted = schema::quote(&name);
|
|
client
|
|
.batch_execute(&format!("CREATE SCHEMA {quoted}"))
|
|
.await
|
|
.map_err(Error::from)
|
|
.unwrap();
|
|
let result = AssertUnwindSafe(async {
|
|
client
|
|
.batch_execute(&format!("SET search_path TO {quoted}"))
|
|
.await
|
|
.map_err(Error::from)
|
|
.unwrap();
|
|
tokio::time::timeout(std::time::Duration::from_secs(30), check(&mut client))
|
|
.await
|
|
.expect("PostgreSQL test deadline exceeded");
|
|
})
|
|
.catch_unwind()
|
|
.await;
|
|
let cleanup = tokio::time::timeout(
|
|
std::time::Duration::from_secs(10),
|
|
client.batch_execute(&format!(
|
|
"ROLLBACK; SET search_path TO pg_catalog; DROP SCHEMA {quoted} CASCADE"
|
|
)),
|
|
)
|
|
.await;
|
|
database.close();
|
|
cleanup
|
|
.expect("PostgreSQL test cleanup timed out")
|
|
.map_err(Error::from)
|
|
.expect("PostgreSQL test schema cleanup failed");
|
|
if let Err(panic) = result {
|
|
std::panic::resume_unwind(panic);
|
|
}
|
|
}
|