Skip to content

Commit 99e58ae

Browse files
authored
feat(orm): support custom migrations (#455)
1 parent 2ec8197 commit 99e58ae

27 files changed

+857
-21
lines changed

cot-cli/src/args.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ pub enum MigrationCommands {
4848
List(MigrationListArgs),
4949
/// Generate migrations for a Cot project
5050
Make(MigrationMakeArgs),
51+
/// Create a new empty migration
52+
New(MigrationNewArgs),
53+
}
54+
55+
#[derive(Debug, Args)]
56+
pub struct MigrationNewArgs {
57+
/// Name of the migration
58+
pub name: String,
59+
/// Path to the crate directory to create the migration in [default: current
60+
/// directory]
61+
pub path: Option<PathBuf>,
62+
/// Name of the app to use in the migration (default: crate name)
63+
#[arg(long)]
64+
pub app_name: Option<String>,
5165
}
5266

5367
#[derive(Debug, Args)]

cot-cli/src/handlers.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ use anyhow::Context;
44
use clap::CommandFactory;
55

66
use crate::args::{
7-
Cli, CompletionsArgs, ManpagesArgs, MigrationListArgs, MigrationMakeArgs, ProjectNewArgs,
7+
Cli, CompletionsArgs, ManpagesArgs, MigrationListArgs, MigrationMakeArgs, MigrationNewArgs,
8+
ProjectNewArgs,
9+
};
10+
use crate::migration_generator::{
11+
MigrationGeneratorOptions, create_new_migration, list_migrations, make_migrations,
812
};
9-
use crate::migration_generator::{MigrationGeneratorOptions, list_migrations, make_migrations};
1013
use crate::new_project::{CotSource, new_project};
1114

1215
pub fn handle_new_project(
@@ -59,6 +62,21 @@ pub fn handle_migration_make(
5962
make_migrations(&path, options).with_context(|| "unable to create migrations")
6063
}
6164

65+
pub fn handle_migration_new(
66+
MigrationNewArgs {
67+
name,
68+
path,
69+
app_name,
70+
}: MigrationNewArgs,
71+
) -> anyhow::Result<()> {
72+
let path = path.unwrap_or(PathBuf::from("."));
73+
let options = MigrationGeneratorOptions {
74+
app_name,
75+
output_dir: None,
76+
};
77+
create_new_migration(&path, &name, options).with_context(|| "unable to create migration")
78+
}
79+
6280
pub fn handle_cli_manpages(
6381
ManpagesArgs { output_dir, create }: ManpagesArgs,
6482
) -> anyhow::Result<()> {
@@ -127,6 +145,19 @@ mod tests {
127145
assert!(result.is_err());
128146
}
129147

148+
#[test]
149+
fn migration_new_wrong_directory() {
150+
let args = MigrationNewArgs {
151+
name: "test_migration".to_string(),
152+
path: Some(PathBuf::from("nonexistent")),
153+
app_name: None,
154+
};
155+
156+
let result = handle_migration_new(args);
157+
158+
assert!(result.is_err());
159+
}
160+
130161
#[test]
131162
fn generate_manpages() {
132163
let temp_dir = tempfile::tempdir().unwrap();

cot-cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ fn main() -> anyhow::Result<()> {
2525
Commands::Migration(cmd) => match cmd {
2626
MigrationCommands::List(args) => handlers::handle_migration_list(args),
2727
MigrationCommands::Make(args) => handlers::handle_migration_make(args),
28+
MigrationCommands::New(args) => handlers::handle_migration_new(args),
2829
},
2930
}
3031
}

cot-cli/src/migration_generator.rs

Lines changed: 161 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,52 @@ fn make_package_migrations(
6868
Ok(())
6969
}
7070

71+
pub fn create_new_migration(
72+
path: &Path,
73+
name: &str,
74+
options: MigrationGeneratorOptions,
75+
) -> anyhow::Result<()> {
76+
let Some(manager) = CargoTomlManager::from_path(path)? else {
77+
bail!("Cargo.toml not found in the specified directory or any parent directory.")
78+
};
79+
80+
match manager {
81+
CargoTomlManager::Workspace(workspace) => {
82+
let Some(package) = workspace.get_current_package_manager() else {
83+
bail!(
84+
"Generating migrations for workspaces is not supported yet. \
85+
Please generate migrations for each package separately."
86+
);
87+
};
88+
create_package_new_migration(package, name, options)
89+
}
90+
CargoTomlManager::Package(package) => create_package_new_migration(&package, name, options),
91+
}
92+
}
93+
94+
fn create_package_new_migration(
95+
manager: &PackageManager,
96+
name: &str,
97+
options: MigrationGeneratorOptions,
98+
) -> anyhow::Result<()> {
99+
let crate_name = manager.get_package_name().to_string();
100+
let manifest_path = manager.get_manifest_path();
101+
102+
let generator = MigrationGenerator::new(manifest_path, crate_name, options);
103+
let migration = generator
104+
.generate_custom_migration(name)
105+
.context("unable to generate migration")?;
106+
107+
generator
108+
.write_migrations(&migration)
109+
.context("unable to write migrations")?;
110+
generator
111+
.write_migrations_module()
112+
.context("unable to write migrations.rs")?;
113+
114+
Ok(())
115+
}
116+
71117
pub fn list_migrations(path: &Path) -> anyhow::Result<HashMap<String, Vec<String>>> {
72118
if let Some(manager) = CargoTomlManager::from_path(path)? {
73119
let mut migration_list = HashMap::new();
@@ -157,7 +203,7 @@ impl MigrationGenerator {
157203
if operations.is_empty() {
158204
Ok(None)
159205
} else {
160-
let migration_name = migration_processor.next_migration_name()?;
206+
let migration_name = migration_processor.next_auto_migration_name()?;
161207
let dependencies = migration_processor.base_dependencies();
162208

163209
let migration =
@@ -166,6 +212,60 @@ impl MigrationGenerator {
166212
}
167213
}
168214

215+
pub fn generate_custom_migration(&self, name: &str) -> anyhow::Result<MigrationAsSource> {
216+
let source_files = self.get_source_files()?;
217+
self.generate_custom_migration_from_files(name, source_files)
218+
}
219+
220+
pub fn generate_custom_migration_from_files(
221+
&self,
222+
name: &str,
223+
source_files: Vec<SourceFile>,
224+
) -> anyhow::Result<MigrationAsSource> {
225+
let AppState { migrations, .. } = self.process_source_files(source_files)?;
226+
let migration_processor = MigrationProcessor::new(migrations)?;
227+
228+
let migration_name = migration_processor.next_migration_name_with_suffix(name)?;
229+
let dependencies = migration_processor.base_dependencies();
230+
231+
let dependencies_repr: Vec<_> = dependencies.iter().map(Repr::repr).collect();
232+
233+
let app_name = self.options.app_name.as_ref().unwrap_or(&self.crate_name);
234+
235+
let migration_def = quote! {
236+
#[derive(Debug, Copy, Clone)]
237+
pub(super) struct Migration;
238+
239+
impl ::cot::db::migrations::Migration for Migration {
240+
const APP_NAME: &'static str = #app_name;
241+
const MIGRATION_NAME: &'static str = #migration_name;
242+
const DEPENDENCIES: &'static [::cot::db::migrations::MigrationDependency] = &[
243+
#(#dependencies_repr,)*
244+
];
245+
const OPERATIONS: &'static [::cot::db::migrations::Operation] = &[
246+
::cot::db::migrations::Operation::custom(forwards).backwards(backwards).build(),
247+
];
248+
}
249+
250+
#[::cot::db::migrations::migration_op]
251+
async fn forwards(_ctx: ::cot::db::migrations::MigrationContext<'_>) -> ::cot::db::Result<()> {
252+
Ok(())
253+
}
254+
255+
#[::cot::db::migrations::migration_op]
256+
async fn backwards(_ctx: ::cot::db::migrations::MigrationContext<'_>) -> ::cot::db::Result<()> {
257+
Err(::cot::db::DatabaseError::MigrationError(
258+
::cot::db::migrations::MigrationEngineError::Custom("Backwards migration not implemented".into())
259+
))
260+
}
261+
};
262+
263+
Ok(MigrationAsSource::new(
264+
migration_name,
265+
Self::generate_migration(migration_def, TokenStream::new()),
266+
))
267+
}
268+
169269
pub fn write_migrations(&self, migration: &MigrationAsSource) -> anyhow::Result<()> {
170270
print_status_msg(
171271
StatusType::Creating,
@@ -847,11 +947,32 @@ impl MigrationProcessor {
847947
migration_models.into_values().cloned().collect()
848948
}
849949

850-
fn next_migration_name(&self) -> anyhow::Result<String> {
950+
fn next_auto_migration_name(&self) -> anyhow::Result<String> {
851951
if self.migrations.is_empty() {
852952
return Ok(format!("{MIGRATIONS_MODULE_PREFIX}0001_initial"));
853953
}
854954

955+
let migration_number = self.get_next_migration_number()?;
956+
let now = chrono::Utc::now();
957+
let date_time = now.format("%Y%m%d_%H%M%S");
958+
959+
Ok(format!(
960+
"{MIGRATIONS_MODULE_PREFIX}{migration_number:04}_auto_{date_time}"
961+
))
962+
}
963+
964+
fn next_migration_name_with_suffix(&self, suffix: &str) -> anyhow::Result<String> {
965+
let migration_number = self.get_next_migration_number()?;
966+
Ok(format!(
967+
"{MIGRATIONS_MODULE_PREFIX}{migration_number:04}_{suffix}"
968+
))
969+
}
970+
971+
fn get_next_migration_number(&self) -> anyhow::Result<u32> {
972+
if self.migrations.is_empty() {
973+
return Ok(1);
974+
}
975+
855976
let last_migration = self.migrations.last().unwrap();
856977
let last_migration_number = last_migration
857978
.name
@@ -871,13 +992,7 @@ impl MigrationProcessor {
871992
)
872993
})?;
873994

874-
let migration_number = last_migration_number + 1;
875-
let now = chrono::Utc::now();
876-
let date_time = now.format("%Y%m%d_%H%M%S");
877-
878-
Ok(format!(
879-
"{MIGRATIONS_MODULE_PREFIX}{migration_number:04}_auto_{date_time}"
880-
))
995+
Ok(last_migration_number + 1)
881996
}
882997

883998
/// Returns the list of dependencies for the next migration, based on the
@@ -1441,7 +1556,7 @@ mod tests {
14411556
let migrations = vec![];
14421557
let processor = MigrationProcessor::new(migrations).unwrap();
14431558

1444-
let next_migration_name = processor.next_migration_name().unwrap();
1559+
let next_migration_name = processor.next_auto_migration_name().unwrap();
14451560
assert_eq!(next_migration_name, "m_0001_initial");
14461561
}
14471562

@@ -1473,6 +1588,42 @@ mod tests {
14731588
);
14741589
}
14751590

1591+
#[test]
1592+
fn migration_processor_next_migration_name_with_suffix() {
1593+
let migrations = vec![Migration {
1594+
app_name: "app1".to_string(),
1595+
name: "m_0001_initial".to_string(),
1596+
models: vec![],
1597+
}];
1598+
let processor = MigrationProcessor::new(migrations).unwrap();
1599+
1600+
let next_name = processor.next_migration_name_with_suffix("custom").unwrap();
1601+
assert_eq!(next_name, "m_0002_custom");
1602+
}
1603+
1604+
#[test]
1605+
fn create_new_migration_check_files_exist() {
1606+
let tempdir = tempfile::tempdir().unwrap();
1607+
let cargo_toml_path = tempdir.path().join("Cargo.toml");
1608+
std::fs::create_dir(tempdir.path().join("src")).unwrap();
1609+
std::fs::write(
1610+
&cargo_toml_path,
1611+
"[package]\nname = \"testapp\"\nversion = \"0.1.0\"\nedition = \"2021\"",
1612+
)
1613+
.unwrap();
1614+
1615+
let options = MigrationGeneratorOptions::default();
1616+
create_new_migration(tempdir.path(), "my_custom", options).unwrap();
1617+
1618+
let migration_file = tempdir.path().join("src/migrations/m_0001_my_custom.rs");
1619+
assert!(migration_file.exists());
1620+
1621+
let migrations_mod = tempdir.path().join("src/migrations.rs");
1622+
assert!(migrations_mod.exists());
1623+
let contents = std::fs::read_to_string(migrations_mod).unwrap();
1624+
assert!(contents.contains("pub mod m_0001_my_custom;"));
1625+
}
1626+
14761627
#[test]
14771628
fn toposort_operations() {
14781629
let mut operations = vec![

cot-cli/tests/migration_generator.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,31 @@ fn create_model_compile_test() {
181181
content: migration_content,
182182
} = migration_opt;
183183

184+
compile_test(src, &migration_name, &migration_content);
185+
}
186+
187+
#[test]
188+
#[cfg_attr(
189+
miri,
190+
ignore = "unsupported operation: extern static `pidfd_spawnp` is not supported by Miri"
191+
)]
192+
fn custom_migration_compile_test() {
193+
let generator = test_generator();
194+
let src = "fn main() {}";
195+
let source_files = vec![SourceFile::parse(PathBuf::from("main.rs"), src).unwrap()];
196+
197+
let migration_opt = generator
198+
.generate_custom_migration_from_files("custom", source_files)
199+
.unwrap();
200+
let MigrationAsSource {
201+
name: migration_name,
202+
content: migration_content,
203+
} = migration_opt;
204+
205+
compile_test(src, &migration_name, &migration_content);
206+
}
207+
208+
fn compile_test(src: &str, migration_name: &str, migration_content: &str) {
184209
let source_with_migrations = format!(
185210
r"
186211
{src}

0 commit comments

Comments
 (0)