Skip to content

Commit 692c1a1

Browse files
committed
chore: more fixes, tests, docs
1 parent 4081a59 commit 692c1a1

File tree

6 files changed

+277
-19
lines changed

6 files changed

+277
-19
lines changed

flareon-cli/src/main.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
extern crate core;
2-
31
mod migration_generator;
42
mod utils;
53

@@ -27,6 +25,9 @@ enum Commands {
2725
/// Path to the crate directory to generate migrations for (default:
2826
/// current directory)
2927
path: Option<PathBuf>,
28+
/// Name of the app to use in the migration (default: crate name)
29+
#[arg(long)]
30+
app_name: Option<String>,
3031
/// Directory to write the migrations to (default: migrations/ directory
3132
/// in the crate's src/ directory)
3233
#[arg(long)]
@@ -46,9 +47,16 @@ fn main() -> anyhow::Result<()> {
4647
.init();
4748

4849
match cli.command {
49-
Commands::MakeMigrations { path, output_dir } => {
50+
Commands::MakeMigrations {
51+
path,
52+
app_name,
53+
output_dir,
54+
} => {
5055
let path = path.unwrap_or_else(|| PathBuf::from("."));
51-
let options = MigrationGeneratorOptions { output_dir };
56+
let options = MigrationGeneratorOptions {
57+
app_name,
58+
output_dir,
59+
};
5260
make_migrations(&path, options).with_context(|| "unable to create migrations")?;
5361
}
5462
}

flareon-cli/src/migration_generator.rs

Lines changed: 123 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub fn make_migrations(path: &Path, options: MigrationGeneratorOptions) -> anyho
4848

4949
#[derive(Debug, Clone, Default)]
5050
pub struct MigrationGeneratorOptions {
51+
pub app_name: Option<String>,
5152
pub output_dir: Option<PathBuf>,
5253
}
5354

@@ -82,6 +83,7 @@ impl MigrationGenerator {
8283
Ok(())
8384
}
8485

86+
/// Generate migrations as a ready-to-write source code.
8587
pub fn generate_migrations_to_write(
8688
&mut self,
8789
source_files: Vec<SourceFile>,
@@ -95,6 +97,8 @@ impl MigrationGenerator {
9597
}
9698
}
9799

100+
/// Generate migrations and return internal structures that can be used to
101+
/// generate source code.
98102
pub fn generate_migrations(
99103
&mut self,
100104
source_files: Vec<SourceFile>,
@@ -319,7 +323,10 @@ impl MigrationGenerator {
319323
fn make_create_model_operation(app_model: &ModelInSource) -> DynOperation {
320324
DynOperation::CreateModel {
321325
table_name: app_model.model.table_name.clone(),
322-
model_ty: app_model.model.resolved_ty.clone().expect("resolved_ty is expected to be present when parsing the entire file with symbol resolver"),
326+
model_ty: app_model.model.resolved_ty.clone().expect(
327+
"resolved_ty is expected to be present when \
328+
parsing the entire file with symbol resolver",
329+
),
323330
fields: app_model.model.fields.clone(),
324331
}
325332
}
@@ -381,7 +388,10 @@ impl MigrationGenerator {
381388
fn make_add_field_operation(app_model: &ModelInSource, field: &Field) -> DynOperation {
382389
DynOperation::AddField {
383390
table_name: app_model.model.table_name.clone(),
384-
model_ty: app_model.model.resolved_ty.clone().expect("resolved_ty is expected to be present when parsing the entire file with symbol resolver"),
391+
model_ty: app_model.model.resolved_ty.clone().expect(
392+
"resolved_ty is expected to be present \
393+
when parsing the entire file with symbol resolver",
394+
),
385395
field: field.clone(),
386396
}
387397
}
@@ -426,7 +436,7 @@ impl MigrationGenerator {
426436
.map(|dependency| dependency.repr())
427437
.collect();
428438

429-
let app_name = &self.crate_name;
439+
let app_name = self.options.app_name.as_ref().unwrap_or(&self.crate_name);
430440
let migration_name = &migration.migration_name;
431441
let migration_def = quote! {
432442
#[derive(Debug, Copy, Clone)]
@@ -666,6 +676,8 @@ pub struct GeneratedMigration {
666676
}
667677

668678
impl GeneratedMigration {
679+
/// Get the list of [`DynDependency`] for all foreign keys that point
680+
/// to models that are **not** created in this migration.
669681
fn get_foreign_key_dependencies(&self) -> Vec<DynDependency> {
670682
let create_ops = self.get_create_ops_map();
671683
let ops_adding_foreign_keys = self.get_ops_adding_foreign_keys();
@@ -682,6 +694,16 @@ impl GeneratedMigration {
682694
dependencies
683695
}
684696

697+
/// Removes dependency cycles by removing operations that create cycles.
698+
///
699+
/// This method tries to minimize the number of operations added by
700+
/// calculating the minimum feedback arc set of the dependency graph.
701+
///
702+
/// This method modifies the `operations` field in place.
703+
///
704+
/// # See also
705+
///
706+
/// * [`Self::remove_dependency`]
685707
fn remove_cycles(&mut self) {
686708
let graph = self.construct_dependency_graph();
687709

@@ -701,6 +723,11 @@ impl GeneratedMigration {
701723
}
702724
}
703725

726+
/// Remove a dependency between two operations.
727+
///
728+
/// This is done by removing foreign keys from the `from` operation that
729+
/// point to the model created by `to` operation, and creating a new
730+
/// `AddField` operation for each removed foreign key.
704731
#[must_use]
705732
fn remove_dependency(from: &mut DynOperation, to: &DynOperation) -> Vec<DynOperation> {
706733
match from {
@@ -712,7 +739,10 @@ impl GeneratedMigration {
712739
let to_type = match to {
713740
DynOperation::CreateModel { model_ty, .. } => model_ty,
714741
DynOperation::AddField { .. } => {
715-
unreachable!("AddField operation shouldn't be a dependency of CreateModel because it doesn't create a new model")
742+
unreachable!(
743+
"AddField operation shouldn't be a dependency of CreateModel \
744+
because it doesn't create a new model"
745+
)
716746
}
717747
};
718748
trace!(
@@ -745,18 +775,36 @@ impl GeneratedMigration {
745775
}
746776
}
747777

778+
/// Topologically sort operations in this migration.
779+
///
780+
/// This is to ensure that operations will be applied in the correct order.
781+
/// If there are no dependencies between operations, the order of operations
782+
/// will not be modified.
783+
///
784+
/// This method modifies the `operations` field in place.
785+
///
786+
/// # Panics
787+
///
788+
/// This method should be called after removing cycles; otherwise it will
789+
/// panic.
748790
fn toposort_operations(&mut self) {
749791
let graph = self.construct_dependency_graph();
750792

751793
let sorted = petgraph::algo::toposort(&graph, None)
752794
.expect("cycles shouldn't exist after removing them");
753795
let mut sorted = sorted
754796
.into_iter()
755-
.map(petgraph::prelude::NodeIndex::index)
797+
.map(petgraph::graph::NodeIndex::index)
756798
.collect::<Vec<_>>();
757799
flareon::__private::apply_permutation(&mut self.operations, &mut sorted);
758800
}
759801

802+
/// Construct a graph that represents reverse dependencies between
803+
/// operations in this migration.
804+
///
805+
/// The graph is directed and has an edge from operation A to operation B
806+
/// if operation B creates a foreign key that points to a model created by
807+
/// operation A.
760808
#[must_use]
761809
fn construct_dependency_graph(&mut self) -> DiGraph<usize, (), usize> {
762810
let create_ops = self.get_create_ops_map();
@@ -769,7 +817,11 @@ impl GeneratedMigration {
769817
}
770818
for (i, dependency_ty) in &ops_adding_foreign_keys {
771819
if let Some(&dependency) = create_ops.get(dependency_ty) {
772-
graph.add_edge(NodeIndex::new(dependency), NodeIndex::new(*i), ());
820+
graph.add_edge(
821+
petgraph::graph::NodeIndex::new(dependency),
822+
petgraph::graph::NodeIndex::new(*i),
823+
(),
824+
);
773825
}
774826
}
775827

@@ -855,16 +907,19 @@ impl Repr for Field {
855907
let mut tokens = quote! {
856908
::flareon::db::migrations::Field::new(::flareon::db::Identifier::new(#column_name), <#ty as ::flareon::db::DatabaseField>::TYPE)
857909
};
858-
if self
859-
.auto_value
860-
.expect("auto_value is expected to be present when parsing the entire file with symbol resolver")
861-
{
910+
if self.auto_value.expect(
911+
"auto_value is expected to be present \
912+
when parsing the entire file with symbol resolver",
913+
) {
862914
tokens = quote! { #tokens.auto() }
863915
}
864916
if self.primary_key {
865917
tokens = quote! { #tokens.primary_key() }
866918
}
867-
if let Some(fk_spec) = self.foreign_key.clone().expect("foreign_key is expected to be present when parsing the entire file with symbol resolver") {
919+
if let Some(fk_spec) = self.foreign_key.clone().expect(
920+
"foreign_key is expected to be present \
921+
when parsing the entire file with symbol resolver",
922+
) {
868923
let to_model = &fk_spec.to_model;
869924

870925
tokens = quote! {
@@ -966,7 +1021,8 @@ fn is_field_foreign_key_to(field: &Field, ty: &syn::Type) -> bool {
9661021
/// Returns [`None`] if the field is not a foreign key.
9671022
fn foreign_key_for_field(field: &Field) -> Option<syn::Type> {
9681023
match field.foreign_key.clone().expect(
969-
"foreign_key is expected to be present when parsing the entire file with symbol resolver",
1024+
"foreign_key is expected to be present \
1025+
when parsing the entire file with symbol resolver",
9701026
) {
9711027
None => None,
9721028
Some(foreign_key_spec) => Some(foreign_key_spec.to_model),
@@ -1339,4 +1395,59 @@ mod tests {
13391395
model_type: parse_quote!(crate::Table4),
13401396
}));
13411397
}
1398+
1399+
#[test]
1400+
fn make_add_field_operation() {
1401+
let app_model = ModelInSource {
1402+
model_item: parse_quote! {
1403+
struct TestModel {
1404+
id: i32,
1405+
field1: i32,
1406+
}
1407+
},
1408+
model: Model {
1409+
name: format_ident!("TestModel"),
1410+
original_name: "TestModel".to_string(),
1411+
resolved_ty: Some(parse_quote!(TestModel)),
1412+
model_type: Default::default(),
1413+
table_name: "test_model".to_string(),
1414+
pk_field: Field {
1415+
field_name: format_ident!("id"),
1416+
column_name: "id".to_string(),
1417+
ty: parse_quote!(i32),
1418+
auto_value: MaybeUnknown::Known(true),
1419+
primary_key: true,
1420+
unique: false,
1421+
foreign_key: MaybeUnknown::Known(None),
1422+
},
1423+
fields: vec![],
1424+
},
1425+
};
1426+
1427+
let field = Field {
1428+
field_name: format_ident!("new_field"),
1429+
column_name: "new_field".to_string(),
1430+
ty: parse_quote!(i32),
1431+
auto_value: MaybeUnknown::Known(false),
1432+
primary_key: false,
1433+
unique: false,
1434+
foreign_key: MaybeUnknown::Known(None),
1435+
};
1436+
1437+
let operation = MigrationGenerator::make_add_field_operation(&app_model, &field);
1438+
1439+
match operation {
1440+
DynOperation::AddField {
1441+
table_name,
1442+
model_ty,
1443+
field: op_field,
1444+
} => {
1445+
assert_eq!(table_name, "test_model");
1446+
assert_eq!(model_ty, parse_quote!(TestModel));
1447+
assert_eq!(op_field.column_name, "new_field");
1448+
assert_eq!(op_field.ty, parse_quote!(i32));
1449+
}
1450+
_ => panic!("Expected AddField operation"),
1451+
}
1452+
}
13421453
}

flareon-cli/tests/migration_generator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ fn create_models_foreign_key_cycle() {
120120
}
121121

122122
#[test]
123-
fn create_models_foreign_two_migrations() {
123+
fn create_models_foreign_key_two_migrations() {
124124
let mut generator = test_generator();
125125

126126
let src = include_str!("migration_generator/foreign_key_two_migrations/step_1.rs");

flareon/src/db.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,4 +1163,44 @@ mod tests {
11631163
LimitedString::<5>::new("test").unwrap(),
11641164
);
11651165
}
1166+
1167+
#[test]
1168+
fn db_field_value_is_auto() {
1169+
let auto_value = DbFieldValue::Auto;
1170+
assert!(auto_value.is_auto());
1171+
assert!(!auto_value.is_value());
1172+
}
1173+
1174+
#[test]
1175+
fn db_field_value_is_value() {
1176+
let value = DbFieldValue::Value(42.into());
1177+
assert!(value.is_value());
1178+
assert!(!value.is_auto());
1179+
}
1180+
1181+
#[test]
1182+
fn db_field_value_unwrap() {
1183+
let value = DbFieldValue::Value(42.into());
1184+
assert_eq!(value.unwrap_value(), 42.into());
1185+
}
1186+
1187+
#[test]
1188+
#[should_panic(expected = "called DbValue::unwrap_value() on a wrong DbValue variant")]
1189+
fn db_field_value_unwrap_panic() {
1190+
let auto_value = DbFieldValue::Auto;
1191+
let _ = auto_value.unwrap_value();
1192+
}
1193+
1194+
#[test]
1195+
fn db_field_value_expect() {
1196+
let value = DbFieldValue::Value(42.into());
1197+
assert_eq!(value.expect_value("expected a value"), 42.into());
1198+
}
1199+
1200+
#[test]
1201+
#[should_panic(expected = "expected a value")]
1202+
fn db_field_value_expect_panic() {
1203+
let auto_value = DbFieldValue::Auto;
1204+
let _ = auto_value.expect_value("expected a value");
1205+
}
11661206
}

flareon/src/db/migrations.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ impl Field {
451451
}
452452
}
453453

454-
#[derive(Debug, Copy, Clone)]
454+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
455455
struct ForeignKeyReference {
456456
model: Identifier,
457457
field: Identifier,
@@ -848,7 +848,7 @@ mod tests {
848848
}
849849

850850
#[test]
851-
fn test_field_new() {
851+
fn field_new() {
852852
let field = Field::new(Identifier::new("id"), ColumnType::Integer)
853853
.primary_key()
854854
.auto()
@@ -861,6 +861,26 @@ mod tests {
861861
assert!(field.null);
862862
}
863863

864+
#[test]
865+
fn field_foreign_key() {
866+
let field = Field::new(Identifier::new("parent"), ColumnType::Integer).foreign_key(
867+
Identifier::new("testapp__parent"),
868+
Identifier::new("id"),
869+
ForeignKeyOnDeletePolicy::Restrict,
870+
ForeignKeyOnUpdatePolicy::Restrict,
871+
);
872+
873+
assert_eq!(
874+
field.foreign_key,
875+
Some(ForeignKeyReference {
876+
model: Identifier::new("testapp__parent"),
877+
field: Identifier::new("id"),
878+
on_delete: ForeignKeyOnDeletePolicy::Restrict,
879+
on_update: ForeignKeyOnUpdatePolicy::Restrict,
880+
})
881+
);
882+
}
883+
864884
#[test]
865885
fn test_migration_wrapper() {
866886
let migration = MigrationWrapper::new(TestMigration);

0 commit comments

Comments
 (0)