use crate::config::types::TrackingTable;
use crate::migration::section_parser::MigrationSection;
use crate::migration_tracking::section_tracking::SectionStatus;
use crate::migration_tracking::{format_tracking_table_name, version_from_db, version_to_db};
use crate::modules::{Subscription, SubscriptionSource};
use anyhow::{Context, Result};
use sqlx::PgPool;
use std::str::FromStr;
#[derive(Clone)]
pub struct TrackingStore {
pool: PgPool,
main: String,
sections: String,
modules: String,
}
impl TrackingStore {
pub fn new(pool: &PgPool, tracking_table: &TrackingTable) -> Result<Self> {
let main = format_tracking_table_name(tracking_table)?;
let suffixed = |suffix: &str| {
format!(
r#""{}"."{}_{}""#,
tracking_table.schema, tracking_table.name, suffix
)
};
Ok(Self {
pool: pool.clone(),
main,
sections: suffixed("sections"),
modules: suffixed("modules"),
})
}
pub fn main_table(&self) -> &str {
&self.main
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
pub fn sections_table(&self) -> &str {
&self.sections
}
fn covered_predicate(col: &str) -> String {
format!("{col} IN ('completed', 'satisfied')")
}
pub async fn target_is_established(&self) -> Result<bool> {
let covered = |alias: &str| Self::covered_predicate(alias);
let established: bool = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT EXISTS(SELECT 1 FROM {sections} WHERE {cov} AND NOT is_baseline)
OR EXISTS(SELECT 1 FROM {sections} s1
WHERE s1.is_baseline AND {cov_s1}
AND NOT EXISTS(SELECT 1 FROM {sections} s2
WHERE s2.is_baseline
AND s2.migration_version = s1.migration_version
AND NOT ({cov_s2})))",
sections = self.sections,
cov = covered("status"),
cov_s1 = covered("s1.status"),
cov_s2 = covered("s2.status"),
)))
.fetch_one(&self.pool)
.await?;
Ok(established)
}
pub async fn main_table_is_empty(&self) -> Result<bool> {
let has_row: bool = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT EXISTS(SELECT 1 FROM {})",
self.main
)))
.fetch_one(&self.pool)
.await
.context("Failed to check whether the tracking table is empty")?;
Ok(!has_row)
}
pub async fn baseline_stored_checksum(&self, version: u64) -> Result<Option<String>> {
let checksum: Option<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT checksum FROM {} WHERE version = $1 AND is_baseline = TRUE",
self.main
)))
.bind(version_to_db(version)?)
.fetch_optional(&self.pool)
.await?;
Ok(checksum)
}
pub async fn applied_baseline_watermark(&self) -> Result<Option<u64>> {
let watermark: Option<i64> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT MAX(m.version) FROM {main} m
WHERE m.is_baseline
AND EXISTS (
SELECT 1 FROM {sections} s
WHERE s.is_baseline AND s.migration_version = m.version)
AND NOT EXISTS (
SELECT 1 FROM {sections} s
WHERE s.is_baseline AND s.migration_version = m.version
AND NOT ({cov}))",
main = self.main,
sections = self.sections,
cov = Self::covered_predicate("s.status"),
)))
.fetch_one(&self.pool)
.await?;
Ok(watermark.map(version_from_db))
}
pub async fn established_module_literals(&self) -> Result<std::collections::BTreeSet<String>> {
let modules: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT DISTINCT module FROM {} WHERE {} AND module IS NOT NULL",
self.sections,
Self::covered_predicate("status"),
)))
.fetch_all(&self.pool)
.await?;
Ok(modules.into_iter().collect())
}
pub async fn covered_baseline_section_names(
&self,
version: u64,
) -> Result<std::collections::BTreeSet<String>> {
let names: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT section_name FROM {sections}
WHERE is_baseline AND migration_version = $1 AND {cov}",
sections = self.sections,
cov = Self::covered_predicate("status"),
)))
.bind(version_to_db(version)?)
.fetch_all(&self.pool)
.await?;
Ok(names.into_iter().collect())
}
pub async fn completed_migration_sections(
&self,
) -> Result<std::collections::BTreeSet<(u64, String)>> {
let rows: Vec<(i64, String)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
"SELECT migration_version, section_name FROM {} \
WHERE NOT is_baseline AND status = 'completed'",
self.sections,
)))
.fetch_all(&self.pool)
.await?;
Ok(rows.into_iter().map(|(v, n)| (v as u64, n)).collect())
}
pub async fn migration_listing(&self) -> Result<Vec<(i64, String, String, i64, bool, bool)>> {
let rows = sqlx::query_as(sqlx::AssertSqlSafe(format!(
"SELECT m.version, m.description, m.applied_at::TEXT,
COUNT(s.section_name) FILTER (WHERE NOT ({cov})) AS incomplete,
m.is_baseline,
(m.is_baseline AND COUNT(s.section_name) = 0) AS consumed_re_anchor
FROM {main} m
LEFT JOIN {sections} s
ON s.migration_version = m.version AND s.is_baseline = m.is_baseline
GROUP BY m.version, m.is_baseline, m.description, m.applied_at
ORDER BY m.version",
main = self.main,
sections = self.sections,
cov = Self::covered_predicate("s.status"),
)))
.fetch_all(&self.pool)
.await?;
Ok(rows)
}
pub async fn migration_listing_legacy(
&self,
) -> Result<Vec<(i64, String, String, i64, bool, bool)>> {
let rows: Vec<(i64, String, String, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
"SELECT version, description, applied_at::TEXT, is_baseline FROM {} ORDER BY version",
self.main
)))
.fetch_all(&self.pool)
.await?;
Ok(rows
.into_iter()
.map(|(v, d, a, b)| (v, d, a, 0, b, false))
.collect())
}
pub async fn section_module_statuses(
&self,
) -> Result<Vec<(Option<String>, bool, SectionStatus)>> {
let rows: Vec<(Option<String>, bool, String)> = sqlx::query_as(sqlx::AssertSqlSafe(
format!("SELECT module, is_baseline, status FROM {}", self.sections),
))
.fetch_all(&self.pool)
.await?;
rows.into_iter()
.map(|(module, is_baseline, status)| {
Ok((module, is_baseline, SectionStatus::from_str(&status)?))
})
.collect()
}
pub async fn incomplete_baseline_sections(&self) -> Result<Vec<(u64, String, Option<String>)>> {
let rows: Vec<(i64, String, Option<String>)> =
sqlx::query_as(sqlx::AssertSqlSafe(format!(
"SELECT migration_version, section_name, module FROM {} \
WHERE is_baseline AND NOT ({})",
self.sections,
Self::covered_predicate("status"),
)))
.fetch_all(&self.pool)
.await?;
Ok(rows
.into_iter()
.map(|(v, name, module)| (v as u64, name, module))
.collect())
}
pub async fn ensure_subscription_tables(&self) -> Result<()> {
sqlx::query(sqlx::AssertSqlSafe(format!(
r#"CREATE TABLE IF NOT EXISTS {modules} (
module TEXT PRIMARY KEY,
adopted_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
adopted_by TEXT NOT NULL DEFAULT CURRENT_USER,
source TEXT NOT NULL
)"#,
modules = self.modules
)))
.execute(&self.pool)
.await
.with_context(|| format!("Failed to create subscription table {}", self.modules))?;
Ok(())
}
pub async fn consumed_through_cursor(&self) -> Result<Option<u64>> {
let cursor: Option<i64> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT MAX(version) FROM {} WHERE is_baseline = TRUE",
self.main
)))
.fetch_one(&self.pool)
.await
.context("Failed to read the consumed-through cursor")?;
Ok(cursor.map(version_from_db))
}
pub async fn record_crossing_consumption<'e>(
&self,
executor: impl sqlx::PgExecutor<'e>,
version: u64,
checksum: &str,
) -> Result<()> {
let stored: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"INSERT INTO {main} (version, description, checksum, is_baseline)
VALUES ($1, 'baseline', $2, TRUE)
ON CONFLICT (version, is_baseline)
DO UPDATE SET checksum = {main}.checksum
RETURNING checksum",
main = self.main
)))
.bind(version_to_db(version)?)
.bind(checksum)
.fetch_one(executor)
.await
.with_context(|| format!("Failed to record consumption of re-anchor {version}"))?;
if stored != checksum {
anyhow::bail!(
"re-anchor {version} was edited after it was consumed here.\n\
Recorded checksum: {stored}\n\
Current file: {checksum}\n\n\
A consumed re-anchor is immutable — its remap instructions are pinned so \
every target that crossed it agrees on what moved. Restore \
baseline_{version}.sql to the consumed content, or reset this target and \
re-provision from the current baseline."
);
}
Ok(())
}
pub async fn record_sections_satisfied(
&self,
conn: &mut sqlx::PgConnection,
version: u64,
is_baseline: bool,
sections: &[(i32, MigrationSection)],
) -> Result<()> {
for (order, section) in sections {
crate::migration_tracking::section_tracking::insert_satisfied_section(
&mut *conn,
&self.sections,
version,
is_baseline,
*order,
section,
)
.await?;
}
Ok(())
}
pub async fn subscription_tables_exist(&self) -> Result<bool> {
let exists: bool = sqlx::query_scalar("SELECT to_regclass($1) IS NOT NULL")
.bind(&self.modules)
.fetch_one(&self.pool)
.await?;
Ok(exists)
}
pub async fn load_subscription(&self) -> Result<Subscription> {
let modules: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT module FROM {}",
self.modules
)))
.fetch_all(&self.pool)
.await
.context("Failed to read the module subscription")?;
Ok(Subscription {
modules: modules.into_iter().collect(),
})
}
pub async fn add_module<'e>(
&self,
executor: impl sqlx::PgExecutor<'e>,
module: &str,
source: &SubscriptionSource,
) -> Result<()> {
sqlx::query(sqlx::AssertSqlSafe(format!(
"INSERT INTO {} (module, source) VALUES ($1, $2)
ON CONFLICT (module) DO NOTHING",
self.modules
)))
.bind(module)
.bind(source.as_db_string())
.execute(executor)
.await
.with_context(|| format!("Failed to subscribe module '{module}'"))?;
Ok(())
}
pub async fn remove_module<'e>(
&self,
executor: impl sqlx::PgExecutor<'e>,
module: &str,
) -> Result<()> {
sqlx::query(sqlx::AssertSqlSafe(format!(
"DELETE FROM {} WHERE module = $1",
self.modules
)))
.bind(module)
.execute(executor)
.await
.with_context(|| format!("Failed to unsubscribe module '{module}'"))?;
Ok(())
}
}