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