use indoc::formatdoc;
use uuid::Uuid;
use crate::meta_storage::psql::PsqlClient;
use crate::meta_storage::tests::utils::psql_backend;
use crate::meta_storage::{MetaBackend, MetaConnApi};
use crate::schema::{RunFootprint, RunMetadata, RunState, TableShape};
const REBUILD_SCHEMA: &str = "operon_rebuild_footprint";
const SAME_VERSION_SCHEMA: &str = "operon_rebuild_footprint_same";
const BUMPED_VERSION_SCHEMA: &str = "operon_rebuild_footprint_bumped";
const PARTIAL_SCHEMA: &str = "operon_rebuild_footprint_partial";
async fn drop_schema(client: PsqlClient<'_>, schema: &str) {
let stmt = format!("DROP SCHEMA IF EXISTS {schema} CASCADE;");
let _num_rows = client.execute(&stmt, &[]).await.expect("drop the schema");
}
async fn init_unversioned_footprint(client: PsqlClient<'_>, schema: &str, run_id: Uuid) {
let stmt = formatdoc! {"
CREATE TABLE {schema}.runs (
key TEXT PRIMARY KEY CHECK (key = 'global'),
run_id UUID NOT NULL UNIQUE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
finished_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
state TEXT NOT NULL DEFAULT 'running' CHECK (
state IN ('running', 'paused', 'completed', 'aborted')
)
);
CREATE TABLE {schema}.run_executions (
execution_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
run_id UUID REFERENCES {schema}.runs(run_id) ON DELETE CASCADE,
started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
ended_at TIMESTAMP WITH TIME ZONE,
end_reason TEXT CHECK (
end_reason IN (NULL, 'paused', 'completed', 'aborted')
)
);
INSERT INTO {schema}.runs (key, run_id, state)
VALUES ('global', '{run_id}', 'paused');
INSERT INTO {schema}.run_executions (run_id, end_reason)
VALUES ('{run_id}', 'paused');"
};
client
.batch_execute(&stmt)
.await
.expect("build the unversioned footprint tables");
}
async fn set_recorded_version(client: PsqlClient<'_>, schema: &str, version: &str) {
let stmt =
format!("UPDATE {schema}._footprint_version SET version = '{version}' WHERE id = 'runs';");
let _num_rows = client
.execute(&stmt, &[])
.await
.expect("record another version");
}
async fn row_count(client: PsqlClient<'_>, schema: &str, table: &str) -> i64 {
let stmt = format!("SELECT COUNT(*) FROM {schema}.{table};");
let rows = client.query(&stmt, &[]).await.expect("count the rows");
rows.first().expect("a count comes back").get(0)
}
#[tokio::test]
async fn footprint_tables_are_rebuilt_when_they_predate_versioning() {
let Some(psql) = psql_backend(REBUILD_SCHEMA) else {
eprintln!("skipping: POSTGRES_URI is not set, so there is no Postgres to build tables in");
return;
};
let conn = psql.scheduler_conn().await.expect("scheduler conn");
let client = conn.as_client();
drop_schema(client, REBUILD_SCHEMA).await;
client.init_schema().await.expect("init_schema");
let run_id = Uuid::new_v4();
init_unversioned_footprint(client, REBUILD_SCHEMA, run_id).await;
assert_eq!(row_count(client, REBUILD_SCHEMA, "runs").await, 1);
assert_eq!(row_count(client, REBUILD_SCHEMA, "run_executions").await, 1);
let _ = client.init_footprint().await.expect("init_footprint");
assert_eq!(row_count(client, REBUILD_SCHEMA, "run_executions").await, 0);
assert_eq!(
client
.get_footprint()
.await
.expect("get_footprint after rebuild")
.expect("the run is still recorded")
.metadata,
RunMetadata::new(run_id, RunState::Aborted)
);
let footprint = RunFootprint::new(Uuid::new_v4(), RunState::Stopped);
client
.upsert_run(&footprint)
.await
.expect("record a graceful stop");
assert_eq!(
client
.get_footprint()
.await
.expect("get_footprint after the stop")
.expect("a footprint was recorded")
.metadata
.state,
RunState::Stopped
);
}
#[tokio::test]
async fn footprint_tables_are_rebuilt_when_the_recorded_version_differs() {
let Some(psql) = psql_backend(BUMPED_VERSION_SCHEMA) else {
eprintln!("skipping: POSTGRES_URI is not set, so there is no Postgres to build tables in");
return;
};
let conn = psql.scheduler_conn().await.expect("scheduler conn");
let client = conn.as_client();
drop_schema(client, BUMPED_VERSION_SCHEMA).await;
client.init_schema().await.expect("init_schema");
let _ = client.init_footprint().await.expect("first init_footprint");
let footprint = RunFootprint::new(Uuid::new_v4(), RunState::Stopped);
client.upsert_run(&footprint).await.expect("record a run");
client
.put_execution(footprint.metadata.run_id, Uuid::new_v4())
.await
.expect("record an execution");
let recorded = client
.get_footprint()
.await
.expect("get_footprint before the rebuild")
.expect("the run is recorded");
set_recorded_version(client, BUMPED_VERSION_SCHEMA, "0").await;
let _ = client
.init_footprint()
.await
.expect("second init_footprint");
let rebuilt = client
.get_footprint()
.await
.expect("get_footprint after rebuild")
.expect("the run is still recorded");
assert_eq!(
rebuilt.metadata,
RunMetadata::new(footprint.metadata.run_id, RunState::Aborted)
);
assert_eq!(rebuilt.at, recorded.at);
assert_eq!(
row_count(client, BUMPED_VERSION_SCHEMA, "run_executions").await,
0
);
}
#[tokio::test]
async fn footprint_tables_survive_an_init_at_the_same_version() {
let Some(psql) = psql_backend(SAME_VERSION_SCHEMA) else {
eprintln!("skipping: POSTGRES_URI is not set, so there is no Postgres to build tables in");
return;
};
let conn = psql.scheduler_conn().await.expect("scheduler conn");
let client = conn.as_client();
drop_schema(client, SAME_VERSION_SCHEMA).await;
client.init_schema().await.expect("init_schema");
let _ = client.init_footprint().await.expect("first init_footprint");
let footprint = RunFootprint::new(Uuid::new_v4(), RunState::Stopped);
client.upsert_run(&footprint).await.expect("record a run");
client
.put_execution(footprint.metadata.run_id, Uuid::new_v4())
.await
.expect("record an execution");
let _ = client
.init_footprint()
.await
.expect("second init_footprint");
assert_eq!(
client
.get_footprint()
.await
.expect("get_footprint after the second init")
.expect("the run is still recorded")
.metadata,
footprint.metadata
);
assert_eq!(
row_count(client, SAME_VERSION_SCHEMA, "run_executions").await,
1
);
}
#[tokio::test]
async fn footprint_tables_are_rebuilt_when_only_some_of_them_stand() {
let Some(psql) = psql_backend(PARTIAL_SCHEMA) else {
eprintln!("skipping: POSTGRES_URI is not set, so there is no Postgres to build tables in");
return;
};
let conn = psql.scheduler_conn().await.expect("scheduler conn");
let client = conn.as_client();
drop_schema(client, PARTIAL_SCHEMA).await;
client.init_schema().await.expect("init_schema");
let _ = client.init_footprint().await.expect("first init_footprint");
let footprint = RunFootprint::new(Uuid::new_v4(), RunState::Stopped);
client.upsert_run(&footprint).await.expect("record a run");
let stmt = format!("DROP TABLE {PARTIAL_SCHEMA}.run_executions;");
let _num_rows = client
.execute(&stmt, &[])
.await
.expect("drop run_executions");
assert_eq!(
client
.init_footprint()
.await
.expect("second init_footprint"),
TableShape::STALE
);
assert_eq!(
client
.get_footprint()
.await
.expect("get_footprint after rebuild")
.expect("the run is still recorded")
.metadata,
RunMetadata::new(footprint.metadata.run_id, RunState::Aborted)
);
let run_id = Uuid::new_v4();
client
.upsert_run(&RunFootprint::new(run_id, RunState::Running))
.await
.expect("record a run after the rebuild");
client
.put_execution(run_id, Uuid::new_v4())
.await
.expect("record an execution after the rebuild");
assert_eq!(row_count(client, PARTIAL_SCHEMA, "run_executions").await, 1);
}