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