Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -30,6 +30,8 @@ smallvec.workspace = true
hashbrown.workspace = true
enum-as-inner.workspace = true
enum-map.workspace = true
insta.workspace = true
termcolor.workspace = true

[dev-dependencies]
spacetimedb-lib = { path = "../lib", features = ["test"] }
Expand Down
160 changes: 121 additions & 39 deletions crates/schema/src/auto_migrate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use core::{cmp::Ordering, ops::BitOr};

use crate::{def::*, error::PrettyAlgebraicType, identifier::Identifier};
use formatter::format_plan;
use spacetimedb_data_structures::{
error_stream::{CollectAllErrors, CombineErrors, ErrorStream},
map::HashSet,
Expand All @@ -13,6 +14,9 @@ use spacetimedb_sats::{
layout::{HasLayout, SumTypeLayout},
WithTypespace,
};
use termcolor_formatter::{ColorScheme, TermColorFormatter};
mod formatter;
mod termcolor_formatter;

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

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

#[derive(Copy, Clone, PartialEq, Eq)]
pub enum PrettyPrintStyle {
AnsiColor,
NoColor,
}

impl<'def> MigratePlan<'def> {
/// Get the old `ModuleDef` for this migration plan.
pub fn old_def(&self) -> &'def ModuleDef {
Expand All @@ -39,6 +49,28 @@ impl<'def> MigratePlan<'def> {
MigratePlan::Auto(plan) => plan.new,
}
}

pub fn pretty_print(&self, style: PrettyPrintStyle) -> anyhow::Result<String> {
use PrettyPrintStyle::*;

match self {
MigratePlan::Manual(_) => {
anyhow::bail!("Manual migration plans are not yet supported for pretty printing.")
}

MigratePlan::Auto(plan) => match style {
NoColor => {
let mut fmt = TermColorFormatter::new(ColorScheme::default(), termcolor::ColorChoice::Never);
format_plan(&mut fmt, plan).map(|_| fmt.to_string())
}
AnsiColor => {
let mut fmt = TermColorFormatter::new(ColorScheme::default(), termcolor::ColorChoice::AlwaysAnsi);
format_plan(&mut fmt, plan).map(|_| fmt.to_string())
}
}
.map_err(|e| anyhow::anyhow!("Failed to format migration plan: {e}")),
}
}
}

/// A plan for a manual migration.
Expand Down Expand Up @@ -766,20 +798,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 @@ -789,7 +820,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 @@ -802,59 +833,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 @@ -866,7 +899,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 @@ -884,13 +917,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 @@ -899,18 +932,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 @@ -921,28 +954,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 @@ -1392,4 +1430,48 @@ 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(PrettyPrintStyle::AnsiColor)
.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(PrettyPrintStyle::AnsiColor)
.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(PrettyPrintStyle::NoColor)
.expect("should pretty print")
);
}
}
Loading
Loading