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