From b2dab3aef719a686019dc89b7a1da862db7ac6f5 Mon Sep 17 00:00:00 2001 From: Andrews Innovations Date: Mon, 23 Jun 2025 23:11:37 -0500 Subject: [PATCH 1/4] Added create-migration command line instruction This update adds a command line interface to create migrations directly with sqlpage, and have it manage the timestamps and uniqueness of migration names. --- sqlpage/migrations/README.md | 8 ++++++- src/app_config.rs | 18 +++++++++++++++ src/main.rs | 44 ++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/sqlpage/migrations/README.md b/sqlpage/migrations/README.md index b263393c..d8768cc2 100644 --- a/sqlpage/migrations/README.md +++ b/sqlpage/migrations/README.md @@ -21,7 +21,13 @@ that is greater than the previous one. Use commands like `ALTER TABLE` to update the schema declaratively instead of modifying the existing `CREATE TABLE` statements. -If you try to edit an existing migration, SQLPage will not run it again, will detect +If you try to edit an existing migration, SQLPage will not run it again, it will detect that the migration has already executed. Also, if the migration is different than the one that was executed, SQLPage will throw an error as the database structure must match. + +## Creating migrations on the command line + +You can create a migration directly with sqlpage by running the command "sqlpage create-migration [migration_name]" + +For example if you run 'sqlpage create-migration "Example Migration 1"' on the command line, you will find a new file under "sqlpage/migrations" folder called "[timestamp]_example_migration_1.sql" where timestamp is the current time when you ran the command. ## Running migrations diff --git a/src/app_config.rs b/src/app_config.rs index 584d32a2..5b04e49a 100644 --- a/src/app_config.rs +++ b/src/app_config.rs @@ -22,6 +22,20 @@ pub struct Cli { /// The path to the configuration file. #[clap(short = 'c', long)] pub config_file: Option, + + /// Subcommands for additional functionality. + #[clap(subcommand)] + pub command: Option, +} + +/// Enum for subcommands. +#[derive(Parser)] +pub enum Commands { + /// Create a new migration file. + CreateMigration { + /// Name of the migration. + migration_name: String, + }, } #[cfg(not(feature = "lambda-web"))] @@ -686,6 +700,7 @@ mod test { web_root: Some(PathBuf::from(".")), config_dir: None, config_file: None, + command: None, }; let config = AppConfig::from_cli(&cli).unwrap(); @@ -726,6 +741,7 @@ mod test { web_root: None, config_dir: None, config_file: Some(config_file_path.clone()), + command: None, }; let config = AppConfig::from_cli(&cli).unwrap(); @@ -744,6 +760,7 @@ mod test { web_root: Some(cli_web_dir.clone()), config_dir: None, config_file: Some(config_file_path), + command: None, }; let config = AppConfig::from_cli(&cli_with_web_root).unwrap(); @@ -773,6 +790,7 @@ mod test { web_root: None, config_dir: None, config_file: None, + command: None, }; let config = AppConfig::from_cli(&cli).unwrap(); diff --git a/src/main.rs b/src/main.rs index da1f83ba..ffbd570d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use clap::Parser; use sqlpage::{ app_config, webserver::{self, Database}, @@ -18,6 +19,18 @@ async fn start() -> anyhow::Result<()> { let db = Database::init(&app_config).await?; webserver::database::migrations::apply(&app_config, &db).await?; let state = AppState::init_with_db(&app_config, db).await?; + + let cli = app_config::Cli::parse(); + + if let Some(command) = cli.command { + match command { + app_config::Commands::CreateMigration { migration_name } => { + create_migration_file(&migration_name)?; + return Ok(()); + } + } + } + log::debug!("Starting server..."); webserver::http::run_server(&app_config, state).await?; log::info!("Server stopped gracefully. Goodbye!"); @@ -41,3 +54,34 @@ fn init_logging() { Err(e) => log::error!("Error loading .env file: {e}"), } } + +fn create_migration_file(migration_name: &str) -> anyhow::Result<()> { + use chrono::Utc; + use std::fs; + use std::path::Path; + + let timestamp = Utc::now().format("%Y%m%d%H%M%S").to_string(); + let snake_case_name = migration_name + .replace(|c: char| !c.is_alphanumeric(), "_") + .to_lowercase(); + let file_name = format!("{}_{}.sql", timestamp, snake_case_name); + let migrations_dir = Path::new("sqlpage/migrations"); + + if !migrations_dir.exists() { + fs::create_dir_all(migrations_dir)?; + } + + let mut unique_file_name = file_name.clone(); + let mut counter = 1; + + while migrations_dir.join(&unique_file_name).exists() { + unique_file_name = format!("{}_{}_{}.sql", timestamp, snake_case_name, counter); + counter += 1; + } + + let file_path = migrations_dir.join(unique_file_name); + fs::write(file_path, "-- Write your migration here\n")?; + + println!("Migration created successfully."); + Ok(()) +} From 0a214f91835a8c39f269486213be4c0c5ec593ce Mon Sep 17 00:00:00 2001 From: Andrews Innovations Date: Tue, 24 Jun 2025 15:13:01 -0500 Subject: [PATCH 2/4] Update sqlpage/migrations/README.md Co-authored-by: Ophir LOJKINE --- sqlpage/migrations/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlpage/migrations/README.md b/sqlpage/migrations/README.md index d8768cc2..0141938b 100644 --- a/sqlpage/migrations/README.md +++ b/sqlpage/migrations/README.md @@ -25,7 +25,7 @@ If you try to edit an existing migration, SQLPage will not run it again, it will ## Creating migrations on the command line -You can create a migration directly with sqlpage by running the command "sqlpage create-migration [migration_name]" +You can create a migration directly with sqlpage by running the command `sqlpage create-migration [migration_name]` For example if you run 'sqlpage create-migration "Example Migration 1"' on the command line, you will find a new file under "sqlpage/migrations" folder called "[timestamp]_example_migration_1.sql" where timestamp is the current time when you ran the command. From 3761bda178379ce088a1c94ac4bc72f50d12494d Mon Sep 17 00:00:00 2001 From: Andrews Innovations Date: Tue, 24 Jun 2025 15:13:21 -0500 Subject: [PATCH 3/4] Update sqlpage/migrations/README.md Co-authored-by: Ophir LOJKINE --- sqlpage/migrations/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlpage/migrations/README.md b/sqlpage/migrations/README.md index 0141938b..9d2765fc 100644 --- a/sqlpage/migrations/README.md +++ b/sqlpage/migrations/README.md @@ -27,7 +27,7 @@ If you try to edit an existing migration, SQLPage will not run it again, it will You can create a migration directly with sqlpage by running the command `sqlpage create-migration [migration_name]` -For example if you run 'sqlpage create-migration "Example Migration 1"' on the command line, you will find a new file under "sqlpage/migrations" folder called "[timestamp]_example_migration_1.sql" where timestamp is the current time when you ran the command. +For example if you run `sqlpage create-migration "Example Migration 1"` on the command line, you will find a new file under the `sqlpage/migrations` folder called `[timestamp]_example_migration_1.sql` where timestamp is the current time when you ran the command. ## Running migrations From 484cb35baa42a3de61d85af9c5854dd365b7eb53 Mon Sep 17 00:00:00 2001 From: Andrews Innovations Date: Tue, 24 Jun 2025 21:21:25 -0500 Subject: [PATCH 4/4] Updated with changes for config directory, migration output Now respects the configuration_directory environment variable. It also outputs the path of the new migration created when it is created, and takes care to display that path relative to the current working directory. Lastly, the execution of create-migration command was moved above the rest of the initialization, so that creating a new migration does not run existing migrations. This allows you to create multiple migrations before you run sqlpage normally again to execute them. --- src/main.rs | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index ffbd570d..ff16b378 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,21 +16,25 @@ async fn main() { async fn start() -> anyhow::Result<()> { let app_config = app_config::load_from_cli()?; - let db = Database::init(&app_config).await?; - webserver::database::migrations::apply(&app_config, &db).await?; - let state = AppState::init_with_db(&app_config, db).await?; - let cli = app_config::Cli::parse(); if let Some(command) = cli.command { match command { app_config::Commands::CreateMigration { migration_name } => { - create_migration_file(&migration_name)?; + // Pass configuration_directory from app_config + create_migration_file( + &migration_name, + app_config.configuration_directory.to_str().unwrap(), + )?; return Ok(()); } } } + let db = Database::init(&app_config).await?; + webserver::database::migrations::apply(&app_config, &db).await?; + let state = AppState::init_with_db(&app_config, db).await?; + log::debug!("Starting server..."); webserver::http::run_server(&app_config, state).await?; log::info!("Server stopped gracefully. Goodbye!"); @@ -55,7 +59,10 @@ fn init_logging() { } } -fn create_migration_file(migration_name: &str) -> anyhow::Result<()> { +fn create_migration_file( + migration_name: &str, + configuration_directory: &str, +) -> anyhow::Result<()> { use chrono::Utc; use std::fs; use std::path::Path; @@ -65,10 +72,10 @@ fn create_migration_file(migration_name: &str) -> anyhow::Result<()> { .replace(|c: char| !c.is_alphanumeric(), "_") .to_lowercase(); let file_name = format!("{}_{}.sql", timestamp, snake_case_name); - let migrations_dir = Path::new("sqlpage/migrations"); + let migrations_dir = Path::new(configuration_directory).join("migrations"); if !migrations_dir.exists() { - fs::create_dir_all(migrations_dir)?; + fs::create_dir_all(&migrations_dir)?; } let mut unique_file_name = file_name.clone(); @@ -80,8 +87,23 @@ fn create_migration_file(migration_name: &str) -> anyhow::Result<()> { } let file_path = migrations_dir.join(unique_file_name); - fs::write(file_path, "-- Write your migration here\n")?; + fs::write(&file_path, "-- Write your migration here\n")?; - println!("Migration created successfully."); + // the following code cleans up the display path to show where the migration was created + // relative to the current working directory, and then outputs the path to the migration + let file_path_canon = file_path.canonicalize().unwrap_or(file_path.clone()); + let cwd_canon = std::env::current_dir()? + .canonicalize() + .unwrap_or(std::env::current_dir()?); + let rel_path = match file_path_canon.strip_prefix(&cwd_canon) { + Ok(p) => p, + Err(_) => file_path_canon.as_path(), + }; + let mut display_path_str = rel_path.display().to_string(); + if display_path_str.starts_with("\\\\?\\") { + display_path_str = display_path_str.trim_start_matches("\\\\?\\").to_string(); + } + display_path_str = display_path_str.replace('\\', "/"); + println!("Migration file created: {}", display_path_str); Ok(()) }