Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/schema/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ hashbrown.workspace = true
enum-as-inner.workspace = true
enum-map.workspace = true
colored.workspace = true
regex.workspace = true
insta.workspace = true

[dev-dependencies]
spacetimedb-lib = { path = "../lib", features = ["test"] }
Expand Down
144 changes: 100 additions & 44 deletions crates/schema/src/auto_migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use spacetimedb_sats::{
};
mod ansi_formatter;
mod formatter;
mod plain_formatter;

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

Expand Down Expand Up @@ -44,16 +45,24 @@ impl<'def> MigratePlan<'def> {
}
}

pub fn pretty_print(&self) -> anyhow::Result<String> {
pub fn pretty_print(&self, no_color: bool) -> anyhow::Result<String> {
match self {
MigratePlan::Manual(_) => {
anyhow::bail!("Manual migration plans are not yet supported for pretty printing.")
}
MigratePlan::Auto(plan) => {
let mut fmt = AnsiFormatter::new(1024, ColorScheme::default());
format_plan(&mut fmt, plan)
.map_err(|e| anyhow::anyhow!("Failed to format migration plan: {e}"))
.map(|_| fmt.to_string())
if no_color {
let mut fmt = plain_formatter::PlainFormatter::new(1024);
format_plan(&mut fmt, plan)
.map_err(|e| anyhow::anyhow!("Failed to format migration plan: {e}"))
.map(|_| fmt.to_string())
} else {
// Use the ANSI formatter with colors.
let mut fmt = AnsiFormatter::new(1024, ColorScheme::default());
format_plan(&mut fmt, plan)
.map_err(|e| anyhow::anyhow!("Failed to format migration plan: {e}"))
.map(|_| fmt.to_string())
}
}
}
}
Expand Down Expand Up @@ -780,20 +789,19 @@ mod tests {
use v9::{RawModuleDefV9Builder, TableAccess};
use validate::tests::expect_identifier;

#[test]
fn successful_auto_migration() {
let mut old_builder = RawModuleDefV9Builder::new();
let old_schedule_at = old_builder.add_type::<ScheduleAt>();
let old_sum_ty = AlgebraicType::sum([("v1", AlgebraicType::U64)]);
let old_sum_refty = old_builder.add_algebraic_type([], "sum", old_sum_ty, true);
old_builder
fn initial_module_def() -> ModuleDef {
let mut builder = RawModuleDefV9Builder::new();
let schedule_at = builder.add_type::<ScheduleAt>();
let sum_ty = AlgebraicType::sum([("v1", AlgebraicType::U64)]);
let sum_refty = builder.add_algebraic_type([], "sum", sum_ty, true);
builder
.build_table_with_new_type(
"Apples",
ProductType::from([
("id", AlgebraicType::U64),
("name", AlgebraicType::String),
("count", AlgebraicType::U16),
("sum", old_sum_refty.into()),
("sum", sum_refty.into()),
]),
true,
)
Expand All @@ -803,7 +811,7 @@ mod tests {
.with_index(btree([0, 1]), "id_name_index")
.finish();

old_builder
builder
.build_table_with_new_type(
"Bananas",
ProductType::from([
Expand All @@ -816,59 +824,61 @@ mod tests {
.with_access(TableAccess::Public)
.finish();

let old_deliveries_type = old_builder
let deliveries_type = builder
.build_table_with_new_type(
"Deliveries",
ProductType::from([
("scheduled_id", AlgebraicType::U64),
("scheduled_at", old_schedule_at.clone()),
("sum", AlgebraicType::array(old_sum_refty.into())),
("scheduled_at", schedule_at.clone()),
("sum", AlgebraicType::array(sum_refty.into())),
]),
true,
)
.with_auto_inc_primary_key(0)
.with_index_no_accessor_name(btree(0))
.with_schedule("check_deliveries", 1)
.finish();
old_builder.add_reducer(
builder.add_reducer(
"check_deliveries",
ProductType::from([("a", AlgebraicType::Ref(old_deliveries_type))]),
ProductType::from([("a", AlgebraicType::Ref(deliveries_type))]),
None,
);

old_builder
builder
.build_table_with_new_type(
"Inspections",
ProductType::from([
("scheduled_id", AlgebraicType::U64),
("scheduled_at", old_schedule_at.clone()),
("scheduled_at", schedule_at.clone()),
]),
true,
)
.with_auto_inc_primary_key(0)
.with_index_no_accessor_name(btree(0))
.finish();

old_builder.add_row_level_security("SELECT * FROM Apples");
builder.add_row_level_security("SELECT * FROM Apples");

let old_def: ModuleDef = old_builder
builder
.finish()
.try_into()
.expect("old_def should be a valid database definition");
.expect("old_def should be a valid database definition")
}

let mut new_builder = RawModuleDefV9Builder::new();
let _ = new_builder.add_type::<u32>(); // reposition ScheduleAt in the typespace, should have no effect.
let new_schedule_at = new_builder.add_type::<ScheduleAt>();
let new_sum_ty = AlgebraicType::sum([("v1", AlgebraicType::U64), ("v2", AlgebraicType::Bool)]);
let new_sum_refty = new_builder.add_algebraic_type([], "sum", new_sum_ty, true);
new_builder
fn updated_module_def() -> ModuleDef {
let mut builder = RawModuleDefV9Builder::new();
let _ = builder.add_type::<u32>(); // reposition ScheduleAt in the typespace, should have no effect.
let schedule_at = builder.add_type::<ScheduleAt>();
let sum_ty = AlgebraicType::sum([("v1", AlgebraicType::U64), ("v2", AlgebraicType::Bool)]);
let sum_refty = builder.add_algebraic_type([], "sum", sum_ty, true);
builder
.build_table_with_new_type(
"Apples",
ProductType::from([
("id", AlgebraicType::U64),
("name", AlgebraicType::String),
("count", AlgebraicType::U16),
("sum", new_sum_refty.into()),
("sum", sum_refty.into()),
]),
true,
)
Expand All @@ -880,7 +890,7 @@ mod tests {
.with_index(btree([0, 2]), "id_count_index")
.finish();

new_builder
builder
.build_table_with_new_type(
"Bananas",
ProductType::from([
Expand All @@ -898,13 +908,13 @@ mod tests {
.with_access(TableAccess::Private)
.finish();

let new_deliveries_type = new_builder
let deliveries_type = builder
.build_table_with_new_type(
"Deliveries",
ProductType::from([
("scheduled_id", AlgebraicType::U64),
("scheduled_at", new_schedule_at.clone()),
("sum", AlgebraicType::array(new_sum_refty.into())),
("scheduled_at", schedule_at.clone()),
("sum", AlgebraicType::array(sum_refty.into())),
]),
true,
)
Expand All @@ -913,18 +923,18 @@ mod tests {
// remove schedule def
.finish();

new_builder.add_reducer(
builder.add_reducer(
"check_deliveries",
ProductType::from([("a", AlgebraicType::Ref(new_deliveries_type))]),
ProductType::from([("a", AlgebraicType::Ref(deliveries_type))]),
None,
);

let new_inspections_type = new_builder
let new_inspections_type = builder
.build_table_with_new_type(
"Inspections",
ProductType::from([
("scheduled_id", AlgebraicType::U64),
("scheduled_at", new_schedule_at.clone()),
("scheduled_at", schedule_at.clone()),
]),
true,
)
Expand All @@ -935,28 +945,33 @@ mod tests {
.finish();

// add reducer.
new_builder.add_reducer(
builder.add_reducer(
"perform_inspection",
ProductType::from([("a", AlgebraicType::Ref(new_inspections_type))]),
None,
);

// Add new table
new_builder
builder
.build_table_with_new_type("Oranges", ProductType::from([("id", AlgebraicType::U32)]), true)
.with_index(btree(0), "id_index")
.with_column_sequence(0)
.with_unique_constraint(0)
.with_primary_key(0)
.finish();

new_builder.add_row_level_security("SELECT * FROM Bananas");
builder.add_row_level_security("SELECT * FROM Bananas");

let new_def: ModuleDef = new_builder
builder
.finish()
.try_into()
.expect("new_def should be a valid database definition");
.expect("new_def should be a valid database definition")
}

#[test]
fn successful_auto_migration() {
let old_def = initial_module_def();
let new_def = updated_module_def();
let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed");

let apples = expect_identifier("Apples");
Expand Down Expand Up @@ -1404,4 +1419,45 @@ mod tests {
// but different columns from an old one.
// We've left the check in, just in case this changes in the future.
}
#[test]
fn print_empty_to_populated_schema_migration() {
// Start with completely empty schema
let old_builder = RawModuleDefV9Builder::new();
let old_def: ModuleDef = old_builder
.finish()
.try_into()
.expect("old_def should be a valid database definition");

let new_def = initial_module_def();
let plan = ponder_migrate(&old_def, &new_def).expect("auto migration should succeed");

insta::assert_snapshot!(
"empty_to_populated_migration",
plan.pretty_print(false).expect("should pretty print")
);
}

#[test]
fn print_supervised_migration() {
let old_def = initial_module_def();
let new_def = updated_module_def();
let plan = ponder_migrate(&old_def, &new_def).expect("auto migration should succeed");

insta::assert_snapshot!(
"updated pretty print",
plan.pretty_print(false).expect("should pretty print")
);
}

#[test]
fn no_color_print_supervised_migration() {
let old_def = initial_module_def();
let new_def = updated_module_def();
let plan = ponder_migrate(&old_def, &new_def).expect("auto migration should succeed");

insta::assert_snapshot!(
"updated pretty print no color",
plan.pretty_print(true).expect("should pretty print")
);
}
}
Loading
Loading