Skip to main content

systemprompt_database/lifecycle/migrations/
mark_applied.rs

1//! Record an extension migration as applied without running its SQL.
2//!
3//! Recovers the partial-state case where a migration's schema changes are
4//! present in the database but no row exists in `extension_migrations` to
5//! track them. Distinct from [`super::repair::repair_drift`], which deletes
6//! and re-applies migrations whose stored checksum has drifted: that path
7//! requires the migration to be idempotent and re-executable. Here, the
8//! operator asserts the migration is already applied; the service only
9//! computes the current checksum and writes the tracking row.
10
11use super::MigrationService;
12use systemprompt_extension::{Extension, LoaderError};
13
14#[derive(Debug, Clone)]
15pub struct MarkAppliedOutcome {
16    pub extension_id: String,
17    pub version: u32,
18    pub name: String,
19    pub checksum: String,
20}
21
22impl MigrationService<'_> {
23    pub async fn mark_applied(
24        &self,
25        extension: &dyn Extension,
26        version: u32,
27    ) -> Result<MarkAppliedOutcome, LoaderError> {
28        let ext_id = extension.metadata().id;
29
30        let migration = extension
31            .migrations()
32            .into_iter()
33            .find(|m| m.version == version)
34            .ok_or_else(|| LoaderError::MigrationFailed {
35                extension: ext_id.to_owned(),
36                message: format!(
37                    "Migration version {version} is not defined for extension '{ext_id}'"
38                ),
39            })?;
40
41        self.ensure_migrations_table_exists().await?;
42
43        let applied = self.get_applied_migrations(ext_id).await?;
44        if applied.iter().any(|m| m.version == version) {
45            return Err(LoaderError::MigrationFailed {
46                extension: ext_id.to_owned(),
47                message: format!(
48                    "Migration {version} ('{}') is already tracked as applied for extension \
49                     '{ext_id}'; nothing to do",
50                    migration.name
51                ),
52            });
53        }
54
55        let id = format!("{ext_id}_{:03}", migration.version);
56        let checksum = migration.checksum();
57
58        self.db
59            .execute(
60                &"INSERT INTO extension_migrations (id, extension_id, version, name, checksum) \
61                  VALUES ($1, $2, $3, $4, $5)",
62                &[&id, &ext_id, &migration.version, &migration.name, &checksum],
63            )
64            .await
65            .map_err(|e| LoaderError::MigrationFailed {
66                extension: ext_id.to_owned(),
67                message: format!("Failed to record migration as applied: {e}"),
68            })?;
69
70        Ok(MarkAppliedOutcome {
71            extension_id: ext_id.to_owned(),
72            version: migration.version,
73            name: migration.name.clone(),
74            checksum,
75        })
76    }
77}