Skip to content

Commit 9bd40d6

Browse files
Shubham8287kazimuthbfops
authored
Automigration pretty print (#3121)
# Description of Changes Pretty print for Auto migration. # API and ABI breaking changes NA # Expected complexity level and risk 1 # Testing - Added snapshot tests. <img width="838" height="746" alt="Screenshot from 2025-08-06 17-44-13" src="https://github.com/user-attachments/assets/a5b33e32-c52e-4a16-9a4d-b8b184390663" /> --------- Co-authored-by: James Gilles <[email protected]> Co-authored-by: Zeke Foppa <[email protected]>
1 parent eda20a0 commit 9bd40d6

8 files changed

+1159
-39
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/schema/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ smallvec.workspace = true
3030
hashbrown.workspace = true
3131
enum-as-inner.workspace = true
3232
enum-map.workspace = true
33+
insta.workspace = true
34+
termcolor.workspace = true
3335

3436
[dev-dependencies]
3537
spacetimedb-lib = { path = "../lib", features = ["test"] }

crates/schema/src/auto_migrate.rs

Lines changed: 121 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use core::{cmp::Ordering, ops::BitOr};
22

33
use crate::{def::*, error::PrettyAlgebraicType, identifier::Identifier};
4+
use formatter::format_plan;
45
use spacetimedb_data_structures::{
56
error_stream::{CollectAllErrors, CombineErrors, ErrorStream},
67
map::HashSet,
@@ -13,6 +14,9 @@ use spacetimedb_sats::{
1314
layout::{HasLayout, SumTypeLayout},
1415
WithTypespace,
1516
};
17+
use termcolor_formatter::{ColorScheme, TermColorFormatter};
18+
mod formatter;
19+
mod termcolor_formatter;
1620

1721
pub type Result<T> = std::result::Result<T, ErrorStream<AutoMigrateError>>;
1822

@@ -23,6 +27,12 @@ pub enum MigratePlan<'def> {
2327
Auto(AutoMigratePlan<'def>),
2428
}
2529

30+
#[derive(Copy, Clone, PartialEq, Eq)]
31+
pub enum PrettyPrintStyle {
32+
AnsiColor,
33+
NoColor,
34+
}
35+
2636
impl<'def> MigratePlan<'def> {
2737
/// Get the old `ModuleDef` for this migration plan.
2838
pub fn old_def(&self) -> &'def ModuleDef {
@@ -39,6 +49,28 @@ impl<'def> MigratePlan<'def> {
3949
MigratePlan::Auto(plan) => plan.new,
4050
}
4151
}
52+
53+
pub fn pretty_print(&self, style: PrettyPrintStyle) -> anyhow::Result<String> {
54+
use PrettyPrintStyle::*;
55+
56+
match self {
57+
MigratePlan::Manual(_) => {
58+
anyhow::bail!("Manual migration plans are not yet supported for pretty printing.")
59+
}
60+
61+
MigratePlan::Auto(plan) => match style {
62+
NoColor => {
63+
let mut fmt = TermColorFormatter::new(ColorScheme::default(), termcolor::ColorChoice::Never);
64+
format_plan(&mut fmt, plan).map(|_| fmt.to_string())
65+
}
66+
AnsiColor => {
67+
let mut fmt = TermColorFormatter::new(ColorScheme::default(), termcolor::ColorChoice::AlwaysAnsi);
68+
format_plan(&mut fmt, plan).map(|_| fmt.to_string())
69+
}
70+
}
71+
.map_err(|e| anyhow::anyhow!("Failed to format migration plan: {e}")),
72+
}
73+
}
4274
}
4375

4476
/// A plan for a manual migration.
@@ -766,20 +798,19 @@ mod tests {
766798
use v9::{RawModuleDefV9Builder, TableAccess};
767799
use validate::tests::expect_identifier;
768800

769-
#[test]
770-
fn successful_auto_migration() {
771-
let mut old_builder = RawModuleDefV9Builder::new();
772-
let old_schedule_at = old_builder.add_type::<ScheduleAt>();
773-
let old_sum_ty = AlgebraicType::sum([("v1", AlgebraicType::U64)]);
774-
let old_sum_refty = old_builder.add_algebraic_type([], "sum", old_sum_ty, true);
775-
old_builder
801+
fn initial_module_def() -> ModuleDef {
802+
let mut builder = RawModuleDefV9Builder::new();
803+
let schedule_at = builder.add_type::<ScheduleAt>();
804+
let sum_ty = AlgebraicType::sum([("v1", AlgebraicType::U64)]);
805+
let sum_refty = builder.add_algebraic_type([], "sum", sum_ty, true);
806+
builder
776807
.build_table_with_new_type(
777808
"Apples",
778809
ProductType::from([
779810
("id", AlgebraicType::U64),
780811
("name", AlgebraicType::String),
781812
("count", AlgebraicType::U16),
782-
("sum", old_sum_refty.into()),
813+
("sum", sum_refty.into()),
783814
]),
784815
true,
785816
)
@@ -789,7 +820,7 @@ mod tests {
789820
.with_index(btree([0, 1]), "id_name_index")
790821
.finish();
791822

792-
old_builder
823+
builder
793824
.build_table_with_new_type(
794825
"Bananas",
795826
ProductType::from([
@@ -802,59 +833,61 @@ mod tests {
802833
.with_access(TableAccess::Public)
803834
.finish();
804835

805-
let old_deliveries_type = old_builder
836+
let deliveries_type = builder
806837
.build_table_with_new_type(
807838
"Deliveries",
808839
ProductType::from([
809840
("scheduled_id", AlgebraicType::U64),
810-
("scheduled_at", old_schedule_at.clone()),
811-
("sum", AlgebraicType::array(old_sum_refty.into())),
841+
("scheduled_at", schedule_at.clone()),
842+
("sum", AlgebraicType::array(sum_refty.into())),
812843
]),
813844
true,
814845
)
815846
.with_auto_inc_primary_key(0)
816847
.with_index_no_accessor_name(btree(0))
817848
.with_schedule("check_deliveries", 1)
818849
.finish();
819-
old_builder.add_reducer(
850+
builder.add_reducer(
820851
"check_deliveries",
821-
ProductType::from([("a", AlgebraicType::Ref(old_deliveries_type))]),
852+
ProductType::from([("a", AlgebraicType::Ref(deliveries_type))]),
822853
None,
823854
);
824855

825-
old_builder
856+
builder
826857
.build_table_with_new_type(
827858
"Inspections",
828859
ProductType::from([
829860
("scheduled_id", AlgebraicType::U64),
830-
("scheduled_at", old_schedule_at.clone()),
861+
("scheduled_at", schedule_at.clone()),
831862
]),
832863
true,
833864
)
834865
.with_auto_inc_primary_key(0)
835866
.with_index_no_accessor_name(btree(0))
836867
.finish();
837868

838-
old_builder.add_row_level_security("SELECT * FROM Apples");
869+
builder.add_row_level_security("SELECT * FROM Apples");
839870

840-
let old_def: ModuleDef = old_builder
871+
builder
841872
.finish()
842873
.try_into()
843-
.expect("old_def should be a valid database definition");
874+
.expect("old_def should be a valid database definition")
875+
}
844876

845-
let mut new_builder = RawModuleDefV9Builder::new();
846-
let _ = new_builder.add_type::<u32>(); // reposition ScheduleAt in the typespace, should have no effect.
847-
let new_schedule_at = new_builder.add_type::<ScheduleAt>();
848-
let new_sum_ty = AlgebraicType::sum([("v1", AlgebraicType::U64), ("v2", AlgebraicType::Bool)]);
849-
let new_sum_refty = new_builder.add_algebraic_type([], "sum", new_sum_ty, true);
850-
new_builder
877+
fn updated_module_def() -> ModuleDef {
878+
let mut builder = RawModuleDefV9Builder::new();
879+
let _ = builder.add_type::<u32>(); // reposition ScheduleAt in the typespace, should have no effect.
880+
let schedule_at = builder.add_type::<ScheduleAt>();
881+
let sum_ty = AlgebraicType::sum([("v1", AlgebraicType::U64), ("v2", AlgebraicType::Bool)]);
882+
let sum_refty = builder.add_algebraic_type([], "sum", sum_ty, true);
883+
builder
851884
.build_table_with_new_type(
852885
"Apples",
853886
ProductType::from([
854887
("id", AlgebraicType::U64),
855888
("name", AlgebraicType::String),
856889
("count", AlgebraicType::U16),
857-
("sum", new_sum_refty.into()),
890+
("sum", sum_refty.into()),
858891
]),
859892
true,
860893
)
@@ -866,7 +899,7 @@ mod tests {
866899
.with_index(btree([0, 2]), "id_count_index")
867900
.finish();
868901

869-
new_builder
902+
builder
870903
.build_table_with_new_type(
871904
"Bananas",
872905
ProductType::from([
@@ -884,13 +917,13 @@ mod tests {
884917
.with_access(TableAccess::Private)
885918
.finish();
886919

887-
let new_deliveries_type = new_builder
920+
let deliveries_type = builder
888921
.build_table_with_new_type(
889922
"Deliveries",
890923
ProductType::from([
891924
("scheduled_id", AlgebraicType::U64),
892-
("scheduled_at", new_schedule_at.clone()),
893-
("sum", AlgebraicType::array(new_sum_refty.into())),
925+
("scheduled_at", schedule_at.clone()),
926+
("sum", AlgebraicType::array(sum_refty.into())),
894927
]),
895928
true,
896929
)
@@ -899,18 +932,18 @@ mod tests {
899932
// remove schedule def
900933
.finish();
901934

902-
new_builder.add_reducer(
935+
builder.add_reducer(
903936
"check_deliveries",
904-
ProductType::from([("a", AlgebraicType::Ref(new_deliveries_type))]),
937+
ProductType::from([("a", AlgebraicType::Ref(deliveries_type))]),
905938
None,
906939
);
907940

908-
let new_inspections_type = new_builder
941+
let new_inspections_type = builder
909942
.build_table_with_new_type(
910943
"Inspections",
911944
ProductType::from([
912945
("scheduled_id", AlgebraicType::U64),
913-
("scheduled_at", new_schedule_at.clone()),
946+
("scheduled_at", schedule_at.clone()),
914947
]),
915948
true,
916949
)
@@ -921,28 +954,33 @@ mod tests {
921954
.finish();
922955

923956
// add reducer.
924-
new_builder.add_reducer(
957+
builder.add_reducer(
925958
"perform_inspection",
926959
ProductType::from([("a", AlgebraicType::Ref(new_inspections_type))]),
927960
None,
928961
);
929962

930963
// Add new table
931-
new_builder
964+
builder
932965
.build_table_with_new_type("Oranges", ProductType::from([("id", AlgebraicType::U32)]), true)
933966
.with_index(btree(0), "id_index")
934967
.with_column_sequence(0)
935968
.with_unique_constraint(0)
936969
.with_primary_key(0)
937970
.finish();
938971

939-
new_builder.add_row_level_security("SELECT * FROM Bananas");
972+
builder.add_row_level_security("SELECT * FROM Bananas");
940973

941-
let new_def: ModuleDef = new_builder
974+
builder
942975
.finish()
943976
.try_into()
944-
.expect("new_def should be a valid database definition");
977+
.expect("new_def should be a valid database definition")
978+
}
945979

980+
#[test]
981+
fn successful_auto_migration() {
982+
let old_def = initial_module_def();
983+
let new_def = updated_module_def();
946984
let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");
947985

948986
let apples = expect_identifier("Apples");
@@ -1392,4 +1430,48 @@ mod tests {
13921430
// but different columns from an old one.
13931431
// We've left the check in, just in case this changes in the future.
13941432
}
1433+
#[test]
1434+
fn print_empty_to_populated_schema_migration() {
1435+
// Start with completely empty schema
1436+
let old_builder = RawModuleDefV9Builder::new();
1437+
let old_def: ModuleDef = old_builder
1438+
.finish()
1439+
.try_into()
1440+
.expect("old_def should be a valid database definition");
1441+
1442+
let new_def = initial_module_def();
1443+
let plan = ponder_migrate(&old_def, &new_def).expect("auto migration should succeed");
1444+
1445+
insta::assert_snapshot!(
1446+
"empty_to_populated_migration",
1447+
plan.pretty_print(PrettyPrintStyle::AnsiColor)
1448+
.expect("should pretty print")
1449+
);
1450+
}
1451+
1452+
#[test]
1453+
fn print_supervised_migration() {
1454+
let old_def = initial_module_def();
1455+
let new_def = updated_module_def();
1456+
let plan = ponder_migrate(&old_def, &new_def).expect("auto migration should succeed");
1457+
1458+
insta::assert_snapshot!(
1459+
"updated pretty print",
1460+
plan.pretty_print(PrettyPrintStyle::AnsiColor)
1461+
.expect("should pretty print")
1462+
);
1463+
}
1464+
1465+
#[test]
1466+
fn no_color_print_supervised_migration() {
1467+
let old_def = initial_module_def();
1468+
let new_def = updated_module_def();
1469+
let plan = ponder_migrate(&old_def, &new_def).expect("auto migration should succeed");
1470+
1471+
insta::assert_snapshot!(
1472+
"updated pretty print no color",
1473+
plan.pretty_print(PrettyPrintStyle::NoColor)
1474+
.expect("should pretty print")
1475+
);
1476+
}
13951477
}

0 commit comments

Comments
 (0)