1use sim_relation_core::{ColumnName, ConstraintName, IndexName, RelationId, TableName};
16use sim_relation_plan::CheckedMutation;
17use sim_relation_schema::{Column, Constraint, Index, Schema, Table};
18
19#[derive(Clone, Debug)]
21pub enum OperationKind {
22 CreateTable(Table),
24 DropTable(TableName),
26 RenameTable {
28 from: TableName,
30 to: TableName,
32 },
33 AddColumn {
35 table: TableName,
37 column: Column,
39 },
40 DropColumn {
42 table: TableName,
44 column: ColumnName,
46 },
47 RenameColumn {
49 table: TableName,
51 from: ColumnName,
53 to: ColumnName,
55 },
56 AlterColumn {
58 table: TableName,
60 column: ColumnName,
62 },
63 AddConstraint {
65 table: TableName,
67 constraint: Constraint,
69 },
70 DropConstraint {
72 table: TableName,
74 constraint: ConstraintName,
76 },
77 AddIndex {
79 table: TableName,
81 index: Index,
83 },
84 DropIndex {
86 table: TableName,
88 index: IndexName,
90 },
91 Backfill(Box<CheckedMutation>),
93}
94
95#[derive(Clone, Debug)]
98pub struct Operation {
99 before: RelationId,
100 after: Schema,
101 kind: OperationKind,
102}
103impl Operation {
104 pub fn new(before: RelationId, after: Schema, kind: OperationKind) -> Self {
106 Self {
107 before,
108 after,
109 kind,
110 }
111 }
112 pub fn before(&self) -> &RelationId {
114 &self.before
115 }
116 pub fn after(&self) -> &Schema {
118 &self.after
119 }
120 pub fn kind(&self) -> &OperationKind {
122 &self.kind
123 }
124}
125
126#[derive(Clone, Debug)]
128pub struct Revision {
129 id: RelationId,
130 parent: Option<RelationId>,
131 target: RelationId,
132 operations: Vec<Operation>,
133}
134impl Revision {
135 pub fn new(
138 id: RelationId,
139 parent: Option<RelationId>,
140 target: RelationId,
141 operations: Vec<Operation>,
142 ) -> Self {
143 Self {
144 id,
145 parent,
146 target,
147 operations,
148 }
149 }
150 pub fn id(&self) -> &RelationId {
152 &self.id
153 }
154 pub fn parent(&self) -> Option<&RelationId> {
156 self.parent.as_ref()
157 }
158 pub fn target(&self) -> &RelationId {
160 &self.target
161 }
162 pub fn operations(&self) -> &[Operation] {
164 &self.operations
165 }
166}
167
168#[derive(Clone, Debug)]
170pub struct MigrationProgram {
171 pub base_revision: RelationId,
173 pub base_schema: Schema,
175 pub revisions: Vec<Revision>,
177 pub target_schema: RelationId,
179}
180
181#[derive(Clone, Debug)]
183pub struct CheckedProgram {
184 program: MigrationProgram,
185}
186impl CheckedProgram {
187 pub fn program(&self) -> &MigrationProgram {
189 &self.program
190 }
191}
192
193#[derive(Clone, Debug, PartialEq, Eq)]
195pub enum MigrationError {
196 WrongParent,
198 StaleBefore,
200 InvalidBackfill,
202 RevisionTargetMismatch,
204 ProgramTargetMismatch,
206 IncompleteOperationCoverage,
208 AuthoredOperationRequired,
210 Identity,
212}
213
214pub fn admit(program: MigrationProgram) -> Result<CheckedProgram, MigrationError> {
216 let mut schema = program.base_schema.clone();
217 let mut parent = program.base_revision.clone();
218 for revision in &program.revisions {
219 if revision.parent.as_ref() != Some(&parent) {
220 return Err(MigrationError::WrongParent);
221 }
222 for operation in &revision.operations {
223 let current = schema.id().map_err(|_| MigrationError::Identity)?;
224 if operation.before != current {
225 return Err(MigrationError::StaleBefore);
226 }
227 validate_operation(&schema, operation)?;
228 if let OperationKind::Backfill(mutation) = &operation.kind
229 && mutation.schema_id() != ¤t
230 {
231 return Err(MigrationError::InvalidBackfill);
232 }
233 schema = operation.after.clone();
234 }
235 if schema.id().map_err(|_| MigrationError::Identity)? != revision.target {
236 return Err(MigrationError::RevisionTargetMismatch);
237 }
238 parent = revision.id.clone();
239 }
240 if schema.id().map_err(|_| MigrationError::Identity)? != program.target_schema {
241 return Err(MigrationError::ProgramTargetMismatch);
242 }
243 Ok(CheckedProgram { program })
244}
245
246fn validate_operation(before: &Schema, operation: &Operation) -> Result<(), MigrationError> {
247 let after = &operation.after;
248 let bt = before.tables();
249 let at = after.tables();
250 let ok = match &operation.kind {
251 OperationKind::CreateTable(table) => {
252 !has_table(bt, table.name())
253 && has_table(at, table.name())
254 && at.len() == bt.len() + 1
255 && bt.iter().all(|old| at.contains(old))
256 }
257 OperationKind::DropTable(name) => {
258 has_table(bt, name)
259 && !has_table(at, name)
260 && bt.len() == at.len() + 1
261 && at.iter().all(|new| bt.contains(new))
262 }
263 OperationKind::AddColumn { table, column } => {
264 table_pair(bt, at, table).is_some_and(|(b, a)| {
265 !has_column(b, column.name())
266 && has_column(a, column.name())
267 && a.columns().len() == b.columns().len() + 1
268 && b.columns().iter().all(|old| a.columns().contains(old))
269 && b.constraints() == a.constraints()
270 && b.indexes() == a.indexes()
271 && same_other_tables(bt, at, table)
272 })
273 }
274 OperationKind::DropColumn { table, column } => {
275 table_pair(bt, at, table).is_some_and(|(b, a)| {
276 has_column(b, column)
277 && !has_column(a, column)
278 && b.columns().len() == a.columns().len() + 1
279 && a.columns().iter().all(|new| b.columns().contains(new))
280 && b.constraints() == a.constraints()
281 && b.indexes() == a.indexes()
282 && same_other_tables(bt, at, table)
283 })
284 }
285 OperationKind::AddConstraint { table, constraint } => table_pair(bt, at, table)
286 .is_some_and(|(b, a)| {
287 a.constraints().len() == b.constraints().len() + 1
288 && a.constraints().contains(constraint)
289 }),
290 OperationKind::DropConstraint { table, .. } => table_pair(bt, at, table)
291 .is_some_and(|(b, a)| b.constraints().len() == a.constraints().len() + 1),
292 OperationKind::AddIndex { table, index } => {
293 table_pair(bt, at, table).is_some_and(|(b, a)| {
294 a.indexes().len() == b.indexes().len() + 1 && a.indexes().contains(index)
295 })
296 }
297 OperationKind::DropIndex { table, .. } => table_pair(bt, at, table)
298 .is_some_and(|(b, a)| b.indexes().len() == a.indexes().len() + 1),
299 OperationKind::RenameTable { from, to } => {
300 has_table(bt, from) && !has_table(bt, to) && !has_table(at, from) && has_table(at, to)
301 }
302 OperationKind::RenameColumn { table, from, to } => {
303 table_pair(bt, at, table).is_some_and(|(b, a)| {
304 has_column(b, from)
305 && !has_column(b, to)
306 && !has_column(a, from)
307 && has_column(a, to)
308 })
309 }
310 OperationKind::AlterColumn { table, column } => {
311 table_pair(bt, at, table).is_some_and(|(b, a)| {
312 has_column(b, column) && has_column(a, column) && b.columns() != a.columns()
313 })
314 }
315 OperationKind::Backfill(_) => before.id().ok() == after.id().ok(),
316 };
317 if ok {
318 Ok(())
319 } else {
320 Err(MigrationError::IncompleteOperationCoverage)
321 }
322}
323fn has_table(tables: &[Table], name: &TableName) -> bool {
324 tables.iter().any(|t| t.name() == name)
325}
326fn has_column(table: &Table, name: &ColumnName) -> bool {
327 table.columns().iter().any(|c| c.name() == name)
328}
329fn table_pair<'a>(
330 before: &'a [Table],
331 after: &'a [Table],
332 name: &TableName,
333) -> Option<(&'a Table, &'a Table)> {
334 Some((
335 before.iter().find(|t| t.name() == name)?,
336 after.iter().find(|t| t.name() == name)?,
337 ))
338}
339fn same_other_tables(before: &[Table], after: &[Table], changed: &TableName) -> bool {
340 before.len() == after.len()
341 && before
342 .iter()
343 .filter(|table| table.name() != changed)
344 .all(|table| after.contains(table))
345}
346
347pub fn derive_lossless(before: &Schema, after: &Schema) -> Result<Vec<Operation>, MigrationError> {
350 let mut operations = Vec::new();
351 let mut current = before.clone();
352 for table in after.tables() {
353 match current.tables().iter().find(|t| t.name() == table.name()) {
354 None => {
355 if after.tables().len() != before.tables().len() + 1 {
356 return Err(MigrationError::AuthoredOperationRequired);
357 }
358 operations.push(Operation::new(
359 current.id().map_err(|_| MigrationError::Identity)?,
360 after.clone(),
361 OperationKind::CreateTable(table.clone()),
362 ));
363 current = after.clone();
364 }
365 Some(old) if old != table => {
366 let additions: Vec<_> = table
367 .columns()
368 .iter()
369 .filter(|c| !has_column(old, c.name()))
370 .collect();
371 if additions.len() != 1
372 || !additions[0].nullable()
373 || table.columns().len() != old.columns().len() + 1
374 || after.tables().len() != before.tables().len()
375 {
376 return Err(MigrationError::AuthoredOperationRequired);
377 }
378 operations.push(Operation::new(
379 current.id().map_err(|_| MigrationError::Identity)?,
380 after.clone(),
381 OperationKind::AddColumn {
382 table: table.name().clone(),
383 column: additions[0].clone(),
384 },
385 ));
386 current = after.clone();
387 }
388 _ => {}
389 }
390 }
391 if current.id().map_err(|_| MigrationError::Identity)?
392 != after.id().map_err(|_| MigrationError::Identity)?
393 {
394 return Err(MigrationError::AuthoredOperationRequired);
395 }
396 Ok(operations)
397}
398
399#[derive(Clone, Copy, Debug, PartialEq, Eq)]
401pub struct MigrationCapabilities {
402 pub transactional_ddl: bool,
404 pub post_apply_introspection: bool,
406}
407impl MigrationCapabilities {
408 pub fn require(self) -> Result<(), CapabilityError> {
410 if self.transactional_ddl && self.post_apply_introspection {
411 Ok(())
412 } else {
413 Err(CapabilityError)
414 }
415 }
416}
417#[derive(Clone, Copy, Debug, PartialEq, Eq)]
419pub struct CapabilityError;
420
421#[derive(Clone, Debug, PartialEq, Eq)]
423pub struct SchemaAttestation {
424 pub logical_schema: RelationId,
426 pub physical_schema: RelationId,
428 pub revision: RelationId,
430}
431
432#[derive(Clone, Debug, PartialEq, Eq)]
434pub struct AdoptionManifest {
435 pub logical_schema: RelationId,
437 pub physical_schema: RelationId,
439}
440impl AdoptionManifest {
441 pub fn verify(&self, live_physical_schema: &RelationId) -> Result<(), AdoptionError> {
444 if &self.physical_schema == live_physical_schema {
445 Ok(())
446 } else {
447 Err(AdoptionError::ExternalDrift)
448 }
449 }
450}
451#[derive(Clone, Copy, Debug, PartialEq, Eq)]
453pub enum AdoptionError {
454 ExternalDrift,
456}