spacetimedb_schema_2/
auto_migrate.rs

1use crate::{def::*, error::PrettyAlgebraicType, identifier::Identifier};
2use spacetimedb_data_structures::{
3    error_stream::{CollectAllErrors, CombineErrors, ErrorStream},
4    map::HashSet,
5};
6use spacetimedb_lib::db::raw_def::v9::{RawRowLevelSecurityDefV9, TableType};
7use spacetimedb_sats::WithTypespace;
8
9pub type Result<T> = std::result::Result<T, ErrorStream<AutoMigrateError>>;
10
11/// A plan for a migration.
12#[derive(Debug)]
13pub enum MigratePlan<'def> {
14    Manual(ManualMigratePlan<'def>),
15    Auto(AutoMigratePlan<'def>),
16}
17
18impl<'def> MigratePlan<'def> {
19    /// Get the old `ModuleDef` for this migration plan.
20    pub fn old_def(&self) -> &'def ModuleDef {
21        match self {
22            MigratePlan::Manual(plan) => plan.old,
23            MigratePlan::Auto(plan) => plan.old,
24        }
25    }
26
27    /// Get the new `ModuleDef` for this migration plan.
28    pub fn new_def(&self) -> &'def ModuleDef {
29        match self {
30            MigratePlan::Manual(plan) => plan.new,
31            MigratePlan::Auto(plan) => plan.new,
32        }
33    }
34}
35
36/// A plan for a manual migration.
37/// `new` must have a reducer marked with `Lifecycle::Update`.
38#[derive(Debug)]
39pub struct ManualMigratePlan<'def> {
40    pub old: &'def ModuleDef,
41    pub new: &'def ModuleDef,
42}
43
44/// A plan for an automatic migration.
45#[derive(Debug)]
46pub struct AutoMigratePlan<'def> {
47    /// The old database definition.
48    pub old: &'def ModuleDef,
49    /// The new database definition.
50    pub new: &'def ModuleDef,
51    /// The checks to perform before the automatic migration.
52    /// There is also an implied check: that the schema in the database is compatible with the old ModuleDef.
53    pub prechecks: Vec<AutoMigratePrecheck<'def>>,
54    /// The migration steps to perform.
55    /// Order should not matter, as the steps are independent.
56    pub steps: Vec<AutoMigrateStep<'def>>,
57}
58
59/// Checks that must be performed before performing an automatic migration.
60/// These checks can access table contents and other database state.
61#[derive(PartialEq, Eq, Debug)]
62pub enum AutoMigratePrecheck<'def> {
63    /// Perform a check that adding a sequence is valid (the relevant column contains no values
64    /// greater than the sequence's start value).
65    CheckAddSequenceRangeValid(<SequenceDef as ModuleDefLookup>::Key<'def>),
66}
67
68/// A step in an automatic migration.
69#[derive(PartialEq, Eq, Debug)]
70pub enum AutoMigrateStep<'def> {
71    /// Add a table, including all indexes, constraints, and sequences.
72    /// There will NOT be separate steps in the plan for adding indexes, constraints, and sequences.
73    AddTable(<TableDef as ModuleDefLookup>::Key<'def>),
74    /// Add an index.
75    AddIndex(<IndexDef as ModuleDefLookup>::Key<'def>),
76    /// Remove an index.
77    RemoveIndex(<IndexDef as ModuleDefLookup>::Key<'def>),
78    /// Remove a constraint.
79    RemoveConstraint(<ConstraintDef as ModuleDefLookup>::Key<'def>),
80    /// Add a sequence.
81    AddSequence(<SequenceDef as ModuleDefLookup>::Key<'def>),
82    /// Remove a sequence.
83    RemoveSequence(<SequenceDef as ModuleDefLookup>::Key<'def>),
84    /// Change the access of a table.
85    ChangeAccess(<TableDef as ModuleDefLookup>::Key<'def>),
86    /// Add a schedule annotation to a table.
87    AddSchedule(<ScheduleDef as ModuleDefLookup>::Key<'def>),
88    /// Remove a schedule annotation from a table.
89    RemoveSchedule(<ScheduleDef as ModuleDefLookup>::Key<'def>),
90    /// Add a row-level security query.
91    AddRowLevelSecurity(<RawRowLevelSecurityDefV9 as ModuleDefLookup>::Key<'def>),
92    /// Remove a row-level security query.
93    RemoveRowLevelSecurity(<RawRowLevelSecurityDefV9 as ModuleDefLookup>::Key<'def>),
94}
95
96/// Something that might prevent an automatic migration.
97#[derive(thiserror::Error, Debug, PartialEq, Eq, PartialOrd, Ord)]
98pub enum AutoMigrateError {
99    #[error("Adding a column {column} to table {table} requires a manual migration")]
100    AddColumn { table: Identifier, column: Identifier },
101
102    #[error("Removing a column {column} from table {table} requires a manual migration")]
103    RemoveColumn { table: Identifier, column: Identifier },
104
105    #[error("Reordering table {table} requires a manual migration")]
106    ReorderTable { table: Identifier },
107
108    #[error(
109        "Changing the type of column {column} in table {table} from {type1:?} to {type2:?} requires a manual migration"
110    )]
111    ChangeColumnType {
112        table: Identifier,
113        column: Identifier,
114        type1: PrettyAlgebraicType,
115        type2: PrettyAlgebraicType,
116    },
117
118    #[error("Adding a unique constraint {constraint} requires a manual migration")]
119    AddUniqueConstraint { constraint: Identifier },
120
121    #[error("Changing a unique constraint {constraint} requires a manual migration")]
122    ChangeUniqueConstraint { constraint: Identifier },
123
124    #[error("Removing the table {table} requires a manual migration")]
125    RemoveTable { table: Identifier },
126
127    #[error("Changing the table type of table {table} from {type1:?} to {type2:?} requires a manual migration")]
128    ChangeTableType {
129        table: Identifier,
130        type1: TableType,
131        type2: TableType,
132    },
133
134    #[error(
135        "Changing the accessor name on index {index} from {old_accessor:?} to {new_accessor:?} requires a manual migration"
136    )]
137    ChangeIndexAccessor {
138        index: Identifier,
139        old_accessor: Option<Identifier>,
140        new_accessor: Option<Identifier>,
141    },
142}
143
144/// Construct a migration plan.
145/// If `new` has an `__update__` reducer, return a manual migration plan.
146/// Otherwise, try to plan an automatic migration. This may fail.
147pub fn ponder_migrate<'def>(old: &'def ModuleDef, new: &'def ModuleDef) -> Result<MigratePlan<'def>> {
148    // TODO(1.0): Implement this function.
149    // Currently we only can do automatic migrations.
150    ponder_auto_migrate(old, new).map(MigratePlan::Auto)
151}
152
153/// Construct an automatic migration plan, or reject with reasons why automatic migration can't be performed.
154pub fn ponder_auto_migrate<'def>(old: &'def ModuleDef, new: &'def ModuleDef) -> Result<AutoMigratePlan<'def>> {
155    // Both the old and new database definitions have already been validated (this is enforced by the types).
156    // All we have to do is walk through and compare them.
157    let mut plan = AutoMigratePlan {
158        old,
159        new,
160        steps: Vec::new(),
161        prechecks: Vec::new(),
162    };
163
164    let tables_ok = auto_migrate_tables(&mut plan);
165
166    // Our diffing algorithm will detect added constraints / indexes / sequences in new tables, we use this to filter those out.
167    // They're handled by adding the root table.
168    let new_tables: HashSet<&Identifier> = diff(plan.old, plan.new, ModuleDef::tables)
169        .filter_map(|diff| match diff {
170            Diff::Add { new } => Some(&new.name),
171            _ => None,
172        })
173        .collect();
174    let indexes_ok = auto_migrate_indexes(&mut plan, &new_tables);
175    let sequences_ok = auto_migrate_sequences(&mut plan, &new_tables);
176    let constraints_ok = auto_migrate_constraints(&mut plan, &new_tables);
177    // IMPORTANT: RLS auto-migrate steps must come last,
178    // since they assume that any schema changes, like adding or dropping tables,
179    // have already been reflected in the database state.
180    let rls_ok = auto_migrate_row_level_security(&mut plan);
181
182    let ((), (), (), (), ()) = (tables_ok, indexes_ok, sequences_ok, constraints_ok, rls_ok).combine_errors()?;
183
184    Ok(plan)
185}
186
187/// A diff between two items.
188/// `Add` means the item is present in the new `ModuleDef` but not the old.
189/// `Remove` means the item is present in the old `ModuleDef` but not the new.
190/// `MaybeChange` indicates the item is present in both.
191enum Diff<'def, T> {
192    Add { new: &'def T },
193    Remove { old: &'def T },
194    MaybeChange { old: &'def T, new: &'def T },
195}
196
197/// Diff a collection of items, looking them up in both the old and new `ModuleDef` by their `ModuleDefLookup::Key`.
198/// Keys are required to be stable across migrations, which mak
199fn diff<'def, T: ModuleDefLookup, I: Iterator<Item = &'def T>>(
200    old: &'def ModuleDef,
201    new: &'def ModuleDef,
202    iter: impl Fn(&'def ModuleDef) -> I,
203) -> impl Iterator<Item = Diff<'def, T>> {
204    iter(old)
205        .map(move |old_item| match T::lookup(new, old_item.key()) {
206            Some(new_item) => Diff::MaybeChange {
207                old: old_item,
208                new: new_item,
209            },
210            None => Diff::Remove { old: old_item },
211        })
212        .chain(iter(new).filter_map(move |new_item| {
213            if T::lookup(old, new_item.key()).is_none() {
214                Some(Diff::Add { new: new_item })
215            } else {
216                None
217            }
218        }))
219}
220
221fn auto_migrate_tables(plan: &mut AutoMigratePlan<'_>) -> Result<()> {
222    diff(plan.old, plan.new, ModuleDef::tables)
223        .map(|table_diff| -> Result<()> {
224            match table_diff {
225                Diff::Add { new } => {
226                    plan.steps.push(AutoMigrateStep::AddTable(new.key()));
227                    Ok(())
228                }
229                // TODO: When we remove tables, we should also remove their dependencies, including row-level security.
230                Diff::Remove { old } => Err(AutoMigrateError::RemoveTable {
231                    table: old.name.clone(),
232                }
233                .into()),
234                Diff::MaybeChange { old, new } => auto_migrate_table(plan, old, new),
235            }
236        })
237        .collect_all_errors()
238}
239
240fn auto_migrate_table<'def>(plan: &mut AutoMigratePlan<'def>, old: &'def TableDef, new: &'def TableDef) -> Result<()> {
241    let key = old.key();
242    let type_ok: Result<()> = if old.table_type == new.table_type {
243        Ok(())
244    } else {
245        Err(AutoMigrateError::ChangeTableType {
246            table: old.name.clone(),
247            type1: old.table_type,
248            type2: new.table_type,
249        }
250        .into())
251    };
252    if old.table_access != new.table_access {
253        plan.steps.push(AutoMigrateStep::ChangeAccess(key));
254    }
255    if old.schedule != new.schedule {
256        // Note: this handles the case where there's an altered ScheduleDef for some reason.
257        if let Some(old_schedule) = old.schedule.as_ref() {
258            plan.steps.push(AutoMigrateStep::RemoveSchedule(old_schedule.key()));
259        }
260        if let Some(new_schedule) = new.schedule.as_ref() {
261            plan.steps.push(AutoMigrateStep::AddSchedule(new_schedule.key()));
262        }
263    }
264
265    let columns_ok: Result<()> = diff(plan.old, plan.new, |def| {
266        def.lookup_expect::<TableDef>(key).columns.iter()
267    })
268    .map(|col_diff| -> Result<()> {
269        match col_diff {
270            Diff::Add { new } => Err(AutoMigrateError::AddColumn {
271                table: new.table_name.clone(),
272                column: new.name.clone(),
273            }
274            .into()),
275            Diff::Remove { old } => Err(AutoMigrateError::RemoveColumn {
276                table: old.table_name.clone(),
277                column: old.name.clone(),
278            }
279            .into()),
280            Diff::MaybeChange { old, new } => {
281                let old_ty = WithTypespace::new(plan.old.typespace(), &old.ty)
282                    .resolve_refs()
283                    .expect("valid TableDef must have valid type refs");
284                let new_ty = WithTypespace::new(plan.new.typespace(), &new.ty)
285                    .resolve_refs()
286                    .expect("valid TableDef must have valid type refs");
287                let types_ok = if old_ty == new_ty {
288                    Ok(())
289                } else {
290                    Err(AutoMigrateError::ChangeColumnType {
291                        table: old.table_name.clone(),
292                        column: old.name.clone(),
293                        type1: old_ty.clone().into(),
294                        type2: new_ty.clone().into(),
295                    }
296                    .into())
297                };
298                // Note that the diff algorithm relies on `ModuleDefLookup` for `ColumnDef`,
299                // which looks up columns by NAME, NOT position: precisely to allow this step to work!
300                let positions_ok = if old.col_id == new.col_id {
301                    Ok(())
302                } else {
303                    Err(AutoMigrateError::ReorderTable {
304                        table: old.table_name.clone(),
305                    }
306                    .into())
307                };
308                let ((), ()) = (types_ok, positions_ok).combine_errors()?;
309                Ok(())
310            }
311        }
312    })
313    .collect_all_errors();
314
315    let ((), ()) = (type_ok, columns_ok).combine_errors()?;
316    Ok(())
317}
318
319fn auto_migrate_indexes(plan: &mut AutoMigratePlan<'_>, new_tables: &HashSet<&Identifier>) -> Result<()> {
320    diff(plan.old, plan.new, ModuleDef::indexes)
321        .map(|index_diff| -> Result<()> {
322            match index_diff {
323                Diff::Add { new } => {
324                    if !new_tables.contains(&plan.new.stored_in_table_def(&new.name).unwrap().name) {
325                        plan.steps.push(AutoMigrateStep::AddIndex(new.key()));
326                    }
327                    Ok(())
328                }
329                Diff::Remove { old } => {
330                    plan.steps.push(AutoMigrateStep::RemoveIndex(old.key()));
331                    Ok(())
332                }
333                Diff::MaybeChange { old, new } => {
334                    if old.accessor_name != new.accessor_name {
335                        Err(AutoMigrateError::ChangeIndexAccessor {
336                            index: old.name.clone(),
337                            old_accessor: old.accessor_name.clone(),
338                            new_accessor: new.accessor_name.clone(),
339                        }
340                        .into())
341                    } else {
342                        if old.algorithm != new.algorithm {
343                            plan.steps.push(AutoMigrateStep::RemoveIndex(old.key()));
344                            plan.steps.push(AutoMigrateStep::AddIndex(old.key()));
345                        }
346                        Ok(())
347                    }
348                }
349            }
350        })
351        .collect_all_errors()
352}
353
354fn auto_migrate_sequences(plan: &mut AutoMigratePlan, new_tables: &HashSet<&Identifier>) -> Result<()> {
355    diff(plan.old, plan.new, ModuleDef::sequences)
356        .map(|sequence_diff| -> Result<()> {
357            match sequence_diff {
358                Diff::Add { new } => {
359                    if !new_tables.contains(&plan.new.stored_in_table_def(&new.name).unwrap().name) {
360                        plan.prechecks
361                            .push(AutoMigratePrecheck::CheckAddSequenceRangeValid(new.key()));
362                        plan.steps.push(AutoMigrateStep::AddSequence(new.key()));
363                    }
364                    Ok(())
365                }
366                Diff::Remove { old } => {
367                    plan.steps.push(AutoMigrateStep::RemoveSequence(old.key()));
368                    Ok(())
369                }
370                Diff::MaybeChange { old, new } => {
371                    // we do not need to check column ids, since in an automigrate, column ids are not changed.
372                    if old != new {
373                        plan.prechecks
374                            .push(AutoMigratePrecheck::CheckAddSequenceRangeValid(new.key()));
375                        plan.steps.push(AutoMigrateStep::RemoveSequence(old.key()));
376                        plan.steps.push(AutoMigrateStep::AddSequence(new.key()));
377                    }
378                    Ok(())
379                }
380            }
381        })
382        .collect_all_errors()
383}
384
385fn auto_migrate_constraints(plan: &mut AutoMigratePlan, new_tables: &HashSet<&Identifier>) -> Result<()> {
386    diff(plan.old, plan.new, ModuleDef::constraints)
387        .map(|constraint_diff| -> Result<()> {
388            match constraint_diff {
389                Diff::Add { new } => {
390                    if new_tables.contains(&plan.new.stored_in_table_def(&new.name).unwrap().name) {
391                        // it's okay to add a constraint in a new table.
392                        Ok(())
393                    } else {
394                        // it's not okay to add a new constraint to an existing table.
395                        Err(AutoMigrateError::AddUniqueConstraint {
396                            constraint: new.name.clone(),
397                        }
398                        .into())
399                    }
400                }
401                Diff::Remove { old } => {
402                    plan.steps.push(AutoMigrateStep::RemoveConstraint(old.key()));
403                    Ok(())
404                }
405                Diff::MaybeChange { old, new } => {
406                    if old == new {
407                        Ok(())
408                    } else {
409                        Err(AutoMigrateError::ChangeUniqueConstraint {
410                            constraint: old.name.clone(),
411                        }
412                        .into())
413                    }
414                }
415            }
416        })
417        .collect_all_errors()
418}
419
420// Because we can refer to many tables and fields on the row level-security query, we need to remove all of them,
421// then add the new ones, instead of trying to track the graph of dependencies.
422fn auto_migrate_row_level_security(plan: &mut AutoMigratePlan) -> Result<()> {
423    for rls in plan.old.row_level_security() {
424        plan.steps.push(AutoMigrateStep::RemoveRowLevelSecurity(rls.key()));
425    }
426    for rls in plan.new.row_level_security() {
427        plan.steps.push(AutoMigrateStep::AddRowLevelSecurity(rls.key()));
428    }
429
430    Ok(())
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436    use spacetimedb_data_structures::expect_error_matching;
437    use spacetimedb_lib::{db::raw_def::*, AlgebraicType, ProductType, ScheduleAt};
438    use spacetimedb_primitives::{ColId, ColList};
439    use v9::{RawIndexAlgorithm, RawModuleDefV9Builder, TableAccess};
440    use validate::tests::expect_identifier;
441
442    #[test]
443    fn successful_auto_migration() {
444        let mut old_builder = RawModuleDefV9Builder::new();
445        let old_schedule_at = old_builder.add_type::<ScheduleAt>();
446        old_builder
447            .build_table_with_new_type(
448                "Apples",
449                ProductType::from([
450                    ("id", AlgebraicType::U64),
451                    ("name", AlgebraicType::String),
452                    ("count", AlgebraicType::U16),
453                ]),
454                true,
455            )
456            .with_column_sequence(0, Some("Apples_sequence".into()))
457            .with_unique_constraint(ColId(0), Some("Apples_unique_constraint".into()))
458            .with_index(
459                RawIndexAlgorithm::BTree {
460                    columns: ColList::from([0]),
461                },
462                "id_index",
463                Some("Apples_id_index".into()),
464            )
465            .with_index(
466                RawIndexAlgorithm::BTree {
467                    columns: ColList::from([0, 1]),
468                },
469                "id_name_index",
470                Some("Apples_id_name_index".into()),
471            )
472            .finish();
473
474        old_builder
475            .build_table_with_new_type(
476                "Bananas",
477                ProductType::from([
478                    ("id", AlgebraicType::U64),
479                    ("name", AlgebraicType::String),
480                    ("count", AlgebraicType::U16),
481                ]),
482                true,
483            )
484            .with_access(TableAccess::Public)
485            .finish();
486
487        let old_deliveries_type = old_builder
488            .build_table_with_new_type(
489                "Deliveries",
490                ProductType::from([
491                    ("scheduled_id", AlgebraicType::U64),
492                    ("scheduled_at", old_schedule_at.clone()),
493                ]),
494                true,
495            )
496            .with_auto_inc_primary_key(0)
497            .with_schedule("check_deliveries", 1, None)
498            .finish();
499        old_builder.add_reducer(
500            "check_deliveries",
501            ProductType::from([("a", AlgebraicType::Ref(old_deliveries_type))]),
502            None,
503        );
504
505        old_builder
506            .build_table_with_new_type(
507                "Inspections",
508                ProductType::from([
509                    ("scheduled_id", AlgebraicType::U64),
510                    ("scheduled_at", old_schedule_at.clone()),
511                ]),
512                true,
513            )
514            .with_auto_inc_primary_key(0)
515            .finish();
516
517        old_builder.add_row_level_security("SELECT * FROM Apples");
518
519        let old_def: ModuleDef = old_builder
520            .finish()
521            .try_into()
522            .expect("old_def should be a valid database definition");
523
524        let mut new_builder = RawModuleDefV9Builder::new();
525        let _ = new_builder.add_type::<u32>(); // reposition ScheduleAt in the typespace, should have no effect.
526        let new_schedule_at = new_builder.add_type::<ScheduleAt>();
527        new_builder
528            .build_table_with_new_type(
529                "Apples",
530                ProductType::from([
531                    ("id", AlgebraicType::U64),
532                    ("name", AlgebraicType::String),
533                    ("count", AlgebraicType::U16),
534                ]),
535                true,
536            )
537            // remove sequence
538            // remove unique constraint
539            .with_index(
540                RawIndexAlgorithm::BTree {
541                    columns: ColList::from([0]),
542                },
543                "id_index",
544                Some("Apples_id_index".into()),
545            )
546            // remove ["id", "name"] index
547            // add ["id", "count"] index
548            .with_index(
549                RawIndexAlgorithm::BTree {
550                    columns: ColList::from([0, 2]),
551                },
552                "id_count_index",
553                Some("Apples_id_count_index".into()),
554            )
555            .finish();
556
557        new_builder
558            .build_table_with_new_type(
559                "Bananas",
560                ProductType::from([
561                    ("id", AlgebraicType::U64),
562                    ("name", AlgebraicType::String),
563                    ("count", AlgebraicType::U16),
564                ]),
565                true,
566            )
567            // add column sequence
568            .with_column_sequence(0, Some("Bananas_sequence".into()))
569            // change access
570            .with_access(TableAccess::Private)
571            .finish();
572
573        let new_deliveries_type = new_builder
574            .build_table_with_new_type(
575                "Deliveries",
576                ProductType::from([
577                    ("scheduled_id", AlgebraicType::U64),
578                    ("scheduled_at", new_schedule_at.clone()),
579                ]),
580                true,
581            )
582            .with_auto_inc_primary_key(0)
583            // remove schedule def
584            .finish();
585
586        new_builder.add_reducer(
587            "check_deliveries",
588            ProductType::from([("a", AlgebraicType::Ref(new_deliveries_type))]),
589            None,
590        );
591
592        let new_inspections_type = new_builder
593            .build_table_with_new_type(
594                "Inspections",
595                ProductType::from([
596                    ("scheduled_id", AlgebraicType::U64),
597                    ("scheduled_at", new_schedule_at.clone()),
598                ]),
599                true,
600            )
601            .with_auto_inc_primary_key(0)
602            // add schedule def
603            .with_schedule("perform_inspection", 1, None)
604            .finish();
605
606        // add reducer.
607        new_builder.add_reducer(
608            "perform_inspection",
609            ProductType::from([("a", AlgebraicType::Ref(new_inspections_type))]),
610            None,
611        );
612
613        // Add new table
614        new_builder
615            .build_table_with_new_type("Oranges", ProductType::from([("id", AlgebraicType::U32)]), true)
616            .with_index(
617                RawIndexAlgorithm::BTree {
618                    columns: ColList::from([0]),
619                },
620                "id_index",
621                None,
622            )
623            .with_column_sequence(0, None)
624            .with_unique_constraint(0, None)
625            .with_primary_key(0)
626            .finish();
627
628        new_builder.add_row_level_security("SELECT * FROM Bananas");
629
630        let new_def: ModuleDef = new_builder
631            .finish()
632            .try_into()
633            .expect("new_def should be a valid database definition");
634
635        let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
636
637        let bananas = expect_identifier("Bananas");
638        let oranges = expect_identifier("Oranges");
639
640        let bananas_sequence = expect_identifier("Bananas_sequence");
641        let apples_unique_constraint = expect_identifier("Apples_unique_constraint");
642        let apples_sequence = expect_identifier("Apples_sequence");
643        let apples_id_name_index = expect_identifier("Apples_id_name_index");
644        let apples_id_count_index = expect_identifier("Apples_id_count_index");
645        let deliveries_schedule = expect_identifier("schedule_Deliveries");
646        let inspections_schedule = expect_identifier("schedule_Inspections");
647
648        assert_eq!(plan.prechecks.len(), 1);
649        assert_eq!(
650            plan.prechecks[0],
651            AutoMigratePrecheck::CheckAddSequenceRangeValid(&bananas_sequence)
652        );
653        let sql_old = RawRowLevelSecurityDefV9 {
654            sql: "SELECT * FROM Apples".into(),
655        };
656
657        let sql_new = RawRowLevelSecurityDefV9 {
658            sql: "SELECT * FROM Bananas".into(),
659        };
660
661        assert!(plan.steps.contains(&AutoMigrateStep::RemoveSequence(&apples_sequence)));
662        assert!(plan
663            .steps
664            .contains(&AutoMigrateStep::RemoveConstraint(&apples_unique_constraint)));
665        assert!(plan
666            .steps
667            .contains(&AutoMigrateStep::RemoveIndex(&apples_id_name_index)));
668        assert!(plan.steps.contains(&AutoMigrateStep::AddIndex(&apples_id_count_index)));
669
670        assert!(plan.steps.contains(&AutoMigrateStep::ChangeAccess(&bananas)));
671        assert!(plan.steps.contains(&AutoMigrateStep::AddSequence(&bananas_sequence)));
672
673        assert!(plan.steps.contains(&AutoMigrateStep::AddTable(&oranges)));
674
675        assert!(plan
676            .steps
677            .contains(&AutoMigrateStep::RemoveSchedule(&deliveries_schedule)));
678        assert!(plan
679            .steps
680            .contains(&AutoMigrateStep::AddSchedule(&inspections_schedule)));
681
682        assert!(plan
683            .steps
684            .contains(&AutoMigrateStep::RemoveRowLevelSecurity(&sql_old.sql)));
685        assert!(plan.steps.contains(&AutoMigrateStep::AddRowLevelSecurity(&sql_new.sql)));
686    }
687
688    #[test]
689    fn auto_migration_errors() {
690        let mut old_builder = RawModuleDefV9Builder::new();
691
692        old_builder
693            .build_table_with_new_type(
694                "Apples",
695                ProductType::from([
696                    ("id", AlgebraicType::U64),
697                    ("name", AlgebraicType::String),
698                    ("count", AlgebraicType::U16),
699                ]),
700                true,
701            )
702            .with_index(
703                RawIndexAlgorithm::BTree {
704                    columns: ColList::from([0]),
705                },
706                "id_index",
707                Some("Apples_id_index".into()),
708            )
709            .with_unique_constraint(ColList::from_iter([1, 2]), Some("Apples_changing_constraint".into()))
710            .with_type(TableType::User)
711            .finish();
712
713        old_builder
714            .build_table_with_new_type(
715                "Bananas",
716                ProductType::from([
717                    ("id", AlgebraicType::U64),
718                    ("name", AlgebraicType::String),
719                    ("count", AlgebraicType::U16),
720                ]),
721                true,
722            )
723            .finish();
724
725        let old_def: ModuleDef = old_builder
726            .finish()
727            .try_into()
728            .expect("old_def should be a valid database definition");
729
730        let mut new_builder = RawModuleDefV9Builder::new();
731
732        new_builder
733            .build_table_with_new_type(
734                "Apples",
735                ProductType::from([
736                    ("name", AlgebraicType::U32), // change type of `name`
737                    ("id", AlgebraicType::U64),   // change order
738                    // remove count
739                    ("weight", AlgebraicType::U16), // add weight
740                ]),
741                true,
742            )
743            .with_index(
744                RawIndexAlgorithm::BTree {
745                    columns: ColList::from([0]),
746                },
747                "id_index_new_accessor", // change accessor name
748                Some("Apples_id_index".into()),
749            )
750            .with_unique_constraint(ColList::from_iter([0, 1]), Some("Apples_changing_constraint".into()))
751            .with_unique_constraint(ColId(0), Some("Apples_name_unique_constraint".into())) // add unique constraint
752            .with_type(TableType::System) // change type
753            .finish();
754
755        // Invalid row-level security queries can't be detected in the ponder_auto_migrate function, they
756        // are detected when executing the plan because they depend on the database state.
757        // new_builder.add_row_level_security("SELECT wrong");
758
759        // remove Bananas
760        let new_def: ModuleDef = new_builder
761            .finish()
762            .try_into()
763            .expect("new_def should be a valid database definition");
764
765        let result = ponder_auto_migrate(&old_def, &new_def);
766
767        let apples = expect_identifier("Apples");
768        let bananas = expect_identifier("Bananas");
769
770        let apples_name_unique_constraint = expect_identifier("Apples_name_unique_constraint");
771        let apples_changing_constraint = expect_identifier("Apples_changing_constraint");
772
773        let weight = expect_identifier("weight");
774        let count = expect_identifier("count");
775        let name = expect_identifier("name");
776
777        expect_error_matching!(
778            result,
779            AutoMigrateError::AddColumn {
780                table,
781                column
782            } => table == &apples && column == &weight
783        );
784
785        expect_error_matching!(
786            result,
787            AutoMigrateError::RemoveColumn {
788                table,
789                column
790            } => table == &apples && column == &count
791        );
792
793        expect_error_matching!(
794            result,
795            AutoMigrateError::ReorderTable { table } => table == &apples
796        );
797
798        expect_error_matching!(
799            result,
800            AutoMigrateError::ChangeColumnType {
801                table,
802                column,
803                type1,
804                type2
805            } => table == &apples && column == &name && type1.0 == AlgebraicType::String && type2.0 == AlgebraicType::U32
806        );
807
808        expect_error_matching!(
809            result,
810            AutoMigrateError::AddUniqueConstraint { constraint } => constraint == &apples_name_unique_constraint
811        );
812
813        expect_error_matching!(
814            result,
815            AutoMigrateError::ChangeTableType { table, type1, type2 } => table == &apples && type1 == &TableType::User && type2 == &TableType::System
816        );
817
818        expect_error_matching!(
819            result,
820            AutoMigrateError::RemoveTable { table } => table == &bananas
821        );
822
823        let apples_id_index = expect_identifier("Apples_id_index");
824        let accessor_old = expect_identifier("id_index");
825        let accessor_new = expect_identifier("id_index_new_accessor");
826        expect_error_matching!(
827            result,
828            AutoMigrateError::ChangeIndexAccessor {
829                index,
830                old_accessor,
831                new_accessor
832            } => index == &apples_id_index && old_accessor.as_ref() == Some(&accessor_old) && new_accessor.as_ref() == Some(&accessor_new)
833        );
834
835        expect_error_matching!(
836            result,
837            AutoMigrateError::ChangeUniqueConstraint { constraint } => constraint == &apples_changing_constraint
838        );
839    }
840}