1use sql_orm_core::CrateIdentity;
4
5mod diff;
6mod filesystem;
7mod operation;
8mod snapshot;
9
10pub use diff::{
11 diff_column_operations, diff_relational_operations, diff_schema_and_table_operations,
12};
13pub use filesystem::{
14 MigrationEntry, MigrationScaffold, build_database_downgrade_script,
15 build_database_update_script, create_migration_scaffold,
16 create_migration_scaffold_with_snapshot, latest_migration, list_migrations,
17 read_latest_model_snapshot, read_model_snapshot, write_migration_down_sql,
18 write_migration_up_sql, write_model_snapshot,
19};
20pub use operation::{
21 AddColumn, AddForeignKey, AlterColumn, CreateIndex, CreateSchema, CreateTable, DropColumn,
22 DropForeignKey, DropIndex, DropSchema, DropTable, MigrationOperation, RenameColumn,
23 RenameTable,
24};
25pub use snapshot::{
26 ColumnSnapshot, ForeignKeySnapshot, IndexColumnSnapshot, IndexSnapshot, ModelSnapshot,
27 SchemaSnapshot, TableSnapshot,
28};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct MigrationEngine;
33
34pub const CRATE_IDENTITY: CrateIdentity = CrateIdentity {
35 name: "sql-orm-migrate",
36 responsibility: "code-first snapshots, diffs and migration operations",
37};
38
39#[cfg(test)]
40mod tests {
41 use super::{
42 AddColumn, AddForeignKey, AlterColumn, CRATE_IDENTITY, ColumnSnapshot, CreateIndex,
43 CreateSchema, CreateTable, DropColumn, DropForeignKey, DropIndex, DropSchema, DropTable,
44 ForeignKeySnapshot, IndexColumnSnapshot, IndexSnapshot, MigrationEngine,
45 MigrationOperation, ModelSnapshot, RenameColumn, RenameTable, SchemaSnapshot,
46 TableSnapshot,
47 };
48 use sql_orm_core::{
49 ColumnMetadata, EntityMetadata, ForeignKeyMetadata, IdentityMetadata, IndexColumnMetadata,
50 IndexMetadata, PrimaryKeyMetadata, ReferentialAction, SqlServerType,
51 };
52
53 const CUSTOMER_COLUMNS: [ColumnMetadata; 3] = [
54 ColumnMetadata {
55 rust_field: "id",
56 column_name: "id",
57 renamed_from: None,
58 sql_type: SqlServerType::BigInt,
59 nullable: false,
60 primary_key: true,
61 identity: Some(IdentityMetadata::new(1, 1)),
62 default_sql: None,
63 computed_sql: None,
64 rowversion: false,
65 insertable: false,
66 updatable: false,
67 max_length: None,
68 precision: None,
69 scale: None,
70 },
71 ColumnMetadata {
72 rust_field: "email",
73 column_name: "email",
74 renamed_from: None,
75 sql_type: SqlServerType::NVarChar,
76 nullable: false,
77 primary_key: false,
78 identity: None,
79 default_sql: None,
80 computed_sql: None,
81 rowversion: false,
82 insertable: true,
83 updatable: true,
84 max_length: Some(160),
85 precision: None,
86 scale: None,
87 },
88 ColumnMetadata {
89 rust_field: "version",
90 column_name: "version",
91 renamed_from: None,
92 sql_type: SqlServerType::RowVersion,
93 nullable: false,
94 primary_key: false,
95 identity: None,
96 default_sql: None,
97 computed_sql: None,
98 rowversion: true,
99 insertable: false,
100 updatable: false,
101 max_length: None,
102 precision: None,
103 scale: None,
104 },
105 ];
106
107 const CUSTOMER_PK_COLUMNS: [&str; 1] = ["id"];
108 const CUSTOMER_INDEX_COLUMNS: [IndexColumnMetadata; 1] = [IndexColumnMetadata::asc("email")];
109 const CUSTOMER_INDEXES: [IndexMetadata; 1] = [IndexMetadata {
110 name: "ix_customers_email",
111 columns: &CUSTOMER_INDEX_COLUMNS,
112 unique: true,
113 }];
114 const CUSTOMER_METADATA: EntityMetadata = EntityMetadata {
115 rust_name: "Customer",
116 schema: "sales",
117 table: "customers",
118 renamed_from: None,
119 columns: &CUSTOMER_COLUMNS,
120 primary_key: PrimaryKeyMetadata::new(Some("pk_customers"), &CUSTOMER_PK_COLUMNS),
121 indexes: &CUSTOMER_INDEXES,
122 foreign_keys: &[],
123 navigations: &[],
124 };
125
126 const TENANT_COLUMNS: [ColumnMetadata; 2] = [
127 ColumnMetadata {
128 rust_field: "id",
129 column_name: "id",
130 renamed_from: None,
131 sql_type: SqlServerType::BigInt,
132 nullable: false,
133 primary_key: true,
134 identity: Some(IdentityMetadata::new(100, 5)),
135 default_sql: None,
136 computed_sql: None,
137 rowversion: false,
138 insertable: false,
139 updatable: false,
140 max_length: None,
141 precision: None,
142 scale: None,
143 },
144 ColumnMetadata {
145 rust_field: "display_name",
146 column_name: "display_name",
147 renamed_from: None,
148 sql_type: SqlServerType::NVarChar,
149 nullable: false,
150 primary_key: false,
151 identity: None,
152 default_sql: Some("'tenant'"),
153 computed_sql: None,
154 rowversion: false,
155 insertable: true,
156 updatable: true,
157 max_length: Some(120),
158 precision: None,
159 scale: None,
160 },
161 ];
162
163 const TENANT_PK_COLUMNS: [&str; 1] = ["id"];
164 const TENANT_METADATA: EntityMetadata = EntityMetadata {
165 rust_name: "Tenant",
166 schema: "admin",
167 table: "tenants",
168 renamed_from: None,
169 columns: &TENANT_COLUMNS,
170 primary_key: PrimaryKeyMetadata::new(None, &TENANT_PK_COLUMNS),
171 indexes: &[],
172 foreign_keys: &[],
173 navigations: &[],
174 };
175
176 const COMPOSITE_ORDER_COLUMNS: [ColumnMetadata; 3] = [
177 ColumnMetadata {
178 rust_field: "id",
179 column_name: "id",
180 renamed_from: None,
181 sql_type: SqlServerType::BigInt,
182 nullable: false,
183 primary_key: true,
184 identity: Some(IdentityMetadata::new(1, 1)),
185 default_sql: None,
186 computed_sql: None,
187 rowversion: false,
188 insertable: false,
189 updatable: false,
190 max_length: None,
191 precision: None,
192 scale: None,
193 },
194 ColumnMetadata {
195 rust_field: "customer_id",
196 column_name: "customer_id",
197 renamed_from: None,
198 sql_type: SqlServerType::BigInt,
199 nullable: false,
200 primary_key: false,
201 identity: None,
202 default_sql: None,
203 computed_sql: None,
204 rowversion: false,
205 insertable: true,
206 updatable: true,
207 max_length: None,
208 precision: None,
209 scale: None,
210 },
211 ColumnMetadata {
212 rust_field: "total_cents",
213 column_name: "total_cents",
214 renamed_from: None,
215 sql_type: SqlServerType::BigInt,
216 nullable: false,
217 primary_key: false,
218 identity: None,
219 default_sql: None,
220 computed_sql: None,
221 rowversion: false,
222 insertable: true,
223 updatable: true,
224 max_length: None,
225 precision: None,
226 scale: None,
227 },
228 ];
229 const COMPOSITE_ORDER_PK_COLUMNS: [&str; 1] = ["id"];
230 const COMPOSITE_ORDER_INDEX_COLUMNS: [IndexColumnMetadata; 2] = [
231 IndexColumnMetadata::asc("customer_id"),
232 IndexColumnMetadata::desc("total_cents"),
233 ];
234 const COMPOSITE_ORDER_INDEXES: [IndexMetadata; 1] = [IndexMetadata {
235 name: "ix_orders_customer_total",
236 columns: &COMPOSITE_ORDER_INDEX_COLUMNS,
237 unique: false,
238 }];
239 const COMPOSITE_ORDER_METADATA: EntityMetadata = EntityMetadata {
240 rust_name: "CompositeOrder",
241 schema: "sales",
242 table: "orders",
243 renamed_from: None,
244 columns: &COMPOSITE_ORDER_COLUMNS,
245 primary_key: PrimaryKeyMetadata::new(Some("pk_orders"), &COMPOSITE_ORDER_PK_COLUMNS),
246 indexes: &COMPOSITE_ORDER_INDEXES,
247 foreign_keys: &[],
248 navigations: &[],
249 };
250
251 const ORDER_COLUMNS: [ColumnMetadata; 2] = [
252 ColumnMetadata {
253 rust_field: "id",
254 column_name: "id",
255 renamed_from: None,
256 sql_type: SqlServerType::BigInt,
257 nullable: false,
258 primary_key: true,
259 identity: Some(IdentityMetadata::new(1, 1)),
260 default_sql: None,
261 computed_sql: None,
262 rowversion: false,
263 insertable: false,
264 updatable: false,
265 max_length: None,
266 precision: None,
267 scale: None,
268 },
269 ColumnMetadata {
270 rust_field: "customer_id",
271 column_name: "customer_id",
272 renamed_from: None,
273 sql_type: SqlServerType::BigInt,
274 nullable: false,
275 primary_key: false,
276 identity: None,
277 default_sql: None,
278 computed_sql: None,
279 rowversion: false,
280 insertable: true,
281 updatable: true,
282 max_length: None,
283 precision: None,
284 scale: None,
285 },
286 ];
287
288 const ORDER_PK_COLUMNS: [&str; 1] = ["id"];
289 const ORDER_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
290 "fk_orders_customer_id_customers",
291 &["customer_id"],
292 "sales",
293 "customers",
294 &["id"],
295 ReferentialAction::NoAction,
296 ReferentialAction::NoAction,
297 )];
298 const ORDER_METADATA: EntityMetadata = EntityMetadata {
299 rust_name: "Order",
300 schema: "sales",
301 table: "orders",
302 renamed_from: None,
303 columns: &ORDER_COLUMNS,
304 primary_key: PrimaryKeyMetadata::new(Some("pk_orders"), &ORDER_PK_COLUMNS),
305 indexes: &[],
306 foreign_keys: &ORDER_FOREIGN_KEYS,
307 navigations: &[],
308 };
309
310 const ORDER_ALLOCATION_COLUMNS: [ColumnMetadata; 3] = [
311 ColumnMetadata {
312 rust_field: "id",
313 column_name: "id",
314 renamed_from: None,
315 sql_type: SqlServerType::BigInt,
316 nullable: false,
317 primary_key: true,
318 identity: Some(IdentityMetadata::new(1, 1)),
319 default_sql: None,
320 computed_sql: None,
321 rowversion: false,
322 insertable: false,
323 updatable: false,
324 max_length: None,
325 precision: None,
326 scale: None,
327 },
328 ColumnMetadata {
329 rust_field: "customer_id",
330 column_name: "customer_id",
331 renamed_from: None,
332 sql_type: SqlServerType::BigInt,
333 nullable: false,
334 primary_key: false,
335 identity: None,
336 default_sql: None,
337 computed_sql: None,
338 rowversion: false,
339 insertable: true,
340 updatable: true,
341 max_length: None,
342 precision: None,
343 scale: None,
344 },
345 ColumnMetadata {
346 rust_field: "branch_id",
347 column_name: "branch_id",
348 renamed_from: None,
349 sql_type: SqlServerType::BigInt,
350 nullable: false,
351 primary_key: false,
352 identity: None,
353 default_sql: None,
354 computed_sql: None,
355 rowversion: false,
356 insertable: true,
357 updatable: true,
358 max_length: None,
359 precision: None,
360 scale: None,
361 },
362 ];
363 const ORDER_ALLOCATION_PK_COLUMNS: [&str; 1] = ["id"];
364 const ORDER_ALLOCATION_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
365 "fk_order_allocations_customer_branch_customers",
366 &["customer_id", "branch_id"],
367 "sales",
368 "customers",
369 &["id", "branch_id"],
370 ReferentialAction::SetDefault,
371 ReferentialAction::Cascade,
372 )];
373 const ORDER_ALLOCATION_METADATA: EntityMetadata = EntityMetadata {
374 rust_name: "OrderAllocation",
375 schema: "sales",
376 table: "order_allocations",
377 renamed_from: None,
378 columns: &ORDER_ALLOCATION_COLUMNS,
379 primary_key: PrimaryKeyMetadata::new(
380 Some("pk_order_allocations"),
381 &ORDER_ALLOCATION_PK_COLUMNS,
382 ),
383 indexes: &[],
384 foreign_keys: &ORDER_ALLOCATION_FOREIGN_KEYS,
385 navigations: &[],
386 };
387
388 #[test]
389 fn declares_migration_boundary() {
390 let engine = MigrationEngine;
391 assert_eq!(engine, MigrationEngine);
392 assert!(CRATE_IDENTITY.responsibility.contains("migration"));
393 }
394
395 #[test]
396 fn model_snapshot_exposes_schema_table_column_and_index_lookups() {
397 let snapshot = ModelSnapshot::new(vec![SchemaSnapshot::new(
398 "sales",
399 vec![TableSnapshot::new(
400 "customers",
401 vec![
402 ColumnSnapshot::new(
403 "id",
404 SqlServerType::BigInt,
405 false,
406 true,
407 Some(IdentityMetadata::new(1, 1)),
408 None,
409 None,
410 false,
411 false,
412 false,
413 None,
414 None,
415 None,
416 ),
417 ColumnSnapshot::new(
418 "email",
419 SqlServerType::NVarChar,
420 false,
421 false,
422 None,
423 None,
424 None,
425 false,
426 true,
427 true,
428 Some(160),
429 None,
430 None,
431 ),
432 ],
433 Some("pk_customers".to_string()),
434 vec!["id".to_string()],
435 vec![IndexSnapshot::new(
436 "ix_customers_email",
437 vec![IndexColumnSnapshot::asc("email")],
438 false,
439 )],
440 vec![],
441 )],
442 )]);
443
444 let schema = snapshot.schema("sales").expect("schema must exist");
445 let table = schema.table("customers").expect("table must exist");
446 let id = table.column("id").expect("column must exist");
447 let index = table.index("ix_customers_email").expect("index must exist");
448
449 assert_eq!(table.primary_key_name.as_deref(), Some("pk_customers"));
450 assert_eq!(table.primary_key_columns, vec!["id"]);
451 assert_eq!(id.identity, Some(IdentityMetadata::new(1, 1)));
452 assert_eq!(index.columns, vec![IndexColumnSnapshot::asc("email")]);
453 }
454
455 #[test]
456 fn column_snapshot_preserves_sql_server_specific_shape() {
457 let column = ColumnSnapshot::new(
458 "version",
459 SqlServerType::RowVersion,
460 false,
461 false,
462 None,
463 Some("CONVERT(binary(8), 0)".to_string()),
464 Some("([major] + [minor])".to_string()),
465 true,
466 false,
467 false,
468 Some(8),
469 Some(18),
470 Some(4),
471 );
472
473 assert_eq!(column.name, "version");
474 assert_eq!(column.sql_type, SqlServerType::RowVersion);
475 assert_eq!(column.default_sql.as_deref(), Some("CONVERT(binary(8), 0)"));
476 assert_eq!(column.computed_sql.as_deref(), Some("([major] + [minor])"));
477 assert!(column.rowversion);
478 assert!(!column.insertable);
479 assert!(!column.updatable);
480 assert_eq!(column.max_length, Some(8));
481 assert_eq!(column.precision, Some(18));
482 assert_eq!(column.scale, Some(4));
483 }
484
485 #[test]
486 fn table_snapshot_can_be_built_from_entity_metadata() {
487 let table = TableSnapshot::from(&CUSTOMER_METADATA);
488
489 assert_eq!(table.name, "customers");
490 assert_eq!(table.primary_key_name.as_deref(), Some("pk_customers"));
491 assert_eq!(table.primary_key_columns, vec!["id"]);
492 assert_eq!(table.columns.len(), 3);
493 assert_eq!(table.columns[0].name, "id");
494 assert_eq!(table.columns[1].name, "email");
495 assert_eq!(table.indexes.len(), 1);
496 assert_eq!(table.indexes[0].name, "ix_customers_email");
497 assert!(table.indexes[0].unique);
498 assert!(table.foreign_keys.is_empty());
499 }
500
501 #[test]
502 fn table_snapshot_preserves_foreign_keys_from_entity_metadata() {
503 let table = TableSnapshot::from(&ORDER_METADATA);
504 let foreign_key = table
505 .foreign_key("fk_orders_customer_id_customers")
506 .expect("foreign key must exist");
507
508 assert_eq!(table.foreign_keys.len(), 1);
509 assert_eq!(foreign_key.columns, vec!["customer_id"]);
510 assert_eq!(foreign_key.referenced_schema, "sales");
511 assert_eq!(foreign_key.referenced_table, "customers");
512 assert_eq!(foreign_key.referenced_columns, vec!["id"]);
513 assert_eq!(foreign_key.on_delete, ReferentialAction::NoAction);
514 assert_eq!(foreign_key.on_update, ReferentialAction::NoAction);
515 }
516
517 #[test]
518 fn table_snapshot_preserves_composite_foreign_keys_from_entity_metadata() {
519 let table = TableSnapshot::from(&ORDER_ALLOCATION_METADATA);
520 let foreign_key = table
521 .foreign_key("fk_order_allocations_customer_branch_customers")
522 .expect("composite foreign key must exist");
523
524 assert_eq!(table.foreign_keys.len(), 1);
525 assert_eq!(foreign_key.columns, vec!["customer_id", "branch_id"]);
526 assert_eq!(foreign_key.referenced_schema, "sales");
527 assert_eq!(foreign_key.referenced_table, "customers");
528 assert_eq!(foreign_key.referenced_columns, vec!["id", "branch_id"]);
529 assert_eq!(foreign_key.on_delete, ReferentialAction::SetDefault);
530 assert_eq!(foreign_key.on_update, ReferentialAction::Cascade);
531 }
532
533 #[test]
534 fn table_snapshot_preserves_composite_indexes_from_entity_metadata() {
535 let table = TableSnapshot::from(&COMPOSITE_ORDER_METADATA);
536 let index = table
537 .index("ix_orders_customer_total")
538 .expect("composite index must exist");
539
540 assert_eq!(table.indexes.len(), 1);
541 assert_eq!(
542 index.columns,
543 vec![
544 IndexColumnSnapshot::asc("customer_id"),
545 IndexColumnSnapshot::desc("total_cents"),
546 ]
547 );
548 assert!(!index.unique);
549 }
550
551 #[test]
552 fn model_snapshot_groups_entities_by_schema_and_sorts_tables() {
553 let snapshot =
554 ModelSnapshot::from_entities(&[&ORDER_METADATA, &TENANT_METADATA, &CUSTOMER_METADATA]);
555
556 assert_eq!(snapshot.schemas.len(), 2);
557 assert_eq!(snapshot.schemas[0].name, "admin");
558 assert_eq!(snapshot.schemas[1].name, "sales");
559
560 let admin = snapshot.schema("admin").expect("admin schema must exist");
561 assert_eq!(admin.tables.len(), 1);
562 assert_eq!(admin.tables[0].name, "tenants");
563
564 let sales = snapshot.schema("sales").expect("sales schema must exist");
565 assert_eq!(
566 sales
567 .tables
568 .iter()
569 .map(|table| table.name.as_str())
570 .collect::<Vec<_>>(),
571 vec!["customers", "orders"]
572 );
573 assert_eq!(
574 sales
575 .table("customers")
576 .expect("customers table must exist")
577 .column("email")
578 .expect("email column must exist")
579 .max_length,
580 Some(160)
581 );
582 }
583
584 #[test]
585 fn migration_operations_cover_minimum_stage_seven_surface() {
586 let create_schema = MigrationOperation::CreateSchema(CreateSchema::new("sales"));
587 let drop_schema = MigrationOperation::DropSchema(DropSchema::new("legacy"));
588 let create_table = MigrationOperation::CreateTable(CreateTable::new(
589 "sales",
590 TableSnapshot::from(&CUSTOMER_METADATA),
591 ));
592 let drop_table = MigrationOperation::DropTable(DropTable::new("sales", "customers"));
593 let add_column = MigrationOperation::AddColumn(AddColumn::new(
594 "sales",
595 "customers",
596 ColumnSnapshot::from(&CUSTOMER_COLUMNS[1]),
597 ));
598 let drop_column =
599 MigrationOperation::DropColumn(DropColumn::new("sales", "customers", "email"));
600 let rename_column = MigrationOperation::RenameColumn(RenameColumn::new(
601 "sales",
602 "customers",
603 "email",
604 "email_address",
605 ));
606 let rename_table =
607 MigrationOperation::RenameTable(RenameTable::new("sales", "customers", "clients"));
608 let alter_column = MigrationOperation::AlterColumn(AlterColumn::new(
609 "sales",
610 "customers",
611 ColumnSnapshot::from(&CUSTOMER_COLUMNS[1]),
612 ColumnSnapshot::new(
613 "email",
614 SqlServerType::NVarChar,
615 false,
616 false,
617 None,
618 None,
619 None,
620 false,
621 true,
622 true,
623 Some(255),
624 None,
625 None,
626 ),
627 ));
628 let create_index = MigrationOperation::CreateIndex(CreateIndex::new(
629 "sales",
630 "customers",
631 IndexSnapshot::new(
632 "ix_customers_email",
633 vec![IndexColumnSnapshot::asc("email")],
634 true,
635 ),
636 ));
637 let drop_index = MigrationOperation::DropIndex(DropIndex::new(
638 "sales",
639 "customers",
640 "ix_customers_email",
641 ));
642 let add_foreign_key = MigrationOperation::AddForeignKey(AddForeignKey::new(
643 "sales",
644 "orders",
645 ForeignKeySnapshot::new(
646 "fk_orders_customer_id_customers",
647 vec!["customer_id".to_string()],
648 "sales",
649 "customers",
650 vec!["id".to_string()],
651 ReferentialAction::NoAction,
652 ReferentialAction::NoAction,
653 ),
654 ));
655 let drop_foreign_key = MigrationOperation::DropForeignKey(DropForeignKey::new(
656 "sales",
657 "orders",
658 "fk_orders_customer_id_customers",
659 ));
660
661 assert_eq!(create_schema.schema_name(), "sales");
662 assert_eq!(drop_schema.schema_name(), "legacy");
663 assert_eq!(create_table.schema_name(), "sales");
664 assert_eq!(create_table.table_name(), Some("customers"));
665 assert_eq!(drop_table.table_name(), Some("customers"));
666 assert_eq!(add_column.table_name(), Some("customers"));
667 assert_eq!(drop_column.table_name(), Some("customers"));
668 assert_eq!(rename_column.table_name(), Some("customers"));
669 assert_eq!(rename_table.table_name(), Some("clients"));
670 assert_eq!(alter_column.table_name(), Some("customers"));
671 assert_eq!(create_index.table_name(), Some("customers"));
672 assert_eq!(drop_index.table_name(), Some("customers"));
673 assert_eq!(add_foreign_key.table_name(), Some("orders"));
674 assert_eq!(drop_foreign_key.table_name(), Some("orders"));
675 }
676
677 #[test]
678 fn alter_column_retains_previous_and_next_shapes() {
679 let previous = ColumnSnapshot::from(&CUSTOMER_COLUMNS[1]);
680 let next = ColumnSnapshot::new(
681 "email",
682 SqlServerType::NVarChar,
683 true,
684 false,
685 None,
686 Some("'unknown'".to_string()),
687 None,
688 false,
689 true,
690 true,
691 Some(255),
692 None,
693 None,
694 );
695
696 let operation = AlterColumn::new("sales", "customers", previous.clone(), next.clone());
697
698 assert_eq!(operation.schema_name, "sales");
699 assert_eq!(operation.table_name, "customers");
700 assert_eq!(operation.previous, previous);
701 assert_eq!(operation.next, next);
702 }
703}