use super::exec::{TrackingWrite, check_cross_extension_alters, execute_statements_transactional};
use super::{ChecksumDrift, MigrationService};
use crate::lifecycle::installation::BootstrapLockGuard;
use crate::services::SqlExecutor;
use systemprompt_extension::{Extension, LoaderError, Migration};
use systemprompt_identifiers::ToDbValue;
const UPDATE_CHECKSUM_SQL: &str =
"UPDATE extension_migrations SET checksum = $3 WHERE extension_id = $1 AND version = $2";
#[derive(Debug, Default, Clone)]
pub struct RepairResult {
pub repaired: Vec<ChecksumDrift>,
pub migrations_run: usize,
}
impl MigrationService<'_> {
pub async fn repair_drift(
&self,
extension: &dyn Extension,
) -> Result<RepairResult, LoaderError> {
let status = self.status(extension).await?;
if status.drift.is_empty() {
return Ok(RepairResult::default());
}
let guard = BootstrapLockGuard::acquire(self.db).await?;
let outcome = self.reapply_drifted(extension, &status.drift).await;
let pending = match outcome {
Ok(()) => self.run_pending_migrations(extension).await,
Err(e) => Err(e),
};
guard.release().await;
let result = pending?;
Ok(RepairResult {
repaired: status.drift,
migrations_run: result.migrations_run,
})
}
pub async fn reconcile_drift(
&self,
extension: &dyn Extension,
) -> Result<RepairResult, LoaderError> {
let status = self.status(extension).await?;
if status.drift.is_empty() {
return Ok(RepairResult::default());
}
let guard = BootstrapLockGuard::acquire(self.db).await?;
let mut outcome = Ok(());
for drift in &status.drift {
if let Err(e) = self.rewrite_checksum(drift).await {
outcome = Err(e);
break;
}
}
guard.release().await;
outcome?;
Ok(RepairResult {
repaired: status.drift,
migrations_run: 0,
})
}
async fn rewrite_checksum(&self, drift: &ChecksumDrift) -> Result<(), LoaderError> {
self.db
.execute(
&UPDATE_CHECKSUM_SQL,
&[&drift.extension_id, &drift.version, &drift.current_checksum],
)
.await
.map_err(|e| LoaderError::MigrationFailed {
extension: drift.extension_id.clone(),
message: format!(
"Failed to rewrite checksum for migration {} ('{}'): {e}",
drift.version, drift.name
),
})?;
Ok(())
}
async fn reapply_drifted(
&self,
extension: &dyn Extension,
drift: &[ChecksumDrift],
) -> Result<(), LoaderError> {
let ext_id = extension.metadata().id;
let migrations = extension.migrations();
for d in drift {
let migration = migrations
.iter()
.find(|m| m.version == d.version)
.ok_or_else(|| LoaderError::MigrationFailed {
extension: ext_id.to_owned(),
message: format!(
"Drifted migration {} ('{}') is no longer declared by extension \
'{ext_id}'",
d.version, d.name
),
})?;
self.reapply_one(extension, migration, d).await?;
}
Ok(())
}
async fn reapply_one(
&self,
extension: &dyn Extension,
migration: &Migration,
drift: &ChecksumDrift,
) -> Result<(), LoaderError> {
let ext_id = extension.metadata().id;
check_cross_extension_alters(extension, migration)?;
tracing::info!(
extension = %ext_id,
version = migration.version,
name = %migration.name,
no_transaction = migration.no_transaction,
"Re-applying drifted migration"
);
let update_params: [&dyn ToDbValue; 3] =
[&drift.extension_id, &drift.version, &drift.current_checksum];
if migration.no_transaction {
SqlExecutor::execute_statements_parsed(self.db, migration.sql)
.await
.map_err(|e| LoaderError::MigrationFailed {
extension: ext_id.to_owned(),
message: format!(
"Failed to re-apply drifted migration {} ({}): {e}",
migration.version, migration.name
),
})?;
self.rewrite_checksum(drift).await
} else {
let statements = SqlExecutor::parse_sql_statements(migration.sql).map_err(|e| {
LoaderError::MigrationFailed {
extension: ext_id.to_owned(),
message: format!(
"Failed to parse migration {} ({}): {e}",
migration.version, migration.name
),
}
})?;
execute_statements_transactional(
self.db,
&statements,
ext_id,
migration,
Some(TrackingWrite {
sql: UPDATE_CHECKSUM_SQL,
params: &update_params,
}),
)
.await
}
}
}