mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
test(buzz-db): drop partition scratch schemas on panic (#8293)
## Summary
The partition Postgres tests dropped their scratch schema with a
`drop_schema` call at the end of each test body. When a test panicked,
that call never ran, so the schema leaked into the shared database,
along with any `{schema}_*` sibling schemas or reader role the test
created. A reviewer on #7993 hit this locally: the leaked schema plus a
database-wide catalog query made later runs fail. CI hides it because
the test wrapper gives each process its own database.
`scratch_pool` / `scratch_pool_with_max_connections` now return `(pool,
ScratchSchema)`. The guard owns the admin pool and schema name and
cleans up on `Drop`:
- **Fresh thread and connection.** `Drop` runs on the test's runtime
thread, which can't drive the test's pools while it blocks, so cleanup
runs on its own thread with its own runtime and connection.
- **Terminates the test's backends first.** Both pools record each
connection's backend pid. A test that panics mid-transaction keeps its
locks, because the rollback would only run on the blocked runtime.
Without this step `DROP SCHEMA` waits on those locks.
- **Drops `{name}` plus every schema and role named `{name}_*`.** This
replaces the per-test sibling-schema and `DROP ROLE` cleanup. The reader
role is now named `{schema}_reader`.
- **Bounded.** `lock_timeout = '10s'`. Failures print a warning and
never panic inside `Drop`.
The 52 call sites are migrated mechanically. Test-only change.
### Related issue
Follow-up from review on #7993. No existing issue found.
### Testing
- New `scratch_schema_is_dropped_when_a_test_panics_holding_a_lock`: the
test body creates a sibling schema, takes `ACCESS EXCLUSIVE` on a
scratch table in an open transaction, and panics. The test then asserts
no `{name}` / `{name}_*` schema remains.
- Proved it can fail: with the backend-termination step disabled,
cleanup hits the lock timeout and the test fails, reporting both leaked
schemas.
- Partition Postgres lane: `scripts/postgres-test-run.sh -p buzz-db
--lib -E 'test(partition)'`, 53/53 passed.
- The same filter run directly against the shared `buzz` database: 53/53
passed, then 0 `partition_audit_test_%` schemas and 0
`partition_audit_%` roles remained.
- `cargo clippy -p buzz-db --tests` clean. Pre-push hooks passed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Ian Oberst <ioberst@block.xyz>
Co-authored-by: Ian Oberst <ioberst@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2201,49 +2201,207 @@ mod tests {
|
||||
}
|
||||
|
||||
pub(super) mod postgres_tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::Connection;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(in crate::store::partition) async fn scratch_pool() -> (PgPool, PgPool, String) {
|
||||
/// A uniquely named scratch schema that is dropped when the guard goes
|
||||
/// out of scope, including when the test panics.
|
||||
///
|
||||
/// Cleanup also drops every schema and role named `{name}_*`, so a test
|
||||
/// that needs extra schemas or roles derives their names from `name`.
|
||||
///
|
||||
/// Bind the guard to a named variable such as `_scratch` for the whole
|
||||
/// test: `let (pool, _) = scratch_pool().await` drops it, and the
|
||||
/// schema with it, immediately.
|
||||
pub(in crate::store::partition) struct ScratchSchema {
|
||||
pub(in crate::store::partition) admin: PgPool,
|
||||
pub(in crate::store::partition) name: String,
|
||||
url: String,
|
||||
backends: Arc<Mutex<Vec<i32>>>,
|
||||
}
|
||||
|
||||
impl Drop for ScratchSchema {
|
||||
fn drop(&mut self) {
|
||||
// Drop runs on the test's runtime thread, which cannot drive the
|
||||
// test's pools while it blocks here, so clean up over a fresh
|
||||
// connection on a thread with its own runtime.
|
||||
let url = std::mem::take(&mut self.url);
|
||||
let name = self.name.clone();
|
||||
let backends = std::mem::take(
|
||||
&mut *self
|
||||
.backends
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner),
|
||||
);
|
||||
let cleanup = std::thread::spawn(move || {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|error| error.to_string())?
|
||||
.block_on(drop_scratch_objects(&url, &name, &backends))
|
||||
})
|
||||
.join();
|
||||
let error = match cleanup {
|
||||
Ok(Ok(())) => return,
|
||||
Ok(Err(error)) => error,
|
||||
Err(_) => "cleanup thread panicked".to_owned(),
|
||||
};
|
||||
eprintln!(
|
||||
"warning: failed to drop scratch schema {}: {error}",
|
||||
self.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn drop_scratch_objects(
|
||||
url: &str,
|
||||
name: &str,
|
||||
backends: &[i32],
|
||||
) -> std::result::Result<(), String> {
|
||||
let mut connection = PgConnection::connect(url)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
// Every step is idempotent, so keep going after a failure and
|
||||
// report all of them rather than skipping the remaining objects.
|
||||
let mut errors = Vec::new();
|
||||
let mut record = |step: &str, result: sqlx::Result<()>| {
|
||||
if let Err(error) = result {
|
||||
errors.push(format!("{step}: {error}"));
|
||||
}
|
||||
};
|
||||
let result = sqlx::query("SET lock_timeout = '10s'")
|
||||
.execute(&mut connection)
|
||||
.await
|
||||
.map(drop);
|
||||
record("set lock_timeout", result);
|
||||
// A panicking test can leave a transaction holding locks in the
|
||||
// schema; its rollback would only run on the blocked test runtime.
|
||||
let result = sqlx::query(
|
||||
"SELECT pg_terminate_backend(pid, 5000) FROM pg_stat_activity \
|
||||
WHERE pid = ANY($1) AND datname = current_database()",
|
||||
)
|
||||
.bind(backends)
|
||||
.execute(&mut connection)
|
||||
.await
|
||||
.map(drop);
|
||||
record("terminate test backends", result);
|
||||
let schemas: Vec<String> = sqlx::query_scalar(
|
||||
"SELECT nspname::text FROM pg_namespace \
|
||||
WHERE nspname = $1 OR starts_with(nspname, $1 || '_')",
|
||||
)
|
||||
.bind(name)
|
||||
.fetch_all(&mut connection)
|
||||
.await
|
||||
.unwrap_or_else(|error| {
|
||||
record("list schemas", Err(error));
|
||||
Vec::new()
|
||||
});
|
||||
for schema in schemas {
|
||||
let result = sqlx::query(sqlx::AssertSqlSafe(format!(
|
||||
"DROP SCHEMA IF EXISTS {} CASCADE",
|
||||
quote_identifier(&schema)
|
||||
)))
|
||||
.execute(&mut connection)
|
||||
.await
|
||||
.map(drop);
|
||||
record(&format!("drop schema {schema}"), result);
|
||||
}
|
||||
let roles: Vec<String> = sqlx::query_scalar(
|
||||
"SELECT rolname::text FROM pg_roles WHERE starts_with(rolname, $1 || '_')",
|
||||
)
|
||||
.bind(name)
|
||||
.fetch_all(&mut connection)
|
||||
.await
|
||||
.unwrap_or_else(|error| {
|
||||
record("list roles", Err(error));
|
||||
Vec::new()
|
||||
});
|
||||
for role in roles {
|
||||
let result = sqlx::query(sqlx::AssertSqlSafe(format!(
|
||||
"DROP ROLE IF EXISTS {}",
|
||||
quote_identifier(&role)
|
||||
)))
|
||||
.execute(&mut connection)
|
||||
.await
|
||||
.map(drop);
|
||||
record(&format!("drop role {role}"), result);
|
||||
}
|
||||
record("close connection", connection.close().await);
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors.join("; "))
|
||||
}
|
||||
}
|
||||
|
||||
/// Pool options that record each connection's backend pid in `backends`
|
||||
/// and point its `search_path` at `search_path`, when given.
|
||||
fn tracked_pool_options(
|
||||
search_path: Option<String>,
|
||||
backends: Arc<Mutex<Vec<i32>>>,
|
||||
) -> PgPoolOptions {
|
||||
PgPoolOptions::new().after_connect(move |connection, _| {
|
||||
let search_path = search_path.clone();
|
||||
let backends = Arc::clone(&backends);
|
||||
Box::pin(async move {
|
||||
if let Some(schema) = search_path {
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!("SET search_path TO {schema}")))
|
||||
.execute(&mut *connection)
|
||||
.await?;
|
||||
}
|
||||
let pid: i32 = sqlx::query_scalar("SELECT pg_backend_pid()")
|
||||
.fetch_one(&mut *connection)
|
||||
.await?;
|
||||
backends
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(pid);
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// A pool whose `search_path` is a fresh scratch schema, plus the guard
|
||||
/// that drops it. Bind the guard to a named variable, never `_`.
|
||||
pub(in crate::store::partition) async fn scratch_pool() -> (PgPool, ScratchSchema) {
|
||||
scratch_pool_with_max_connections(4).await
|
||||
}
|
||||
|
||||
pub(in crate::store::partition) async fn scratch_pool_with_max_connections(
|
||||
max_connections: u32,
|
||||
) -> (PgPool, PgPool, String) {
|
||||
) -> (PgPool, ScratchSchema) {
|
||||
let url = crate::test_support::database_url();
|
||||
let schema = format!("partition_audit_test_{}", Uuid::new_v4().simple());
|
||||
let admin = PgPool::connect(&url).await.expect("connect admin pool");
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!("CREATE SCHEMA {schema}")))
|
||||
.execute(&admin)
|
||||
.await
|
||||
.expect("create scratch schema");
|
||||
let search_path_schema = schema.clone();
|
||||
let pool = PgPoolOptions::new()
|
||||
.max_connections(max_connections)
|
||||
.after_connect(move |connection, _| {
|
||||
let schema = search_path_schema.clone();
|
||||
Box::pin(async move {
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!("SET search_path TO {schema}")))
|
||||
.execute(connection)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
let name = format!("partition_audit_test_{}", Uuid::new_v4().simple());
|
||||
let backends = Arc::new(Mutex::new(Vec::new()));
|
||||
let admin = tracked_pool_options(None, Arc::clone(&backends))
|
||||
.connect(&url)
|
||||
.await
|
||||
.expect("connect scratch pool");
|
||||
(pool, admin, schema)
|
||||
}
|
||||
|
||||
pub(in crate::store::partition) async fn drop_schema(admin: &PgPool, schema: &str) {
|
||||
let _ = sqlx::query(sqlx::AssertSqlSafe(format!(
|
||||
"DROP SCHEMA IF EXISTS {schema} CASCADE"
|
||||
.expect("connect admin pool");
|
||||
let scratch = ScratchSchema {
|
||||
admin,
|
||||
name,
|
||||
url,
|
||||
backends,
|
||||
};
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!(
|
||||
"CREATE SCHEMA {}",
|
||||
scratch.name
|
||||
)))
|
||||
.execute(admin)
|
||||
.await;
|
||||
.execute(&scratch.admin)
|
||||
.await
|
||||
.expect("create scratch schema");
|
||||
let pool =
|
||||
tracked_pool_options(Some(scratch.name.clone()), Arc::clone(&scratch.backends))
|
||||
.max_connections(max_connections)
|
||||
.connect(&scratch.url)
|
||||
.await
|
||||
.expect("connect scratch pool");
|
||||
(pool, scratch)
|
||||
}
|
||||
|
||||
pub(in crate::store::partition) async fn seed_parents(pool: &PgPool) {
|
||||
@@ -2412,10 +2570,80 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires Postgres"]
|
||||
fn scratch_schema_is_dropped_when_a_test_panics_holding_a_lock() {
|
||||
let runtime = || {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("build test runtime")
|
||||
};
|
||||
let (name_sender, name_receiver) = std::sync::mpsc::channel();
|
||||
let outcome = std::thread::spawn(move || {
|
||||
runtime().block_on(async move {
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!(
|
||||
"CREATE SCHEMA {}_children",
|
||||
scratch.name
|
||||
)))
|
||||
.execute(&scratch.admin)
|
||||
.await
|
||||
.expect("create sibling schema");
|
||||
for statement in [
|
||||
format!("CREATE ROLE {}_reader NOLOGIN", scratch.name),
|
||||
format!("GRANT USAGE ON SCHEMA {0} TO {0}_reader", scratch.name),
|
||||
] {
|
||||
sqlx::query(sqlx::AssertSqlSafe(statement))
|
||||
.execute(&scratch.admin)
|
||||
.await
|
||||
.expect("create scratch role");
|
||||
}
|
||||
let mut holder = pool.begin().await.expect("begin lock holder");
|
||||
sqlx::query("LOCK TABLE events IN ACCESS EXCLUSIVE MODE")
|
||||
.execute(&mut *holder)
|
||||
.await
|
||||
.expect("lock scratch table");
|
||||
name_sender
|
||||
.send(scratch.name.clone())
|
||||
.expect("send scratch schema name");
|
||||
panic!("deliberate panic while holding a scratch schema lock");
|
||||
})
|
||||
})
|
||||
.join();
|
||||
assert!(outcome.is_err(), "the scratch test body must panic");
|
||||
let name = name_receiver.recv().expect("scratch schema name");
|
||||
|
||||
let (schemas, roles): (Vec<String>, Vec<String>) = runtime().block_on(async {
|
||||
let mut connection = PgConnection::connect(&crate::test_support::database_url())
|
||||
.await
|
||||
.expect("connect leftover check");
|
||||
let schemas = sqlx::query_scalar(
|
||||
"SELECT nspname::text FROM pg_namespace \
|
||||
WHERE nspname = $1 OR starts_with(nspname, $1 || '_')",
|
||||
)
|
||||
.bind(&name)
|
||||
.fetch_all(&mut connection)
|
||||
.await
|
||||
.expect("list leftover schemas");
|
||||
let roles = sqlx::query_scalar(
|
||||
"SELECT rolname::text FROM pg_roles WHERE starts_with(rolname, $1 || '_')",
|
||||
)
|
||||
.bind(&name)
|
||||
.fetch_all(&mut connection)
|
||||
.await
|
||||
.expect("list leftover roles");
|
||||
(schemas, roles)
|
||||
});
|
||||
assert!(schemas.is_empty(), "leaked scratch schemas: {schemas:?}");
|
||||
assert!(roles.is_empty(), "leaked scratch roles: {roles:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn fresh_layout_is_serving_safe_and_audit_is_read_only() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_fresh_layout(&pool).await;
|
||||
let before = catalog_snapshot(&pool).await;
|
||||
let audit = audit_partition_catalog_at(&pool, 3, fixed_now())
|
||||
@@ -2431,13 +2659,12 @@ mod tests {
|
||||
.all(|month| month.kind == MonthCoverageKind::CoveredByCatchAll)
|
||||
}));
|
||||
assert_eq!(catalog_snapshot(&pool).await, before);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn wrong_root_partition_key_never_proves_coverage() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents_with_keys(&pool, "alternate_at", "delivered_at").await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
create_child(
|
||||
@@ -2473,13 +2700,13 @@ mod tests {
|
||||
.expect("delivery_log audit");
|
||||
assert!(delivery_log.partition_key_valid);
|
||||
assert!(delivery_log.serving_safe);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn occupied_catch_all_is_reported_as_degraded() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
let schema = scratch.name.as_str();
|
||||
seed_fresh_layout(&pool).await;
|
||||
sqlx::query(
|
||||
"INSERT INTO events (created_at, alternate_at) \
|
||||
@@ -2519,17 +2746,17 @@ mod tests {
|
||||
assert!(delivery_log.children.iter().any(|child| {
|
||||
child.name == "delivery_log_p_future" && child.catch_all_nonempty == Some(false)
|
||||
}));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn occupied_cross_schema_catch_all_is_reported_as_degraded() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
let schema = scratch.name.as_str();
|
||||
seed_fresh_layout(&pool).await;
|
||||
let child_schema = format!("{schema}_children");
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!("CREATE SCHEMA {child_schema}")))
|
||||
.execute(&admin)
|
||||
.execute(&scratch.admin)
|
||||
.await
|
||||
.expect("create child schema");
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!(
|
||||
@@ -2567,20 +2794,19 @@ mod tests {
|
||||
&& leaf.name == "events_p_future"
|
||||
&& leaf.root_child_schema == child_schema
|
||||
}));
|
||||
drop_schema(&admin, &schema).await;
|
||||
drop_schema(&admin, &child_schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn same_named_cross_schema_children_keep_distinct_trigger_metadata() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
let schema = scratch.name.as_str();
|
||||
seed_parents(&pool).await;
|
||||
let first_schema = format!("{schema}_first");
|
||||
let second_schema = format!("{schema}_second");
|
||||
for child_schema in [&first_schema, &second_schema] {
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!("CREATE SCHEMA {child_schema}")))
|
||||
.execute(&admin)
|
||||
.execute(&scratch.admin)
|
||||
.await
|
||||
.expect("create child schema");
|
||||
}
|
||||
@@ -2676,19 +2902,17 @@ mod tests {
|
||||
.expect("serialized coverage leaves")
|
||||
.iter()
|
||||
.any(|leaf| leaf["schema"] == second_schema));
|
||||
drop_schema(&admin, &schema).await;
|
||||
drop_schema(&admin, &first_schema).await;
|
||||
drop_schema(&admin, &second_schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn report_retains_successful_table_when_another_table_errors() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
let schema = scratch.name.as_str();
|
||||
seed_fresh_layout(&pool).await;
|
||||
let role = format!("partition_audit_reader_{}", Uuid::new_v4().simple());
|
||||
let role = format!("{schema}_reader");
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!("CREATE ROLE {role} NOLOGIN")))
|
||||
.execute(&admin)
|
||||
.execute(&scratch.admin)
|
||||
.await
|
||||
.expect("create restricted audit role");
|
||||
for statement in [
|
||||
@@ -2696,12 +2920,12 @@ mod tests {
|
||||
format!("GRANT SELECT ON {schema}.events_p_future TO {role}"),
|
||||
] {
|
||||
sqlx::query(sqlx::AssertSqlSafe(statement))
|
||||
.execute(&admin)
|
||||
.execute(&scratch.admin)
|
||||
.await
|
||||
.expect("grant restricted audit access");
|
||||
}
|
||||
let url = crate::test_support::database_url();
|
||||
let role_schema = schema.clone();
|
||||
let role_schema = schema.to_owned();
|
||||
let role_name = role.clone();
|
||||
let restricted = PgPoolOptions::new()
|
||||
.max_connections(1)
|
||||
@@ -2731,17 +2955,12 @@ mod tests {
|
||||
assert!(!report.serving_safe());
|
||||
|
||||
restricted.close().await;
|
||||
drop_schema(&admin, &schema).await;
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!("DROP ROLE {role}")))
|
||||
.execute(&admin)
|
||||
.await
|
||||
.expect("drop restricted audit role");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn pending_detach_never_proves_coverage() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_fresh_layout(&pool).await;
|
||||
|
||||
let mut blocker = pool.acquire().await.expect("acquire blocker");
|
||||
@@ -2824,13 +3043,12 @@ mod tests {
|
||||
&& child.pending_detach
|
||||
&& child.kind == PartitionChildKind::Anomalous
|
||||
}));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn repaired_layout_recognizes_bounds_not_catch_all_name() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
create_child(
|
||||
@@ -2907,13 +3125,12 @@ mod tests {
|
||||
assert!(august.tables.iter().all(PartitionTableAudit::degraded));
|
||||
assert_eq!(august.outcome(), PartitionAuditOutcome::Degraded);
|
||||
assert_eq!(catalog_snapshot(&pool).await, before);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn uncovered_months_are_created_with_trigger_parity() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
create_child(
|
||||
@@ -2937,13 +3154,12 @@ mod tests {
|
||||
.iter()
|
||||
.all(|month| month.kind == MonthCoverageKind::CoveredByMonthly)
|
||||
&& table.missing_trigger_count() == 0));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn default_partitions_report_occupancy_without_mutation() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_uncovered_layout(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!(
|
||||
@@ -3037,13 +3253,12 @@ mod tests {
|
||||
.await
|
||||
.expect("check canonical relation"));
|
||||
}
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn parent_default_does_not_mask_gap_below_explicit_sibling() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for (table, column) in [("events", "created_at"), ("delivery_log", "delivered_at")] {
|
||||
create_child(
|
||||
@@ -3116,13 +3331,13 @@ mod tests {
|
||||
.execute(&pool)
|
||||
.await
|
||||
.is_err());
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn total_audit_timeout_bounds_blocked_occupancy_probes() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
let schema = scratch.name.as_str();
|
||||
seed_uncovered_layout(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!(
|
||||
@@ -3132,7 +3347,11 @@ mod tests {
|
||||
.await
|
||||
.expect("create default partition");
|
||||
}
|
||||
let mut blocker = admin.begin().await.expect("begin blocking transaction");
|
||||
let mut blocker = scratch
|
||||
.admin
|
||||
.begin()
|
||||
.await
|
||||
.expect("begin blocking transaction");
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!(
|
||||
"LOCK TABLE {schema}.events_default IN ACCESS EXCLUSIVE MODE"
|
||||
)))
|
||||
@@ -3157,13 +3376,12 @@ mod tests {
|
||||
.all(|error| error.error.contains("exceeded total timeout of 100ms")));
|
||||
|
||||
blocker.rollback().await.expect("release blocking lock");
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn create_pins_non_iso_date_style() {
|
||||
let (pool, admin, schema) = scratch_pool_with_max_connections(1).await;
|
||||
let (pool, _scratch) = scratch_pool_with_max_connections(1).await;
|
||||
seed_uncovered_layout(&pool).await;
|
||||
sqlx::query("SET DateStyle TO 'SQL, DMY'")
|
||||
.execute(&pool)
|
||||
@@ -3186,13 +3404,12 @@ mod tests {
|
||||
.await
|
||||
.expect("show DateStyle");
|
||||
assert_eq!(date_style, "SQL, DMY");
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn create_pins_non_utc_time_zone() {
|
||||
let (pool, admin, schema) = scratch_pool_with_max_connections(1).await;
|
||||
let (pool, _scratch) = scratch_pool_with_max_connections(1).await;
|
||||
seed_uncovered_layout(&pool).await;
|
||||
sqlx::query("SET TimeZone TO 'America/Los_Angeles'")
|
||||
.execute(&pool)
|
||||
@@ -3215,13 +3432,12 @@ mod tests {
|
||||
.await
|
||||
.expect("show TimeZone");
|
||||
assert_eq!(time_zone, "America/Los_Angeles");
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn disabled_child_trigger_degrades_parity() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_fresh_layout(&pool).await;
|
||||
sqlx::query("ALTER TABLE events_p_future DISABLE TRIGGER partition_probe")
|
||||
.execute(&pool)
|
||||
@@ -3240,13 +3456,12 @@ mod tests {
|
||||
assert!(events.children.iter().any(|child| {
|
||||
child.name == "events_p_future" && child.missing_triggers == ["partition_probe"]
|
||||
}));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn always_enabled_parent_trigger_preserves_parity() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_fresh_layout(&pool).await;
|
||||
sqlx::query("ALTER TABLE events ENABLE ALWAYS TRIGGER partition_probe")
|
||||
.execute(&pool)
|
||||
@@ -3266,13 +3481,13 @@ mod tests {
|
||||
.children
|
||||
.iter()
|
||||
.all(|child| child.missing_triggers.is_empty()));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn disabled_nested_leaf_trigger_degrades_parity() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
let schema = scratch.name.as_str();
|
||||
seed_parents(&pool).await;
|
||||
for (table, column) in [("events", "created_at"), ("delivery_log", "delivered_at")] {
|
||||
let nested = format!("{table}_nested");
|
||||
@@ -3339,18 +3554,18 @@ mod tests {
|
||||
assert_eq!(events.missing_trigger_count(), 1);
|
||||
let expected = format!(
|
||||
"{}:partition_probe",
|
||||
qualified_relation_name(&schema, "events_nested_first")
|
||||
qualified_relation_name(schema, "events_nested_first")
|
||||
);
|
||||
assert!(events.children.iter().any(|child| {
|
||||
child.name == "events_nested" && child.missing_triggers == [expected.as_str()]
|
||||
}));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn child_only_nested_leaf_trigger_degrades_parity() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
let schema = scratch.name.as_str();
|
||||
seed_parents(&pool).await;
|
||||
create_nested_child(
|
||||
&pool,
|
||||
@@ -3396,31 +3611,29 @@ mod tests {
|
||||
assert_eq!(events.extra_trigger_count(), 1);
|
||||
let expected = format!(
|
||||
"{}:child_only_probe",
|
||||
qualified_relation_name(&schema, "events_nested_first")
|
||||
qualified_relation_name(schema, "events_nested_first")
|
||||
);
|
||||
assert!(events.children.iter().any(|child| {
|
||||
child.name == "events_nested" && child.extra_triggers == [expected.as_str()]
|
||||
}));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn catch_all_skips_create_without_catalog_mutation() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_fresh_layout(&pool).await;
|
||||
let before = catalog_snapshot(&pool).await;
|
||||
ensure_future_partitions_at(&pool, 3, true, fixed_now())
|
||||
.await
|
||||
.expect("covered no-op");
|
||||
assert_eq!(catalog_snapshot(&pool).await, before);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn kill_switch_audits_but_does_not_create() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
create_child(
|
||||
@@ -3445,13 +3658,12 @@ mod tests {
|
||||
.months
|
||||
.iter()
|
||||
.all(|month| month.kind == MonthCoverageKind::Uncovered)));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn nested_children_without_leaves_do_not_prove_coverage() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for (table, column) in [("events", "created_at"), ("delivery_log", "delivered_at")] {
|
||||
create_nested_child(
|
||||
@@ -3477,13 +3689,12 @@ mod tests {
|
||||
&& table.months[0].kind == MonthCoverageKind::Uncovered
|
||||
}));
|
||||
assert_eq!(catalog_snapshot(&pool).await, before);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn nested_descendant_leaves_prove_coverage_but_remain_degraded() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for (table, column) in [("events", "created_at"), ("delivery_log", "delivered_at")] {
|
||||
let nested = format!("{table}_nested");
|
||||
@@ -3519,13 +3730,12 @@ mod tests {
|
||||
&& table.months[0].kind == MonthCoverageKind::CoveredByMonthly
|
||||
}));
|
||||
assert_eq!(catalog_snapshot(&pool).await, before);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn occupied_nested_catch_all_is_reported_on_leaf_and_root_child() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for (table, column) in [("events", "created_at"), ("delivery_log", "delivered_at")] {
|
||||
let nested = format!("{table}_nested");
|
||||
@@ -3584,13 +3794,12 @@ mod tests {
|
||||
assert!(delivery_log.coverage_leaves.iter().any(|leaf| {
|
||||
leaf.name == "delivery_log_nested_future" && leaf.catch_all_nonempty == Some(false)
|
||||
}));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn nested_different_partition_key_does_not_prove_coverage() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
let nested = format!("{table}_nested");
|
||||
@@ -3623,13 +3832,12 @@ mod tests {
|
||||
&& table.coverage_leaves.is_empty()
|
||||
&& table.months[0].kind == MonthCoverageKind::Uncovered
|
||||
}));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn audit_pins_non_iso_session_rendering() {
|
||||
let (pool, admin, schema) = scratch_pool_with_max_connections(1).await;
|
||||
let (pool, _scratch) = scratch_pool_with_max_connections(1).await;
|
||||
seed_fresh_layout(&pool).await;
|
||||
sqlx::query("SET DateStyle TO 'SQL, DMY'")
|
||||
.execute(&pool)
|
||||
@@ -3658,13 +3866,12 @@ mod tests {
|
||||
.expect("show TimeZone");
|
||||
assert_eq!(date_style, "SQL, DMY");
|
||||
assert_eq!(time_zone, "America/Los_Angeles");
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn audit_transaction_applies_and_restores_statement_timeout() {
|
||||
let (pool, admin, schema) = scratch_pool_with_max_connections(1).await;
|
||||
let (pool, _scratch) = scratch_pool_with_max_connections(1).await;
|
||||
sqlx::query("SET statement_timeout TO '37s'")
|
||||
.execute(&pool)
|
||||
.await
|
||||
@@ -3692,13 +3899,12 @@ mod tests {
|
||||
.await
|
||||
.expect("show restored statement timeout");
|
||||
assert_eq!(restored, "37s");
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn statement_triggers_are_not_part_of_leaf_parity() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
sqlx::query(sqlx::AssertSqlSafe(format!(
|
||||
@@ -3734,13 +3940,12 @@ mod tests {
|
||||
&& table.missing_trigger_count() == 0
|
||||
&& table.extra_trigger_count() == 0
|
||||
}));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn canonical_name_with_wrong_bounds_is_a_real_creation_error() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
create_child(
|
||||
@@ -3776,13 +3981,13 @@ mod tests {
|
||||
.find(|table| table.table == "events")
|
||||
.expect("events audit");
|
||||
assert_eq!(events.months[0].kind, MonthCoverageKind::Uncovered);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn canonical_name_attached_to_wrong_parent_is_a_real_creation_error() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
let schema = scratch.name.as_str();
|
||||
seed_parents(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
create_child(
|
||||
@@ -3833,18 +4038,17 @@ mod tests {
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(&schema)
|
||||
.bind(schema)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("check attachment");
|
||||
assert!(!attached_to_events);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn unrelated_canonical_relation_is_a_real_creation_error() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
create_child(
|
||||
@@ -3869,13 +4073,12 @@ mod tests {
|
||||
assert!(relation_name_exists(&pool, "events_p2026_09")
|
||||
.await
|
||||
.expect("query unrelated relation"));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn anomalous_child_and_trigger_mismatch_degrade_without_aborting() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
create_child(
|
||||
@@ -3923,7 +4126,6 @@ mod tests {
|
||||
.iter()
|
||||
.any(|child| child.extra_triggers == ["child_only_probe"])
|
||||
}));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,8 +328,8 @@ mod postgres_tests {
|
||||
|
||||
use super::*;
|
||||
use crate::store::partition::tests::postgres_tests::{
|
||||
catalog_snapshot, create_child, drop_schema, fixed_now, scratch_pool,
|
||||
scratch_pool_with_max_connections, seed_parents,
|
||||
catalog_snapshot, create_child, fixed_now, scratch_pool, scratch_pool_with_max_connections,
|
||||
seed_parents,
|
||||
};
|
||||
|
||||
/// The repaired incident layout for both managed parents, optionally with
|
||||
@@ -441,7 +441,7 @@ mod postgres_tests {
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn empty_catch_all_advances_to_canonical_layout_and_is_idempotent() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, true).await;
|
||||
|
||||
let audit = maintain(&pool).await.expect("advance");
|
||||
@@ -474,13 +474,12 @@ mod postgres_tests {
|
||||
let before = catalog_snapshot(&pool).await;
|
||||
maintain(&pool).await.expect("idempotent rerun");
|
||||
assert_eq!(catalog_snapshot(&pool).await, before);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn canonical_catch_all_name_is_replaced_in_one_transaction() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
for table in PARTITIONED_TABLES {
|
||||
create_child(
|
||||
@@ -506,13 +505,12 @@ mod postgres_tests {
|
||||
.children
|
||||
.iter()
|
||||
.any(|child| child.name == format!("{}_p2026_09", table.table))));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn populated_catch_all_is_refused_before_any_lock() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, false).await;
|
||||
sqlx::query("INSERT INTO events (created_at, alternate_at) VALUES ('2027-02-01', now())")
|
||||
.execute(&pool)
|
||||
@@ -544,13 +542,12 @@ mod postgres_tests {
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(events_only(&after), events_only(&before));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn counterpart_contention_fails_fast_and_rolls_back() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, true).await;
|
||||
let before = catalog_snapshot(&pool).await;
|
||||
let holder = hold_lock(
|
||||
@@ -575,13 +572,12 @@ mod postgres_tests {
|
||||
assert_eq!(catalog_snapshot(&pool).await, before);
|
||||
|
||||
assert_advanced(&maintain(&pool).await.expect("advance after release"));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn counterpart_reader_does_not_block_advancement() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, true).await;
|
||||
let holder = hold_lock(&pool, "SELECT count(*) FROM communities").await;
|
||||
|
||||
@@ -589,13 +585,12 @@ mod postgres_tests {
|
||||
drop(holder);
|
||||
|
||||
assert_advanced(&result.expect("advance beside a counterpart reader"));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn writer_checking_a_foreign_key_behind_maintenance_does_not_deadlock() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, true).await;
|
||||
// An in-flight insert: the parent first, then the foreign-key check.
|
||||
let mut writer = hold_lock(&pool, "LOCK TABLE ONLY events IN ROW EXCLUSIVE MODE").await;
|
||||
@@ -615,7 +610,7 @@ mod postgres_tests {
|
||||
WHERE $1 = ANY(pg_blocking_pids(pid)))",
|
||||
)
|
||||
.bind(writer_pid)
|
||||
.fetch_one(&admin)
|
||||
.fetch_one(&scratch.admin)
|
||||
.await
|
||||
.expect("check queued maintenance");
|
||||
if queued {
|
||||
@@ -650,7 +645,6 @@ mod postgres_tests {
|
||||
.expect("maintenance task")
|
||||
.expect("advance after the writer commits"),
|
||||
);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
async fn wait_until_blocked_by(admin: &PgPool, pid: i32, waiters: i64) {
|
||||
@@ -673,7 +667,7 @@ mod postgres_tests {
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn foreign_key_committed_while_waiting_on_the_parent_is_locked_nowait() {
|
||||
let (pool, admin, schema) = scratch_pool_with_max_connections(6).await;
|
||||
let (pool, scratch) = scratch_pool_with_max_connections(6).await;
|
||||
seed_repaired_layout(&pool, true).await;
|
||||
sqlx::query("CREATE TABLE reviewers (id UUID PRIMARY KEY)")
|
||||
.execute(&pool)
|
||||
@@ -708,12 +702,12 @@ mod postgres_tests {
|
||||
.expect("roll back counterpart writer");
|
||||
}
|
||||
});
|
||||
wait_until_blocked_by(&admin, migration_pid, 1).await;
|
||||
wait_until_blocked_by(&scratch.admin, migration_pid, 1).await;
|
||||
let maintenance = tokio::spawn({
|
||||
let pool = pool.clone();
|
||||
async move { maintain(&pool).await }
|
||||
});
|
||||
wait_until_blocked_by(&admin, migration_pid, 2).await;
|
||||
wait_until_blocked_by(&scratch.admin, migration_pid, 2).await;
|
||||
|
||||
migration.commit().await.expect("commit migration");
|
||||
let result = maintenance.await.expect("maintenance task");
|
||||
@@ -733,7 +727,6 @@ mod postgres_tests {
|
||||
release_writer.send(()).expect("release counterpart writer");
|
||||
writer.await.expect("counterpart writer task");
|
||||
assert_advanced(&maintain(&pool).await.expect("advance after release"));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
async fn backend_pid(connection: &mut PgConnection) -> i32 {
|
||||
@@ -753,7 +746,7 @@ mod postgres_tests {
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn foreign_key_referencing_the_parent_committed_while_waiting_is_refused() {
|
||||
let (pool, admin, schema) = scratch_pool_with_max_connections(6).await;
|
||||
let (pool, scratch) = scratch_pool_with_max_connections(6).await;
|
||||
seed_repaired_layout(&pool, true).await;
|
||||
sqlx::query(
|
||||
"CREATE TABLE event_refs \
|
||||
@@ -775,7 +768,7 @@ mod postgres_tests {
|
||||
let pool = pool.clone();
|
||||
async move { maintain(&pool).await }
|
||||
});
|
||||
wait_until_blocked_by(&admin, migration_pid, 1).await;
|
||||
wait_until_blocked_by(&scratch.admin, migration_pid, 1).await;
|
||||
migration.commit().await.expect("commit migration");
|
||||
|
||||
let result = maintenance.await.expect("maintenance task");
|
||||
@@ -783,13 +776,12 @@ mod postgres_tests {
|
||||
refused_events_only(&result, "events is referenced by a foreign key"),
|
||||
"{result:?}"
|
||||
);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn catch_all_foreign_keys_outside_the_parent_are_refused() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, true).await;
|
||||
sqlx::query("CREATE TABLE reviewers (id UUID PRIMARY KEY)")
|
||||
.execute(&pool)
|
||||
@@ -842,13 +834,12 @@ mod postgres_tests {
|
||||
}
|
||||
drop(holder);
|
||||
assert_advanced(&maintain(&pool).await.expect("advance without the keys"));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn catch_all_foreign_key_committed_while_waiting_on_the_parent_is_refused() {
|
||||
let (pool, admin, schema) = scratch_pool_with_max_connections(6).await;
|
||||
let (pool, scratch) = scratch_pool_with_max_connections(6).await;
|
||||
seed_repaired_layout(&pool, true).await;
|
||||
sqlx::query("CREATE TABLE reviewers (id UUID PRIMARY KEY)")
|
||||
.execute(&pool)
|
||||
@@ -868,7 +859,7 @@ mod postgres_tests {
|
||||
let pool = pool.clone();
|
||||
async move { maintain(&pool).await }
|
||||
});
|
||||
wait_until_blocked_by(&admin, reader_pid, 1).await;
|
||||
wait_until_blocked_by(&scratch.admin, reader_pid, 1).await;
|
||||
migration.commit().await.expect("commit migration");
|
||||
reader.commit().await.expect("release parent");
|
||||
|
||||
@@ -885,13 +876,12 @@ mod postgres_tests {
|
||||
.await
|
||||
.expect("count catch-all key");
|
||||
assert_eq!(kept, 1);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn deadlock_victim_is_classified_as_a_lock_timeout() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_parents(&pool).await;
|
||||
let mut first = hold_lock(&pool, "LOCK TABLE ONLY events IN ACCESS EXCLUSIVE MODE").await;
|
||||
let mut second = hold_lock(
|
||||
@@ -925,13 +915,12 @@ mod postgres_tests {
|
||||
PartitionMaintenanceOutcome::LockTimeout
|
||||
);
|
||||
drop((first, second));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn parent_contention_times_out_without_blocking_the_other_table() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, true).await;
|
||||
let holder = hold_lock(&pool, "LOCK TABLE ONLY events IN ACCESS SHARE MODE").await;
|
||||
|
||||
@@ -961,13 +950,12 @@ mod postgres_tests {
|
||||
audited_at: audit.audited_at,
|
||||
tables: delivery_log,
|
||||
});
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn held_maintenance_lock_skips_without_ddl() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, false).await;
|
||||
let before = catalog_snapshot(&pool).await;
|
||||
let holder = hold_lock(
|
||||
@@ -982,13 +970,12 @@ mod postgres_tests {
|
||||
.expect("skipped_locked is not a failure");
|
||||
assert_eq!(catalog_snapshot(&pool).await, before);
|
||||
drop(holder);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn concurrent_runners_converge_on_one_layout() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, true).await;
|
||||
|
||||
let (first, second) = tokio::join!(maintain(&pool), maintain(&pool));
|
||||
@@ -1012,13 +999,12 @@ mod postgres_tests {
|
||||
}
|
||||
// Whatever a fail-fast loser left undone, the next pass converges.
|
||||
assert_advanced(&maintain(&pool).await.expect("follow-up pass"));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn client_deadline_rolls_back_and_discards_the_session() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, false).await;
|
||||
let before = catalog_snapshot(&pool).await;
|
||||
// The parent lock wait (2s) outlasts the 300ms client deadline.
|
||||
@@ -1050,7 +1036,7 @@ mod postgres_tests {
|
||||
"SELECT pid FROM pg_stat_activity WHERE $1 = ANY(pg_blocking_pids(pid))",
|
||||
)
|
||||
.bind(holder_pid)
|
||||
.fetch_all(&admin)
|
||||
.fetch_all(&scratch.admin)
|
||||
.await
|
||||
.expect("sessions blocked by the holder");
|
||||
let mut gone = blocked.is_empty();
|
||||
@@ -1063,7 +1049,7 @@ mod postgres_tests {
|
||||
"SELECT EXISTS (SELECT 1 FROM pg_stat_activity WHERE pid = ANY($1))",
|
||||
)
|
||||
.bind(&blocked)
|
||||
.fetch_one(&admin)
|
||||
.fetch_one(&scratch.admin)
|
||||
.await
|
||||
.expect("check abandoned backend");
|
||||
gone = !alive;
|
||||
@@ -1086,13 +1072,12 @@ mod postgres_tests {
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(events_only(&after), events_only(&before));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn catch_all_with_child_only_trigger_is_refused() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, false).await;
|
||||
sqlx::query(
|
||||
"CREATE TRIGGER catch_all_only BEFORE INSERT ON events_p_future_next \
|
||||
@@ -1120,13 +1105,12 @@ mod postgres_tests {
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(events_only(&after), events_only(&before));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn stale_audit_after_a_concurrent_advance_is_a_noop() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, false).await;
|
||||
// A runner audits, then loses the race to a runner that commits first.
|
||||
let stale = audit_partition_catalog_at(&pool, PARTITION_MANAGER_MONTHS_AHEAD, fixed_now())
|
||||
@@ -1167,13 +1151,12 @@ mod postgres_tests {
|
||||
.await
|
||||
.expect("losing runner");
|
||||
assert_advanced(&reported);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn replan_that_finds_only_disabled_work_is_skipped_disabled() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, false).await;
|
||||
sqlx::query("DROP TABLE events_p_future_next")
|
||||
.execute(&pool)
|
||||
@@ -1210,13 +1193,12 @@ mod postgres_tests {
|
||||
(PartitionMaintenanceOutcome::SkippedDisabled, None)
|
||||
);
|
||||
assert_eq!(catalog_snapshot(&pool).await, before);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn audit_retries_when_a_listed_partition_is_dropped() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, false).await;
|
||||
let mut dropper = pool.begin().await.expect("begin dropper");
|
||||
let dropper_pid: i32 = sqlx::query_scalar("SELECT pg_backend_pid()")
|
||||
@@ -1242,7 +1224,7 @@ mod postgres_tests {
|
||||
WHERE $1 = ANY(pg_blocking_pids(pid)))",
|
||||
)
|
||||
.bind(dropper_pid)
|
||||
.fetch_one(&admin)
|
||||
.fetch_one(&scratch.admin)
|
||||
.await
|
||||
.expect("check audit queue");
|
||||
if queued {
|
||||
@@ -1263,13 +1245,12 @@ mod postgres_tests {
|
||||
.children
|
||||
.iter()
|
||||
.all(|child| child.name != "events_p_future_next"));
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn unrelated_catch_all_name_is_an_operator_error() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, false).await;
|
||||
sqlx::query("CREATE TABLE events_p_future (unrelated BOOLEAN)")
|
||||
.execute(&pool)
|
||||
@@ -1297,13 +1278,12 @@ mod postgres_tests {
|
||||
.filter(|(name, ..)| name.starts_with("events_"))
|
||||
.count()
|
||||
);
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn incoming_foreign_key_is_refused_under_lock() {
|
||||
let (pool, admin, schema) = scratch_pool().await;
|
||||
let (pool, _scratch) = scratch_pool().await;
|
||||
seed_repaired_layout(&pool, false).await;
|
||||
sqlx::query(
|
||||
"CREATE TABLE event_refs (created_at TIMESTAMPTZ, alternate_at TIMESTAMPTZ, id BIGINT, \
|
||||
@@ -1325,6 +1305,5 @@ mod postgres_tests {
|
||||
.iter()
|
||||
.any(|(name, ..)| name == "events_p_future_next"));
|
||||
assert_ne!(after, before, "delivery_log still advances");
|
||||
drop_schema(&admin, &schema).await;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user