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/// Frozen migration baseline for the transitional 0.6.0 release.
55///
56/// The released 0.6.0 binary predates `msb __schema-baseline --json`, so
57/// downgrade uses this fixture when inspecting that exact target. Do not extend
58/// this list when adding later migrations; future targets should answer with
59/// their own hidden baseline command.
60pub const BASELINE_0_6_0_MIGRATIONS: &[&str] = &[
61    "m20260305_000001_create_image_tables",
62    "m20260305_000002_create_sandbox_tables",
63    "m20260305_000003_create_storage_tables",
64    "m20260305_000004_create_sandbox_images_table",
65    "m20260410_000001_erofs_image_schema",
66    "m20260501_000001_create_snapshot_index",
67    "m20260517_000001_drop_sandbox_metric",
68    "m20260527_000001_migrate_oci_rootfs_source",
69    "m20260531_000001_create_sandbox_labels",
70    "m20260531_000002_index_sandbox_labels_key_value",
71    "m20260606_000001_named_volume_kinds",
72    "m20260621_000001_add_sandbox_ephemeral",
73    MAINTENANCE_LEASE_MIGRATION_ID,
74];
75
76/// Metadata for every migration in `Migrator::migrations()` order.
77pub const MIGRATION_METADATA: &[MigrationMetadata] = &[
78    MigrationMetadata {
79        id: "m20260305_000001_create_image_tables",
80        reversible: true,
81        affects_cache: true,
82        affects_user_data: false,
83        summary: "remove legacy OCI image catalog tables",
84    },
85    MigrationMetadata {
86        id: "m20260305_000002_create_sandbox_tables",
87        reversible: true,
88        affects_cache: false,
89        affects_user_data: false,
90        summary: "remove sandbox and run tables",
91    },
92    MigrationMetadata {
93        id: "m20260305_000003_create_storage_tables",
94        reversible: true,
95        affects_cache: false,
96        affects_user_data: false,
97        summary: "remove volume and snapshot storage tables",
98    },
99    MigrationMetadata {
100        id: "m20260305_000004_create_sandbox_images_table",
101        reversible: true,
102        affects_cache: true,
103        affects_user_data: false,
104        summary: "remove sandbox image references",
105    },
106    MigrationMetadata {
107        id: "m20260410_000001_erofs_image_schema",
108        reversible: true,
109        affects_cache: true,
110        affects_user_data: false,
111        summary: "remove EROFS rootfs catalog tables",
112    },
113    MigrationMetadata {
114        id: "m20260501_000001_create_snapshot_index",
115        reversible: true,
116        affects_cache: false,
117        affects_user_data: false,
118        summary: "remove snapshot index table",
119    },
120    MigrationMetadata {
121        id: "m20260517_000001_drop_sandbox_metric",
122        reversible: false,
123        affects_cache: false,
124        affects_user_data: false,
125        summary: "restore legacy sandbox metrics table",
126    },
127    MigrationMetadata {
128        id: "m20260527_000001_migrate_oci_rootfs_source",
129        reversible: false,
130        affects_cache: false,
131        affects_user_data: false,
132        summary: "rewrite OCI rootfs config back to the legacy string shape",
133    },
134    MigrationMetadata {
135        id: "m20260531_000001_create_sandbox_labels",
136        reversible: true,
137        affects_cache: false,
138        affects_user_data: false,
139        summary: "remove sandbox labels table",
140    },
141    MigrationMetadata {
142        id: "m20260531_000002_index_sandbox_labels_key_value",
143        reversible: true,
144        affects_cache: false,
145        affects_user_data: false,
146        summary: "remove sandbox label key/value index",
147    },
148    MigrationMetadata {
149        id: "m20260606_000001_named_volume_kinds",
150        reversible: true,
151        affects_cache: false,
152        affects_user_data: false,
153        summary: "remove named volume kind columns and attachments",
154    },
155    MigrationMetadata {
156        id: "m20260621_000001_add_sandbox_ephemeral",
157        reversible: true,
158        affects_cache: false,
159        affects_user_data: false,
160        summary: "remove sandbox ephemeral flag",
161    },
162    MigrationMetadata {
163        id: MAINTENANCE_LEASE_MIGRATION_ID,
164        reversible: true,
165        affects_cache: false,
166        affects_user_data: false,
167        summary: "remove maintenance lease table",
168    },
169    MigrationMetadata {
170        id: ACTIVE_CONFIG_MIGRATION_ID,
171        reversible: true,
172        affects_cache: false,
173        affects_user_data: false,
174        summary: "remove active sandbox config snapshots",
175    },
176    MigrationMetadata {
177        id: "m20260708_000001_migrate_bind_rootfs_source",
178        reversible: true,
179        affects_cache: false,
180        affects_user_data: true,
181        summary: "rewrite bind rootfs config back to the legacy string shape",
182    },
183    MigrationMetadata {
184        id: "m20260710_000001_migrate_root_disk",
185        reversible: true,
186        affects_cache: false,
187        affects_user_data: true,
188        summary: "rewrite root disk config back to the upper size shape",
189    },
190    MigrationMetadata {
191        id: SNAPSHOT_SCOPE_MIGRATION_ID,
192        reversible: true,
193        affects_cache: false,
194        affects_user_data: false,
195        summary: "remove snapshot scope index metadata",
196    },
197    MigrationMetadata {
198        id: SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID,
199        reversible: true,
200        affects_cache: false,
201        affects_user_data: true,
202        summary: "reverse final snapshot descriptors before removing migration state",
203    },
204    MigrationMetadata {
205        id: CPU_ALLOCATION_MIGRATION_ID,
206        reversible: true,
207        affects_cache: false,
208        affects_user_data: false,
209        summary: "remove cooperative host CPU allocation tables",
210    },
211    MigrationMetadata {
212        id: WRITEBACK_ALLOCATION_MIGRATION_ID,
213        reversible: true,
214        affects_cache: false,
215        affects_user_data: false,
216        summary: "remove host-global writeback allocation state",
217    },
218    MigrationMetadata {
219        id: MEMORY_ALLOCATION_NODES_MIGRATION_ID,
220        reversible: true,
221        affects_cache: false,
222        affects_user_data: false,
223        summary: "remove cooperative NUMA memory allocation state",
224    },
225    MigrationMetadata {
226        id: SANDBOX_LABEL_REBUILD_MIGRATION_ID,
227        reversible: true,
228        affects_cache: false,
229        affects_user_data: false,
230        summary: "retain the rebuilt sandbox label index",
231    },
232    MigrationMetadata {
233        id: SHARED_CPU_ALLOCATION_MIGRATION_ID,
234        reversible: true,
235        affects_cache: false,
236        affects_user_data: false,
237        summary: "restore exclusive logical CPU allocation rows",
238    },
239    MigrationMetadata {
240        id: MOUNT_OWNER_CONFIG_MIGRATION_ID,
241        reversible: true,
242        affects_cache: false,
243        affects_user_data: false,
244        summary: "remove the compatibility marker after confirming no persisted mount ownership",
245    },
246    MigrationMetadata {
247        // This backdated migration first shipped in v0.6.16. Keep it after
248        // the v0.6.15 mount-owner marker so released databases stay prefixes.
249        id: SANDBOX_NETWORK_SLOT_MIGRATION_ID,
250        // The column is deliberately left in place on rollback (SQLite has
251        // no DROP COLUMN on every supported version); `up` probes for it so a
252        // re-upgrade after this rollback succeeds.
253        reversible: true,
254        affects_cache: false,
255        affects_user_data: false,
256        summary: "retain the compatible sandbox network slot column",
257    },
258];
259
260//--------------------------------------------------------------------------------------------------
261// Types
262//--------------------------------------------------------------------------------------------------
263
264/// Downgrade metadata for one migration.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub struct MigrationMetadata {
267    /// Migration identifier returned by `MigrationName::name()`.
268    pub id: &'static str,
269
270    /// Whether `down()` actually restores a target-compatible schema/state.
271    pub reversible: bool,
272
273    /// Whether rolling this migration back invalidates re-pullable image cache
274    /// contents on disk.
275    pub affects_cache: bool,
276
277    /// Whether rolling this migration back may leave snapshots or disk-backed
278    /// named volumes in a format the target release cannot read.
279    pub affects_user_data: bool,
280
281    /// Short human-readable summary used in destructive downgrade prompts.
282    pub summary: &'static str,
283}
284
285//--------------------------------------------------------------------------------------------------
286// Functions
287//--------------------------------------------------------------------------------------------------
288
289/// Return all migration identifiers in schema order.
290pub fn migration_ids() -> impl Iterator<Item = &'static str> {
291    MIGRATION_METADATA.iter().map(|metadata| metadata.id)
292}
293
294/// Resolve an unordered collection of applied migration identifiers to its canonical prefix.
295///
296/// SeaORM records migration timestamps with insufficient precision to recover execution order
297/// when several migrations run together. Treat the migration table as a set and use this binary's
298/// append-only metadata as the only source of ordering instead.
299pub fn canonical_applied_prefix<'a>(
300    applied_ids: impl IntoIterator<Item = &'a str>,
301) -> Option<&'static [MigrationMetadata]> {
302    let mut applied_count = 0;
303    let applied_ids: HashSet<_> = applied_ids
304        .into_iter()
305        .inspect(|_| applied_count += 1)
306        .collect();
307
308    // Duplicate migration rows are invalid even if their distinct identifiers resemble a prefix.
309    if applied_ids.len() != applied_count {
310        return None;
311    }
312
313    let prefix = MIGRATION_METADATA.get(..applied_count)?;
314    prefix
315        .iter()
316        .all(|metadata| applied_ids.contains(metadata.id))
317        .then_some(prefix)
318}
319
320//--------------------------------------------------------------------------------------------------
321// Tests
322//--------------------------------------------------------------------------------------------------
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::{Migrator, MigratorTrait};
328
329    #[test]
330    fn metadata_matches_migrator_order() {
331        let migrations = Migrator::migrations();
332        let migrator_ids: Vec<_> = migrations
333            .iter()
334            .map(|migration| migration.name().to_string())
335            .collect();
336        let metadata_ids: Vec<_> = migration_ids().map(str::to_string).collect();
337
338        assert_eq!(metadata_ids, migrator_ids);
339    }
340
341    #[test]
342    fn canonical_applied_prefix_uses_metadata_order() {
343        let applied = [
344            MOUNT_OWNER_CONFIG_MIGRATION_ID,
345            SANDBOX_NETWORK_SLOT_MIGRATION_ID,
346            SHARED_CPU_ALLOCATION_MIGRATION_ID,
347            SANDBOX_LABEL_REBUILD_MIGRATION_ID,
348            MEMORY_ALLOCATION_NODES_MIGRATION_ID,
349            WRITEBACK_ALLOCATION_MIGRATION_ID,
350            SNAPSHOT_ARTIFACT_TRANSITION_MIGRATION_ID,
351            CPU_ALLOCATION_MIGRATION_ID,
352        ];
353        let prefix_len = MIGRATION_METADATA.len();
354        let mut all_applied: Vec<_> = MIGRATION_METADATA[..prefix_len - applied.len()]
355            .iter()
356            .map(|metadata| metadata.id)
357            .collect();
358        all_applied.extend(applied);
359
360        let prefix = canonical_applied_prefix(all_applied).expect("valid unordered prefix");
361        assert_eq!(prefix, MIGRATION_METADATA);
362    }
363
364    #[test]
365    fn canonical_applied_prefix_rejects_gaps_and_unknown_migrations() {
366        let without_first = MIGRATION_METADATA
367            .iter()
368            .skip(1)
369            .map(|metadata| metadata.id);
370        assert!(canonical_applied_prefix(without_first).is_none());
371
372        let with_unknown = MIGRATION_METADATA
373            .iter()
374            .map(|metadata| metadata.id)
375            .chain(["m20990101_000001_future"]);
376        assert!(canonical_applied_prefix(with_unknown).is_none());
377    }
378
379    #[test]
380    fn released_v0_6_15_migrations_remain_a_prefix() {
381        let applied: Vec<_> = migration_ids()
382            .take_while(|id| *id != SANDBOX_NETWORK_SLOT_MIGRATION_ID)
383            .collect();
384
385        assert_eq!(applied.last(), Some(&MOUNT_OWNER_CONFIG_MIGRATION_ID));
386        assert!(canonical_applied_prefix(applied).is_some());
387    }
388
389    #[test]
390    fn frozen_0_6_0_baseline_is_current_prefix() {
391        let metadata_ids: Vec<_> = migration_ids().collect();
392        assert!(metadata_ids.starts_with(BASELINE_0_6_0_MIGRATIONS));
393    }
394}