use sim_relation_core::{ColumnName, ConstraintName, IndexName, RelationId, TableName};
use sim_relation_plan::CheckedMutation;
use sim_relation_schema::{Column, Constraint, Index, Schema, Table};
#[derive(Clone, Debug)]
pub enum OperationKind {
CreateTable(Table),
DropTable(TableName),
RenameTable {
from: TableName,
to: TableName,
},
AddColumn {
table: TableName,
column: Column,
},
DropColumn {
table: TableName,
column: ColumnName,
},
RenameColumn {
table: TableName,
from: ColumnName,
to: ColumnName,
},
AlterColumn {
table: TableName,
column: ColumnName,
},
AddConstraint {
table: TableName,
constraint: Constraint,
},
DropConstraint {
table: TableName,
constraint: ConstraintName,
},
AddIndex {
table: TableName,
index: Index,
},
DropIndex {
table: TableName,
index: IndexName,
},
Backfill(Box<CheckedMutation>),
}
#[derive(Clone, Debug)]
pub struct Operation {
before: RelationId,
after: Schema,
kind: OperationKind,
}
impl Operation {
pub fn new(before: RelationId, after: Schema, kind: OperationKind) -> Self {
Self {
before,
after,
kind,
}
}
pub fn before(&self) -> &RelationId {
&self.before
}
pub fn after(&self) -> &Schema {
&self.after
}
pub fn kind(&self) -> &OperationKind {
&self.kind
}
}
#[derive(Clone, Debug)]
pub struct Revision {
id: RelationId,
parent: Option<RelationId>,
target: RelationId,
operations: Vec<Operation>,
}
impl Revision {
pub fn new(
id: RelationId,
parent: Option<RelationId>,
target: RelationId,
operations: Vec<Operation>,
) -> Self {
Self {
id,
parent,
target,
operations,
}
}
pub fn id(&self) -> &RelationId {
&self.id
}
pub fn parent(&self) -> Option<&RelationId> {
self.parent.as_ref()
}
pub fn target(&self) -> &RelationId {
&self.target
}
pub fn operations(&self) -> &[Operation] {
&self.operations
}
}
#[derive(Clone, Debug)]
pub struct MigrationProgram {
pub base_revision: RelationId,
pub base_schema: Schema,
pub revisions: Vec<Revision>,
pub target_schema: RelationId,
}
#[derive(Clone, Debug)]
pub struct CheckedProgram {
program: MigrationProgram,
}
impl CheckedProgram {
pub fn program(&self) -> &MigrationProgram {
&self.program
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MigrationError {
WrongParent,
StaleBefore,
InvalidBackfill,
RevisionTargetMismatch,
ProgramTargetMismatch,
IncompleteOperationCoverage,
AuthoredOperationRequired,
Identity,
}
pub fn admit(program: MigrationProgram) -> Result<CheckedProgram, MigrationError> {
let mut schema = program.base_schema.clone();
let mut parent = program.base_revision.clone();
for revision in &program.revisions {
if revision.parent.as_ref() != Some(&parent) {
return Err(MigrationError::WrongParent);
}
for operation in &revision.operations {
let current = schema.id().map_err(|_| MigrationError::Identity)?;
if operation.before != current {
return Err(MigrationError::StaleBefore);
}
validate_operation(&schema, operation)?;
if let OperationKind::Backfill(mutation) = &operation.kind
&& mutation.schema_id() != ¤t
{
return Err(MigrationError::InvalidBackfill);
}
schema = operation.after.clone();
}
if schema.id().map_err(|_| MigrationError::Identity)? != revision.target {
return Err(MigrationError::RevisionTargetMismatch);
}
parent = revision.id.clone();
}
if schema.id().map_err(|_| MigrationError::Identity)? != program.target_schema {
return Err(MigrationError::ProgramTargetMismatch);
}
Ok(CheckedProgram { program })
}
fn validate_operation(before: &Schema, operation: &Operation) -> Result<(), MigrationError> {
let after = &operation.after;
let bt = before.tables();
let at = after.tables();
let ok = match &operation.kind {
OperationKind::CreateTable(table) => {
!has_table(bt, table.name())
&& has_table(at, table.name())
&& at.len() == bt.len() + 1
&& bt.iter().all(|old| at.contains(old))
}
OperationKind::DropTable(name) => {
has_table(bt, name)
&& !has_table(at, name)
&& bt.len() == at.len() + 1
&& at.iter().all(|new| bt.contains(new))
}
OperationKind::AddColumn { table, column } => {
table_pair(bt, at, table).is_some_and(|(b, a)| {
!has_column(b, column.name())
&& has_column(a, column.name())
&& a.columns().len() == b.columns().len() + 1
&& b.columns().iter().all(|old| a.columns().contains(old))
&& b.constraints() == a.constraints()
&& b.indexes() == a.indexes()
&& same_other_tables(bt, at, table)
})
}
OperationKind::DropColumn { table, column } => {
table_pair(bt, at, table).is_some_and(|(b, a)| {
has_column(b, column)
&& !has_column(a, column)
&& b.columns().len() == a.columns().len() + 1
&& a.columns().iter().all(|new| b.columns().contains(new))
&& b.constraints() == a.constraints()
&& b.indexes() == a.indexes()
&& same_other_tables(bt, at, table)
})
}
OperationKind::AddConstraint { table, constraint } => table_pair(bt, at, table)
.is_some_and(|(b, a)| {
a.constraints().len() == b.constraints().len() + 1
&& a.constraints().contains(constraint)
}),
OperationKind::DropConstraint { table, .. } => table_pair(bt, at, table)
.is_some_and(|(b, a)| b.constraints().len() == a.constraints().len() + 1),
OperationKind::AddIndex { table, index } => {
table_pair(bt, at, table).is_some_and(|(b, a)| {
a.indexes().len() == b.indexes().len() + 1 && a.indexes().contains(index)
})
}
OperationKind::DropIndex { table, .. } => table_pair(bt, at, table)
.is_some_and(|(b, a)| b.indexes().len() == a.indexes().len() + 1),
OperationKind::RenameTable { from, to } => {
has_table(bt, from) && !has_table(bt, to) && !has_table(at, from) && has_table(at, to)
}
OperationKind::RenameColumn { table, from, to } => {
table_pair(bt, at, table).is_some_and(|(b, a)| {
has_column(b, from)
&& !has_column(b, to)
&& !has_column(a, from)
&& has_column(a, to)
})
}
OperationKind::AlterColumn { table, column } => {
table_pair(bt, at, table).is_some_and(|(b, a)| {
has_column(b, column) && has_column(a, column) && b.columns() != a.columns()
})
}
OperationKind::Backfill(_) => before.id().ok() == after.id().ok(),
};
if ok {
Ok(())
} else {
Err(MigrationError::IncompleteOperationCoverage)
}
}
fn has_table(tables: &[Table], name: &TableName) -> bool {
tables.iter().any(|t| t.name() == name)
}
fn has_column(table: &Table, name: &ColumnName) -> bool {
table.columns().iter().any(|c| c.name() == name)
}
fn table_pair<'a>(
before: &'a [Table],
after: &'a [Table],
name: &TableName,
) -> Option<(&'a Table, &'a Table)> {
Some((
before.iter().find(|t| t.name() == name)?,
after.iter().find(|t| t.name() == name)?,
))
}
fn same_other_tables(before: &[Table], after: &[Table], changed: &TableName) -> bool {
before.len() == after.len()
&& before
.iter()
.filter(|table| table.name() != changed)
.all(|table| after.contains(table))
}
pub fn derive_lossless(before: &Schema, after: &Schema) -> Result<Vec<Operation>, MigrationError> {
let mut operations = Vec::new();
let mut current = before.clone();
for table in after.tables() {
match current.tables().iter().find(|t| t.name() == table.name()) {
None => {
if after.tables().len() != before.tables().len() + 1 {
return Err(MigrationError::AuthoredOperationRequired);
}
operations.push(Operation::new(
current.id().map_err(|_| MigrationError::Identity)?,
after.clone(),
OperationKind::CreateTable(table.clone()),
));
current = after.clone();
}
Some(old) if old != table => {
let additions: Vec<_> = table
.columns()
.iter()
.filter(|c| !has_column(old, c.name()))
.collect();
if additions.len() != 1
|| !additions[0].nullable()
|| table.columns().len() != old.columns().len() + 1
|| after.tables().len() != before.tables().len()
{
return Err(MigrationError::AuthoredOperationRequired);
}
operations.push(Operation::new(
current.id().map_err(|_| MigrationError::Identity)?,
after.clone(),
OperationKind::AddColumn {
table: table.name().clone(),
column: additions[0].clone(),
},
));
current = after.clone();
}
_ => {}
}
}
if current.id().map_err(|_| MigrationError::Identity)?
!= after.id().map_err(|_| MigrationError::Identity)?
{
return Err(MigrationError::AuthoredOperationRequired);
}
Ok(operations)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MigrationCapabilities {
pub transactional_ddl: bool,
pub post_apply_introspection: bool,
}
impl MigrationCapabilities {
pub fn require(self) -> Result<(), CapabilityError> {
if self.transactional_ddl && self.post_apply_introspection {
Ok(())
} else {
Err(CapabilityError)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CapabilityError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SchemaAttestation {
pub logical_schema: RelationId,
pub physical_schema: RelationId,
pub revision: RelationId,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdoptionManifest {
pub logical_schema: RelationId,
pub physical_schema: RelationId,
}
impl AdoptionManifest {
pub fn verify(&self, live_physical_schema: &RelationId) -> Result<(), AdoptionError> {
if &self.physical_schema == live_physical_schema {
Ok(())
} else {
Err(AdoptionError::ExternalDrift)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AdoptionError {
ExternalDrift,
}