use super::recorder::MigrationRecord;
use super::{
DatabaseMigrationRecorder, Migration, MigrationGraph, MigrationKey, MigrationSource,
ProjectState, Result,
};
pub struct MigrationStateLoader<S: MigrationSource> {
recorder: DatabaseMigrationRecorder,
source: S,
}
impl<S: MigrationSource> MigrationStateLoader<S> {
pub fn new(recorder: DatabaseMigrationRecorder, source: S) -> Self {
Self { recorder, source }
}
pub async fn build_current_state(&self) -> Result<ProjectState> {
let applied_records = self.recorder.get_applied_migrations().await?;
if applied_records.is_empty() {
return Ok(ProjectState::default());
}
let all_migrations = self.source.all_migrations().await?;
let graph = self.build_applied_migration_graph(&applied_records, &all_migrations)?;
let sorted_keys = graph.topological_sort()?;
let mut state = ProjectState::default();
for key in sorted_keys {
if let Some(migration) = all_migrations
.iter()
.find(|m| m.app_label == key.app_label && m.name == key.name)
{
eprintln!(
"[DEBUG] Applying migration: {}/{}",
migration.app_label, migration.name
);
eprintln!("[DEBUG] Operations count: {}", migration.operations.len());
state.apply_migration_operations(&migration.operations, &migration.app_label);
eprintln!(
"[DEBUG] State after applying - models count: {}",
state.models.len()
);
for (app, model_name) in state.models.keys() {
eprintln!("[DEBUG] - {}/{}", app, model_name);
}
}
}
Ok(state)
}
fn build_applied_migration_graph(
&self,
applied_records: &[MigrationRecord],
all_migrations: &[Migration],
) -> Result<MigrationGraph> {
let mut graph = MigrationGraph::new();
let applied_set: std::collections::HashSet<(String, String)> = applied_records
.iter()
.map(|r| (r.app.clone(), r.name.clone()))
.collect();
for migration in all_migrations {
let key_tuple = (migration.app_label.to_string(), migration.name.to_string());
if !applied_set.contains(&key_tuple) {
continue;
}
let key = MigrationKey::new(migration.app_label.clone(), migration.name.clone());
let dependencies: Vec<MigrationKey> = migration
.dependencies
.iter()
.map(|(app, name)| MigrationKey::new(app.clone(), name.clone()))
.collect();
let replaces: Vec<MigrationKey> = migration
.replaces
.iter()
.map(|(app, name)| MigrationKey::new(app.clone(), name.clone()))
.collect();
graph.add_migration_with_replaces(key, dependencies, replaces);
}
Ok(graph)
}
pub async fn get_applied_migrations(&self) -> Result<Vec<MigrationRecord>> {
self.recorder.get_applied_migrations().await
}
pub async fn is_migration_applied(&self, app_label: &str, name: &str) -> Result<bool> {
self.recorder.is_applied(app_label, name).await
}
pub async fn build_state_up_to(
&self,
target_app: &str,
target_name: &str,
) -> Result<ProjectState> {
let all_migrations = self.source.all_migrations().await?;
let mut graph = MigrationGraph::new();
for migration in &all_migrations {
let key = MigrationKey::new(migration.app_label.clone(), migration.name.clone());
let dependencies: Vec<MigrationKey> = migration
.dependencies
.iter()
.map(|(app, name)| MigrationKey::new(app.clone(), name.clone()))
.collect();
let replaces: Vec<MigrationKey> = migration
.replaces
.iter()
.map(|(app, name)| MigrationKey::new(app.clone(), name.clone()))
.collect();
graph.add_migration_with_replaces(key, dependencies, replaces);
}
let root_nodes = graph.get_root_nodes();
let target_key = MigrationKey::new(target_app, target_name);
let path = if root_nodes.is_empty() {
vec![target_key]
} else {
graph.find_migration_path(root_nodes[0], &target_key)?
};
let mut state = ProjectState::default();
for key in path {
if let Some(migration) = all_migrations
.iter()
.find(|m| m.app_label == key.app_label && m.name == key.name)
{
state.apply_migration_operations(&migration.operations, &migration.app_label);
}
}
Ok(state)
}
}
pub async fn build_state_from_files<S: MigrationSource>(source: &S) -> Result<ProjectState> {
let all_migrations = source.all_migrations().await?;
if all_migrations.is_empty() {
return Ok(ProjectState::default());
}
let mut graph = MigrationGraph::new();
for migration in &all_migrations {
let key = MigrationKey::new(migration.app_label.clone(), migration.name.clone());
let dependencies: Vec<MigrationKey> = migration
.dependencies
.iter()
.map(|(app, name)| MigrationKey::new(app.clone(), name.clone()))
.collect();
let replaces: Vec<MigrationKey> = migration
.replaces
.iter()
.map(|(app, name)| MigrationKey::new(app.clone(), name.clone()))
.collect();
graph.add_migration_with_replaces(key, dependencies, replaces);
}
let sorted_keys = graph.topological_sort()?;
let mut state = ProjectState::default();
for key in sorted_keys {
if let Some(migration) = all_migrations
.iter()
.find(|m| m.app_label == key.app_label && m.name == key.name)
{
state.apply_migration_operations(&migration.operations, &migration.app_label);
}
}
Ok(state)
}
#[cfg(test)]
#[cfg(feature = "sqlite")]
mod tests {
use super::*;
use crate::migrations::FieldType;
use crate::migrations::operations::{ColumnDefinition, Operation};
use chrono::Utc;
fn create_migration_record(app: &str, name: &str) -> MigrationRecord {
MigrationRecord {
app: app.to_string(),
name: name.to_string(),
applied: Utc::now(),
}
}
fn create_migration(
app_label: &str,
name: &str,
operations: Vec<Operation>,
dependencies: Vec<(&str, &str)>,
) -> Migration {
Migration {
app_label: app_label.to_string(),
name: name.to_string(),
operations,
dependencies: dependencies
.into_iter()
.map(|(a, n)| (a.to_string(), n.to_string()))
.collect(),
replaces: vec![],
atomic: true,
initial: None,
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
fn create_table_operation(table_name: &str, columns: Vec<&str>) -> Operation {
Operation::CreateTable {
name: table_name.to_string(),
columns: columns
.into_iter()
.map(|col_name| ColumnDefinition {
name: col_name.to_string(),
type_definition: FieldType::VarChar(255),
not_null: false,
primary_key: col_name == "id",
unique: false,
auto_increment: col_name == "id",
default: None,
})
.collect(),
constraints: vec![],
without_rowid: None,
partition: None,
interleave_in_parent: None,
}
}
fn add_column_operation(table_name: &str, column_name: &str) -> Operation {
Operation::AddColumn {
table: table_name.to_string(),
column: ColumnDefinition {
name: column_name.to_string(),
type_definition: FieldType::VarChar(255),
not_null: false,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
mysql_options: None,
}
}
mod build_applied_migration_graph {
use super::*;
use crate::backends::DatabaseConnection;
#[derive(Clone)]
struct MockMigrationSource {
migrations: Vec<Migration>,
}
#[async_trait::async_trait]
impl MigrationSource for MockMigrationSource {
async fn all_migrations(&self) -> Result<Vec<Migration>> {
Ok(self.migrations.clone())
}
}
#[tokio::test]
async fn test_filters_unapplied_migrations() {
let connection = DatabaseConnection::connect_sqlite("sqlite::memory:")
.await
.expect("Failed to connect");
let recorder = crate::migrations::DatabaseMigrationRecorder::new(connection);
let source = MockMigrationSource { migrations: vec![] };
let loader = MigrationStateLoader::new(recorder, source);
let applied_records = vec![create_migration_record("myapp", "0001_initial")];
let all_migrations = vec![
create_migration(
"myapp",
"0001_initial",
vec![create_table_operation("users", vec!["id", "name"])],
vec![],
),
create_migration(
"myapp",
"0002_add_email",
vec![add_column_operation("users", "email")],
vec![("myapp", "0001_initial")],
),
];
let graph = loader
.build_applied_migration_graph(&applied_records, &all_migrations)
.expect("Failed to build graph");
let sorted = graph.topological_sort().expect("Failed to sort");
assert_eq!(sorted.len(), 1);
assert_eq!(sorted[0].app_label, "myapp");
assert_eq!(sorted[0].name, "0001_initial");
}
#[tokio::test]
async fn test_includes_all_applied_migrations() {
let connection = DatabaseConnection::connect_sqlite("sqlite::memory:")
.await
.expect("Failed to connect");
let recorder = crate::migrations::DatabaseMigrationRecorder::new(connection);
let source = MockMigrationSource { migrations: vec![] };
let loader = MigrationStateLoader::new(recorder, source);
let applied_records = vec![
create_migration_record("myapp", "0001_initial"),
create_migration_record("myapp", "0002_add_email"),
];
let all_migrations = vec![
create_migration(
"myapp",
"0001_initial",
vec![create_table_operation("users", vec!["id", "name"])],
vec![],
),
create_migration(
"myapp",
"0002_add_email",
vec![add_column_operation("users", "email")],
vec![("myapp", "0001_initial")],
),
];
let graph = loader
.build_applied_migration_graph(&applied_records, &all_migrations)
.expect("Failed to build graph");
let sorted = graph.topological_sort().expect("Failed to sort");
assert_eq!(sorted.len(), 2);
assert_eq!(sorted[0].name, "0001_initial");
assert_eq!(sorted[1].name, "0002_add_email");
}
#[tokio::test]
async fn test_multiple_apps() {
let connection = DatabaseConnection::connect_sqlite("sqlite::memory:")
.await
.expect("Failed to connect");
let recorder = crate::migrations::DatabaseMigrationRecorder::new(connection);
let source = MockMigrationSource { migrations: vec![] };
let loader = MigrationStateLoader::new(recorder, source);
let applied_records = vec![
create_migration_record("users", "0001_initial"),
create_migration_record("posts", "0001_initial"),
];
let all_migrations = vec![
create_migration(
"users",
"0001_initial",
vec![create_table_operation("users", vec!["id", "name"])],
vec![],
),
create_migration(
"posts",
"0001_initial",
vec![create_table_operation("posts", vec!["id", "title"])],
vec![],
),
];
let graph = loader
.build_applied_migration_graph(&applied_records, &all_migrations)
.expect("Failed to build graph");
let sorted = graph.topological_sort().expect("Failed to sort");
assert_eq!(sorted.len(), 2);
let apps: std::collections::HashSet<_> =
sorted.iter().map(|k| k.app_label.as_str()).collect();
assert!(apps.contains("users"));
assert!(apps.contains("posts"));
}
#[tokio::test]
async fn test_empty_applied_records() {
let connection = DatabaseConnection::connect_sqlite("sqlite::memory:")
.await
.expect("Failed to connect");
let recorder = crate::migrations::DatabaseMigrationRecorder::new(connection);
let source = MockMigrationSource { migrations: vec![] };
let loader = MigrationStateLoader::new(recorder, source);
let applied_records: Vec<MigrationRecord> = vec![];
let all_migrations = vec![create_migration(
"myapp",
"0001_initial",
vec![create_table_operation("users", vec!["id"])],
vec![],
)];
let graph = loader
.build_applied_migration_graph(&applied_records, &all_migrations)
.expect("Failed to build graph");
let sorted = graph.topological_sort().expect("Failed to sort");
assert!(sorted.is_empty());
}
#[tokio::test]
async fn test_empty_all_migrations() {
let connection = DatabaseConnection::connect_sqlite("sqlite::memory:")
.await
.expect("Failed to connect");
let recorder = crate::migrations::DatabaseMigrationRecorder::new(connection);
let source = MockMigrationSource { migrations: vec![] };
let loader = MigrationStateLoader::new(recorder, source);
let applied_records = vec![create_migration_record("myapp", "0001_initial")];
let all_migrations: Vec<Migration> = vec![];
let graph = loader
.build_applied_migration_graph(&applied_records, &all_migrations)
.expect("Failed to build graph");
let sorted = graph.topological_sort().expect("Failed to sort");
assert!(sorted.is_empty());
}
#[tokio::test]
async fn test_cross_app_dependencies() {
let connection = DatabaseConnection::connect_sqlite("sqlite::memory:")
.await
.expect("Failed to connect");
let recorder = crate::migrations::DatabaseMigrationRecorder::new(connection);
let source = MockMigrationSource { migrations: vec![] };
let loader = MigrationStateLoader::new(recorder, source);
let applied_records = vec![
create_migration_record("users", "0001_initial"),
create_migration_record("posts", "0001_initial"),
];
let all_migrations = vec![
create_migration(
"users",
"0001_initial",
vec![create_table_operation("users", vec!["id", "name"])],
vec![],
),
create_migration(
"posts",
"0001_initial",
vec![create_table_operation("posts", vec!["id", "user_id"])],
vec![("users", "0001_initial")], ),
];
let graph = loader
.build_applied_migration_graph(&applied_records, &all_migrations)
.expect("Failed to build graph");
let sorted = graph.topological_sort().expect("Failed to sort");
assert_eq!(sorted.len(), 2);
let users_pos = sorted.iter().position(|k| k.app_label == "users").unwrap();
let posts_pos = sorted.iter().position(|k| k.app_label == "posts").unwrap();
assert!(
users_pos < posts_pos,
"users should come before posts in topological order"
);
}
}
mod project_state_replay {
use super::*;
#[test]
fn test_create_table_creates_model() {
let mut state = ProjectState::default();
let operations = vec![create_table_operation("users", vec!["id", "name", "email"])];
state.apply_migration_operations(&operations, "testapp");
assert_eq!(state.models.len(), 1);
let model = state.models.values().next().unwrap();
assert_eq!(model.table_name, "users");
assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("name"));
assert!(model.fields.contains_key("email"));
}
#[test]
fn test_add_column_adds_field() {
let mut state = ProjectState::default();
let create_ops = vec![create_table_operation("users", vec!["id", "name"])];
state.apply_migration_operations(&create_ops, "testapp");
let add_ops = vec![add_column_operation("users", "email")];
state.apply_migration_operations(&add_ops, "testapp");
let model = state.models.values().next().unwrap();
assert_eq!(model.fields.len(), 3);
assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("name"));
assert!(model.fields.contains_key("email"));
}
#[test]
fn test_drop_column_removes_field() {
let mut state = ProjectState::default();
let create_ops = vec![create_table_operation("users", vec!["id", "name", "email"])];
state.apply_migration_operations(&create_ops, "testapp");
let drop_ops = vec![Operation::DropColumn {
table: "users".to_string(),
column: "email".to_string(),
}];
state.apply_migration_operations(&drop_ops, "testapp");
let model = state.models.values().next().unwrap();
assert_eq!(model.fields.len(), 2);
assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("name"));
assert!(!model.fields.contains_key("email"));
}
#[test]
fn test_drop_table_removes_model() {
let mut state = ProjectState::default();
let create_ops = vec![
create_table_operation("users", vec!["id"]),
create_table_operation("posts", vec!["id"]),
];
state.apply_migration_operations(&create_ops, "testapp");
assert_eq!(state.models.len(), 2);
let drop_ops = vec![Operation::DropTable {
name: "users".to_string(),
}];
state.apply_migration_operations(&drop_ops, "testapp");
assert_eq!(state.models.len(), 1);
let model = state.models.values().next().unwrap();
assert_eq!(model.table_name, "posts");
}
#[test]
fn test_rename_table_updates_name() {
let mut state = ProjectState::default();
let create_ops = vec![create_table_operation("old_users", vec!["id"])];
state.apply_migration_operations(&create_ops, "testapp");
let rename_ops = vec![Operation::RenameTable {
old_name: "old_users".to_string(),
new_name: "users".to_string(),
}];
state.apply_migration_operations(&rename_ops, "testapp");
let model = state.models.values().next().unwrap();
assert_eq!(model.table_name, "users");
}
#[test]
fn test_rename_column_updates_field_name() {
let mut state = ProjectState::default();
let create_ops = vec![create_table_operation("users", vec!["id", "user_name"])];
state.apply_migration_operations(&create_ops, "testapp");
let rename_ops = vec![Operation::RenameColumn {
table: "users".to_string(),
old_name: "user_name".to_string(),
new_name: "name".to_string(),
}];
state.apply_migration_operations(&rename_ops, "testapp");
let model = state.models.values().next().unwrap();
assert!(!model.fields.contains_key("user_name"));
assert!(model.fields.contains_key("name"));
}
#[test]
fn test_sequential_migrations() {
let mut state = ProjectState::default();
let migration1_ops = vec![create_table_operation("users", vec!["id", "name"])];
state.apply_migration_operations(&migration1_ops, "testapp");
let migration2_ops = vec![add_column_operation("users", "email")];
state.apply_migration_operations(&migration2_ops, "testapp");
let migration3_ops = vec![add_column_operation("users", "created_at")];
state.apply_migration_operations(&migration3_ops, "testapp");
let model = state.models.values().next().unwrap();
assert_eq!(model.fields.len(), 4);
assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("name"));
assert!(model.fields.contains_key("email"));
assert!(model.fields.contains_key("created_at"));
}
}
}
#[cfg(test)]
mod build_state_from_files_tests {
use super::*;
use crate::migrations::FieldType;
use crate::migrations::operations::{ColumnDefinition, Operation};
use rstest::rstest;
#[derive(Clone)]
struct MockMigrationSource {
migrations: Vec<Migration>,
}
#[async_trait::async_trait]
impl MigrationSource for MockMigrationSource {
async fn all_migrations(&self) -> Result<Vec<Migration>> {
Ok(self.migrations.clone())
}
}
fn create_migration(
app_label: &str,
name: &str,
operations: Vec<Operation>,
dependencies: Vec<(&str, &str)>,
) -> Migration {
Migration {
app_label: app_label.to_string(),
name: name.to_string(),
operations,
dependencies: dependencies
.into_iter()
.map(|(a, n)| (a.to_string(), n.to_string()))
.collect(),
replaces: vec![],
atomic: true,
initial: None,
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
fn create_table_operation(table_name: &str, columns: Vec<&str>) -> Operation {
Operation::CreateTable {
name: table_name.to_string(),
columns: columns
.into_iter()
.map(|col_name| ColumnDefinition {
name: col_name.to_string(),
type_definition: FieldType::VarChar(255),
not_null: false,
primary_key: col_name == "id",
unique: false,
auto_increment: col_name == "id",
default: None,
})
.collect(),
constraints: vec![],
without_rowid: None,
partition: None,
interleave_in_parent: None,
}
}
fn add_column_operation(table_name: &str, column_name: &str) -> Operation {
Operation::AddColumn {
table: table_name.to_string(),
column: ColumnDefinition {
name: column_name.to_string(),
type_definition: FieldType::VarChar(255),
not_null: false,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
mysql_options: None,
}
}
#[rstest]
#[tokio::test]
async fn test_empty_source_returns_empty_state() {
let source = MockMigrationSource { migrations: vec![] };
let state = build_state_from_files(&source).await.unwrap();
assert!(state.models.is_empty());
}
#[rstest]
#[tokio::test]
async fn test_single_create_table() {
let source = MockMigrationSource {
migrations: vec![create_migration(
"auth",
"0001_initial",
vec![create_table_operation("auth_users", vec!["id", "username"])],
vec![],
)],
};
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 1);
let model = state.find_model_by_table("auth_users").unwrap();
assert_eq!(model.fields.len(), 2);
assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("username"));
}
#[rstest]
#[tokio::test]
async fn test_chained_create_and_add_column() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"auth",
"0001_initial",
vec![create_table_operation("auth_users", vec!["id", "username"])],
vec![],
),
create_migration(
"auth",
"0002_add_email",
vec![add_column_operation("auth_users", "email")],
vec![("auth", "0001_initial")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 1);
let model = state.find_model_by_table("auth_users").unwrap();
assert_eq!(model.fields.len(), 3);
assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("username"));
assert!(model.fields.contains_key("email"));
}
#[rstest]
#[tokio::test]
async fn test_cross_app_dependencies() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"auth",
"0001_initial",
vec![create_table_operation("auth_users", vec!["id", "username"])],
vec![],
),
create_migration(
"posts",
"0001_initial",
vec![create_table_operation(
"posts_post",
vec!["id", "title", "author_id"],
)],
vec![("auth", "0001_initial")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 2);
assert!(state.find_model_by_table("auth_users").is_some());
assert!(state.find_model_by_table("posts_post").is_some());
}
#[rstest]
#[tokio::test]
async fn test_create_then_drop_results_in_empty() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"temp",
"0001_initial",
vec![create_table_operation("temp_data", vec!["id", "value"])],
vec![],
),
create_migration(
"temp",
"0002_drop",
vec![Operation::DropTable {
name: "temp_data".to_string(),
}],
vec![("temp", "0001_initial")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
assert!(state.models.is_empty());
}
#[rstest]
#[tokio::test]
async fn test_multiple_independent_apps() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"auth",
"0001_initial",
vec![create_table_operation("auth_users", vec!["id", "username"])],
vec![],
),
create_migration(
"clusters",
"0001_initial",
vec![create_table_operation(
"clusters_cluster",
vec!["id", "name"],
)],
vec![],
),
create_migration(
"deployments",
"0001_initial",
vec![create_table_operation(
"deployments_deployment",
vec!["id", "status"],
)],
vec![],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 3);
assert!(state.find_model_by_table("auth_users").is_some());
assert!(state.find_model_by_table("clusters_cluster").is_some());
assert!(
state
.find_model_by_table("deployments_deployment")
.is_some()
);
}
#[rstest]
#[tokio::test]
async fn test_rename_column_reflected() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"auth",
"0001_initial",
vec![create_table_operation("auth_users", vec!["id", "name"])],
vec![],
),
create_migration(
"auth",
"0002_rename",
vec![Operation::RenameColumn {
table: "auth_users".to_string(),
old_name: "name".to_string(),
new_name: "full_name".to_string(),
}],
vec![("auth", "0001_initial")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
let model = state.find_model_by_table("auth_users").unwrap();
assert_eq!(model.fields.len(), 2);
assert!(model.fields.contains_key("id"));
assert!(!model.fields.contains_key("name"));
assert!(model.fields.contains_key("full_name"));
}
#[rstest]
#[tokio::test]
async fn test_rename_table_reflected() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"auth",
"0001_initial",
vec![create_table_operation("auth_users", vec!["id", "name"])],
vec![],
),
create_migration(
"auth",
"0002_rename_table",
vec![Operation::RenameTable {
old_name: "auth_users".to_string(),
new_name: "auth_accounts".to_string(),
}],
vec![("auth", "0001_initial")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 1);
assert!(state.find_model_by_table("auth_users").is_none());
assert!(state.find_model_by_table("auth_accounts").is_some());
}
#[rstest]
#[tokio::test]
async fn test_drop_column_reflected() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"auth",
"0001_initial",
vec![create_table_operation(
"auth_users",
vec!["id", "username", "legacy_field"],
)],
vec![],
),
create_migration(
"auth",
"0002_drop_legacy",
vec![Operation::DropColumn {
table: "auth_users".to_string(),
column: "legacy_field".to_string(),
}],
vec![("auth", "0001_initial")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
let model = state.find_model_by_table("auth_users").unwrap();
assert_eq!(model.fields.len(), 2);
assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("username"));
assert!(!model.fields.contains_key("legacy_field"));
}
#[rstest]
#[tokio::test]
async fn test_alter_column_reflected() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"auth",
"0001_initial",
vec![create_table_operation("auth_users", vec!["id", "email"])],
vec![],
),
create_migration(
"auth",
"0002_alter_email",
vec![Operation::AlterColumn {
table: "auth_users".to_string(),
column: "email".to_string(),
old_definition: Some(ColumnDefinition {
name: "email".to_string(),
type_definition: FieldType::VarChar(255),
not_null: false,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
}),
new_definition: ColumnDefinition {
name: "email".to_string(),
type_definition: FieldType::Text,
not_null: true,
primary_key: false,
unique: true,
auto_increment: false,
default: None,
},
mysql_options: None,
}],
vec![("auth", "0001_initial")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
let model = state.find_model_by_table("auth_users").unwrap();
let email_field = model.fields.get("email").unwrap();
assert_eq!(email_field.field_type, FieldType::Text);
}
#[rstest]
#[tokio::test]
async fn test_issue_3199_scenario_reconstruction() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"auth",
"0001_initial",
vec![create_table_operation(
"auth_users",
vec!["id", "username", "password"],
)],
vec![],
),
create_migration(
"auth",
"0002_add_names",
vec![
add_column_operation("auth_users", "first_name"),
add_column_operation("auth_users", "last_name"),
],
vec![("auth", "0001_initial")],
),
create_migration(
"clusters",
"0001_initial",
vec![create_table_operation(
"clusters_cluster",
vec!["id", "name", "region"],
)],
vec![],
),
create_migration(
"deployments",
"0001_initial",
vec![create_table_operation(
"deployments_deployment",
vec!["id", "cluster_id", "status"],
)],
vec![("clusters", "0001_initial")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 3);
let auth_model = state.find_model_by_table("auth_users").unwrap();
assert_eq!(auth_model.fields.len(), 5);
assert!(auth_model.fields.contains_key("first_name"));
assert!(auth_model.fields.contains_key("last_name"));
let clusters_model = state.find_model_by_table("clusters_cluster").unwrap();
assert_eq!(clusters_model.fields.len(), 3);
let deployments_model = state.find_model_by_table("deployments_deployment").unwrap();
assert_eq!(deployments_model.fields.len(), 3);
}
#[rstest]
#[tokio::test]
async fn test_long_migration_chain() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"blog",
"0001_initial",
vec![create_table_operation("blog_post", vec!["id", "title"])],
vec![],
),
create_migration(
"blog",
"0002_add_body",
vec![add_column_operation("blog_post", "body")],
vec![("blog", "0001_initial")],
),
create_migration(
"blog",
"0003_add_author",
vec![add_column_operation("blog_post", "author_id")],
vec![("blog", "0002_add_body")],
),
create_migration(
"blog",
"0004_add_timestamps",
vec![
add_column_operation("blog_post", "created_at"),
add_column_operation("blog_post", "updated_at"),
],
vec![("blog", "0003_add_author")],
),
create_migration(
"blog",
"0005_add_category",
vec![
create_table_operation("blog_category", vec!["id", "name"]),
add_column_operation("blog_post", "category_id"),
],
vec![("blog", "0004_add_timestamps")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 2);
let post = state.find_model_by_table("blog_post").unwrap();
assert_eq!(post.fields.len(), 7);
let category = state.find_model_by_table("blog_category").unwrap();
assert_eq!(category.fields.len(), 2); }
#[rstest]
#[tokio::test]
async fn test_merge_migration_preserves_state() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"contacts",
"0001_initial",
vec![create_table_operation(
"contacts_person",
vec!["id", "name"],
)],
vec![],
),
create_migration(
"contacts",
"0002_add_email",
vec![add_column_operation("contacts_person", "email")],
vec![("contacts", "0001_initial")],
),
create_migration(
"contacts",
"0002_add_phone",
vec![add_column_operation("contacts_person", "phone")],
vec![("contacts", "0001_initial")],
),
create_migration(
"contacts",
"0003_merge_0002_add_email_0002_add_phone",
vec![],
vec![
("contacts", "0002_add_email"),
("contacts", "0002_add_phone"),
],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 1);
let model = state.find_model_by_table("contacts_person").unwrap();
assert_eq!(model.fields.len(), 4); assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("name"));
assert!(model.fields.contains_key("email"));
assert!(model.fields.contains_key("phone"));
}
#[rstest]
#[tokio::test]
async fn test_merge_then_continue_adding_columns() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"auth",
"0001_initial",
vec![create_table_operation("auth_users", vec!["id", "username"])],
vec![],
),
create_migration(
"auth",
"0002_add_email",
vec![add_column_operation("auth_users", "email")],
vec![("auth", "0001_initial")],
),
create_migration(
"auth",
"0002_add_avatar",
vec![add_column_operation("auth_users", "avatar_url")],
vec![("auth", "0001_initial")],
),
create_migration(
"auth",
"0003_merge",
vec![],
vec![("auth", "0002_add_email"), ("auth", "0002_add_avatar")],
),
create_migration(
"auth",
"0004_add_bio",
vec![add_column_operation("auth_users", "bio")],
vec![("auth", "0003_merge")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
let model = state.find_model_by_table("auth_users").unwrap();
assert_eq!(model.fields.len(), 5); assert!(model.fields.contains_key("bio"));
}
#[rstest]
#[tokio::test]
async fn test_cross_app_merge_migrations() {
let source = MockMigrationSource {
migrations: vec![
create_migration(
"auth",
"0001_initial",
vec![create_table_operation("auth_users", vec!["id", "name"])],
vec![],
),
create_migration(
"auth",
"0002_add_email",
vec![add_column_operation("auth_users", "email")],
vec![("auth", "0001_initial")],
),
create_migration(
"auth",
"0002_add_role",
vec![add_column_operation("auth_users", "role")],
vec![("auth", "0001_initial")],
),
create_migration(
"auth",
"0003_merge",
vec![],
vec![("auth", "0002_add_email"), ("auth", "0002_add_role")],
),
create_migration(
"posts",
"0001_initial",
vec![create_table_operation(
"posts_post",
vec!["id", "title", "author_id"],
)],
vec![("auth", "0001_initial")],
),
create_migration(
"posts",
"0002_add_body",
vec![add_column_operation("posts_post", "body")],
vec![("posts", "0001_initial")],
),
create_migration(
"posts",
"0002_add_slug",
vec![add_column_operation("posts_post", "slug")],
vec![("posts", "0001_initial")],
),
create_migration(
"posts",
"0003_merge",
vec![],
vec![("posts", "0002_add_body"), ("posts", "0002_add_slug")],
),
],
};
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 2);
let users = state.find_model_by_table("auth_users").unwrap();
assert_eq!(users.fields.len(), 4);
let posts = state.find_model_by_table("posts_post").unwrap();
assert_eq!(posts.fields.len(), 5); }
}
#[cfg(test)]
mod filesystem_integration_tests {
use super::*;
use crate::migrations::FilesystemSource;
use rstest::rstest;
use tempfile::TempDir;
fn write_migration_file(base: &std::path::Path, app: &str, name: &str, content: &str) {
let dir = base.join(app);
std::fs::create_dir_all(&dir).unwrap();
let file_path = dir.join(format!("{}.rs", name));
std::fs::write(file_path, content).unwrap();
}
#[rstest]
#[tokio::test]
async fn test_single_app_initial_migration_from_files() {
let tmp = TempDir::new().unwrap();
write_migration_file(
tmp.path(),
"todos",
"0001_initial",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0001_initial".to_string(),
app_label: "todos".to_string(),
operations: vec![
Operation::CreateTable {
name: "todos_task".to_string(),
columns: vec![
ColumnDefinition {
name: "id".to_string(),
type_definition: FieldType::Serial,
not_null: true,
primary_key: true,
unique: false,
auto_increment: true,
default: None,
},
ColumnDefinition {
name: "title".to_string(),
type_definition: FieldType::VarChar(255),
not_null: true,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
],
constraints: vec![],
without_rowid: None,
interleave_in_parent: None,
partition: None,
},
],
dependencies: vec![],
replaces: vec![],
atomic: true,
initial: Some(true),
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
let source = FilesystemSource::new(tmp.path());
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 1);
let model = state.find_model_by_table("todos_task").unwrap();
assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("title"));
}
#[rstest]
#[tokio::test]
async fn test_cross_app_migrations_from_files() {
let tmp = TempDir::new().unwrap();
write_migration_file(
tmp.path(),
"auth",
"0001_initial",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0001_initial".to_string(),
app_label: "auth".to_string(),
operations: vec![
Operation::CreateTable {
name: "auth_users".to_string(),
columns: vec![
ColumnDefinition {
name: "id".to_string(),
type_definition: FieldType::Serial,
not_null: true,
primary_key: true,
unique: false,
auto_increment: true,
default: None,
},
ColumnDefinition {
name: "username".to_string(),
type_definition: FieldType::VarChar(150),
not_null: true,
primary_key: false,
unique: true,
auto_increment: false,
default: None,
},
],
constraints: vec![],
without_rowid: None,
interleave_in_parent: None,
partition: None,
},
],
dependencies: vec![],
replaces: vec![],
atomic: true,
initial: Some(true),
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
write_migration_file(
tmp.path(),
"posts",
"0001_initial",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0001_initial".to_string(),
app_label: "posts".to_string(),
operations: vec![
Operation::CreateTable {
name: "posts_post".to_string(),
columns: vec![
ColumnDefinition {
name: "id".to_string(),
type_definition: FieldType::Serial,
not_null: true,
primary_key: true,
unique: false,
auto_increment: true,
default: None,
},
ColumnDefinition {
name: "title".to_string(),
type_definition: FieldType::VarChar(200),
not_null: true,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
ColumnDefinition {
name: "author_id".to_string(),
type_definition: FieldType::Integer,
not_null: true,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
],
constraints: vec![],
without_rowid: None,
interleave_in_parent: None,
partition: None,
},
],
dependencies: vec![
("auth".to_string(), "0001_initial".to_string()),
],
replaces: vec![],
atomic: true,
initial: Some(true),
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![
("auth".to_string(), "0001_initial".to_string()),
]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
let source = FilesystemSource::new(tmp.path());
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 2);
assert!(state.find_model_by_table("auth_users").is_some());
assert!(state.find_model_by_table("posts_post").is_some());
let posts_model = state.find_model_by_table("posts_post").unwrap();
assert_eq!(posts_model.fields.len(), 3);
}
#[rstest]
#[tokio::test]
async fn test_issue_3199_add_column_from_files() {
let tmp = TempDir::new().unwrap();
write_migration_file(
tmp.path(),
"auth",
"0001_initial",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0001_initial".to_string(),
app_label: "auth".to_string(),
operations: vec![
Operation::CreateTable {
name: "auth_users".to_string(),
columns: vec![
ColumnDefinition {
name: "id".to_string(),
type_definition: FieldType::Serial,
not_null: true,
primary_key: true,
unique: false,
auto_increment: true,
default: None,
},
ColumnDefinition {
name: "username".to_string(),
type_definition: FieldType::VarChar(150),
not_null: true,
primary_key: false,
unique: true,
auto_increment: false,
default: None,
},
],
constraints: vec![],
without_rowid: None,
interleave_in_parent: None,
partition: None,
},
],
dependencies: vec![],
replaces: vec![],
atomic: true,
initial: Some(true),
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
write_migration_file(
tmp.path(),
"auth",
"0002_add_names",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0002_add_names".to_string(),
app_label: "auth".to_string(),
operations: vec![
Operation::AddColumn {
table: "auth_users".to_string(),
column: ColumnDefinition {
name: "first_name".to_string(),
type_definition: FieldType::VarChar(100),
not_null: false,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
mysql_options: None,
},
Operation::AddColumn {
table: "auth_users".to_string(),
column: ColumnDefinition {
name: "last_name".to_string(),
type_definition: FieldType::VarChar(100),
not_null: false,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
mysql_options: None,
},
],
dependencies: vec![
("auth".to_string(), "0001_initial".to_string()),
],
replaces: vec![],
atomic: true,
initial: None,
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![
("auth".to_string(), "0001_initial".to_string()),
]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
let source = FilesystemSource::new(tmp.path());
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 1);
let model = state.find_model_by_table("auth_users").unwrap();
assert_eq!(model.fields.len(), 4);
assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("username"));
assert!(model.fields.contains_key("first_name"));
assert!(model.fields.contains_key("last_name"));
}
#[rstest]
#[tokio::test]
async fn test_empty_directory_returns_empty_state() {
let tmp = TempDir::new().unwrap();
let source = FilesystemSource::new(tmp.path());
let state = build_state_from_files(&source).await.unwrap();
assert!(state.models.is_empty());
}
#[rstest]
#[tokio::test]
async fn test_merge_migration_from_files() {
let tmp = TempDir::new().unwrap();
write_migration_file(
tmp.path(),
"contacts",
"0001_initial",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0001_initial".to_string(),
app_label: "contacts".to_string(),
operations: vec![
Operation::CreateTable {
name: "contacts_person".to_string(),
columns: vec![
ColumnDefinition {
name: "id".to_string(),
type_definition: FieldType::Serial,
not_null: true,
primary_key: true,
unique: false,
auto_increment: true,
default: None,
},
ColumnDefinition {
name: "username".to_string(),
type_definition: FieldType::VarChar(150),
not_null: true,
primary_key: false,
unique: true,
auto_increment: false,
default: None,
},
],
constraints: vec![],
without_rowid: None,
interleave_in_parent: None,
partition: None,
},
],
dependencies: vec![],
replaces: vec![],
atomic: true,
initial: Some(true),
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
write_migration_file(
tmp.path(),
"contacts",
"0002_add_email",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0002_add_email".to_string(),
app_label: "contacts".to_string(),
operations: vec![
Operation::AddColumn {
table: "contacts_person".to_string(),
column: ColumnDefinition {
name: "email".to_string(),
type_definition: FieldType::VarChar(255),
not_null: false,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
mysql_options: None,
},
],
dependencies: vec![
("contacts".to_string(), "0001_initial".to_string()),
],
replaces: vec![],
atomic: true,
initial: None,
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![
("contacts".to_string(), "0001_initial".to_string()),
]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
write_migration_file(
tmp.path(),
"contacts",
"0002_add_phone",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0002_add_phone".to_string(),
app_label: "contacts".to_string(),
operations: vec![
Operation::AddColumn {
table: "contacts_person".to_string(),
column: ColumnDefinition {
name: "phone".to_string(),
type_definition: FieldType::VarChar(20),
not_null: false,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
mysql_options: None,
},
],
dependencies: vec![
("contacts".to_string(), "0001_initial".to_string()),
],
replaces: vec![],
atomic: true,
initial: None,
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![
("contacts".to_string(), "0001_initial".to_string()),
]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
write_migration_file(
tmp.path(),
"contacts",
"0003_merge_0002_add_email_0002_add_phone",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0003_merge_0002_add_email_0002_add_phone".to_string(),
app_label: "contacts".to_string(),
operations: vec![],
dependencies: vec![
("contacts".to_string(), "0002_add_email".to_string()),
("contacts".to_string(), "0002_add_phone".to_string()),
],
replaces: vec![],
atomic: true,
initial: None,
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![
("contacts".to_string(), "0002_add_email".to_string()),
("contacts".to_string(), "0002_add_phone".to_string()),
]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
let source = FilesystemSource::new(tmp.path());
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 1);
let model = state.find_model_by_table("contacts_person").unwrap();
assert_eq!(model.fields.len(), 4); assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("username"));
assert!(model.fields.contains_key("email"));
assert!(model.fields.contains_key("phone"));
}
#[rstest]
#[tokio::test]
async fn test_post_merge_migration_from_files() {
let tmp = TempDir::new().unwrap();
write_migration_file(
tmp.path(),
"auth",
"0001_initial",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0001_initial".to_string(),
app_label: "auth".to_string(),
operations: vec![
Operation::CreateTable {
name: "auth_users".to_string(),
columns: vec![
ColumnDefinition {
name: "id".to_string(),
type_definition: FieldType::Serial,
not_null: true,
primary_key: true,
unique: false,
auto_increment: true,
default: None,
},
ColumnDefinition {
name: "username".to_string(),
type_definition: FieldType::VarChar(150),
not_null: true,
primary_key: false,
unique: true,
auto_increment: false,
default: None,
},
],
constraints: vec![],
without_rowid: None,
interleave_in_parent: None,
partition: None,
},
],
dependencies: vec![],
replaces: vec![],
atomic: true,
initial: Some(true),
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
write_migration_file(
tmp.path(),
"auth",
"0002_add_email",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0002_add_email".to_string(),
app_label: "auth".to_string(),
operations: vec![
Operation::AddColumn {
table: "auth_users".to_string(),
column: ColumnDefinition {
name: "email".to_string(),
type_definition: FieldType::VarChar(255),
not_null: false,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
mysql_options: None,
},
],
dependencies: vec![
("auth".to_string(), "0001_initial".to_string()),
],
replaces: vec![],
atomic: true,
initial: None,
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![("auth".to_string(), "0001_initial".to_string())]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
write_migration_file(
tmp.path(),
"auth",
"0002_add_avatar",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0002_add_avatar".to_string(),
app_label: "auth".to_string(),
operations: vec![
Operation::AddColumn {
table: "auth_users".to_string(),
column: ColumnDefinition {
name: "avatar_url".to_string(),
type_definition: FieldType::VarChar(500),
not_null: false,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
mysql_options: None,
},
],
dependencies: vec![
("auth".to_string(), "0001_initial".to_string()),
],
replaces: vec![],
atomic: true,
initial: None,
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![("auth".to_string(), "0001_initial".to_string())]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
write_migration_file(
tmp.path(),
"auth",
"0003_merge",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0003_merge".to_string(),
app_label: "auth".to_string(),
operations: vec![],
dependencies: vec![
("auth".to_string(), "0002_add_email".to_string()),
("auth".to_string(), "0002_add_avatar".to_string()),
],
replaces: vec![],
atomic: true,
initial: None,
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![
("auth".to_string(), "0002_add_email".to_string()),
("auth".to_string(), "0002_add_avatar".to_string()),
]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
write_migration_file(
tmp.path(),
"auth",
"0004_add_bio",
r#"
use reinhardt_db::migrations::prelude::*;
pub fn migration() -> Migration {
Migration {
name: "0004_add_bio".to_string(),
app_label: "auth".to_string(),
operations: vec![
Operation::AddColumn {
table: "auth_users".to_string(),
column: ColumnDefinition {
name: "bio".to_string(),
type_definition: FieldType::Text,
not_null: false,
primary_key: false,
unique: false,
auto_increment: false,
default: None,
},
mysql_options: None,
},
],
dependencies: vec![
("auth".to_string(), "0003_merge".to_string()),
],
replaces: vec![],
atomic: true,
initial: None,
state_only: false,
database_only: false,
swappable_dependencies: vec![],
optional_dependencies: vec![],
}
}
pub fn dependencies() -> Vec<(String, String)> {
vec![("auth".to_string(), "0003_merge".to_string())]
}
pub fn atomic() -> bool {
true
}
pub fn replaces() -> Vec<(String, String)> {
vec![]
}
"#,
);
let source = FilesystemSource::new(tmp.path());
let state = build_state_from_files(&source).await.unwrap();
assert_eq!(state.models.len(), 1);
let model = state.find_model_by_table("auth_users").unwrap();
assert_eq!(model.fields.len(), 5); assert!(model.fields.contains_key("id"));
assert!(model.fields.contains_key("username"));
assert!(model.fields.contains_key("email"));
assert!(model.fields.contains_key("avatar_url"));
assert!(model.fields.contains_key("bio"));
}
}