Skip to content

Commit

Permalink
Add pretty printing code for automigrations to schema
Browse files Browse the repository at this point in the history
  • Loading branch information
kazimuth committed Dec 16, 2024
1 parent 9211708 commit 98c9399
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 24 deletions.
14 changes: 1 addition & 13 deletions crates/core/src/db/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use spacetimedb_lib::db::auth::StTableType;
use spacetimedb_lib::identity::AuthCtx;
use spacetimedb_lib::AlgebraicValue;
use spacetimedb_primitives::ColSet;
use spacetimedb_schema::auto_migrate::{AutoMigratePlan, ManualMigratePlan, MigratePlan};
use spacetimedb_schema::auto_migrate::{AutoMigratePlan, MigratePlan};
use spacetimedb_schema::def::TableDef;
use spacetimedb_schema::schema::{IndexSchema, Schema, SequenceSchema, TableSchema};
use std::sync::Arc;
Expand Down Expand Up @@ -45,22 +45,10 @@ pub fn update_database(
}

match plan {
MigratePlan::Manual(plan) => manual_migrate_database(stdb, tx, plan, system_logger, existing_tables),
MigratePlan::Auto(plan) => auto_migrate_database(stdb, tx, auth_ctx, plan, system_logger, existing_tables),
}
}

/// Manually migrate a database.
fn manual_migrate_database(
_stdb: &RelationalDB,
_tx: &mut MutTxId,
_plan: ManualMigratePlan,
_system_logger: &SystemLogger,
_existing_tables: Vec<Arc<TableSchema>>,
) -> anyhow::Result<()> {
unimplemented!("Manual database migrations are not yet implemented")
}

/// Automatically migrate a database.
fn auto_migrate_database(
stdb: &RelationalDB,
Expand Down
2 changes: 2 additions & 0 deletions crates/schema/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ spacetimedb-sats.workspace = true
spacetimedb-data-structures.workspace = true
spacetimedb-sql-parser.workspace = true

regex.workspace = true
anyhow.workspace = true
colored.workspace = true
indexmap.workspace = true
itertools.workspace = true
lazy_static.workspace = true
Expand Down
13 changes: 2 additions & 11 deletions crates/schema/src/auto_migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,32 @@ use spacetimedb_data_structures::{
use spacetimedb_lib::db::raw_def::v9::{RawRowLevelSecurityDefV9, TableType};
use spacetimedb_sats::WithTypespace;

pub mod pretty_print;

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

/// A plan for a migration.
#[derive(Debug)]
pub enum MigratePlan<'def> {
Manual(ManualMigratePlan<'def>),
Auto(AutoMigratePlan<'def>),
}

impl<'def> MigratePlan<'def> {
/// Get the old `ModuleDef` for this migration plan.
pub fn old_def(&self) -> &'def ModuleDef {
match self {
MigratePlan::Manual(plan) => plan.old,
MigratePlan::Auto(plan) => plan.old,
}
}

/// Get the new `ModuleDef` for this migration plan.
pub fn new_def(&self) -> &'def ModuleDef {
match self {
MigratePlan::Manual(plan) => plan.new,
MigratePlan::Auto(plan) => plan.new,
}
}
}

/// A plan for a manual migration.
/// `new` must have a reducer marked with `Lifecycle::Update`.
#[derive(Debug)]
pub struct ManualMigratePlan<'def> {
pub old: &'def ModuleDef,
pub new: &'def ModuleDef,
}

/// A plan for an automatic migration.
#[derive(Debug)]
pub struct AutoMigratePlan<'def> {
Expand Down
266 changes: 266 additions & 0 deletions crates/schema/src/auto_migrate/pretty_print.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
//! This module provides a function [`pretty_print`](pretty_print) that renders an automatic migration plan to a string.
use super::{AutoMigratePlan, IndexAlgorithm, TableDef};
use crate::{auto_migrate::AutoMigrateStep, def::ConstraintData};
use colored::{self, ColoredString, Colorize};
use lazy_static::lazy_static;
use regex::Regex;
use spacetimedb_lib::db::raw_def::v9::{TableAccess, TableType};
use spacetimedb_primitives::{ColId, ColList};
use spacetimedb_sats::{algebraic_type::fmt::fmt_algebraic_type, WithTypespace};
use std::fmt::{self, Write};

lazy_static! {
// https://superuser.com/questions/380772/removing-ansi-color-codes-from-text-stream
static ref ANSI_ESCAPE_SEQUENCE: Regex = Regex::new(
r#"(?x) # verbose mode
(?:
\x1b \[ [\x30-\x3f]* [\x20-\x2f]* [\x40-\x7e] # CSI sequences (start with "ESC [")
| \x1b [PX^_] .*? \x1b \\ # String Terminator sequences (end with "ESC \")
| \x1b \] [^\x07]* (?: \x07 | \x1b \\ ) # Sequences ending in BEL ("\x07")
| \x1b [\x40-\x5f]
)"#
)
.unwrap();
}

/// Strip ANSI escape sequences from a string.
/// This is needed when printing in a terminal without support for these sequences,
/// such as `CMD.exe`.
pub fn strip_ansi_escape_codes(s: &str) -> String {
ANSI_ESCAPE_SEQUENCE.replace_all(s, "").into_owned()
}

/// Pretty print a migration plan, resulting in a string (containing ANSI escape codes).
/// If you are printing
pub fn pretty_print(plan: &AutoMigratePlan) -> Result<String, fmt::Error> {
let mut out = String::new();
let outr = &mut out;

writeln!(outr, "{}", "Migration plan".blue())?;
writeln!(outr, "{}", "--------------")?;
writeln!(outr, "")?;

let added = "+".green().bold();
let removed = "-".red().bold();

for step in &plan.steps {
match step {
AutoMigrateStep::AddTable(t) => {
let table = plan.new.table(*t).ok_or(fmt::Error)?;

write!(outr, "{} table: {}", added, table_name(&*table.name))?;
if table.table_type == TableType::System {
write!(outr, " (system)")?;
}
match table.table_access {
TableAccess::Private => write!(outr, "(private)")?,
TableAccess::Public => write!(outr, "(public)")?,
}
writeln!(outr)?;
for column in &table.columns {
let resolved = WithTypespace::new(plan.new.typespace(), &column.ty)
.resolve_refs()
.map_err(|_| fmt::Error)?;

writeln!(
outr,
" {} column: {}: {}",
added,
column.name,
fmt_algebraic_type(&resolved)
)?;
}
for constraint in table.constraints.values() {
match &constraint.data {
ConstraintData::Unique(unique) => {
write!(outr, " {} unique constraint on ", added)?;
write_col_list(outr, &unique.columns.clone().into(), table)?;
writeln!(outr)?;
}
}
}
for index in table.indexes.values() {
match &index.algorithm {
IndexAlgorithm::BTree(btree) => {
write!(outr, " {} btree index on ", added)?;
write_col_list(outr, &btree.columns, table)?;
writeln!(outr)?;
}
}
}
for sequence in table.sequences.values() {
let column = column_name(table, sequence.column);
writeln!(outr, " {} sequence on {}", added, column)?;
}
if let Some(schedule) = &table.schedule {
let reducer = reducer_name(&*schedule.reducer_name);
writeln!(outr, " {} schedule calling {}", added, reducer)?;
}
}
AutoMigrateStep::AddIndex(index) => {
let table_def = plan.new.stored_in_table_def(*index).ok_or(fmt::Error)?;
let index_def = table_def.indexes.get(*index).ok_or(fmt::Error)?;

write!(
outr,
"{} index on table {} columns ",
added,
table_name(&*table_def.name)
)?;
write_col_list(outr, index_def.algorithm.columns(), table_def)?;
writeln!(outr)?;
}
AutoMigrateStep::RemoveIndex(index) => {
let table_def = plan.old.stored_in_table_def(*index).ok_or(fmt::Error)?;
let index_def = table_def.indexes.get(*index).ok_or(fmt::Error)?;

write!(
outr,
"{} index on table {} columns ",
removed,
table_name(&*table_def.name)
)?;
write_col_list(outr, index_def.algorithm.columns(), table_def)?;
writeln!(outr)?;
}
AutoMigrateStep::RemoveConstraint(constraint) => {
let table_def = plan.old.stored_in_table_def(constraint).ok_or(fmt::Error)?;
let constraint_def = table_def.constraints.get(*constraint).ok_or(fmt::Error)?;
match &constraint_def.data {
ConstraintData::Unique(unique_constraint_data) => {
write!(
outr,
"{} unique constraint on table {} columns ",
removed,
table_name(&*table_def.name)
)?;
write_col_list(outr, &unique_constraint_data.columns.clone().into(), table_def)?;
writeln!(outr)?;
}
}
}
AutoMigrateStep::AddSequence(sequence) => {
let table_def = plan.new.stored_in_table_def(*sequence).ok_or(fmt::Error)?;
let sequence_def = table_def.sequences.get(*sequence).ok_or(fmt::Error)?;

write!(
outr,
"{} sequence on table {} column {}",
added,
table_name(&*table_def.name),
column_name(table_def, sequence_def.column)
)?;
writeln!(outr)?;
}
AutoMigrateStep::RemoveSequence(sequence) => {
let table_def = plan.old.stored_in_table_def(*sequence).ok_or(fmt::Error)?;
let sequence_def = table_def.sequences.get(*sequence).ok_or(fmt::Error)?;

write!(
outr,
"{} sequence on table {} column {}",
removed,
table_name(&*table_def.name),
column_name(table_def, sequence_def.column)
)?;
writeln!(outr)?;
}
AutoMigrateStep::ChangeAccess(table) => {
let table_def = plan.new.table(*table).ok_or(fmt::Error)?;

write!(
outr,
"{} table access for table {}",
added,
table_name(&*table_def.name)
)?;
match table_def.table_access {
TableAccess::Private => write!(outr, " (public -> private)")?,
TableAccess::Public => write!(outr, " (private -> public)")?,
}
writeln!(outr)?;
}
AutoMigrateStep::AddSchedule(schedule) => {
let table_def = plan.new.table(*schedule).ok_or(fmt::Error)?;
let schedule_def = table_def.schedule.as_ref().ok_or(fmt::Error)?;

let reducer = reducer_name(&*schedule_def.reducer_name);
write!(
outr,
"{} schedule for table {} calling {}",
added,
table_name(&*table_def.name),
reducer
)?;
writeln!(outr)?;
}
AutoMigrateStep::RemoveSchedule(schedule) => {
let table_def = plan.old.table(*schedule).ok_or(fmt::Error)?;
let schedule_def = table_def.schedule.as_ref().ok_or(fmt::Error)?;

let reducer = reducer_name(&*schedule_def.reducer_name);
write!(
outr,
"{} schedule for table {} calling {}",
removed,
table_name(&*table_def.name),
reducer
)?;
writeln!(outr)?;
}
AutoMigrateStep::AddRowLevelSecurity(rls) => {
writeln!(outr, "{} row level security policy:", added)?;
writeln!(outr, "=============================")?;
writeln!(outr, "{}", rls)?;
writeln!(outr, "=============================")?;
}
AutoMigrateStep::RemoveRowLevelSecurity(rls) => {
writeln!(outr, "{} row level security policy:", removed)?;
writeln!(outr, "=============================")?;
writeln!(outr, "{}", rls)?;
writeln!(outr, "=============================")?;
}
}
}

Ok(out)
}

fn column_name(table_def: &TableDef, col_id: ColId) -> ColoredString {
table_def
.columns
.get(col_id.idx())
.map(|def| &*def.name)
.unwrap_or("unknown_column")
.magenta()
}

fn reducer_name(name: &str) -> ColoredString {
name.blue()
}

fn table_name(name: &str) -> ColoredString {
name.green()
}

fn write_col_list(out: &mut String, col_list: &ColList, table_def: &TableDef) -> Result<(), fmt::Error> {
write!(out, "[")?;
for (i, col) in col_list.iter().enumerate() {
let join = if i == 0 { "" } else { ", " };
write!(out, "{}{}", join, column_name(table_def, col))?;
}
write!(out, "]")?;

Ok(())
}

#[cfg(test)]
mod tests {
#[test]
fn test_strip_ansi_escape_sequences() {
let input = "\x1b[31mHello, \x1b[32mworld!\x1b[0m";
let expected = "Hello, world!";
assert_eq!(super::strip_ansi_escape_codes(input), expected);
}
}

0 comments on commit 98c9399

Please sign in to comment.