Skip to content

Commit

Permalink
feat(orm): implement dependencies for migrations
Browse files Browse the repository at this point in the history
For now the system only really create dependency on the previous
migration in the app. Eventually we'll extend this to handle ForeignKeys
and other relationship types as well.
  • Loading branch information
m4tx committed Nov 26, 2024
1 parent 764964c commit eb7c6f5
Show file tree
Hide file tree
Showing 9 changed files with 710 additions and 24 deletions.
1 change: 1 addition & 0 deletions examples/todo-list/src/migrations/m_0001_initial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub(super) struct Migration;
impl ::flareon::db::migrations::Migration for Migration {
const APP_NAME: &'static str = "example-todo-list";
const MIGRATION_NAME: &'static str = "m_0001_initial";
const DEPENDENCIES: &'static [::flareon::db::migrations::MigrationDependency] = &[];
const OPERATIONS: &'static [::flareon::db::migrations::Operation] =
&[::flareon::db::migrations::Operation::create_model()
.table_name(::flareon::db::Identifier::new("todo_item"))
Expand Down
99 changes: 91 additions & 8 deletions flareon-cli/src/migration_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,21 @@ impl MigrationGenerator {
source_files: Vec<SourceFile>,
) -> anyhow::Result<Option<MigrationToWrite>> {
let AppState { models, migrations } = self.process_source_files(source_files)?;
let migration_processor = MigrationProcessor::new(migrations);
let migration_processor = MigrationProcessor::new(migrations)?;
let migration_models = migration_processor.latest_models();
let (modified_models, operations) = self.generate_operations(&models, &migration_models);

if operations.is_empty() {
Ok(None)
} else {
let migration_name = migration_processor.next_migration_name()?;
let content =
self.generate_migration_file_content(&migration_name, &modified_models, operations);
let dependencies = migration_processor.dependencies();
let content = self.generate_migration_file_content(
&migration_name,
&modified_models,
dependencies,
operations,
);
Ok(Some(MigrationToWrite::new(migration_name, content)))
}
}
Expand Down Expand Up @@ -397,12 +402,17 @@ impl MigrationGenerator {
&self,
migration_name: &str,
modified_models: &[ModelInSource],
dependencies: Vec<DynDependency>,
operations: Vec<DynOperation>,
) -> String {
let operations: Vec<_> = operations
.into_iter()
.map(|operation| operation.repr())
.collect();
let dependencies: Vec<_> = dependencies
.into_iter()
.map(|dependency| dependency.repr())
.collect();

let app_name = self.options.app_name.as_ref().unwrap_or(&self.crate_name);
let migration_def = quote! {
Expand All @@ -412,6 +422,9 @@ impl MigrationGenerator {
impl ::flareon::db::migrations::Migration for Migration {
const APP_NAME: &'static str = #app_name;
const MIGRATION_NAME: &'static str = #migration_name;
const DEPENDENCIES: &'static [::flareon::db::migrations::MigrationDependency] = &[
#(#dependencies,)*
];
const OPERATIONS: &'static [::flareon::db::migrations::Operation] = &[
#(#operations,)*
];
Expand Down Expand Up @@ -825,10 +838,9 @@ struct MigrationProcessor {
}

impl MigrationProcessor {
#[must_use]
fn new(mut migrations: Vec<Migration>) -> Self {
MigrationEngine::sort_migrations(&mut migrations);
Self { migrations }
fn new(mut migrations: Vec<Migration>) -> anyhow::Result<Self> {
MigrationEngine::sort_migrations(&mut migrations)?;
Ok(Self { migrations })
}

/// Returns the latest (in the order of applying migrations) versions of the
Expand Down Expand Up @@ -873,6 +885,18 @@ impl MigrationProcessor {

Ok(format!("m_{migration_number:04}_auto_{date_time}"))
}

fn dependencies(&self) -> Vec<DynDependency> {
if self.migrations.is_empty() {
return Vec::new();
}

let last_migration = self.migrations.last().unwrap();
vec![DynDependency::Migration {
app: last_migration.app_name.clone(),
migration: last_migration.name.clone(),
}]
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -960,11 +984,42 @@ impl DynMigration for Migration {
&self.name
}

fn dependencies(&self) -> &[flareon::db::migrations::MigrationDependency] {
&[]
}

fn operations(&self) -> &[flareon::db::migrations::Operation] {
&[]
}
}

/// A version of [`flareon::db::migrations::MigrationDependency`] that can be
/// created at runtime and is using codegen types.
///
/// This is used to generate migration files.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum DynDependency {
Migration { app: String, migration: String },
Model { app: String, model_name: String },
}

impl Repr for DynDependency {
fn repr(&self) -> TokenStream {
match self {
Self::Migration { app, migration } => {
quote! {
::flareon::db::migrations::MigrationDependency::migration(#app, #migration)
}
}
Self::Model { app, model_name } => {
quote! {
::flareon::db::migrations::MigrationDependency::model(#app, #model_name)
}
}
}
}
}

/// A version of [`flareon::db::migrations::Operation`] that can be created at
/// runtime and is using codegen types.
///
Expand Down Expand Up @@ -1053,12 +1108,40 @@ mod tests {
#[test]
fn migration_processor_next_migration_name_empty() {
let migrations = vec![];
let processor = MigrationProcessor::new(migrations);
let processor = MigrationProcessor::new(migrations).unwrap();

let next_migration_name = processor.next_migration_name().unwrap();
assert_eq!(next_migration_name, "m_0001_initial");
}

#[test]
fn migration_processor_dependencies_empty() {
let migrations = vec![];
let processor = MigrationProcessor::new(migrations).unwrap();

let next_migration_name = processor.dependencies();
assert_eq!(next_migration_name, vec![]);
}

#[test]
fn migration_processor_dependencies_previous() {
let migrations = vec![Migration {
app_name: "app1".to_string(),
name: "m0001_initial".to_string(),
models: vec![],
}];
let processor = MigrationProcessor::new(migrations).unwrap();

let next_migration_name = processor.dependencies();
assert_eq!(
next_migration_name,
vec![DynDependency::Migration {
app: "app1".to_string(),
migration: "m0001_initial".to_string(),
}]
);
}

#[test]
fn imports() {
let source = r"
Expand Down
1 change: 1 addition & 0 deletions flareon/src/auth/db/migrations/m_0001_initial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub(super) struct Migration;
impl ::flareon::db::migrations::Migration for Migration {
const APP_NAME: &'static str = "flareon_auth";
const MIGRATION_NAME: &'static str = "m_0001_initial";
const DEPENDENCIES: &'static [::flareon::db::migrations::MigrationDependency] = &[];
const OPERATIONS: &'static [::flareon::db::migrations::Operation] = &[
::flareon::db::migrations::Operation::create_model()
.table_name(::flareon::db::Identifier::new("database_user"))
Expand Down
3 changes: 3 additions & 0 deletions flareon/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ pub enum DatabaseError {
/// Error when decoding database value.
#[error("Error when decoding database value: {0}")]
ValueDecode(Box<dyn std::error::Error + 'static + Send + Sync>),
/// Error when applying migrations.
#[error("Error when applying migrations: {0}")]
MigrationError(#[from] migrations::MigrationEngineError),
}

impl DatabaseError {
Expand Down
Loading

0 comments on commit eb7c6f5

Please sign in to comment.