//! Guards against tri-source enum drift: every domain enum's variant set must //! equal the Postgres `CHECK (... IN (...))` list on its backing column. The //! enums decode fail-closed (an unknown string errors the whole row read), so a //! value the DB permits but the Rust enum can't decode would poison any query //! that touches such a row. This test makes that drift fail in CI instead of at //! the first read of a poisoned row. //! //! `EnumType::VARIANTS` is the single source of truth (generated by //! `impl_str_enum!`); the DB side is read from the live, fully-migrated schema //! so later `ALTER ... CHECK` migrations are accounted for. use crate::harness::TestHarness; use makenotwork::db; use std::collections::BTreeSet; /// Extract the single-quoted string literals from a `pg_get_constraintdef` /// rendering (Postgres renders `col IN ('a','b')` as /// `CHECK ((col)::text = ANY (ARRAY['a'::text, 'b'::text]))`; the allowed values /// are exactly the single-quoted tokens). fn quoted_values(constraint_def: &str) -> BTreeSet { let mut out = BTreeSet::new(); let mut chars = constraint_def.char_indices().peekable(); while let Some((_, c)) = chars.next() { if c == '\'' { let mut val = String::new(); for (_, c2) in chars.by_ref() { if c2 == '\'' { break; } val.push(c2); } out.insert(val); } } out } /// Read the union of allowed values across every single-column CHECK constraint /// on exactly `table.column`. Constraining to `conkey = [column]` excludes /// cross-column CHECKs (which would leak an unrelated column's literals). async fn db_check_values(pool: &sqlx::PgPool, table: &str, column: &str) -> BTreeSet { let defs: Vec = sqlx::query_scalar( "SELECT pg_get_constraintdef(c.oid) \ FROM pg_constraint c JOIN pg_class t ON t.oid = c.conrelid \ WHERE t.relname = $1 AND c.contype = 'c' \ AND c.conkey = ARRAY[ \ (SELECT a.attnum FROM pg_attribute a WHERE a.attrelid = t.oid AND a.attname = $2) \ ]::smallint[]", ) .bind(table) .bind(column) .fetch_all(pool) .await .unwrap(); assert!( !defs.is_empty(), "no CHECK constraint found on {table}.{column} — update the enum-drift registry" ); defs.iter().flat_map(|d| quoted_values(d)).collect() } fn variant_set(variants: &[&str]) -> BTreeSet { variants .iter() .map(std::string::ToString::to_string) .collect() } #[tokio::test] async fn enum_variants_match_db_check_constraints() { let h = TestHarness::new().await; // (label, table, column, Rust variant set). Each pair's DB CHECK list must // equal the enum's VARIANTS. Add a row when a new enum backs a CHECK column. // Only enum-backed columns that actually carry an `IN (...)` CHECK. Columns // that store a broad external status set (e.g. subscriptions.status mirrors // Stripe) intentionally have no CHECK and are not listed. Grow this as new // CHECK-constrained enum columns land. let cases: Vec<(&str, &str, &str, BTreeSet)> = vec![ ( "TransactionStatus", "transactions", "status", variant_set(db::TransactionStatus::VARIANTS), ), ( "DiscountType", "promo_codes", "discount_type", variant_set(db::DiscountType::VARIANTS), ), ( "CodePurpose", "promo_codes", "code_purpose", variant_set(db::CodePurpose::VARIANTS), ), ( "FollowTargetType", "follows", "target_type", variant_set(db::FollowTargetType::VARIANTS), ), ]; let mut failures = Vec::new(); for (label, table, column, variants) in &cases { let db_values = db_check_values(&h.db, table, column).await; if &db_values != variants { failures.push(format!( "{label} ({table}.{column}):\n rust VARIANTS = {variants:?}\n db CHECK = {db_values:?}" )); } } assert!( failures.is_empty(), "enum <-> DB CHECK drift detected:\n{}", failures.join("\n") ); }