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