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 checksum-drift repair, which reconciles rows
6//! that already exist: here, the operator asserts the migration is already
7//! applied; the service only computes the current checksum and writes the
8//! tracking row.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use super::MigrationService;
14use systemprompt_extension::{Extension, LoaderError};
15
16#[derive(Debug, Clone)]
17pub struct MarkAppliedOutcome {
18    pub extension_id: String,
19    pub version: u32,
20    pub name: String,
21    pub checksum: String,
22}
23
24impl MigrationService<'_> {
25    pub async fn mark_applied(
26        &self,
27        extension: &dyn Extension,
28        version: u32,
29    ) -> Result<MarkAppliedOutcome, LoaderError> {
30        let ext_id = extension.metadata().id;
31
32        let migration = extension
33            .migrations()
34            .into_iter()
35            .find(|m| m.version == version)
36            .ok_or_else(|| LoaderError::MigrationFailed {
37                extension: ext_id.to_owned(),
38                message: format!(
39                    "Migration version {version} is not defined for extension '{ext_id}'"
40                ),
41            })?;
42
43        self.ensure_migrations_table_exists().await?;
44
45        let applied = self.get_applied_migrations(ext_id).await?;
46        if applied.iter().any(|m| m.version == version) {
47            return Err(LoaderError::MigrationFailed {
48                extension: ext_id.to_owned(),
49                message: format!(
50                    "Migration {version} ('{}') is already tracked as applied for extension \
51                     '{ext_id}'; nothing to do",
52                    migration.name
53                ),
54            });
55        }
56
57        let id = format!("{ext_id}_{:03}", migration.version);
58        let checksum = migration.checksum();
59
60        self.db
61            .execute(
62                &"INSERT INTO extension_migrations (id, extension_id, version, name, checksum) \
63                  VALUES ($1, $2, $3, $4, $5)",
64                &[&id, &ext_id, &migration.version, &migration.name, &checksum],
65            )
66            .await
67            .map_err(|e| LoaderError::MigrationFailed {
68                extension: ext_id.to_owned(),
69                message: format!("Failed to record migration as applied: {e}"),
70            })?;
71
72        Ok(MarkAppliedOutcome {
73            extension_id: ext_id.to_owned(),
74            version: migration.version,
75            name: migration.name.clone(),
76            checksum,
77        })
78    }
79}