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