systemprompt_database/lifecycle/migrations/
repair.rs1use super::exec::{TrackingWrite, check_cross_extension_alters, execute_statements_transactional};
25use super::{ChecksumDrift, ExtensionMigrationStatus, MigrationService};
26use crate::lifecycle::installation::BootstrapLockGuard;
27use crate::services::SqlExecutor;
28use systemprompt_extension::{Extension, LoaderError, Migration};
29use systemprompt_identifiers::ToDbValue;
30
31const UPDATE_CHECKSUM_SQL: &str =
32 "UPDATE extension_migrations SET checksum = $3 WHERE extension_id = $1 AND version = $2";
33
34#[derive(Debug, Default, Clone)]
35pub struct RepairResult {
36 pub repaired: Vec<ChecksumDrift>,
37 pub reapplied: usize,
38 pub migrations_run: usize,
39}
40
41impl MigrationService<'_> {
42 pub async fn repair_drift(
43 &self,
44 extension: &dyn Extension,
45 ) -> Result<RepairResult, LoaderError> {
46 let status = self.status(extension).await?;
47 Self::refuse_slot_collisions(&status)?;
48
49 if status.drift.is_empty() {
50 return Ok(RepairResult::default());
51 }
52
53 let reapplied = status.drift.len();
54 let guard = BootstrapLockGuard::acquire(self.db).await?;
55 let outcome = self.reapply_drifted(extension, &status.drift).await;
56 let pending = match outcome {
57 Ok(()) => self.run_pending_migrations(extension).await,
58 Err(e) => Err(e),
59 };
60 guard.release().await;
61 let result = pending?;
62
63 Ok(RepairResult {
64 repaired: status.drift,
65 reapplied,
66 migrations_run: result.migrations_run,
67 })
68 }
69
70 pub async fn reconcile_drift(
71 &self,
72 extension: &dyn Extension,
73 ) -> Result<RepairResult, LoaderError> {
74 let status = self.status(extension).await?;
75 Self::refuse_slot_collisions(&status)?;
76
77 if status.drift.is_empty() {
78 return Ok(RepairResult::default());
79 }
80
81 let guard = BootstrapLockGuard::acquire(self.db).await?;
82 let mut outcome = Ok(());
83 for drift in &status.drift {
84 if let Err(e) = self.rewrite_checksum(drift).await {
85 outcome = Err(e);
86 break;
87 }
88 }
89 guard.release().await;
90 outcome?;
91
92 Ok(RepairResult {
93 repaired: status.drift,
94 reapplied: 0,
95 migrations_run: 0,
96 })
97 }
98
99 pub fn refuse_slot_collisions(status: &ExtensionMigrationStatus) -> Result<(), LoaderError> {
100 let Some(collision) = status.slot_collisions.first() else {
101 return Ok(());
102 };
103 Err(LoaderError::MigrationSlotReused {
104 extension: collision.extension_id.clone(),
105 version: collision.version,
106 stored_name: collision.stored_name.clone(),
107 current_name: collision.current_name.clone(),
108 })
109 }
110
111 async fn rewrite_checksum(&self, drift: &ChecksumDrift) -> Result<(), LoaderError> {
112 self.db
113 .execute(
114 &UPDATE_CHECKSUM_SQL,
115 &[&drift.extension_id, &drift.version, &drift.current_checksum],
116 )
117 .await
118 .map_err(|e| LoaderError::MigrationFailed {
119 extension: drift.extension_id.clone(),
120 message: format!(
121 "Failed to rewrite checksum for migration {} ('{}'): {e}",
122 drift.version, drift.name
123 ),
124 })?;
125 Ok(())
126 }
127
128 async fn reapply_drifted(
129 &self,
130 extension: &dyn Extension,
131 drift: &[ChecksumDrift],
132 ) -> Result<(), LoaderError> {
133 let ext_id = extension.metadata().id;
134 let migrations = extension.migrations();
135
136 for d in drift {
137 let migration = migrations
138 .iter()
139 .find(|m| m.version == d.version)
140 .ok_or_else(|| LoaderError::MigrationFailed {
141 extension: ext_id.to_owned(),
142 message: format!(
143 "Drifted migration {} ('{}') is no longer declared by extension \
144 '{ext_id}'",
145 d.version, d.name
146 ),
147 })?;
148 self.reapply_one(extension, migration, d).await?;
149 }
150
151 Ok(())
152 }
153
154 async fn reapply_one(
155 &self,
156 extension: &dyn Extension,
157 migration: &Migration,
158 drift: &ChecksumDrift,
159 ) -> Result<(), LoaderError> {
160 let ext_id = extension.metadata().id;
161
162 check_cross_extension_alters(extension, migration)?;
163
164 tracing::info!(
165 extension = %ext_id,
166 version = migration.version,
167 name = %migration.name,
168 no_transaction = migration.no_transaction,
169 "Re-applying drifted migration"
170 );
171
172 let update_params: [&dyn ToDbValue; 3] =
173 [&drift.extension_id, &drift.version, &drift.current_checksum];
174
175 if migration.no_transaction {
176 SqlExecutor::execute_statements_parsed(self.db, migration.sql)
177 .await
178 .map_err(|e| LoaderError::MigrationFailed {
179 extension: ext_id.to_owned(),
180 message: format!(
181 "Failed to re-apply drifted migration {} ({}): {e}",
182 migration.version, migration.name
183 ),
184 })?;
185 self.rewrite_checksum(drift).await
186 } else {
187 let statements = SqlExecutor::parse_sql_statements(migration.sql).map_err(|e| {
188 LoaderError::MigrationFailed {
189 extension: ext_id.to_owned(),
190 message: format!(
191 "Failed to parse migration {} ({}): {e}",
192 migration.version, migration.name
193 ),
194 }
195 })?;
196 execute_statements_transactional(
197 self.db,
198 &statements,
199 ext_id,
200 migration,
201 Some(TrackingWrite {
202 sql: UPDATE_CHECKSUM_SQL,
203 params: &update_params,
204 }),
205 )
206 .await
207 }
208 }
209}