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