1use std::collections::HashSet;
9
10pub const SCHEMA_BASELINE_FORMAT_VERSION: u32 = 1;
16
17pub const DOWNGRADE_FLOOR: &str = "0.6.0";
19
20pub const MAINTENANCE_LEASE_MIGRATION_ID: &str = "m20260621_000002_create_maintenance_lease";
22
23pub const ACTIVE_CONFIG_MIGRATION_ID: &str = "m20260703_000001_add_sandbox_active_config";
25
26pub const SNAPSHOT_SCOPE_MIGRATION_ID: &str = "m20260714_000001_add_snapshot_scope";
28
29pub const SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID: &str =
31 "m20260723_000001_snapshot_artifact_transition";
32
33pub const CPU_ALLOCATION_MIGRATION_ID: &str = "m20260719_000001_create_cpu_allocations";
35
36pub const WRITEBACK_ALLOCATION_MIGRATION_ID: &str = "m20260803_000001_create_writeback_allocations";
38
39pub const MEMORY_ALLOCATION_NODES_MIGRATION_ID: &str =
41 "m20260808_000001_create_memory_allocation_nodes";
42
43pub const SANDBOX_LABEL_REBUILD_MIGRATION_ID: &str = "m20260810_000001_rebuild_sandbox_labels";
45
46pub const SHARED_CPU_ALLOCATION_MIGRATION_ID: &str = "m20260813_000001_share_cpu_allocations";
48pub const SANDBOX_NETWORK_SLOT_MIGRATION_ID: &str = "m20260818_000001_sandbox_network_slot";
50
51pub const MOUNT_OWNER_CONFIG_MIGRATION_ID: &str = "m20260824_000001_mount_owner_config";
53
54pub const SNAPSHOT_IDENTITY_MIGRATION_ID: &str = "m20260829_000001_split_snapshot_identity";
56
57pub const SNAPSHOT_GROUPS_MIGRATION_ID: &str = "m20260910_000001_snapshot_groups";
59
60pub const BASELINE_0_6_0_MIGRATIONS: &[&str] = &[
67 "m20260305_000001_create_image_tables",
68 "m20260305_000002_create_sandbox_tables",
69 "m20260305_000003_create_storage_tables",
70 "m20260305_000004_create_sandbox_images_table",
71 "m20260410_000001_erofs_image_schema",
72 "m20260501_000001_create_snapshot_index",
73 "m20260517_000001_drop_sandbox_metric",
74 "m20260527_000001_migrate_oci_rootfs_source",
75 "m20260531_000001_create_sandbox_labels",
76 "m20260531_000002_index_sandbox_labels_key_value",
77 "m20260606_000001_named_volume_kinds",
78 "m20260621_000001_add_sandbox_ephemeral",
79 MAINTENANCE_LEASE_MIGRATION_ID,
80];
81
82pub const MIGRATION_METADATA: &[MigrationMetadata] = &[
84 MigrationMetadata {
85 id: "m20260305_000001_create_image_tables",
86 reversible: true,
87 affects_cache: true,
88 affects_user_data: false,
89 summary: "remove legacy OCI image catalog tables",
90 },
91 MigrationMetadata {
92 id: "m20260305_000002_create_sandbox_tables",
93 reversible: true,
94 affects_cache: false,
95 affects_user_data: false,
96 summary: "remove sandbox and run tables",
97 },
98 MigrationMetadata {
99 id: "m20260305_000003_create_storage_tables",
100 reversible: true,
101 affects_cache: false,
102 affects_user_data: false,
103 summary: "remove volume and snapshot storage tables",
104 },
105 MigrationMetadata {
106 id: "m20260305_000004_create_sandbox_images_table",
107 reversible: true,
108 affects_cache: true,
109 affects_user_data: false,
110 summary: "remove sandbox image references",
111 },
112 MigrationMetadata {
113 id: "m20260410_000001_erofs_image_schema",
114 reversible: true,
115 affects_cache: true,
116 affects_user_data: false,
117 summary: "remove EROFS rootfs catalog tables",
118 },
119 MigrationMetadata {
120 id: "m20260501_000001_create_snapshot_index",
121 reversible: true,
122 affects_cache: false,
123 affects_user_data: false,
124 summary: "remove snapshot index table",
125 },
126 MigrationMetadata {
127 id: "m20260517_000001_drop_sandbox_metric",
128 reversible: false,
129 affects_cache: false,
130 affects_user_data: false,
131 summary: "restore legacy sandbox metrics table",
132 },
133 MigrationMetadata {
134 id: "m20260527_000001_migrate_oci_rootfs_source",
135 reversible: false,
136 affects_cache: false,
137 affects_user_data: false,
138 summary: "rewrite OCI rootfs config back to the legacy string shape",
139 },
140 MigrationMetadata {
141 id: "m20260531_000001_create_sandbox_labels",
142 reversible: true,
143 affects_cache: false,
144 affects_user_data: false,
145 summary: "remove sandbox labels table",
146 },
147 MigrationMetadata {
148 id: "m20260531_000002_index_sandbox_labels_key_value",
149 reversible: true,
150 affects_cache: false,
151 affects_user_data: false,
152 summary: "remove sandbox label key/value index",
153 },
154 MigrationMetadata {
155 id: "m20260606_000001_named_volume_kinds",
156 reversible: true,
157 affects_cache: false,
158 affects_user_data: false,
159 summary: "remove named volume kind columns and attachments",
160 },
161 MigrationMetadata {
162 id: "m20260621_000001_add_sandbox_ephemeral",
163 reversible: true,
164 affects_cache: false,
165 affects_user_data: false,
166 summary: "remove sandbox ephemeral flag",
167 },
168 MigrationMetadata {
169 id: MAINTENANCE_LEASE_MIGRATION_ID,
170 reversible: true,
171 affects_cache: false,
172 affects_user_data: false,
173 summary: "remove maintenance lease table",
174 },
175 MigrationMetadata {
176 id: ACTIVE_CONFIG_MIGRATION_ID,
177 reversible: true,
178 affects_cache: false,
179 affects_user_data: false,
180 summary: "remove active sandbox config snapshots",
181 },
182 MigrationMetadata {
183 id: "m20260708_000001_migrate_bind_rootfs_source",
184 reversible: true,
185 affects_cache: false,
186 affects_user_data: true,
187 summary: "rewrite bind rootfs config back to the legacy string shape",
188 },
189 MigrationMetadata {
190 id: "m20260710_000001_migrate_root_disk",
191 reversible: true,
192 affects_cache: false,
193 affects_user_data: true,
194 summary: "rewrite root disk config back to the upper size shape",
195 },
196 MigrationMetadata {
197 id: SNAPSHOT_SCOPE_MIGRATION_ID,
198 reversible: true,
199 affects_cache: false,
200 affects_user_data: false,
201 summary: "remove snapshot scope index metadata",
202 },
203 MigrationMetadata {
204 id: SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID,
205 reversible: true,
206 affects_cache: false,
207 affects_user_data: true,
208 summary: "reverse final snapshot descriptors before removing migration state",
209 },
210 MigrationMetadata {
211 id: CPU_ALLOCATION_MIGRATION_ID,
212 reversible: true,
213 affects_cache: false,
214 affects_user_data: false,
215 summary: "remove cooperative host CPU allocation tables",
216 },
217 MigrationMetadata {
218 id: WRITEBACK_ALLOCATION_MIGRATION_ID,
219 reversible: true,
220 affects_cache: false,
221 affects_user_data: false,
222 summary: "remove host-global writeback allocation state",
223 },
224 MigrationMetadata {
225 id: MEMORY_ALLOCATION_NODES_MIGRATION_ID,
226 reversible: true,
227 affects_cache: false,
228 affects_user_data: false,
229 summary: "remove cooperative NUMA memory allocation state",
230 },
231 MigrationMetadata {
232 id: SANDBOX_LABEL_REBUILD_MIGRATION_ID,
233 reversible: true,
234 affects_cache: false,
235 affects_user_data: false,
236 summary: "retain the rebuilt sandbox label index",
237 },
238 MigrationMetadata {
239 id: SHARED_CPU_ALLOCATION_MIGRATION_ID,
240 reversible: true,
241 affects_cache: false,
242 affects_user_data: false,
243 summary: "restore exclusive logical CPU allocation rows",
244 },
245 MigrationMetadata {
246 id: MOUNT_OWNER_CONFIG_MIGRATION_ID,
247 reversible: true,
248 affects_cache: false,
249 affects_user_data: false,
250 summary: "remove the compatibility marker after confirming no persisted mount ownership",
251 },
252 MigrationMetadata {
253 id: SANDBOX_NETWORK_SLOT_MIGRATION_ID,
256 reversible: true,
260 affects_cache: false,
261 affects_user_data: false,
262 summary: "retain the compatible sandbox network slot column",
263 },
264 MigrationMetadata {
265 id: SNAPSHOT_IDENTITY_MIGRATION_ID,
266 reversible: true,
267 affects_cache: true,
268 affects_user_data: true,
269 summary: "reverse final snapshot descriptors before dropping identity projections",
270 },
271 MigrationMetadata {
272 id: SNAPSHOT_GROUPS_MIGRATION_ID,
273 reversible: true,
274 affects_cache: false,
275 affects_user_data: true,
276 summary: "restore the flat snapshot index only when no groups or duplicate identities remain",
277 },
278];
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub struct MigrationMetadata {
287 pub id: &'static str,
289
290 pub reversible: bool,
292
293 pub affects_cache: bool,
296
297 pub affects_user_data: bool,
300
301 pub summary: &'static str,
303}
304
305pub fn migration_ids() -> impl Iterator<Item = &'static str> {
311 MIGRATION_METADATA.iter().map(|metadata| metadata.id)
312}
313
314pub fn canonical_applied_prefix<'a>(
320 applied_ids: impl IntoIterator<Item = &'a str>,
321) -> Option<&'static [MigrationMetadata]> {
322 let mut applied_count = 0;
323 let applied_ids: HashSet<_> = applied_ids
324 .into_iter()
325 .inspect(|_| applied_count += 1)
326 .collect();
327
328 if applied_ids.len() != applied_count {
330 return None;
331 }
332
333 let prefix = MIGRATION_METADATA.get(..applied_count)?;
334 prefix
335 .iter()
336 .all(|metadata| applied_ids.contains(metadata.id))
337 .then_some(prefix)
338}
339
340#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::{Migrator, MigratorTrait};
348
349 #[test]
350 fn metadata_matches_migrator_order() {
351 let migrations = Migrator::migrations();
352 let migrator_ids: Vec<_> = migrations
353 .iter()
354 .map(|migration| migration.name().to_string())
355 .collect();
356 let metadata_ids: Vec<_> = migration_ids().map(str::to_string).collect();
357
358 assert_eq!(metadata_ids, migrator_ids);
359 }
360
361 #[test]
362 fn canonical_applied_prefix_uses_metadata_order() {
363 let applied = [
364 SNAPSHOT_GROUPS_MIGRATION_ID,
365 SNAPSHOT_IDENTITY_MIGRATION_ID,
366 MOUNT_OWNER_CONFIG_MIGRATION_ID,
367 SANDBOX_NETWORK_SLOT_MIGRATION_ID,
368 SHARED_CPU_ALLOCATION_MIGRATION_ID,
369 SANDBOX_LABEL_REBUILD_MIGRATION_ID,
370 MEMORY_ALLOCATION_NODES_MIGRATION_ID,
371 WRITEBACK_ALLOCATION_MIGRATION_ID,
372 SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID,
373 CPU_ALLOCATION_MIGRATION_ID,
374 ];
375 let prefix_len = MIGRATION_METADATA.len();
376 let mut all_applied: Vec<_> = MIGRATION_METADATA[..prefix_len - applied.len()]
377 .iter()
378 .map(|metadata| metadata.id)
379 .collect();
380 all_applied.extend(applied);
381
382 let prefix = canonical_applied_prefix(all_applied).expect("valid unordered prefix");
383 assert_eq!(prefix, MIGRATION_METADATA);
384 }
385
386 #[test]
387 fn canonical_applied_prefix_rejects_gaps_and_unknown_migrations() {
388 let without_first = MIGRATION_METADATA
389 .iter()
390 .skip(1)
391 .map(|metadata| metadata.id);
392 assert!(canonical_applied_prefix(without_first).is_none());
393
394 let with_unknown = MIGRATION_METADATA
395 .iter()
396 .map(|metadata| metadata.id)
397 .chain(["m20990101_000001_future"]);
398 assert!(canonical_applied_prefix(with_unknown).is_none());
399 }
400
401 #[test]
402 fn released_v0_6_15_migrations_remain_a_prefix() {
403 let applied: Vec<_> = migration_ids()
404 .take_while(|id| *id != SANDBOX_NETWORK_SLOT_MIGRATION_ID)
405 .collect();
406
407 assert_eq!(applied.last(), Some(&MOUNT_OWNER_CONFIG_MIGRATION_ID));
408 assert!(canonical_applied_prefix(applied).is_some());
409 }
410
411 #[test]
412 fn snapshot_stack_follows_released_v0_6_18_prefix() {
413 let released = [
417 "m20260305_000001_create_image_tables",
418 "m20260305_000002_create_sandbox_tables",
419 "m20260305_000003_create_storage_tables",
420 "m20260305_000004_create_sandbox_images_table",
421 "m20260410_000001_erofs_image_schema",
422 "m20260501_000001_create_snapshot_index",
423 "m20260517_000001_drop_sandbox_metric",
424 "m20260527_000001_migrate_oci_rootfs_source",
425 "m20260531_000001_create_sandbox_labels",
426 "m20260531_000002_index_sandbox_labels_key_value",
427 "m20260606_000001_named_volume_kinds",
428 "m20260621_000001_add_sandbox_ephemeral",
429 "m20260621_000002_create_maintenance_lease",
430 "m20260703_000001_add_sandbox_active_config",
431 "m20260708_000001_migrate_bind_rootfs_source",
432 "m20260710_000001_migrate_root_disk",
433 "m20260714_000001_add_snapshot_scope",
434 "m20260723_000001_snapshot_artifact_transition",
435 "m20260719_000001_create_cpu_allocations",
436 "m20260803_000001_create_writeback_allocations",
437 "m20260808_000001_create_memory_allocation_nodes",
438 "m20260810_000001_rebuild_sandbox_labels",
439 "m20260813_000001_share_cpu_allocations",
440 "m20260824_000001_mount_owner_config",
441 "m20260818_000001_sandbox_network_slot",
442 ];
443 let current: Vec<_> = migration_ids().collect();
444 assert!(current.starts_with(&released));
445 assert_eq!(
446 ¤t[released.len()..],
447 &[SNAPSHOT_IDENTITY_MIGRATION_ID, SNAPSHOT_GROUPS_MIGRATION_ID],
448 );
449 assert!(canonical_applied_prefix(released).is_some());
450 }
451
452 #[test]
453 fn frozen_0_6_0_baseline_is_current_prefix() {
454 let metadata_ids: Vec<_> = migration_ids().collect();
455 assert!(metadata_ids.starts_with(BASELINE_0_6_0_MIGRATIONS));
456 }
457}