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