Skip to main content

microsandbox_migration/
schema_metadata.rs

1//! Static metadata for downgrade planning.
2//!
3//! `Migrator::migrations()` owns the executable migration order. This module
4//! keeps the user-facing downgrade metadata in the same crate so release checks
5//! can ensure every migration has an explicit reversibility and cache-impact
6//! decision before a new binary ships.
7
8use std::collections::HashSet;
9
10//--------------------------------------------------------------------------------------------------
11// Constants
12//--------------------------------------------------------------------------------------------------
13
14/// Version of the hidden schema-baseline JSON shape emitted by the CLI.
15pub const SCHEMA_BASELINE_FORMAT_VERSION: u32 = 1;
16
17/// Oldest release supported by the downgrade flow.
18pub const DOWNGRADE_FLOOR: &str = "0.6.0";
19
20/// Migration that introduced the DB-backed maintenance lease table.
21pub const MAINTENANCE_LEASE_MIGRATION_ID: &str = "m20260621_000002_create_maintenance_lease";
22
23/// Migration that introduced desired-vs-active sandbox config tracking.
24pub const ACTIVE_CONFIG_MIGRATION_ID: &str = "m20260703_000001_add_sandbox_active_config";
25
26/// Migration that adds payload scope metadata to the snapshot index.
27pub const SNAPSHOT_SCOPE_MIGRATION_ID: &str = "m20260714_000001_add_snapshot_scope";
28
29/// Migration that projects final snapshot state and journals legacy conversion.
30pub const SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID: &str =
31    "m20260723_000001_snapshot_artifact_transition";
32
33/// Migration that introduces cooperative host CPU allocation state.
34pub const CPU_ALLOCATION_MIGRATION_ID: &str = "m20260719_000001_create_cpu_allocations";
35
36/// Migration that introduces host-global writeback dirty-credit reservations.
37pub const WRITEBACK_ALLOCATION_MIGRATION_ID: &str = "m20260803_000001_create_writeback_allocations";
38
39/// Migration that records per-NUMA-node guest memory promises for CPU allocations.
40pub const MEMORY_ALLOCATION_NODES_MIGRATION_ID: &str =
41    "m20260808_000001_create_memory_allocation_nodes";
42
43/// Migration that rebuilds the sandbox label index from persisted configs.
44pub const SANDBOX_LABEL_REBUILD_MIGRATION_ID: &str = "m20260810_000001_rebuild_sandbox_labels";
45
46/// Migration that permits several managed vCPUs to share one host logical processor.
47pub const SHARED_CPU_ALLOCATION_MIGRATION_ID: &str = "m20260813_000001_share_cpu_allocations";
48/// Migration that adds recyclable network address-pool slot leases.
49pub const SANDBOX_NETWORK_SLOT_MIGRATION_ID: &str = "m20260818_000001_sandbox_network_slot";
50
51/// Migration that prevents old binaries from discarding persisted mount ownership.
52pub const MOUNT_OWNER_CONFIG_MIGRATION_ID: &str = "m20260824_000001_mount_owner_config";
53
54/// Migration that separates stable snapshot identity from descriptor integrity.
55pub const SNAPSHOT_IDENTITY_MIGRATION_ID: &str = "m20260829_000001_split_snapshot_identity";
56
57/// Migration that separates local group membership from portable snapshot identity.
58pub const SNAPSHOT_GROUPS_MIGRATION_ID: &str = "m20260910_000001_snapshot_groups";
59
60/// Frozen migration baseline for the transitional 0.6.0 release.
61///
62/// The released 0.6.0 binary predates `msb __schema-baseline --json`, so
63/// downgrade uses this fixture when inspecting that exact target. Do not extend
64/// this list when adding later migrations; future targets should answer with
65/// their own hidden baseline command.
66pub 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
82/// Metadata for every migration in `Migrator::migrations()` order.
83pub 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        // This backdated migration first shipped in v0.6.16. Keep it after
254        // the v0.6.15 mount-owner marker so released databases stay prefixes.
255        id: SANDBOX_NETWORK_SLOT_MIGRATION_ID,
256        // The column is deliberately left in place on rollback (SQLite has
257        // no DROP COLUMN on every supported version); `up` probes for it so a
258        // re-upgrade after this rollback succeeds.
259        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//--------------------------------------------------------------------------------------------------
281// Types
282//--------------------------------------------------------------------------------------------------
283
284/// Downgrade metadata for one migration.
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub struct MigrationMetadata {
287    /// Migration identifier returned by `MigrationName::name()`.
288    pub id: &'static str,
289
290    /// Whether `down()` actually restores a target-compatible schema/state.
291    pub reversible: bool,
292
293    /// Whether rolling this migration back invalidates re-pullable image cache
294    /// contents on disk.
295    pub affects_cache: bool,
296
297    /// Whether rolling this migration back may leave snapshots or disk-backed
298    /// named volumes in a format the target release cannot read.
299    pub affects_user_data: bool,
300
301    /// Short human-readable summary used in destructive downgrade prompts.
302    pub summary: &'static str,
303}
304
305//--------------------------------------------------------------------------------------------------
306// Functions
307//--------------------------------------------------------------------------------------------------
308
309/// Return all migration identifiers in schema order.
310pub fn migration_ids() -> impl Iterator<Item = &'static str> {
311    MIGRATION_METADATA.iter().map(|metadata| metadata.id)
312}
313
314/// Resolve an unordered collection of applied migration identifiers to its canonical prefix.
315///
316/// SeaORM records migration timestamps with insufficient precision to recover execution order
317/// when several migrations run together. Treat the migration table as a set and use this binary's
318/// append-only metadata as the only source of ordering instead.
319pub 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    // Duplicate migration rows are invalid even if their distinct identifiers resemble a prefix.
329    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//--------------------------------------------------------------------------------------------------
341// Tests
342//--------------------------------------------------------------------------------------------------
343
344#[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        // This is also main's complete prefix at the v0.7.0 integration point.
414        // Keep execution order, not timestamp order: every main migration must
415        // precede the unreleased snapshot migrations when the branches converge.
416        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            &current[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}