systemprompt_database/lifecycle/migrations/
status.rs1use super::MigrationService;
7use std::collections::HashSet;
8use systemprompt_extension::{Extension, LoaderError, Migration};
9use systemprompt_identifiers::ExtensionId;
10
11#[derive(Debug, Clone)]
13pub struct AppliedMigration {
14 pub extension_id: ExtensionId,
15 pub version: u32,
16 pub name: String,
17 pub checksum: String,
18 pub applied_at: Option<String>,
19}
20
21#[derive(Debug, Clone)]
22pub struct PendingMigration {
23 pub extension_id: ExtensionId,
24 pub version: u32,
25 pub name: String,
26 pub sql: &'static str,
27 pub checksum: String,
28 pub no_tx: bool,
29 pub breaks_older: bool,
30}
31
32#[derive(Debug, Clone)]
37pub struct OrphanedMigration {
38 pub extension_id: ExtensionId,
39 pub version: u32,
40 pub name: String,
41}
42
43#[derive(Debug, Clone)]
45pub struct TombstonedSlot {
46 pub extension_id: ExtensionId,
47 pub version: u32,
48 pub name: String,
49 pub tracked: bool,
50}
51
52#[derive(Debug, Clone)]
59pub struct SlotCollision {
60 pub extension_id: ExtensionId,
61 pub version: u32,
62 pub stored_name: String,
63 pub current_name: String,
64}
65
66#[derive(Debug, Clone)]
67pub struct ChecksumDrift {
68 pub extension_id: ExtensionId,
69 pub version: u32,
70 pub name: String,
71 pub stored_checksum: String,
72 pub current_checksum: String,
73}
74
75#[derive(Debug, Clone)]
76pub struct ExtensionMigrationStatus {
77 pub extension_id: ExtensionId,
78 pub applied: Vec<AppliedMigration>,
79 pub pending: Vec<PendingMigration>,
80 pub drift: Vec<ChecksumDrift>,
81 pub slot_collisions: Vec<SlotCollision>,
82 pub orphaned: Vec<OrphanedMigration>,
83 pub tombstoned: Vec<TombstonedSlot>,
84 pub ahead: Vec<u32>,
85}
86
87#[derive(Debug, Default, Clone, Copy)]
88pub struct MigrationResult {
89 pub migrations_run: usize,
90 pub migrations_skipped: usize,
91}
92
93#[derive(Debug)]
94pub struct MigrationStatus {
95 pub extension_id: ExtensionId,
96 pub total_defined: usize,
97 pub total_applied: usize,
98 pub pending_count: usize,
99 pub pending: Vec<Migration>,
100 pub applied: Vec<AppliedMigration>,
101}
102
103impl MigrationService<'_> {
104 pub async fn plan_pending(
105 &self,
106 extension: &dyn Extension,
107 ) -> Result<Vec<PendingMigration>, LoaderError> {
108 let ext_id = &ExtensionId::new(extension.metadata().id);
109 let defined = extension.migrations();
110
111 if defined.is_empty() {
112 return Ok(Vec::new());
113 }
114
115 self.ensure_migrations_table_exists().await?;
116 let applied_versions: HashSet<u32> = self
117 .get_applied_migrations(ext_id)
118 .await?
119 .into_iter()
120 .map(|m| m.version)
121 .collect();
122
123 Ok(defined
124 .into_iter()
125 .filter(|m| !m.tombstone && !applied_versions.contains(&m.version))
126 .map(|m| PendingMigration {
127 extension_id: ext_id.clone(),
128 version: m.version,
129 name: m.name.clone(),
130 sql: m.sql,
131 checksum: m.checksum(),
132 no_tx: m.no_transaction,
133 breaks_older: m.breaks_older(),
134 })
135 .collect())
136 }
137
138 pub async fn status(
139 &self,
140 extension: &dyn Extension,
141 ) -> Result<ExtensionMigrationStatus, LoaderError> {
142 let ext_id = &ExtensionId::new(extension.metadata().id);
143 let defined = extension.migrations();
144
145 self.ensure_migrations_table_exists().await?;
146 let applied = self.get_applied_migrations(ext_id).await?;
147
148 let applied_versions: HashSet<u32> = applied.iter().map(|m| m.version).collect();
149 let applied_rows: std::collections::HashMap<u32, &AppliedMigration> =
150 applied.iter().map(|m| (m.version, m)).collect();
151
152 let mut slots = SlotClassification::default();
153 for m in &defined {
154 let row = applied_rows.get(&m.version).copied();
155 slots.classify(ext_id, m, row, &applied_versions);
156 }
157 let SlotClassification {
158 pending,
159 drift,
160 slot_collisions,
161 tombstoned,
162 } = slots;
163
164 let ahead = super::ahead_versions(&applied, &defined);
165 let orphaned = super::orphaned_versions(&applied, &defined)
166 .into_iter()
167 .map(|version| OrphanedMigration {
168 extension_id: ext_id.clone(),
169 version,
170 name: applied
171 .iter()
172 .find(|m| m.version == version)
173 .map_or_else(String::new, |m| m.name.clone()),
174 })
175 .collect();
176
177 Ok(ExtensionMigrationStatus {
178 extension_id: ext_id.clone(),
179 applied,
180 pending,
181 drift,
182 slot_collisions,
183 orphaned,
184 tombstoned,
185 ahead,
186 })
187 }
188
189 pub async fn get_migration_status(
190 &self,
191 extension: &dyn Extension,
192 ) -> Result<MigrationStatus, LoaderError> {
193 self.ensure_migrations_table_exists().await?;
194
195 let ext_id = &ExtensionId::new(extension.metadata().id);
196 let defined_migrations = extension.migrations();
197 let applied = self.get_applied_migrations(ext_id).await?;
198
199 let applied_versions: HashSet<u32> = applied.iter().map(|m| m.version).collect();
200
201 let pending: Vec<_> = defined_migrations
202 .iter()
203 .filter(|m| !m.tombstone && !applied_versions.contains(&m.version))
204 .cloned()
205 .collect();
206
207 Ok(MigrationStatus {
208 extension_id: ext_id.clone(),
209 total_defined: defined_migrations.len(),
210 total_applied: applied.len(),
211 pending_count: pending.len(),
212 pending,
213 applied,
214 })
215 }
216}
217
218#[derive(Default)]
219struct SlotClassification {
220 pending: Vec<PendingMigration>,
221 drift: Vec<ChecksumDrift>,
222 slot_collisions: Vec<SlotCollision>,
223 tombstoned: Vec<TombstonedSlot>,
224}
225
226impl SlotClassification {
227 fn classify(
228 &mut self,
229 ext_id: &ExtensionId,
230 m: &Migration,
231 row: Option<&AppliedMigration>,
232 applied_versions: &HashSet<u32>,
233 ) {
234 if m.tombstone {
235 self.tombstoned.push(TombstonedSlot {
236 extension_id: ext_id.clone(),
237 version: m.version,
238 name: m.name.clone(),
239 tracked: applied_versions.contains(&m.version),
240 });
241 return;
242 }
243 let current_checksum = m.checksum();
244 let Some(row) = row else {
245 self.pending.push(PendingMigration {
246 extension_id: ext_id.clone(),
247 version: m.version,
248 name: m.name.clone(),
249 sql: m.sql,
250 checksum: current_checksum,
251 no_tx: m.no_transaction,
252 breaks_older: m.breaks_older(),
253 });
254 return;
255 };
256 if row.name != m.name {
257 self.slot_collisions.push(SlotCollision {
258 extension_id: ext_id.clone(),
259 version: m.version,
260 stored_name: row.name.clone(),
261 current_name: m.name.clone(),
262 });
263 } else if !super::checksum_transition::matches_checksum(m, &row.checksum) {
264 self.drift.push(ChecksumDrift {
265 extension_id: ext_id.clone(),
266 version: m.version,
267 name: m.name.clone(),
268 stored_checksum: row.checksum.clone(),
269 current_checksum,
270 });
271 }
272 }
273}