Skip to main content

spacetimedb_schema/
auto_migrate.rs

1use core::{cmp::Ordering, ops::BitOr};
2
3use crate::{def::*, error::PrettyAlgebraicType, identifier::Identifier};
4use formatter::format_plan;
5use spacetimedb_data_structures::{
6    error_stream::{CollectAllErrors, CombineErrors, ErrorStream},
7    map::{HashCollectionExt as _, HashSet},
8};
9use spacetimedb_lib::{
10    db::raw_def::v9::{RawRowLevelSecurityDefV9, TableType},
11    hash_bytes, Identity,
12};
13use spacetimedb_sats::{
14    layout::{HasLayout, SumTypeLayout},
15    raw_identifier::RawIdentifier,
16    AlgebraicType, WithTypespace,
17};
18use termcolor_formatter::{ColorScheme, TermColorFormatter};
19use thiserror::Error;
20mod formatter;
21mod termcolor_formatter;
22
23pub type Result<T> = std::result::Result<T, ErrorStream<AutoMigrateError>>;
24
25/// A plan for a migration.
26#[derive(Debug)]
27pub enum MigratePlan<'def> {
28    Manual(ManualMigratePlan<'def>),
29    Auto(AutoMigratePlan<'def>),
30}
31
32#[derive(Copy, Clone, PartialEq, Eq)]
33pub enum PrettyPrintStyle {
34    AnsiColor,
35    NoColor,
36}
37
38impl<'def> MigratePlan<'def> {
39    /// Get the old `ModuleDef` for this migration plan.
40    pub fn old_def(&self) -> &'def ModuleDef {
41        match self {
42            MigratePlan::Manual(plan) => plan.old,
43            MigratePlan::Auto(plan) => plan.old,
44        }
45    }
46
47    /// Get the new `ModuleDef` for this migration plan.
48    pub fn new_def(&self) -> &'def ModuleDef {
49        match self {
50            MigratePlan::Manual(plan) => plan.new,
51            MigratePlan::Auto(plan) => plan.new,
52        }
53    }
54
55    pub fn breaks_client(&self) -> bool {
56        match self {
57            //TODO: fix it when support for manual migration plans is added.
58            MigratePlan::Manual(_) => true,
59            MigratePlan::Auto(plan) => plan
60                .steps
61                .iter()
62                .any(|step| matches!(step, AutoMigrateStep::DisconnectAllUsers)),
63        }
64    }
65
66    pub fn pretty_print(&self, style: PrettyPrintStyle) -> anyhow::Result<String> {
67        use PrettyPrintStyle::*;
68        match self {
69            MigratePlan::Manual(_) => {
70                anyhow::bail!("Manual migration plans are not yet supported for pretty printing.")
71            }
72
73            MigratePlan::Auto(plan) => match style {
74                NoColor => {
75                    let mut fmt = TermColorFormatter::new(ColorScheme::default(), termcolor::ColorChoice::Never);
76                    format_plan(&mut fmt, plan).map(|_| fmt.to_string())
77                }
78                AnsiColor => {
79                    let mut fmt = TermColorFormatter::new(ColorScheme::default(), termcolor::ColorChoice::AlwaysAnsi);
80                    format_plan(&mut fmt, plan).map(|_| fmt.to_string())
81                }
82            }
83            .map_err(|e| anyhow::anyhow!("Failed to format migration plan: {e}")),
84        }
85    }
86}
87
88/// A migration policy that determines whether a module update is allowed to break client compatibility.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum MigrationPolicy {
91    /// Migration must maintain backward compatibility with existing clients.
92    Compatible,
93    /// To use this, a valid [`MigrationToken`] must be provided.
94    /// The token is issued through the pre-publish API (see the `client-api` crate)
95    /// and proves that the publisher explicitly acknowledged the breaking change.
96    BreakClients(spacetimedb_lib::Hash),
97}
98
99impl MigrationPolicy {
100    /// Verifies whether the given migration plan is allowed under the current policy.
101    ///
102    /// Returns `Ok(())` if allowed, otherwise an appropriate `MigrationPolicyError`
103    fn permits_plan(&self, plan: &MigratePlan<'_>, token: &MigrationToken) -> anyhow::Result<(), MigrationPolicyError> {
104        match self {
105            MigrationPolicy::Compatible => {
106                if plan.breaks_client() {
107                    Err(MigrationPolicyError::ClientBreakingChangeDisallowed)
108                } else {
109                    Ok(())
110                }
111            }
112            MigrationPolicy::BreakClients(expected_hash) => {
113                if token.hash() == *expected_hash {
114                    Ok(())
115                } else {
116                    Err(MigrationPolicyError::InvalidToken)
117                }
118            }
119        }
120    }
121
122    /// Attempts to generate a migration plan and validate it under this policy.
123    ///
124    /// Fails if migration is not permitted by the policy or migration planning fails.
125    pub fn try_migrate<'def>(
126        &self,
127        database_identity: Identity,
128        old_module_hash: spacetimedb_lib::Hash,
129        old_module_def: &'def ModuleDef,
130        new_module_hash: spacetimedb_lib::Hash,
131        new_module_def: &'def ModuleDef,
132    ) -> anyhow::Result<MigratePlan<'def>, MigrationPolicyError> {
133        let plan = ponder_migrate(old_module_def, new_module_def).map_err(MigrationPolicyError::AutoMigrateFailure)?;
134
135        let token = MigrationToken {
136            database_identity,
137            old_module_hash,
138            new_module_hash,
139        };
140        self.permits_plan(&plan, &token)?;
141        Ok(plan)
142    }
143}
144
145#[derive(Debug, Error)]
146pub enum MigrationPolicyError {
147    #[error("Automatic migration planning failed")]
148    AutoMigrateFailure(ErrorStream<AutoMigrateError>),
149
150    #[error("Token provided is invalid or does not match expected hash")]
151    InvalidToken,
152
153    #[error("Migration plan contains a client-breaking change which is disallowed under current policy")]
154    ClientBreakingChangeDisallowed,
155}
156
157/// A token acknowledging a breaking migration.
158///
159/// Note: This token is only intended as a UX safeguard, not as a security measure.
160/// No secret is used in its generation, which means anyone can reproduce it given
161/// the inputs. That is acceptable for our purposes since it only signals user intent,
162/// not authorization.
163pub struct MigrationToken {
164    pub database_identity: Identity,
165    pub old_module_hash: spacetimedb_lib::Hash,
166    pub new_module_hash: spacetimedb_lib::Hash,
167}
168
169impl MigrationToken {
170    pub fn hash(&self) -> spacetimedb_lib::Hash {
171        hash_bytes(
172            format!(
173                "{}{}{}",
174                self.database_identity.to_hex(),
175                self.old_module_hash.to_hex(),
176                self.new_module_hash.to_hex()
177            )
178            .as_str(),
179        )
180    }
181}
182
183/// A plan for a manual migration.
184/// `new` must have a reducer marked with `Lifecycle::Update`.
185#[derive(Debug)]
186pub struct ManualMigratePlan<'def> {
187    pub old: &'def ModuleDef,
188    pub new: &'def ModuleDef,
189}
190
191/// A plan for an automatic migration.
192#[derive(Debug)]
193pub struct AutoMigratePlan<'def> {
194    /// The old database definition.
195    pub old: &'def ModuleDef,
196    /// The new database definition.
197    pub new: &'def ModuleDef,
198    /// The checks to perform before the automatic migration.
199    /// There is also an implied check: that the schema in the database is compatible with the old ModuleDef.
200    pub prechecks: Vec<AutoMigratePrecheck<'def>>,
201    /// The migration steps to perform.
202    /// Order matters: `Remove`s of a particular `Def` must be ordered before `Add`s.
203    pub steps: Vec<AutoMigrateStep<'def>>,
204}
205
206impl AutoMigratePlan<'_> {
207    fn any_step(&self, f: impl Fn(&AutoMigrateStep) -> bool) -> bool {
208        self.steps.iter().any(f)
209    }
210
211    fn disconnects_all_users(&self) -> bool {
212        self.any_step(|step| matches!(step, AutoMigrateStep::DisconnectAllUsers))
213    }
214
215    /// Ensures that `DisconnectAllUsers` is present in the plan.
216    /// If it's already there, this is a no-op.
217    fn ensure_disconnect_all_users(&mut self) {
218        if !self.disconnects_all_users() {
219            self.steps.push(AutoMigrateStep::DisconnectAllUsers);
220        }
221    }
222}
223
224/// Checks that must be performed before performing an automatic migration.
225/// These checks can access table contents and other database state.
226#[derive(PartialEq, Eq, Debug, PartialOrd, Ord)]
227pub enum AutoMigratePrecheck<'def> {
228    /// Perform a check that adding a sequence is valid (the relevant column contains no values
229    /// greater than the sequence's start value).
230    CheckAddSequenceRangeValid(<SequenceDef as ModuleDefLookup>::Key<'def>),
231}
232
233/// A step in an automatic migration.
234#[derive(PartialEq, Eq, Debug, PartialOrd, Ord)]
235pub enum AutoMigrateStep<'def> {
236    // It is important FOR CORRECTNESS that `Remove` variants are declared before `Add` variants in this enum!
237    //
238    // The ordering is used to sort the steps of an auto-migration.
239    // If adds go before removes, and the user tries to remove an index and then re-add it with new configuration,
240    // the following can occur:
241    //
242    // 1. `AddIndex("indexname")`
243    // 2. `RemoveIndex("indexname")`
244    //
245    // This results in the existing index being re-added -- which, at time of writing, does nothing -- and then removed,
246    // resulting in the intended index not being created.
247    //
248    // For now, we just ensure that we declare all `Remove` variants before `Add` variants
249    // and let `#[derive(PartialOrd)]` take care of the rest.
250    //
251    // TODO: when this enum is made serializable, a more durable fix will be needed here.
252    // Probably we will want to have separate arrays of add and remove steps.
253    //
254    /// Remove an index.
255    RemoveIndex(<IndexDef as ModuleDefLookup>::Key<'def>),
256    /// Remove a constraint.
257    RemoveConstraint(<ConstraintDef as ModuleDefLookup>::Key<'def>),
258    /// Remove a sequence.
259    RemoveSequence(<SequenceDef as ModuleDefLookup>::Key<'def>),
260    /// Remove a schedule annotation from a table.
261    RemoveSchedule(<ScheduleDef as ModuleDefLookup>::Key<'def>),
262    /// Remove a view and corresponding view table
263    RemoveView(<ViewDef as ModuleDefLookup>::Key<'def>),
264    /// Remove a row-level security query.
265    RemoveRowLevelSecurity(<RawRowLevelSecurityDefV9 as ModuleDefLookup>::Key<'def>),
266
267    /// Remove an empty table and all its sub-objects (indexes, constraints, sequences).
268    /// Validated at execution time: fails if the table contains data.
269    RemoveTable(<TableDef as ModuleDefLookup>::Key<'def>),
270
271    /// Change the column types of a table, in a layout compatible way.
272    ///
273    /// This should be done before any new indices are added.
274    ChangeColumns(<TableDef as ModuleDefLookup>::Key<'def>),
275    /// Add columns to a table, in a layout-INCOMPATIBLE way.
276    ///
277    /// This is a destructive operation that requires first running a `DisconnectAllUsers`.
278    ///
279    /// The added columns are guaranteed to be contiguous
280    /// and at the end of the table.
281    /// They are also guaranteed to have default values set.
282    ///
283    /// When this step is present,
284    /// no `ChangeColumns` steps will be, for the same table.
285    AddColumns(<TableDef as ModuleDefLookup>::Key<'def>),
286
287    /// Add a table, including all indexes, constraints, and sequences.
288    /// There will NOT be separate steps in the plan for adding indexes, constraints, and sequences.
289    AddTable(<TableDef as ModuleDefLookup>::Key<'def>),
290    /// Add an index.
291    AddIndex(<IndexDef as ModuleDefLookup>::Key<'def>),
292    /// Add a sequence.
293    AddSequence(<SequenceDef as ModuleDefLookup>::Key<'def>),
294    /// Add a schedule annotation to a table.
295    AddSchedule(<ScheduleDef as ModuleDefLookup>::Key<'def>),
296    /// Add a view and corresponding view table
297    AddView(<ViewDef as ModuleDefLookup>::Key<'def>),
298    /// Add a row-level security query.
299    AddRowLevelSecurity(<RawRowLevelSecurityDefV9 as ModuleDefLookup>::Key<'def>),
300
301    /// Change the access of a table.
302    ChangeAccess(<TableDef as ModuleDefLookup>::Key<'def>),
303
304    /// Change the primary key of a table.
305    ///
306    /// This updates the `table_primary_key` field in `st_table`
307    /// to match the new module definition.
308    /// Without this step, a stale primary key in the stored schema
309    /// causes `check_compatible` to fail on the next publish.
310    /// See: <https://github.com/clockworklabs/SpacetimeDB/issues/3934>
311    ChangePrimaryKey(<TableDef as ModuleDefLookup>::Key<'def>),
312
313    /// Recompute a view, update its backing table, and push updates to clients
314    UpdateView(<ViewDef as ModuleDefLookup>::Key<'def>),
315
316    /// Disconnect all users connected to the module.
317    DisconnectAllUsers,
318}
319
320#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
321pub struct ChangeColumnTypeParts {
322    pub table: Identifier,
323    pub column: Identifier,
324    pub type1: PrettyAlgebraicType,
325    pub type2: PrettyAlgebraicType,
326}
327
328/// Something that might prevent an automatic migration.
329#[derive(thiserror::Error, Debug, PartialEq, Eq, PartialOrd, Ord)]
330pub enum AutoMigrateError {
331    #[error("Adding a column {column} to table {table} requires a default value annotation")]
332    AddColumn { table: Identifier, column: Identifier },
333
334    #[error("Removing a column {column} from table {table} requires a manual migration")]
335    RemoveColumn { table: Identifier, column: Identifier },
336
337    #[error("Reordering table {table} requires a manual migration")]
338    ReorderTable { table: Identifier },
339
340    #[error(
341        "Changing the type of column {} in table {} from {:?} to {:?} requires a manual migration",
342        .0.column, .0.table, .0.type1, .0.type2
343    )]
344    ChangeColumnType(ChangeColumnTypeParts),
345
346    #[error(
347        "Changing a type within column {} in table {} from {:?} to {:?} requires a manual migration",
348        .0.column, .0.table, .0.type1, .0.type2
349    )]
350    ChangeWithinColumnType(ChangeColumnTypeParts),
351
352    #[error(
353        "Changing the type of column {} in table {} from {:?} to {:?}, with fewer variants, requires a manual migration",
354        .0.column, .0.table, .0.type1, .0.type2
355    )]
356    ChangeColumnTypeFewerVariants(ChangeColumnTypeParts),
357
358    #[error(
359        "Changing a type within column {} in table {} from {:?} to {:?}, with fewer variants, requires a manual migration",
360        .0.column, .0.table, .0.type1, .0.type2
361    )]
362    ChangeWithinColumnTypeFewerVariants(ChangeColumnTypeParts),
363
364    #[error(
365        "Changing the type of column {} in table {} from {:?} to {:?}, with a renamed variant, requires a manual migration",
366        .0.column, .0.table, .0.type1, .0.type2
367    )]
368    ChangeColumnTypeRenamedVariant(ChangeColumnTypeParts),
369
370    #[error(
371        "Changing the type of column {} in table {} from {:?} to {:?}, with a renamed variant, requires a manual migration",
372        .0.column, .0.table, .0.type1, .0.type2
373    )]
374    ChangeWithinColumnTypeRenamedVariant(ChangeColumnTypeParts),
375
376    #[error(
377        "Changing the type of column {} in table {} from {:?} to {:?}, requires a manual migration, due to size mismatch",
378        .0.column, .0.table, .0.type1, .0.type2
379    )]
380    ChangeColumnTypeSizeMismatch(ChangeColumnTypeParts),
381
382    #[error(
383        "Changing a type within column {} in table {} from {:?} to {:?}, requires a manual migration, due to size mismatch",
384        .0.column, .0.table, .0.type1, .0.type2
385    )]
386    ChangeWithinColumnTypeSizeMismatch(ChangeColumnTypeParts),
387
388    #[error(
389        "Changing the type of column {} in table {} from {:?} to {:?}, requires a manual migration, due to alignment mismatch",
390        .0.column, .0.table, .0.type1, .0.type2
391    )]
392    ChangeColumnTypeAlignMismatch(ChangeColumnTypeParts),
393
394    #[error(
395        "Changing a type within column {} in table {} from {:?} to {:?}, requires a manual migration, due to alignment mismatch",
396        .0.column, .0.table, .0.type1, .0.type2
397    )]
398    ChangeWithinColumnTypeAlignMismatch(ChangeColumnTypeParts),
399
400    #[error(
401        "Changing the type of column {} in table {} from {:?} to {:?}, with fewer fields, requires a manual migration",
402        .0.column, .0.table, .0.type1, .0.type2
403    )]
404    ChangeColumnTypeFewerFields(ChangeColumnTypeParts),
405
406    #[error(
407        "Changing a type within column {} in table {} from {:?} to {:?}, with fewer fields, requires a manual migration",
408        .0.column, .0.table, .0.type1, .0.type2
409    )]
410    ChangeWithinColumnTypeFewerFields(ChangeColumnTypeParts),
411
412    #[error(
413        "Changing the type of column {} in table {} from {:?} to {:?}, with a renamed field, requires a manual migration",
414        .0.column, .0.table, .0.type1, .0.type2
415    )]
416    ChangeColumnTypeRenamedField(ChangeColumnTypeParts),
417
418    #[error(
419        "Changing the type of column {} in table {} from {:?} to {:?}, with a renamed field, requires a manual migration",
420        .0.column, .0.table, .0.type1, .0.type2
421    )]
422    ChangeWithinColumnTypeRenamedField(ChangeColumnTypeParts),
423
424    #[error("Adding a unique constraint {constraint} requires a manual migration")]
425    AddUniqueConstraint { constraint: RawIdentifier },
426
427    #[error("Changing a unique constraint {constraint} requires a manual migration")]
428    ChangeUniqueConstraint { constraint: RawIdentifier },
429
430    #[error("Changing the table type of table {table} from {type1:?} to {type2:?} requires a manual migration")]
431    ChangeTableType {
432        table: Identifier,
433        type1: TableType,
434        type2: TableType,
435    },
436
437    #[error("Changing the event flag of table {table} requires a manual migration")]
438    ChangeTableEventFlag { table: Identifier },
439
440    #[error(
441        "Changing the accessor name on index {index} from {old_accessor:?} to {new_accessor:?} requires a manual migration"
442    )]
443    ChangeIndexAccessor {
444        index: RawIdentifier,
445        old_accessor: Option<Identifier>,
446        new_accessor: Option<Identifier>,
447    },
448}
449
450/// Construct a migration plan.
451/// If `new` has an `__update__` reducer, return a manual migration plan.
452/// Otherwise, try to plan an automatic migration. This may fail.
453pub fn ponder_migrate<'def>(old: &'def ModuleDef, new: &'def ModuleDef) -> Result<MigratePlan<'def>> {
454    // TODO(1.0): Implement this function.
455    // Currently we only can do automatic migrations.
456    ponder_auto_migrate(old, new).map(MigratePlan::Auto)
457}
458
459/// Construct an automatic migration plan, or reject with reasons why automatic migration can't be performed.
460pub fn ponder_auto_migrate<'def>(old: &'def ModuleDef, new: &'def ModuleDef) -> Result<AutoMigratePlan<'def>> {
461    // Both the old and new database definitions have already been validated (this is enforced by the types).
462    // All we have to do is walk through and compare them.
463    let mut plan = AutoMigratePlan {
464        old,
465        new,
466        steps: Vec::new(),
467        prechecks: Vec::new(),
468    };
469
470    let views_ok = auto_migrate_views(&mut plan);
471    let tables_ok = auto_migrate_tables(&mut plan);
472
473    // Filter out sub-objects of added/removed tables — they're handled by `AddTable`/`RemoveTable`.
474    let (new_tables, removed_tables): (HashSet<&Identifier>, HashSet<&Identifier>) =
475        diff(plan.old, plan.new, ModuleDef::tables).fold(
476            (HashSet::new(), HashSet::new()),
477            |(mut added, mut removed), diff| {
478                match diff {
479                    Diff::Add { new } => {
480                        added.insert(&new.name);
481                    }
482                    Diff::Remove { old } => {
483                        removed.insert(&old.name);
484                    }
485                    Diff::MaybeChange { .. } => {}
486                }
487                (added, removed)
488            },
489        );
490    let indexes_ok = auto_migrate_indexes(&mut plan, &new_tables, &removed_tables);
491    let sequences_ok = auto_migrate_sequences(&mut plan, &new_tables, &removed_tables);
492    let constraints_ok = auto_migrate_constraints(&mut plan, &new_tables, &removed_tables);
493    // IMPORTANT: RLS auto-migrate steps must come last,
494    // since they assume that any schema changes, like adding or dropping tables,
495    // have already been reflected in the database state.
496    let rls_ok = auto_migrate_row_level_security(&mut plan);
497
498    let ((), (), (), (), (), ()) =
499        (views_ok, tables_ok, indexes_ok, sequences_ok, constraints_ok, rls_ok).combine_errors()?;
500
501    plan.steps.sort();
502    plan.prechecks.sort();
503
504    Ok(plan)
505}
506
507/// A diff between two items.
508/// `Add` means the item is present in the new `ModuleDef` but not the old.
509/// `Remove` means the item is present in the old `ModuleDef` but not the new.
510/// `MaybeChange` indicates the item is present in both.
511#[derive(Debug)]
512enum Diff<'def, T> {
513    Add { new: &'def T },
514    Remove { old: &'def T },
515    MaybeChange { old: &'def T, new: &'def T },
516}
517
518/// Diff a collection of items, looking them up in both the old and new `ModuleDef` by their `ModuleDefLookup::Key`.
519/// Keys are required to be stable across migrations, which makes this possible.
520fn diff<'def, T: ModuleDefLookup, I: Iterator<Item = &'def T>>(
521    old: &'def ModuleDef,
522    new: &'def ModuleDef,
523    iter: impl Fn(&'def ModuleDef) -> I,
524) -> impl Iterator<Item = Diff<'def, T>> {
525    iter(old)
526        .map(move |old_item| match T::lookup(new, old_item.key()) {
527            Some(new_item) => Diff::MaybeChange {
528                old: old_item,
529                new: new_item,
530            },
531            None => Diff::Remove { old: old_item },
532        })
533        .chain(iter(new).filter_map(move |new_item| {
534            if T::lookup(old, new_item.key()).is_none() {
535                Some(Diff::Add { new: new_item })
536            } else {
537                None
538            }
539        }))
540}
541
542fn auto_migrate_views(plan: &mut AutoMigratePlan<'_>) -> Result<()> {
543    diff(plan.old, plan.new, ModuleDef::views)
544        .map(|table_diff| -> Result<()> {
545            match table_diff {
546                Diff::Add { new } => {
547                    plan.steps.push(AutoMigrateStep::AddView(new.key()));
548                    Ok(())
549                }
550                // From the user's perspective, views do not have persistent state.
551                // Hence removal does not require a manual migration - just disconnecting clients.
552                Diff::Remove { old } => {
553                    plan.steps.push(AutoMigrateStep::RemoveView(old.key()));
554                    plan.ensure_disconnect_all_users();
555                    Ok(())
556                }
557                Diff::MaybeChange { old, new } => auto_migrate_view(plan, old, new),
558            }
559        })
560        .collect_all_errors()
561}
562
563fn auto_migrate_view<'def>(plan: &mut AutoMigratePlan<'def>, old: &'def ViewDef, new: &'def ViewDef) -> Result<()> {
564    let key = old.key();
565
566    if old.is_public != new.is_public {
567        plan.steps.push(AutoMigrateStep::ChangeAccess(key));
568    }
569
570    // We can always auto-migrate a view because we can always re-compute it.
571    // However certain things require us to disconnect clients:
572    // 1. If we add or remove a column or parameter
573    // 2. If we change the order of the columns or parameters
574    // 3. If we change the types of the columns or parameters
575    // 4. If we change the context parameter
576    let Any(incompatible_return_type) = diff(plan.old, plan.new, |def| {
577        def.lookup_expect::<ViewDef>(key).return_columns.iter()
578    })
579    .map(|col_diff| {
580        match col_diff {
581            // We must disconnect clients if we add or remove a parameter or column
582            Diff::Add { .. } | Diff::Remove { .. } => Any(true),
583            Diff::MaybeChange { old, new } => {
584                if old.col_id != new.col_id {
585                    return Any(true);
586                };
587
588                ensure_old_ty_upgradable_to_new(
589                    false,
590                    &|| old.view_name.clone(),
591                    &|| old.name.clone(),
592                    &WithTypespace::new(plan.old.typespace(), &old.ty)
593                        .resolve_refs()
594                        .expect("valid ViewDefs must have valid type refs"),
595                    &WithTypespace::new(plan.new.typespace(), &new.ty)
596                        .resolve_refs()
597                        .expect("valid ViewDefs must have valid type refs"),
598                )
599                .unwrap_or(Any(true))
600            }
601        }
602    })
603    .collect();
604
605    let Any(incompatible_param_types) = diff(plan.old, plan.new, |def| {
606        def.lookup_expect::<ViewDef>(key).param_columns.iter()
607    })
608    .map(|col_diff| {
609        match col_diff {
610            // We must disconnect clients if we add or remove a parameter or column
611            Diff::Add { .. } | Diff::Remove { .. } => Any(true),
612            Diff::MaybeChange { old, new } => {
613                if old.col_id != new.col_id {
614                    return Any(true);
615                };
616
617                ensure_old_ty_upgradable_to_new(
618                    false,
619                    &|| old.view_name.clone(),
620                    &|| old.name.clone(),
621                    &WithTypespace::new(plan.old.typespace(), &old.ty)
622                        .resolve_refs()
623                        .expect("valid ViewDefs must have valid type refs"),
624                    &WithTypespace::new(plan.new.typespace(), &new.ty)
625                        .resolve_refs()
626                        .expect("valid ViewDefs must have valid type refs"),
627                )
628                .unwrap_or(Any(true))
629            }
630        }
631    })
632    .collect();
633
634    if old.is_anonymous != new.is_anonymous || incompatible_return_type || incompatible_param_types {
635        plan.steps.push(AutoMigrateStep::AddView(new.key()));
636        plan.steps.push(AutoMigrateStep::RemoveView(old.key()));
637
638        plan.ensure_disconnect_all_users();
639    } else {
640        plan.steps.push(AutoMigrateStep::UpdateView(new.key()));
641    }
642
643    Ok(())
644}
645
646fn auto_migrate_tables(plan: &mut AutoMigratePlan<'_>) -> Result<()> {
647    diff(plan.old, plan.new, ModuleDef::tables)
648        .map(|table_diff| -> Result<()> {
649            match table_diff {
650                Diff::Add { new } => {
651                    plan.steps.push(AutoMigrateStep::AddTable(new.key()));
652                    Ok(())
653                }
654                Diff::Remove { old } => {
655                    plan.steps.push(AutoMigrateStep::RemoveTable(old.key()));
656                    plan.ensure_disconnect_all_users();
657                    Ok(())
658                }
659                Diff::MaybeChange { old, new } => auto_migrate_table(plan, old, new),
660            }
661        })
662        .collect_all_errors()
663}
664
665fn auto_migrate_table<'def>(plan: &mut AutoMigratePlan<'def>, old: &'def TableDef, new: &'def TableDef) -> Result<()> {
666    let key = old.key();
667    let type_ok: Result<()> = if old.table_type == new.table_type {
668        Ok(())
669    } else {
670        Err(AutoMigrateError::ChangeTableType {
671            table: old.name.clone(),
672            type1: old.table_type,
673            type2: new.table_type,
674        }
675        .into())
676    };
677    let event_ok: Result<()> = if old.is_event == new.is_event {
678        Ok(())
679    } else {
680        Err(AutoMigrateError::ChangeTableEventFlag {
681            table: old.name.clone(),
682        }
683        .into())
684    };
685    if old.table_access != new.table_access {
686        plan.steps.push(AutoMigrateStep::ChangeAccess(key));
687    }
688    if old.primary_key != new.primary_key {
689        plan.steps.push(AutoMigrateStep::ChangePrimaryKey(key));
690    }
691    if old.schedule != new.schedule {
692        // Note: this handles the case where there's an altered ScheduleDef for some reason.
693        if let Some(old_schedule) = old.schedule.as_ref() {
694            plan.steps.push(AutoMigrateStep::RemoveSchedule(old_schedule.key()));
695        }
696        if let Some(new_schedule) = new.schedule.as_ref() {
697            plan.steps.push(AutoMigrateStep::AddSchedule(new_schedule.key()));
698        }
699    }
700
701    let columns_ok = diff(plan.old, plan.new, |def| {
702        def.lookup_expect::<TableDef>(key).columns.iter()
703    })
704    .map(|col_diff| -> Result<_> {
705        match col_diff {
706            Diff::Add { new } => {
707                if new.default_value.is_some() {
708                    // `row_type_changed`, `columns_added`
709                    Ok(ProductMonoid(Any(false), Any(true)))
710                } else {
711                    Err(AutoMigrateError::AddColumn {
712                        table: new.table_name.clone(),
713                        column: new.name.clone(),
714                    }
715                    .into())
716                }
717            }
718            Diff::Remove { old } => Err(AutoMigrateError::RemoveColumn {
719                table: old.table_name.clone(),
720                column: old.name.clone(),
721            }
722            .into()),
723            Diff::MaybeChange { old, new } => {
724                // Check column type upgradability.
725                let old_ty = WithTypespace::new(plan.old.typespace(), &old.ty)
726                    .resolve_refs()
727                    .expect("valid TableDef must have valid type refs");
728                let new_ty = WithTypespace::new(plan.new.typespace(), &new.ty)
729                    .resolve_refs()
730                    .expect("valid TableDef must have valid type refs");
731                let types_ok = ensure_old_ty_upgradable_to_new(
732                    false,
733                    &|| old.table_name.clone(),
734                    &|| old.name.clone(),
735                    &old_ty,
736                    &new_ty,
737                );
738
739                // Note that the diff algorithm relies on `ModuleDefLookup` for `ColumnDef`,
740                // which looks up columns by NAME, NOT position: precisely to allow this step to work!
741
742                // Note: We reject changes to positions. This means that, if a column was present in the old version of the table,
743                // it must be in the same place in the new version of the table.
744                // This guarantees that any added columns live at the end of the table.
745                let positions_ok = if old.col_id == new.col_id {
746                    Ok(())
747                } else {
748                    Err(AutoMigrateError::ReorderTable {
749                        table: old.table_name.clone(),
750                    }
751                    .into())
752                };
753
754                (types_ok, positions_ok)
755                    .combine_errors()
756                    // row_type_changed, column_added
757                    .map(|(x, _)| ProductMonoid(x, Any(false)))
758            }
759        }
760    })
761    .collect_all_errors::<ProductMonoid<Any, Any>>();
762
763    let ((), (), ProductMonoid(Any(row_type_changed), Any(columns_added))) =
764        (type_ok, event_ok, columns_ok).combine_errors()?;
765
766    // If we're adding a column, we'll rewrite the whole table.
767    // That makes any `ChangeColumns` moot, so we can skip it.
768    if columns_added {
769        plan.ensure_disconnect_all_users();
770        plan.steps.push(AutoMigrateStep::AddColumns(key));
771    } else if row_type_changed {
772        plan.steps.push(AutoMigrateStep::ChangeColumns(key));
773    }
774
775    Ok(())
776}
777
778/// An "any" monoid with `false` as identity and `|` as the operator.
779#[derive(Default)]
780struct Any(bool);
781
782impl FromIterator<Any> for Any {
783    fn from_iter<T: IntoIterator<Item = Any>>(iter: T) -> Self {
784        Any(iter.into_iter().any(|Any(x)| x))
785    }
786}
787
788impl BitOr for Any {
789    type Output = Self;
790    fn bitor(self, rhs: Self) -> Self::Output {
791        Self(self.0 | rhs.0)
792    }
793}
794
795/// A monoid that allows running two `Any`s in parallel.
796#[derive(Default)]
797struct ProductMonoid<M1, M2>(M1, M2);
798
799impl<M1: BitOr<Output = M1>, M2: BitOr<Output = M2>> BitOr for ProductMonoid<M1, M2> {
800    type Output = Self;
801
802    fn bitor(self, rhs: Self) -> Self::Output {
803        Self(self.0 | rhs.0, self.1 | rhs.1)
804    }
805}
806
807impl<M1: BitOr<Output = M1> + Default, M2: BitOr<Output = M2> + Default> FromIterator<ProductMonoid<M1, M2>>
808    for ProductMonoid<M1, M2>
809{
810    fn from_iter<T: IntoIterator<Item = ProductMonoid<M1, M2>>>(iter: T) -> Self {
811        iter.into_iter().reduce(|p1, p2| p1 | p2).unwrap_or_default()
812    }
813}
814
815fn ensure_old_ty_upgradable_to_new(
816    within: bool,
817    old_container_name: &impl Fn() -> Identifier,
818    old_column_name: &impl Fn() -> Identifier,
819    old_ty: &AlgebraicType,
820    new_ty: &AlgebraicType,
821) -> Result<Any> {
822    use AutoMigrateError::*;
823    // Ensures an `old_ty` within `old` is upgradable to `new_ty`.
824    let ensure =
825        |(old_ty, new_ty)| ensure_old_ty_upgradable_to_new(true, old_container_name, old_column_name, old_ty, new_ty);
826
827    // Returns a `ChangeColumnTypeParts` error using the current `old_ty` and `new_ty`.
828    let parts_for_error = || ChangeColumnTypeParts {
829        table: old_container_name(),
830        column: old_column_name(),
831        type1: old_ty.clone().into(),
832        type2: new_ty.clone().into(),
833    };
834
835    match (old_ty, new_ty) {
836        // For sums, we allow the variants in `old_ty` to be a prefix of `new_ty`.
837        (AlgebraicType::Sum(old_ty), AlgebraicType::Sum(new_ty)) => {
838            let old_vars = &*old_ty.variants;
839            let new_vars = &*new_ty.variants;
840
841            // The number of variants in `new_ty` cannot decrease.
842            let var_lens_ok = match old_vars.len().cmp(&new_vars.len()) {
843                Ordering::Less => Ok(Any(true)),
844                Ordering::Equal => Ok(Any(false)),
845                Ordering::Greater if within => Err(ChangeWithinColumnTypeFewerVariants(parts_for_error()).into()),
846                Ordering::Greater => Err(ChangeColumnTypeFewerVariants(parts_for_error()).into()),
847            };
848
849            // The variants in `old_ty` must be upgradable to those in `old_ty`.
850            // Strict equality is *not* imposed in the prefix!
851            let prefix_ok = old_vars
852                .iter()
853                .zip(new_vars)
854                .map(|(o, n)| {
855                    // Ensure type compatibility.
856                    let res_ty = ensure((&o.algebraic_type, &n.algebraic_type));
857                    // Ensure name doesn't change.
858                    let res_name = if o.name() == n.name() {
859                        Ok(())
860                    } else if within {
861                        Err(ChangeWithinColumnTypeRenamedVariant(parts_for_error()).into())
862                    } else {
863                        Err(ChangeColumnTypeRenamedVariant(parts_for_error()).into())
864                    };
865                    (res_ty, res_name).combine_errors().map(|(c, ())| c)
866                })
867                .collect_all_errors::<Any>();
868
869            // The old and the new sum types must have matching layout sizes and alignments.
870            let old_ty = SumTypeLayout::from(old_ty.clone());
871            let new_ty = SumTypeLayout::from(new_ty.clone());
872            let old_layout = old_ty.layout();
873            let new_layout = new_ty.layout();
874            let size_ok = if old_layout.size == new_layout.size {
875                Ok(())
876            } else if within {
877                Err(ChangeWithinColumnTypeSizeMismatch(parts_for_error()).into())
878            } else {
879                Err(ChangeColumnTypeSizeMismatch(parts_for_error()).into())
880            };
881            let align_ok = if old_layout.align == new_layout.align {
882                Ok(())
883            } else if within {
884                Err(ChangeWithinColumnTypeAlignMismatch(parts_for_error()).into())
885            } else {
886                Err(ChangeColumnTypeAlignMismatch(parts_for_error()).into())
887            };
888
889            let (len_changed, prefix_changed, ..) = (var_lens_ok, prefix_ok, size_ok, align_ok).combine_errors()?;
890            Ok(len_changed | prefix_changed)
891        }
892
893        // For products,
894        // we need to check each field's upgradability due to sums,
895        // and there must be as many fields.
896        // Note that we don't care about field names.
897        (AlgebraicType::Product(old_ty), AlgebraicType::Product(new_ty)) => {
898            // The number of variants in `new_ty` cannot decrease.
899            let len_eq_ok = if old_ty.len() == new_ty.len() {
900                Ok(())
901            } else {
902                Err(if within {
903                    ChangeWithinColumnTypeFewerFields(parts_for_error())
904                } else {
905                    ChangeColumnTypeFewerFields(parts_for_error())
906                }
907                .into())
908            };
909
910            // The fields in `old_ty` must be upgradable to those in `old_ty`.
911            let fields_ok = old_ty
912                .iter()
913                .zip(new_ty.iter())
914                .map(|(o, n)| {
915                    // Ensure type compatibility.
916                    let res_ty = ensure((&o.algebraic_type, &n.algebraic_type));
917                    // Ensure name doesn't change.
918                    let res_name = if o.name() == n.name() {
919                        Ok(())
920                    } else if within {
921                        Err(ChangeWithinColumnTypeRenamedField(parts_for_error()).into())
922                    } else {
923                        Err(ChangeColumnTypeRenamedField(parts_for_error()).into())
924                    };
925                    (res_ty, res_name).combine_errors().map(|(c, ())| c)
926                })
927                .collect_all_errors::<Any>();
928
929            (len_eq_ok, fields_ok).combine_errors().map(|(_, x)| x)
930        }
931
932        // For arrays, we need to check each field's upgradability due to sums.
933        (AlgebraicType::Array(old_ty), AlgebraicType::Array(new_ty)) => ensure_old_ty_upgradable_to_new(
934            true,
935            old_container_name,
936            old_column_name,
937            &old_ty.elem_ty,
938            &new_ty.elem_ty,
939        ),
940
941        // We only have the simple cases left, and there, no change is good change.
942        (old_ty, new_ty) if old_ty == new_ty => Ok(Any(false)),
943        _ => Err(if within {
944            ChangeWithinColumnType(parts_for_error())
945        } else {
946            ChangeColumnType(parts_for_error())
947        }
948        .into()),
949    }
950}
951
952fn auto_migrate_indexes(
953    plan: &mut AutoMigratePlan<'_>,
954    new_tables: &HashSet<&Identifier>,
955    removed_tables: &HashSet<&Identifier>,
956) -> Result<()> {
957    diff(plan.old, plan.new, ModuleDef::indexes)
958        .map(|index_diff| -> Result<()> {
959            match index_diff {
960                Diff::Add { new } => {
961                    if !new_tables.contains(&plan.new.stored_in_table_def(&new.name).unwrap().name) {
962                        plan.steps.push(AutoMigrateStep::AddIndex(new.key()));
963                    }
964                    Ok(())
965                }
966                Diff::Remove { old } => {
967                    if !removed_tables.contains(&plan.old.stored_in_table_def(&old.name).unwrap().name) {
968                        plan.steps.push(AutoMigrateStep::RemoveIndex(old.key()));
969                    }
970                    Ok(())
971                }
972                Diff::MaybeChange { old, new } => {
973                    if old.accessor_name != new.accessor_name {
974                        Err(AutoMigrateError::ChangeIndexAccessor {
975                            index: old.name.clone(),
976                            old_accessor: old.accessor_name.clone(),
977                            new_accessor: new.accessor_name.clone(),
978                        }
979                        .into())
980                    } else {
981                        if old.algorithm != new.algorithm {
982                            plan.steps.push(AutoMigrateStep::RemoveIndex(old.key()));
983                            plan.steps.push(AutoMigrateStep::AddIndex(old.key()));
984                        }
985                        Ok(())
986                    }
987                }
988            }
989        })
990        .collect_all_errors()
991}
992
993fn auto_migrate_sequences(
994    plan: &mut AutoMigratePlan,
995    new_tables: &HashSet<&Identifier>,
996    removed_tables: &HashSet<&Identifier>,
997) -> Result<()> {
998    diff(plan.old, plan.new, ModuleDef::sequences)
999        .map(|sequence_diff| -> Result<()> {
1000            match sequence_diff {
1001                Diff::Add { new } => {
1002                    if !new_tables.contains(&plan.new.stored_in_table_def(&new.name).unwrap().name) {
1003                        plan.prechecks
1004                            .push(AutoMigratePrecheck::CheckAddSequenceRangeValid(new.key()));
1005                        plan.steps.push(AutoMigrateStep::AddSequence(new.key()));
1006                    }
1007                    Ok(())
1008                }
1009                Diff::Remove { old } => {
1010                    if !removed_tables.contains(&plan.old.stored_in_table_def(&old.name).unwrap().name) {
1011                        plan.steps.push(AutoMigrateStep::RemoveSequence(old.key()));
1012                    }
1013                    Ok(())
1014                }
1015                Diff::MaybeChange { old, new } => {
1016                    // we do not need to check column ids, since in an automigrate, column ids are not changed.
1017                    if old != new {
1018                        plan.prechecks
1019                            .push(AutoMigratePrecheck::CheckAddSequenceRangeValid(new.key()));
1020                        plan.steps.push(AutoMigrateStep::RemoveSequence(old.key()));
1021                        plan.steps.push(AutoMigrateStep::AddSequence(new.key()));
1022                    }
1023                    Ok(())
1024                }
1025            }
1026        })
1027        .collect_all_errors()
1028}
1029
1030fn auto_migrate_constraints(
1031    plan: &mut AutoMigratePlan,
1032    new_tables: &HashSet<&Identifier>,
1033    removed_tables: &HashSet<&Identifier>,
1034) -> Result<()> {
1035    diff(plan.old, plan.new, ModuleDef::constraints)
1036        .map(|constraint_diff| -> Result<()> {
1037            match constraint_diff {
1038                Diff::Add { new } => {
1039                    if new_tables.contains(&plan.new.stored_in_table_def(&new.name).unwrap().name) {
1040                        // it's okay to add a constraint in a new table.
1041                        Ok(())
1042                    } else {
1043                        // it's not okay to add a new constraint to an existing table.
1044                        Err(AutoMigrateError::AddUniqueConstraint {
1045                            constraint: new.name.clone(),
1046                        }
1047                        .into())
1048                    }
1049                }
1050                Diff::Remove { old } => {
1051                    if !removed_tables.contains(&plan.old.stored_in_table_def(&old.name).unwrap().name) {
1052                        plan.steps.push(AutoMigrateStep::RemoveConstraint(old.key()));
1053                    }
1054                    Ok(())
1055                }
1056                Diff::MaybeChange { old, new } => {
1057                    if old == new {
1058                        Ok(())
1059                    } else {
1060                        Err(AutoMigrateError::ChangeUniqueConstraint {
1061                            constraint: old.name.clone(),
1062                        }
1063                        .into())
1064                    }
1065                }
1066            }
1067        })
1068        .collect_all_errors()
1069}
1070
1071// Because we can refer to many tables and fields on the row level-security query, we need to remove all of them,
1072// then add the new ones, instead of trying to track the graph of dependencies.
1073fn auto_migrate_row_level_security(plan: &mut AutoMigratePlan) -> Result<()> {
1074    // Track if any RLS rules were changed.
1075    let mut old_rls = HashSet::new();
1076    let mut new_rls = HashSet::new();
1077
1078    for rls in plan.old.row_level_security() {
1079        old_rls.insert(rls.key());
1080        plan.steps.push(AutoMigrateStep::RemoveRowLevelSecurity(rls.key()));
1081    }
1082    for rls in plan.new.row_level_security() {
1083        new_rls.insert(rls.key());
1084        plan.steps.push(AutoMigrateStep::AddRowLevelSecurity(rls.key()));
1085    }
1086
1087    // We can force flush the cache by force disconnecting all clients if an RLS rule has been added, removed, or updated.
1088    if old_rls != new_rls {
1089        plan.ensure_disconnect_all_users();
1090    }
1091
1092    Ok(())
1093}
1094
1095#[cfg(test)]
1096mod tests {
1097    use super::*;
1098    use spacetimedb_data_structures::expect_error_matching;
1099    use spacetimedb_lib::{
1100        db::raw_def::{v9::btree, *},
1101        AlgebraicType, AlgebraicValue, ProductType, ScheduleAt,
1102    };
1103    use spacetimedb_primitives::ColId;
1104    use v9::{RawModuleDefV9Builder, TableAccess};
1105    use validate::tests::expect_identifier;
1106
1107    fn create_module_def(build_module: impl Fn(&mut RawModuleDefV9Builder)) -> ModuleDef {
1108        let mut builder = RawModuleDefV9Builder::new();
1109        build_module(&mut builder);
1110        builder
1111            .finish()
1112            .try_into()
1113            .expect("new_def should be a valid database definition")
1114    }
1115
1116    fn initial_module_def() -> ModuleDef {
1117        let mut builder = RawModuleDefV9Builder::new();
1118        let schedule_at = builder.add_type::<ScheduleAt>();
1119        let sum_ty = AlgebraicType::sum([("v1", AlgebraicType::U64)]);
1120        let sum_refty = builder.add_algebraic_type([], "sum", sum_ty, true);
1121        builder
1122            .build_table_with_new_type(
1123                "Apples",
1124                ProductType::from([
1125                    ("id", AlgebraicType::U64),
1126                    ("name", AlgebraicType::String),
1127                    ("count", AlgebraicType::U16),
1128                    ("sum", sum_refty.into()),
1129                ]),
1130                true,
1131            )
1132            .with_column_sequence(0)
1133            .with_unique_constraint(ColId(0))
1134            .with_index(btree(0), "id_index")
1135            .with_index(btree([0, 1]), "id_name_index")
1136            .finish();
1137
1138        builder
1139            .build_table_with_new_type(
1140                "Bananas",
1141                ProductType::from([
1142                    ("id", AlgebraicType::U64),
1143                    ("name", AlgebraicType::String),
1144                    ("count", AlgebraicType::U16),
1145                ]),
1146                true,
1147            )
1148            .with_access(TableAccess::Public)
1149            .finish();
1150
1151        let deliveries_type = builder
1152            .build_table_with_new_type(
1153                "Deliveries",
1154                ProductType::from([
1155                    ("scheduled_id", AlgebraicType::U64),
1156                    ("scheduled_at", schedule_at.clone()),
1157                    ("sum", AlgebraicType::array(sum_refty.into())),
1158                ]),
1159                true,
1160            )
1161            .with_auto_inc_primary_key(0)
1162            .with_index_no_accessor_name(btree(0))
1163            .with_schedule("check_deliveries", 1)
1164            .finish();
1165        builder.add_reducer(
1166            "check_deliveries",
1167            ProductType::from([("a", AlgebraicType::Ref(deliveries_type))]),
1168            None,
1169        );
1170
1171        // Add a view and add its return type to the typespace
1172        let view_return_ty = AlgebraicType::product([("a", AlgebraicType::U64), ("b", AlgebraicType::U64)]);
1173        let view_return_ty_ref = builder.add_algebraic_type([], "my_view_return", view_return_ty, true);
1174        builder.add_view(
1175            "my_view",
1176            0,
1177            true,
1178            true,
1179            ProductType::from([("x", AlgebraicType::U32), ("y", AlgebraicType::U32)]),
1180            AlgebraicType::option(AlgebraicType::Ref(view_return_ty_ref)),
1181        );
1182
1183        builder
1184            .build_table_with_new_type(
1185                "Inspections",
1186                ProductType::from([
1187                    ("scheduled_id", AlgebraicType::U64),
1188                    ("scheduled_at", schedule_at.clone()),
1189                ]),
1190                true,
1191            )
1192            .with_auto_inc_primary_key(0)
1193            .with_index_no_accessor_name(btree(0))
1194            .finish();
1195
1196        builder.add_row_level_security("SELECT * FROM Apples");
1197
1198        builder
1199            .finish()
1200            .try_into()
1201            .expect("old_def should be a valid database definition")
1202    }
1203
1204    fn updated_module_def() -> ModuleDef {
1205        let mut builder = RawModuleDefV9Builder::new();
1206        let _ = builder.add_type::<u32>(); // reposition ScheduleAt in the typespace, should have no effect.
1207        let schedule_at = builder.add_type::<ScheduleAt>();
1208        let sum_ty = AlgebraicType::sum([("v1", AlgebraicType::U64), ("v2", AlgebraicType::Bool)]);
1209        let sum_refty = builder.add_algebraic_type([], "sum", sum_ty, true);
1210        builder
1211            .build_table_with_new_type(
1212                "Apples",
1213                ProductType::from([
1214                    ("id", AlgebraicType::U64),
1215                    ("name", AlgebraicType::String),
1216                    ("count", AlgebraicType::U16),
1217                    ("sum", sum_refty.into()),
1218                ]),
1219                true,
1220            )
1221            // remove sequence
1222            // remove unique constraint
1223            .with_index(btree(0), "id_index")
1224            // remove ["id", "name"] index
1225            // add ["id", "count"] index
1226            .with_index(btree([0, 2]), "id_count_index")
1227            .finish();
1228
1229        builder
1230            .build_table_with_new_type(
1231                "Bananas",
1232                ProductType::from([
1233                    ("id", AlgebraicType::U64),
1234                    ("name", AlgebraicType::String),
1235                    ("count", AlgebraicType::U16),
1236                    ("freshness", AlgebraicType::U32), // added column!
1237                ]),
1238                true,
1239            )
1240            // add column sequence
1241            .with_column_sequence(0)
1242            .with_default_column_value(3, AlgebraicValue::U32(5))
1243            // change access
1244            .with_access(TableAccess::Private)
1245            .finish();
1246
1247        let deliveries_type = builder
1248            .build_table_with_new_type(
1249                "Deliveries",
1250                ProductType::from([
1251                    ("scheduled_id", AlgebraicType::U64),
1252                    ("scheduled_at", schedule_at.clone()),
1253                    ("sum", AlgebraicType::array(sum_refty.into())),
1254                ]),
1255                true,
1256            )
1257            .with_auto_inc_primary_key(0)
1258            .with_index_no_accessor_name(btree(0))
1259            // remove schedule def
1260            .finish();
1261
1262        builder.add_reducer(
1263            "check_deliveries",
1264            ProductType::from([("a", AlgebraicType::Ref(deliveries_type))]),
1265            None,
1266        );
1267
1268        // Add a view and add its return type to the typespace
1269        let view_return_ty = AlgebraicType::product([("a", AlgebraicType::U64)]);
1270        let view_return_ty_ref = builder.add_algebraic_type([], "my_view_return", view_return_ty, true);
1271        builder.add_view(
1272            "my_view",
1273            0,
1274            true,
1275            true,
1276            ProductType::from([("x", AlgebraicType::U32)]),
1277            AlgebraicType::option(AlgebraicType::Ref(view_return_ty_ref)),
1278        );
1279
1280        let new_inspections_type = builder
1281            .build_table_with_new_type(
1282                "Inspections",
1283                ProductType::from([
1284                    ("scheduled_id", AlgebraicType::U64),
1285                    ("scheduled_at", schedule_at.clone()),
1286                ]),
1287                true,
1288            )
1289            .with_auto_inc_primary_key(0)
1290            .with_index_no_accessor_name(btree(0))
1291            // add schedule def
1292            .with_schedule("perform_inspection", 1)
1293            .finish();
1294
1295        // add reducer.
1296        builder.add_reducer(
1297            "perform_inspection",
1298            ProductType::from([("a", AlgebraicType::Ref(new_inspections_type))]),
1299            None,
1300        );
1301
1302        // Add new table
1303        builder
1304            .build_table_with_new_type("Oranges", ProductType::from([("id", AlgebraicType::U32)]), true)
1305            .with_index(btree(0), "id_index")
1306            .with_column_sequence(0)
1307            .with_unique_constraint(0)
1308            .with_primary_key(0)
1309            .finish();
1310
1311        builder.add_row_level_security("SELECT * FROM Bananas");
1312
1313        builder
1314            .finish()
1315            .try_into()
1316            .expect("new_def should be a valid database definition")
1317    }
1318
1319    #[test]
1320    fn successful_auto_migration() {
1321        let old_def = initial_module_def();
1322        let new_def = updated_module_def();
1323        let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
1324
1325        let apples = expect_identifier("Apples");
1326        let bananas = expect_identifier("Bananas");
1327        let deliveries = expect_identifier("Deliveries");
1328        let oranges = expect_identifier("Oranges");
1329        let my_view = expect_identifier("my_view");
1330
1331        let bananas_sequence: RawIdentifier = "Bananas_id_seq".into();
1332        let apples_unique_constraint: RawIdentifier = "Apples_id_key".into();
1333        let apples_sequence: RawIdentifier = "Apples_id_seq".into();
1334        let apples_id_name_index: RawIdentifier = "Apples_id_name_idx_btree".into();
1335        let apples_id_count_index: RawIdentifier = "Apples_id_count_idx_btree".into();
1336        let deliveries_schedule = expect_identifier("Deliveries_sched");
1337        let inspections_schedule = expect_identifier("Inspections_sched");
1338
1339        assert!(plan.prechecks.is_sorted());
1340
1341        assert_eq!(plan.prechecks.len(), 1);
1342        assert_eq!(
1343            plan.prechecks[0],
1344            AutoMigratePrecheck::CheckAddSequenceRangeValid(&bananas_sequence)
1345        );
1346        let sql_old = RawRowLevelSecurityDefV9 {
1347            sql: "SELECT * FROM Apples".into(),
1348        };
1349
1350        let sql_new = RawRowLevelSecurityDefV9 {
1351            sql: "SELECT * FROM Bananas".into(),
1352        };
1353
1354        let steps = &plan.steps[..];
1355
1356        assert!(steps.is_sorted());
1357
1358        assert!(
1359            steps.contains(&AutoMigrateStep::RemoveSequence(&apples_sequence)),
1360            "{steps:?}"
1361        );
1362        assert!(
1363            steps.contains(&AutoMigrateStep::RemoveConstraint(&apples_unique_constraint)),
1364            "{steps:?}"
1365        );
1366        assert!(
1367            steps.contains(&AutoMigrateStep::RemoveIndex(&apples_id_name_index)),
1368            "{steps:?}"
1369        );
1370        assert!(
1371            steps.contains(&AutoMigrateStep::AddIndex(&apples_id_count_index)),
1372            "{steps:?}"
1373        );
1374
1375        assert!(steps.contains(&AutoMigrateStep::ChangeAccess(&bananas)), "{steps:?}");
1376        assert!(
1377            steps.contains(&AutoMigrateStep::AddSequence(&bananas_sequence)),
1378            "{steps:?}"
1379        );
1380
1381        assert!(steps.contains(&AutoMigrateStep::AddTable(&oranges)), "{steps:?}");
1382
1383        assert!(
1384            steps.contains(&AutoMigrateStep::RemoveSchedule(&deliveries_schedule)),
1385            "{steps:?}"
1386        );
1387        assert!(
1388            steps.contains(&AutoMigrateStep::AddSchedule(&inspections_schedule)),
1389            "{steps:?}"
1390        );
1391
1392        assert!(
1393            steps.contains(&AutoMigrateStep::RemoveRowLevelSecurity(&sql_old.sql)),
1394            "{steps:?}"
1395        );
1396        assert!(
1397            steps.contains(&AutoMigrateStep::AddRowLevelSecurity(&sql_new.sql)),
1398            "{steps:?}"
1399        );
1400
1401        assert!(steps.contains(&AutoMigrateStep::ChangeColumns(&apples)), "{steps:?}");
1402        assert!(
1403            steps.contains(&AutoMigrateStep::ChangeColumns(&deliveries)),
1404            "{steps:?}"
1405        );
1406
1407        assert!(steps.contains(&AutoMigrateStep::DisconnectAllUsers), "{steps:?}");
1408        assert!(steps.contains(&AutoMigrateStep::AddColumns(&bananas)), "{steps:?}");
1409        // Column is changed but it will not reflect in steps due to `AutoMigrateStep::AddColumns`
1410        assert!(!steps.contains(&AutoMigrateStep::ChangeColumns(&bananas)), "{steps:?}");
1411
1412        assert!(steps.contains(&AutoMigrateStep::RemoveView(&my_view)), "{steps:?}");
1413        assert!(steps.contains(&AutoMigrateStep::AddView(&my_view)), "{steps:?}");
1414    }
1415
1416    #[test]
1417    fn auto_migration_errors() {
1418        let mut old_builder = RawModuleDefV9Builder::new();
1419
1420        let foo2_ty = AlgebraicType::sum([
1421            ("foo21", AlgebraicType::Bool),
1422            ("foo22", AlgebraicType::U32),
1423            ("foo23", AlgebraicType::U32),
1424        ]);
1425        let foo2_refty = old_builder.add_algebraic_type([], "foo2", foo2_ty.clone(), true);
1426        let foo_ty = AlgebraicType::product([
1427            ("foo1", AlgebraicType::String),
1428            ("foo2", foo2_refty.into()),
1429            ("foo3", AlgebraicType::I32),
1430        ]);
1431        let foo_refty = old_builder.add_algebraic_type([], "foo", foo_ty.clone(), true);
1432        let sum1_ty = AlgebraicType::sum([
1433            ("foo", AlgebraicType::array(foo_refty.into())),
1434            ("bar", AlgebraicType::U128),
1435        ]);
1436        let sum1_refty = old_builder.add_algebraic_type([], "sum1", sum1_ty.clone(), true);
1437
1438        let prod1_ty = AlgebraicType::product([
1439            ("baz", AlgebraicType::Bool),
1440            // We'll remove this field.
1441            ("qux", AlgebraicType::Bool),
1442        ]);
1443        let prod1_refty = old_builder.add_algebraic_type([], "prod1", prod1_ty.clone(), true);
1444
1445        old_builder
1446            .build_table_with_new_type(
1447                "Apples",
1448                ProductType::from([
1449                    ("id", AlgebraicType::U64),
1450                    ("name", AlgebraicType::String),
1451                    ("sum1", sum1_refty.into()),
1452                    ("prod1", prod1_refty.into()),
1453                    ("count", AlgebraicType::U16),
1454                ]),
1455                true,
1456            )
1457            .with_index(btree(0), "id_index")
1458            .with_unique_constraint([1, 2])
1459            .with_index_no_accessor_name(btree([1, 2]))
1460            .with_type(TableType::User)
1461            .finish();
1462
1463        old_builder
1464            .build_table_with_new_type(
1465                "Bananas",
1466                ProductType::from([
1467                    ("id", AlgebraicType::U64),
1468                    ("name", AlgebraicType::String),
1469                    ("count", AlgebraicType::U16),
1470                ]),
1471                true,
1472            )
1473            .finish();
1474
1475        let old_def: ModuleDef = old_builder
1476            .finish()
1477            .try_into()
1478            .expect("old_def should be a valid database definition");
1479        let resolve_old = |ty| old_def.typespace().with_type(ty).resolve_refs().unwrap();
1480
1481        let mut new_builder = RawModuleDefV9Builder::new();
1482
1483        // Remove variant `foo23` and rename variant `foo21` to `bad`.
1484        let new_foo2_ty = AlgebraicType::sum([
1485            ("bad", AlgebraicType::Bool),
1486            // U32 -> U64
1487            ("foo22", AlgebraicType::U64),
1488        ]);
1489        let new_foo2_refty = new_builder.add_algebraic_type([], "foo2", new_foo2_ty.clone(), true);
1490        let new_foo_ty = AlgebraicType::product([
1491            // Remove field `foo3` and rename `foo1` to `bad`.
1492            ("bad", AlgebraicType::String),
1493            ("foo2", new_foo2_refty.into()),
1494        ]);
1495        let new_foo_refty = new_builder.add_algebraic_type([], "foo", new_foo_ty.clone(), true);
1496        let new_sum1_ty = AlgebraicType::sum([
1497            // Remove variant `bar` and rename `foo` to `bad`.
1498            ("bad", AlgebraicType::array(new_foo_refty.into())),
1499        ]);
1500        let new_sum1_refty = new_builder.add_algebraic_type([], "sum1", new_sum1_ty.clone(), true);
1501
1502        let new_prod1_ty = AlgebraicType::product([
1503            // Removed field `qux` and renamed `baz` to `bad`.
1504            ("bad", AlgebraicType::Bool),
1505        ]);
1506        let new_prod1_refty = new_builder.add_algebraic_type([], "prod1", new_prod1_ty.clone(), true);
1507
1508        new_builder
1509            .build_table_with_new_type(
1510                "Apples",
1511                ProductType::from([
1512                    ("name", AlgebraicType::U32), // change type of `name`
1513                    ("id", AlgebraicType::U64),   // change order
1514                    ("sum1", new_sum1_refty.into()),
1515                    ("prod1", new_prod1_refty.into()),
1516                    // remove count
1517                    ("weight", AlgebraicType::U16), // add weight; we don't set a default, which makes this an error.
1518                ]),
1519                true,
1520            )
1521            .with_index(
1522                btree(1),
1523                "id_index_new_accessor", // change accessor name
1524            )
1525            .with_unique_constraint([1, 0])
1526            .with_index_no_accessor_name(btree([1, 0]))
1527            .with_unique_constraint(0)
1528            .with_index_no_accessor_name(btree(0)) // add unique constraint
1529            .with_type(TableType::System) // change type
1530            .finish();
1531
1532        // Invalid row-level security queries can't be detected in the ponder_auto_migrate function, they
1533        // are detected when executing the plan because they depend on the database state.
1534        // new_builder.add_row_level_security("SELECT wrong");
1535
1536        // remove Bananas
1537        let new_def: ModuleDef = new_builder
1538            .finish()
1539            .try_into()
1540            .expect("new_def should be a valid database definition");
1541        let resolve_new = |ty| new_def.typespace().with_type(ty).resolve_refs().unwrap();
1542
1543        let result = ponder_auto_migrate(&old_def, &new_def);
1544
1545        let apples = expect_identifier("Apples");
1546        let _bananas = expect_identifier("Bananas");
1547
1548        let apples_name_unique_constraint = "Apples_name_key";
1549
1550        let weight = expect_identifier("weight");
1551        let count = expect_identifier("count");
1552        let name = expect_identifier("name");
1553        let sum1 = expect_identifier("sum1");
1554        let prod1 = expect_identifier("prod1");
1555
1556        expect_error_matching!(
1557            result,
1558            // This is an error because we didn't set a default value.
1559            AutoMigrateError::AddColumn {
1560                table,
1561                column
1562            } => table == &apples && column == &weight
1563        );
1564
1565        expect_error_matching!(
1566            result,
1567            AutoMigrateError::RemoveColumn {
1568                table,
1569                column
1570            } => table == &apples && column == &count
1571        );
1572
1573        expect_error_matching!(
1574            result,
1575            AutoMigrateError::ReorderTable { table } => table == &apples
1576        );
1577
1578        expect_error_matching!(
1579            result,
1580            AutoMigrateError::ChangeColumnType(ChangeColumnTypeParts {
1581                table,
1582                column,
1583                type1,
1584                type2
1585            }) => table == &apples && column == &name && type1.0 == AlgebraicType::String && type2.0 == AlgebraicType::U32
1586        );
1587
1588        // Rename variant `foo21`.
1589        expect_error_matching!(
1590            result,
1591            AutoMigrateError::ChangeWithinColumnTypeRenamedVariant(ChangeColumnTypeParts {
1592                table,
1593                column,
1594                type1,
1595                type2
1596            }) => table == &apples && column == &sum1
1597            && type1.0 == foo2_ty && type2.0 == new_foo2_ty
1598        );
1599
1600        // foo22: U32 -> U64.
1601        expect_error_matching!(
1602            result,
1603            AutoMigrateError::ChangeWithinColumnType(ChangeColumnTypeParts {
1604                table,
1605                column,
1606                type1,
1607                type2
1608            }) => table == &apples && column == &sum1
1609            && type1.0 == AlgebraicType::U32 && type2.0 == AlgebraicType::U64
1610        );
1611
1612        // Remove variant `foo23`.
1613        expect_error_matching!(
1614            result,
1615            AutoMigrateError::ChangeWithinColumnTypeFewerVariants(ChangeColumnTypeParts {
1616                table,
1617                column,
1618                type1,
1619                type2
1620            }) => table == &apples && column == &sum1
1621            && type1.0 == foo2_ty && type2.0 == new_foo2_ty
1622        );
1623
1624        // Size of inner sum changed.
1625        expect_error_matching!(
1626            result,
1627            AutoMigrateError::ChangeWithinColumnTypeSizeMismatch(ChangeColumnTypeParts {
1628                table,
1629                column,
1630                type1,
1631                type2
1632            }) => table == &apples && column == &sum1
1633            && type1.0 == foo2_ty && type2.0 == new_foo2_ty
1634        );
1635
1636        // Align of inner sum changed.
1637        expect_error_matching!(
1638            result,
1639            AutoMigrateError::ChangeWithinColumnTypeAlignMismatch(ChangeColumnTypeParts {
1640                table,
1641                column,
1642                type1,
1643                type2
1644            }) => table == &apples && column == &sum1
1645            && type1.0 == foo2_ty && type2.0 == new_foo2_ty
1646        );
1647
1648        // Rename field `foo1`.
1649        expect_error_matching!(
1650            result,
1651            AutoMigrateError::ChangeWithinColumnTypeRenamedField(ChangeColumnTypeParts {
1652                table,
1653                column,
1654                type1,
1655                type2
1656            }) => table == &apples && column == &sum1
1657            && type1.0 == resolve_old(&foo_ty) && type2.0 == resolve_new(&new_foo_ty)
1658        );
1659
1660        // Remove field `foo3`.
1661        expect_error_matching!(
1662            result,
1663            AutoMigrateError::ChangeWithinColumnTypeFewerFields(ChangeColumnTypeParts {
1664                table,
1665                column,
1666                type1,
1667                type2
1668            }) => table == &apples && column == &sum1
1669            && type1.0 == resolve_old(&foo_ty) && type2.0 == resolve_new(&new_foo_ty)
1670        );
1671
1672        // Rename variant `bar`.
1673        expect_error_matching!(
1674            result,
1675            AutoMigrateError::ChangeColumnTypeRenamedVariant(ChangeColumnTypeParts {
1676                table,
1677                column,
1678                type1,
1679                type2
1680            }) => table == &apples && column == &sum1
1681            && type1.0 == resolve_old(&sum1_ty) && type2.0 == resolve_new(&new_sum1_ty)
1682        );
1683
1684        // Remove variant `bar`.
1685        expect_error_matching!(
1686            result,
1687            AutoMigrateError::ChangeColumnTypeFewerVariants(ChangeColumnTypeParts {
1688                table,
1689                column,
1690                type1,
1691                type2
1692            }) => table == &apples && column == &sum1
1693            && type1.0 == resolve_old(&sum1_ty) && type2.0 == resolve_new(&new_sum1_ty)
1694        );
1695
1696        // Size of outer sum changed.
1697        expect_error_matching!(
1698            result,
1699            AutoMigrateError::ChangeColumnTypeSizeMismatch(ChangeColumnTypeParts {
1700                table,
1701                column,
1702                type1,
1703                type2
1704            }) => table == &apples && column == &sum1
1705            && type1.0 == resolve_old(&sum1_ty) && type2.0 == resolve_new(&new_sum1_ty)
1706        );
1707
1708        // Align of outer sum changed.
1709        expect_error_matching!(
1710            result,
1711            AutoMigrateError::ChangeColumnTypeAlignMismatch(ChangeColumnTypeParts {
1712                table,
1713                column,
1714                type1,
1715                type2
1716            }) => table == &apples && column == &sum1
1717            && type1.0 == resolve_old(&sum1_ty) && type2.0 == resolve_new(&new_sum1_ty)
1718        );
1719
1720        // Rename field `baz`.
1721        expect_error_matching!(
1722            result,
1723            AutoMigrateError::ChangeColumnTypeRenamedField(ChangeColumnTypeParts {
1724                table,
1725                column,
1726                type1,
1727                type2
1728            }) => table == &apples && column == &prod1
1729            && type1.0 == prod1_ty && type2.0 == new_prod1_ty
1730        );
1731
1732        // Remove field `qux`.
1733        expect_error_matching!(
1734            result,
1735            AutoMigrateError::ChangeColumnTypeFewerFields(ChangeColumnTypeParts {
1736                table,
1737                column,
1738                type1,
1739                type2
1740            }) => table == &apples && column == &prod1
1741            && type1.0 == prod1_ty && type2.0 == new_prod1_ty
1742        );
1743
1744        expect_error_matching!(
1745            result,
1746            AutoMigrateError::AddUniqueConstraint { constraint } => &constraint[..] == apples_name_unique_constraint
1747        );
1748
1749        expect_error_matching!(
1750            result,
1751            AutoMigrateError::ChangeTableType { table, type1, type2 } => table == &apples && type1 == &TableType::User && type2 == &TableType::System
1752        );
1753
1754        // Note: RemoveTable is no longer an error — removing tables is now allowed
1755        // for empty tables; the emptiness check happens at execution time in update.rs.
1756
1757        let apples_id_index = "Apples_id_idx_btree";
1758        let accessor_old = expect_identifier("id_index");
1759        let accessor_new = expect_identifier("id_index_new_accessor");
1760        expect_error_matching!(
1761            result,
1762            AutoMigrateError::ChangeIndexAccessor {
1763                index,
1764                old_accessor,
1765                new_accessor
1766            } => &index[..] == apples_id_index && old_accessor.as_ref() == Some(&accessor_old) && new_accessor.as_ref() == Some(&accessor_new)
1767        );
1768
1769        // It is not currently possible to test for `ChangeUniqueConstraint`, because unique constraint names are now generated during validation,
1770        // and are determined by their columns and table name. So it's impossible to create a unique constraint with the same name
1771        // but different columns from an old one.
1772        // We've left the check in, just in case this changes in the future.
1773    }
1774    #[test]
1775    fn print_empty_to_populated_schema_migration() {
1776        // Start with completely empty schema
1777        let old_builder = RawModuleDefV9Builder::new();
1778        let old_def: ModuleDef = old_builder
1779            .finish()
1780            .try_into()
1781            .expect("old_def should be a valid database definition");
1782
1783        let new_def = initial_module_def();
1784        let plan = ponder_migrate(&old_def, &new_def).expect("auto migration should succeed");
1785
1786        insta::assert_snapshot!(
1787            "empty_to_populated_migration",
1788            plan.pretty_print(PrettyPrintStyle::AnsiColor)
1789                .expect("should pretty print")
1790        );
1791    }
1792
1793    #[test]
1794    fn print_supervised_migration() {
1795        let old_def = initial_module_def();
1796        let new_def = updated_module_def();
1797        let plan = ponder_migrate(&old_def, &new_def).expect("auto migration should succeed");
1798
1799        insta::assert_snapshot!(
1800            "updated pretty print",
1801            plan.pretty_print(PrettyPrintStyle::AnsiColor)
1802                .expect("should pretty print")
1803        );
1804    }
1805
1806    #[test]
1807    fn no_color_print_supervised_migration() {
1808        let old_def = initial_module_def();
1809        let new_def = updated_module_def();
1810        let plan = ponder_migrate(&old_def, &new_def).expect("auto migration should succeed");
1811
1812        insta::assert_snapshot!(
1813            "updated pretty print no color",
1814            plan.pretty_print(PrettyPrintStyle::NoColor)
1815                .expect("should pretty print")
1816        );
1817    }
1818
1819    #[test]
1820    fn add_view() {
1821        let old_def = create_module_def(|_| {});
1822        let new_def = create_module_def(|builder| {
1823            let return_type_ref = builder.add_algebraic_type(
1824                [],
1825                "my_view_return_type",
1826                AlgebraicType::product([("a", AlgebraicType::U64)]),
1827                true,
1828            );
1829            builder.add_view(
1830                "my_view",
1831                0,
1832                true,
1833                true,
1834                ProductType::from([("x", AlgebraicType::U32)]),
1835                AlgebraicType::array(AlgebraicType::Ref(return_type_ref)),
1836            );
1837        });
1838
1839        let my_view = expect_identifier("my_view");
1840
1841        let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
1842        let steps = &plan.steps[..];
1843
1844        assert!(!plan.disconnects_all_users(), "{plan:#?}");
1845        assert!(steps.contains(&AutoMigrateStep::AddView(&my_view)), "{steps:?}");
1846        assert!(!steps.contains(&AutoMigrateStep::RemoveView(&my_view)), "{steps:?}");
1847    }
1848
1849    #[test]
1850    fn remove_view() {
1851        let old_def = create_module_def(|builder| {
1852            let return_type_ref = builder.add_algebraic_type(
1853                [],
1854                "my_view_return_type",
1855                AlgebraicType::product([("a", AlgebraicType::U64)]),
1856                true,
1857            );
1858            builder.add_view(
1859                "my_view",
1860                0,
1861                true,
1862                true,
1863                ProductType::from([("x", AlgebraicType::U32)]),
1864                AlgebraicType::array(AlgebraicType::Ref(return_type_ref)),
1865            );
1866        });
1867        let new_def = create_module_def(|_| {});
1868
1869        let my_view = expect_identifier("my_view");
1870
1871        let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
1872        let steps = &plan.steps[..];
1873
1874        assert!(plan.disconnects_all_users(), "{plan:#?}");
1875        assert!(steps.contains(&AutoMigrateStep::RemoveView(&my_view)), "{steps:?}");
1876        assert!(!steps.contains(&AutoMigrateStep::AddView(&my_view)), "{steps:?}");
1877    }
1878
1879    #[test]
1880    fn migrate_view_recompute() {
1881        struct TestCase {
1882            desc: &'static str,
1883            old_def: ModuleDef,
1884            new_def: ModuleDef,
1885        }
1886
1887        for TestCase {
1888            desc: name,
1889            old_def,
1890            new_def,
1891        } in [
1892            TestCase {
1893                desc: "Return `Vec<T>` instead of `Option<T>`",
1894                old_def: create_module_def(|builder| {
1895                    let return_type_ref = builder.add_algebraic_type(
1896                        [],
1897                        "my_view_return_type",
1898                        AlgebraicType::product([("a", AlgebraicType::U64)]),
1899                        true,
1900                    );
1901                    builder.add_view(
1902                        "my_view",
1903                        0,
1904                        true,
1905                        true,
1906                        ProductType::from([("x", AlgebraicType::U32)]),
1907                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
1908                    );
1909                }),
1910                new_def: create_module_def(|builder| {
1911                    let return_type_ref = builder.add_algebraic_type(
1912                        [],
1913                        "my_view_return_type",
1914                        AlgebraicType::product([("a", AlgebraicType::U64)]),
1915                        true,
1916                    );
1917                    builder.add_view(
1918                        "my_view",
1919                        0,
1920                        true,
1921                        true,
1922                        ProductType::from([("x", AlgebraicType::U32)]),
1923                        AlgebraicType::array(AlgebraicType::Ref(return_type_ref)),
1924                    );
1925                }),
1926            },
1927            TestCase {
1928                desc: "No change; recompute view",
1929                old_def: create_module_def(|builder| {
1930                    let return_type_ref = builder.add_algebraic_type(
1931                        [],
1932                        "my_view_return_type",
1933                        AlgebraicType::product([("a", AlgebraicType::U64)]),
1934                        true,
1935                    );
1936                    builder.add_view(
1937                        "my_view",
1938                        0,
1939                        true,
1940                        true,
1941                        ProductType::from([("x", AlgebraicType::U32)]),
1942                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
1943                    );
1944                }),
1945                new_def: create_module_def(|builder| {
1946                    let return_type_ref = builder.add_algebraic_type(
1947                        [],
1948                        "my_view_return_type",
1949                        AlgebraicType::product([("a", AlgebraicType::U64)]),
1950                        true,
1951                    );
1952                    builder.add_view(
1953                        "my_view",
1954                        0,
1955                        true,
1956                        true,
1957                        ProductType::from([("x", AlgebraicType::U32)]),
1958                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
1959                    );
1960                }),
1961            },
1962        ] {
1963            let my_view = expect_identifier("my_view");
1964
1965            let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
1966            let steps = &plan.steps[..];
1967
1968            assert!(!plan.disconnects_all_users(), "{name}, plan: {plan:#?}");
1969
1970            assert!(
1971                steps.contains(&AutoMigrateStep::UpdateView(&my_view)),
1972                "{name}, steps: {steps:?}"
1973            );
1974            assert!(
1975                !steps.contains(&AutoMigrateStep::AddView(&my_view)),
1976                "{name}, steps: {steps:?}"
1977            );
1978            assert!(
1979                !steps.contains(&AutoMigrateStep::RemoveView(&my_view)),
1980                "{name}, steps: {steps:?}"
1981            );
1982        }
1983    }
1984
1985    #[test]
1986    fn migrate_view_disconnect_clients() {
1987        struct TestCase {
1988            desc: &'static str,
1989            old_def: ModuleDef,
1990            new_def: ModuleDef,
1991        }
1992
1993        for TestCase {
1994            desc: name,
1995            old_def,
1996            new_def,
1997        } in [
1998            TestCase {
1999                desc: "Change context parameter",
2000                old_def: create_module_def(|builder| {
2001                    let return_type_ref = builder.add_algebraic_type(
2002                        [],
2003                        "my_view_return_type",
2004                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2005                        true,
2006                    );
2007                    builder.add_view(
2008                        "my_view",
2009                        0,
2010                        true,
2011                        true,
2012                        ProductType::from([("x", AlgebraicType::U32)]),
2013                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2014                    );
2015                }),
2016                new_def: create_module_def(|builder| {
2017                    let return_type_ref = builder.add_algebraic_type(
2018                        [],
2019                        "my_view_return_type",
2020                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2021                        true,
2022                    );
2023                    builder.add_view(
2024                        "my_view",
2025                        0,
2026                        true,
2027                        false,
2028                        ProductType::from([("x", AlgebraicType::U32)]),
2029                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2030                    );
2031                }),
2032            },
2033            TestCase {
2034                desc: "Add parameter",
2035                old_def: create_module_def(|builder| {
2036                    let return_type_ref = builder.add_algebraic_type(
2037                        [],
2038                        "my_view_return_type",
2039                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2040                        true,
2041                    );
2042                    builder.add_view(
2043                        "my_view",
2044                        0,
2045                        true,
2046                        true,
2047                        ProductType::from([("x", AlgebraicType::U32)]),
2048                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2049                    );
2050                }),
2051                new_def: create_module_def(|builder| {
2052                    let return_type_ref = builder.add_algebraic_type(
2053                        [],
2054                        "my_view_return_type",
2055                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2056                        true,
2057                    );
2058                    builder.add_view(
2059                        "my_view",
2060                        0,
2061                        true,
2062                        true,
2063                        ProductType::from([("x", AlgebraicType::U32), ("y", AlgebraicType::U32)]),
2064                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2065                    );
2066                }),
2067            },
2068            TestCase {
2069                desc: "Remove parameter",
2070                old_def: create_module_def(|builder| {
2071                    let return_type_ref = builder.add_algebraic_type(
2072                        [],
2073                        "my_view_return_type",
2074                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2075                        true,
2076                    );
2077                    builder.add_view(
2078                        "my_view",
2079                        0,
2080                        true,
2081                        true,
2082                        ProductType::from([("x", AlgebraicType::U32), ("y", AlgebraicType::U32)]),
2083                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2084                    );
2085                }),
2086                new_def: create_module_def(|builder| {
2087                    let return_type_ref = builder.add_algebraic_type(
2088                        [],
2089                        "my_view_return_type",
2090                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2091                        true,
2092                    );
2093                    builder.add_view(
2094                        "my_view",
2095                        0,
2096                        true,
2097                        true,
2098                        ProductType::from([("x", AlgebraicType::U32)]),
2099                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2100                    );
2101                }),
2102            },
2103            TestCase {
2104                desc: "Reorder parameters",
2105                old_def: create_module_def(|builder| {
2106                    let return_type_ref = builder.add_algebraic_type(
2107                        [],
2108                        "my_view_return_type",
2109                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2110                        true,
2111                    );
2112                    builder.add_view(
2113                        "my_view",
2114                        0,
2115                        true,
2116                        true,
2117                        ProductType::from([("x", AlgebraicType::U32), ("y", AlgebraicType::U32)]),
2118                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2119                    );
2120                }),
2121                new_def: create_module_def(|builder| {
2122                    let return_type_ref = builder.add_algebraic_type(
2123                        [],
2124                        "my_view_return_type",
2125                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2126                        true,
2127                    );
2128                    builder.add_view(
2129                        "my_view",
2130                        0,
2131                        true,
2132                        true,
2133                        ProductType::from([("y", AlgebraicType::U32), ("x", AlgebraicType::U32)]),
2134                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2135                    );
2136                }),
2137            },
2138            TestCase {
2139                desc: "Change parameter type",
2140                old_def: create_module_def(|builder| {
2141                    let return_type_ref = builder.add_algebraic_type(
2142                        [],
2143                        "my_view_return_type",
2144                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2145                        true,
2146                    );
2147                    builder.add_view(
2148                        "my_view",
2149                        0,
2150                        true,
2151                        true,
2152                        ProductType::from([("x", AlgebraicType::U32)]),
2153                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2154                    );
2155                }),
2156                new_def: create_module_def(|builder| {
2157                    let return_type_ref = builder.add_algebraic_type(
2158                        [],
2159                        "my_view_return_type",
2160                        AlgebraicType::product([("a", AlgebraicType::String)]),
2161                        true,
2162                    );
2163                    builder.add_view(
2164                        "my_view",
2165                        0,
2166                        true,
2167                        true,
2168                        ProductType::from([("x", AlgebraicType::U32)]),
2169                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2170                    );
2171                }),
2172            },
2173            TestCase {
2174                desc: "Add column",
2175                old_def: create_module_def(|builder| {
2176                    let return_type_ref = builder.add_algebraic_type(
2177                        [],
2178                        "my_view_return_type",
2179                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2180                        true,
2181                    );
2182                    builder.add_view(
2183                        "my_view",
2184                        0,
2185                        true,
2186                        true,
2187                        ProductType::from([("x", AlgebraicType::U32)]),
2188                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2189                    );
2190                }),
2191                new_def: create_module_def(|builder| {
2192                    let return_type_ref = builder.add_algebraic_type(
2193                        [],
2194                        "my_view_return_type",
2195                        AlgebraicType::product([("a", AlgebraicType::U64), ("b", AlgebraicType::U64)]),
2196                        true,
2197                    );
2198                    builder.add_view(
2199                        "my_view",
2200                        0,
2201                        true,
2202                        true,
2203                        ProductType::from([("x", AlgebraicType::U32)]),
2204                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2205                    );
2206                }),
2207            },
2208            TestCase {
2209                desc: "Remove column",
2210                old_def: create_module_def(|builder| {
2211                    let return_type_ref = builder.add_algebraic_type(
2212                        [],
2213                        "my_view_return_type",
2214                        AlgebraicType::product([("a", AlgebraicType::U64), ("b", AlgebraicType::U64)]),
2215                        true,
2216                    );
2217                    builder.add_view(
2218                        "my_view",
2219                        0,
2220                        true,
2221                        true,
2222                        ProductType::from([("x", AlgebraicType::U32)]),
2223                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2224                    );
2225                }),
2226                new_def: create_module_def(|builder| {
2227                    let return_type_ref = builder.add_algebraic_type(
2228                        [],
2229                        "my_view_return_type",
2230                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2231                        true,
2232                    );
2233                    builder.add_view(
2234                        "my_view",
2235                        0,
2236                        true,
2237                        true,
2238                        ProductType::from([("x", AlgebraicType::U32)]),
2239                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2240                    );
2241                }),
2242            },
2243            TestCase {
2244                desc: "Reorder columns",
2245                old_def: create_module_def(|builder| {
2246                    let return_type_ref = builder.add_algebraic_type(
2247                        [],
2248                        "my_view_return_type",
2249                        AlgebraicType::product([("a", AlgebraicType::U64), ("b", AlgebraicType::U64)]),
2250                        true,
2251                    );
2252                    builder.add_view(
2253                        "my_view",
2254                        0,
2255                        true,
2256                        true,
2257                        ProductType::from([("x", AlgebraicType::U32)]),
2258                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2259                    );
2260                }),
2261                new_def: create_module_def(|builder| {
2262                    let return_type_ref = builder.add_algebraic_type(
2263                        [],
2264                        "my_view_return_type",
2265                        AlgebraicType::product([("b", AlgebraicType::U64), ("a", AlgebraicType::U64)]),
2266                        true,
2267                    );
2268                    builder.add_view(
2269                        "my_view",
2270                        0,
2271                        true,
2272                        true,
2273                        ProductType::from([("x", AlgebraicType::U32)]),
2274                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2275                    );
2276                }),
2277            },
2278            TestCase {
2279                desc: "Change column type",
2280                old_def: create_module_def(|builder| {
2281                    let return_type_ref = builder.add_algebraic_type(
2282                        [],
2283                        "my_view_return_type",
2284                        AlgebraicType::product([("a", AlgebraicType::U64)]),
2285                        true,
2286                    );
2287                    builder.add_view(
2288                        "my_view",
2289                        0,
2290                        true,
2291                        true,
2292                        ProductType::from([("x", AlgebraicType::U32)]),
2293                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2294                    );
2295                }),
2296                new_def: create_module_def(|builder| {
2297                    let return_type_ref = builder.add_algebraic_type(
2298                        [],
2299                        "my_view_return_type",
2300                        AlgebraicType::product([("a", AlgebraicType::String)]),
2301                        true,
2302                    );
2303                    builder.add_view(
2304                        "my_view",
2305                        0,
2306                        true,
2307                        true,
2308                        ProductType::from([("x", AlgebraicType::U32)]),
2309                        AlgebraicType::option(AlgebraicType::Ref(return_type_ref)),
2310                    );
2311                }),
2312            },
2313        ] {
2314            let my_view = expect_identifier("my_view");
2315
2316            let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2317            let steps = &plan.steps[..];
2318
2319            assert!(plan.disconnects_all_users(), "{name}, plan: {plan:?}");
2320
2321            assert!(
2322                steps.contains(&AutoMigrateStep::AddView(&my_view)),
2323                "{name}, steps: {steps:?}"
2324            );
2325            assert!(
2326                steps.contains(&AutoMigrateStep::RemoveView(&my_view)),
2327                "{name}, steps: {steps:?}"
2328            );
2329            assert!(
2330                !steps.contains(&AutoMigrateStep::UpdateView(&my_view)),
2331                "{name}, steps: {steps:?}"
2332            );
2333        }
2334    }
2335
2336    #[test]
2337    fn change_rls_disconnect_clients() {
2338        let old_def = create_module_def(|_builder| {});
2339
2340        let new_def = create_module_def(|_builder| {});
2341
2342        let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2343        assert!(!plan.disconnects_all_users(), "{plan:#?}");
2344
2345        let old_def = create_module_def(|builder| {
2346            builder.add_row_level_security("SELECT true;");
2347        });
2348        let new_def = create_module_def(|builder| {
2349            builder.add_row_level_security("SELECT false;");
2350        });
2351
2352        let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2353        assert!(plan.disconnects_all_users(), "{plan:#?}");
2354
2355        let old_def = create_module_def(|builder| {
2356            builder.add_row_level_security("SELECT true;");
2357        });
2358
2359        let new_def = create_module_def(|_builder| {
2360            // Remove RLS
2361        });
2362        let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2363        assert!(plan.disconnects_all_users(), "{plan:#?}");
2364
2365        let old_def = create_module_def(|_builder| {});
2366
2367        let new_def = create_module_def(|builder| {
2368            builder.add_row_level_security("SELECT false;");
2369        });
2370        let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2371        assert!(plan.disconnects_all_users(), "{plan:#?}");
2372
2373        let old_def = create_module_def(|builder| {
2374            builder.add_row_level_security("SELECT true;");
2375        });
2376
2377        let new_def = create_module_def(|builder| {
2378            builder.add_row_level_security("SELECT true;");
2379        });
2380        let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
2381        assert!(!plan.disconnects_all_users(), "{plan:#?}");
2382    }
2383
2384    fn create_v10_module_def(build_module: impl Fn(&mut v10::RawModuleDefV10Builder)) -> ModuleDef {
2385        let mut builder = v10::RawModuleDefV10Builder::new();
2386        build_module(&mut builder);
2387        builder
2388            .finish()
2389            .try_into()
2390            .expect("should be a valid module definition")
2391    }
2392
2393    #[test]
2394    fn test_change_event_flag_rejected() {
2395        // non-event → event
2396        let old = create_v10_module_def(|builder| {
2397            builder
2398                .build_table_with_new_type("Events", ProductType::from([("id", AlgebraicType::U64)]), true)
2399                .finish();
2400        });
2401        let new = create_v10_module_def(|builder| {
2402            builder
2403                .build_table_with_new_type("events", ProductType::from([("id", AlgebraicType::U64)]), true)
2404                .with_event(true)
2405                .finish();
2406        });
2407
2408        let result = ponder_auto_migrate(&old, &new);
2409        expect_error_matching!(
2410            result,
2411            AutoMigrateError::ChangeTableEventFlag { table } => &table[..] == "events"
2412        );
2413
2414        // event → non-event (reverse direction)
2415        let result = ponder_auto_migrate(&new, &old);
2416        expect_error_matching!(
2417            result,
2418            AutoMigrateError::ChangeTableEventFlag { table } => &table[..] == "events"
2419        );
2420    }
2421
2422    #[test]
2423    fn test_same_event_flag_accepted() {
2424        // Both event → no error
2425        let old = create_v10_module_def(|builder| {
2426            builder
2427                .build_table_with_new_type("Events", ProductType::from([("id", AlgebraicType::U64)]), true)
2428                .with_event(true)
2429                .finish();
2430        });
2431        let new = create_v10_module_def(|builder| {
2432            builder
2433                .build_table_with_new_type("Events", ProductType::from([("id", AlgebraicType::U64)]), true)
2434                .with_event(true)
2435                .finish();
2436        });
2437
2438        ponder_auto_migrate(&old, &new).expect("same event flag should succeed");
2439    }
2440
2441    #[test]
2442    fn remove_table_produces_step() {
2443        let old = create_module_def(|builder| {
2444            builder
2445                .build_table_with_new_type("Keep", ProductType::from([("id", AlgebraicType::U64)]), true)
2446                .with_access(TableAccess::Public)
2447                .finish();
2448            builder
2449                .build_table_with_new_type("Drop", ProductType::from([("id", AlgebraicType::U64)]), true)
2450                .with_access(TableAccess::Public)
2451                .finish();
2452        });
2453        let new = create_module_def(|builder| {
2454            builder
2455                .build_table_with_new_type("Keep", ProductType::from([("id", AlgebraicType::U64)]), true)
2456                .with_access(TableAccess::Public)
2457                .finish();
2458        });
2459
2460        let drop_table = expect_identifier("Drop");
2461        let plan = ponder_auto_migrate(&old, &new).expect("removing a table should produce a valid plan");
2462        assert_eq!(
2463            plan.steps,
2464            &[
2465                AutoMigrateStep::RemoveTable(&drop_table),
2466                AutoMigrateStep::DisconnectAllUsers,
2467            ],
2468        );
2469    }
2470
2471    #[test]
2472    fn remove_table_does_not_produce_orphan_sub_object_steps() {
2473        let old = create_module_def(|builder| {
2474            builder
2475                .build_table_with_new_type("Drop", ProductType::from([("id", AlgebraicType::U64)]), true)
2476                .with_unique_constraint(0)
2477                .with_index(btree(0), "Drop_id_idx")
2478                .with_access(TableAccess::Public)
2479                .finish();
2480        });
2481        let new = create_module_def(|_builder| {});
2482
2483        let drop_table = expect_identifier("Drop");
2484        let plan = ponder_auto_migrate(&old, &new).expect("removing a table should produce a valid plan");
2485        assert_eq!(
2486            plan.steps,
2487            &[
2488                AutoMigrateStep::RemoveTable(&drop_table),
2489                AutoMigrateStep::DisconnectAllUsers,
2490            ],
2491            "plan should only contain RemoveTable + DisconnectAllUsers, no orphan sub-object steps"
2492        );
2493    }
2494}