From 59dccad0d50fd0276a0eb6cc1a81e0fe937788d1 Mon Sep 17 00:00:00 2001 From: Marcus Cveticanin Date: Tue, 19 Nov 2024 10:30:40 +0100 Subject: [PATCH] Adding Njord CLI back - continuing work --- Cargo.lock | 192 +++++++++++- Cargo.toml | 2 + njord_cli/Cargo.toml | 32 ++ njord_cli/src/command.rs | 183 +++++++++++ njord_cli/src/main.rs | 75 +++++ njord_cli/src/migration.rs | 287 ++++++++++++++++++ njord_cli/src/util.rs | 255 ++++++++++++++++ .../mariadb/down.sql | 0 .../mariadb/up.sql | 0 .../mssql/down.sql | 0 .../mssql/up.sql | 0 .../mysql/down.sql | 0 .../mysql/up.sql | 0 .../oracle/down.sql | 0 .../oracle/up.sql | 0 .../postgres/down.sql | 6 + .../postgres/up.sql | 36 +++ .../sqlite/down.sql | 1 + .../sqlite/up.sql | 5 + njord_cli/templates/njord.toml | 8 + njord_examples/generate.sh | 9 + .../down.sql | 1 + .../00000000000000_njord_initial_setup/up.sql | 5 + .../00000000000001_init_tables/down.sql | 0 .../00000000000001_init_tables/up.sql | 32 ++ njord_examples/rollback.sh | 9 + njord_examples/run.sh | 9 + njord_examples/setup.sh | 10 + njord_examples/sqlite_cli/Cargo.toml | 8 + njord_examples/sqlite_cli/generate.sh | 9 + .../down.sql | 1 + .../00000000000000_njord_initial_setup/up.sql | 5 + .../00000000000001_init_tables/down.sql | 0 .../00000000000001_init_tables/up.sql | 32 ++ njord_examples/sqlite_cli/njord.toml | 8 + njord_examples/sqlite_cli/rollback.sh | 9 + njord_examples/sqlite_cli/run.sh | 9 + njord_examples/sqlite_cli/setup.sh | 10 + njord_examples/sqlite_cli/src/main.rs | 3 + njord_examples/sqlite_cli/src/schema.rs | 26 ++ 40 files changed, 1267 insertions(+), 10 deletions(-) create mode 100644 njord_cli/Cargo.toml create mode 100644 njord_cli/src/command.rs create mode 100644 njord_cli/src/main.rs create mode 100644 njord_cli/src/migration.rs create mode 100644 njord_cli/src/util.rs create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/mariadb/down.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/mariadb/up.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/mssql/down.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/mssql/up.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/mysql/down.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/mysql/up.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/oracle/down.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/oracle/up.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/postgres/down.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/postgres/up.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/sqlite/down.sql create mode 100644 njord_cli/templates/migrations/00000000000000_njord_initial_setup/sqlite/up.sql create mode 100644 njord_cli/templates/njord.toml create mode 100755 njord_examples/generate.sh create mode 100644 njord_examples/migrations/00000000000000_njord_initial_setup/down.sql create mode 100644 njord_examples/migrations/00000000000000_njord_initial_setup/up.sql create mode 100644 njord_examples/migrations/00000000000001_init_tables/down.sql create mode 100644 njord_examples/migrations/00000000000001_init_tables/up.sql create mode 100755 njord_examples/rollback.sh create mode 100755 njord_examples/run.sh create mode 100755 njord_examples/setup.sh create mode 100644 njord_examples/sqlite_cli/Cargo.toml create mode 100755 njord_examples/sqlite_cli/generate.sh create mode 100644 njord_examples/sqlite_cli/migrations/00000000000000_njord_initial_setup/down.sql create mode 100644 njord_examples/sqlite_cli/migrations/00000000000000_njord_initial_setup/up.sql create mode 100644 njord_examples/sqlite_cli/migrations/00000000000001_init_tables/down.sql create mode 100644 njord_examples/sqlite_cli/migrations/00000000000001_init_tables/up.sql create mode 100644 njord_examples/sqlite_cli/njord.toml create mode 100755 njord_examples/sqlite_cli/rollback.sh create mode 100755 njord_examples/sqlite_cli/run.sh create mode 100755 njord_examples/sqlite_cli/setup.sh create mode 100644 njord_examples/sqlite_cli/src/main.rs create mode 100644 njord_examples/sqlite_cli/src/schema.rs diff --git a/Cargo.lock b/Cargo.lock index f41f1924..859fafb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -333,7 +382,9 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -348,6 +399,46 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "clap_lex" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + [[package]] name = "cmake" version = "0.1.51" @@ -357,6 +448,12 @@ dependencies = [ "cc", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "connection-string" version = "0.2.0" @@ -605,7 +702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -893,6 +990,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1076,6 +1179,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.10" @@ -1232,7 +1341,7 @@ dependencies = [ "hermit-abi", "libc", "wasi", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1291,7 +1400,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afe0450cc9344afff34915f8328600ab5ae19260802a334d0f72d2d5bdda3bfe" dependencies = [ "darling 0.20.10", - "heck", + "heck 0.4.1", "num-bigint", "proc-macro-crate", "proc-macro-error", @@ -1384,6 +1493,20 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "njord_cli" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "njord", + "njord_derive", + "rusqlite", + "serde", + "serde_derive", + "toml", +] + [[package]] name = "njord_derive" version = "0.4.0" @@ -1852,7 +1975,7 @@ dependencies = [ "libc", "spin", "untrusted", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1936,7 +2059,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2046,7 +2169,7 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2126,6 +2249,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2203,7 +2335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2224,6 +2356,14 @@ dependencies = [ "tokio", ] +[[package]] +name = "sqlite_cli" +version = "0.1.0" +dependencies = [ + "njord", + "njord_derive", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2338,7 +2478,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2461,7 +2601,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2520,11 +2660,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -2533,6 +2688,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -2658,6 +2815,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.10.0" @@ -2789,7 +2952,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2846,6 +3009,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 24c69247..87dbdc9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,12 @@ members = [ "njord", "njord_derive", + "njord_cli", "njord_examples/sqlite", "njord_examples/mysql", "njord_examples/oracle", "njord_examples/mssql", + "njord_examples/sqlite_cli" ] resolver = "2" diff --git a/njord_cli/Cargo.toml b/njord_cli/Cargo.toml new file mode 100644 index 00000000..ea09b8fd --- /dev/null +++ b/njord_cli/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "njord_cli" +version = "0.1.0" +edition = "2021" +authors = ["Marcus Cvjeticanin "] +description = "Provides the CLI for Njord." +license = "BSD-3-Clause" +documentation = "https://docs.rs/njord/latest/njord/" +repository = "https://github.com/njord-rs/njord" +readme = "../README.md" +rust-version = "1.81.0" + +[[bin]] +name = "njord" +path = "src/main.rs" +doc = false + +[features] +default = [ + "sqlite", +] # to disable this the user needs to run with the flag --no-default-features +sqlite = [] + +[dependencies] +njord = { version = "0.4.0", path = "../njord" } +njord_derive = { version = "0.4.0", path = "../njord_derive" } +rusqlite = { version = "0.32.1", features = ["bundled"] } +clap = { version = "4.5.3", features = ["cargo", "derive"] } +toml = "0.8.12" +serde = { version = "1.0", features = ["derive"] } +serde_derive = "1.0" +chrono = "0.4" diff --git a/njord_cli/src/command.rs b/njord_cli/src/command.rs new file mode 100644 index 00000000..da559c1e --- /dev/null +++ b/njord_cli/src/command.rs @@ -0,0 +1,183 @@ +use clap::ArgMatches; +use std::fs; +use std::path::Path; + +use crate::migration::{generate, rollback, run}; + +/// Initializes Njord with an empty migrations directory and a `njord.toml` config file. +/// +/// This function is responsible for setting up the initial configuration for Njord, a migration +/// tool. It does not require any command-line arguments and initializes Njord with an empty +/// migrations directory and a `njord.toml` configuration file. This allows users to start fresh +/// with an initial setup for managing database migrations. +/// +/// # Panics +/// +/// This function does not panic. +/// +/// # Notes +/// +/// - The migrations directory will be empty initially. +/// - A `njord.toml` configuration file will be created with default settings. +pub fn handle_setup() { + println!("Setting up Njord with an empty migrations directory and a njord.toml config file..."); + + // include content of njord.toml template + let toml_content = include_str!("../templates/njord.toml"); + + // include the content of up.sql and down.sql templates + #[cfg(feature = "sqlite")] + let (up_sql_content, down_sql_content) = ( + include_str!("../templates/migrations/00000000000000_njord_initial_setup/sqlite/up.sql"), + include_str!("../templates/migrations/00000000000000_njord_initial_setup/sqlite/down.sql"), + ); + + #[cfg(feature = "postgres")] + let (up_sql_content, down_sql_content) = ( + include_str!("../templates/migrations/00000000000000_njord_initial_setup/postgres/up.sql"), + include_str!( + "../templates/migrations/00000000000000_njord_initial_setup/postgres/down.sql" + ), + ); + + #[cfg(feature = "mysql")] + let (up_sql_content, down_sql_content) = ( + include_str!("../templates/migrations/00000000000000_njord_initial_setup/mysql/up.sql"), + include_str!("../templates/migrations/00000000000000_njord_initial_setup/mysql/down.sql"), + ); + + #[cfg(feature = "mariadb")] + let (up_sql_content, down_sql_content) = ( + include_str!("../templates/migrations/00000000000000_njord_initial_setup/mariadb/up.sql"), + include_str!("../templates/migrations/00000000000000_njord_initial_setup/mariadb/down.sql"), + ); + + #[cfg(feature = "oracle")] + let (up_sql_content, down_sql_content) = ( + include_str!("../templates/migrations/00000000000000_njord_initial_setup/oracle/up.sql"), + include_str!("../templates/migrations/00000000000000_njord_initial_setup/oracle/down.sql"), + ); + + #[cfg(feature = "mssql")] + let (up_sql_content, down_sql_content) = ( + include_str!("../templates/migrations/00000000000000_njord_initial_setup/mssql/up.sql"), + include_str!("../templates/migrations/00000000000000_njord_initial_setup/mssql/down.sql"), + ); + + // determine the current dir where njord is running from + if let Ok(current_dir) = std::env::current_dir() { + let destination_path = current_dir.join("njord.toml"); + + if !destination_path.exists() { + if let Err(err) = fs::write(&destination_path, toml_content) { + eprintln!("Error writing njord.toml: {}", err) + } else { + println!("njord.toml successfully copied to the current directory.") + } + } else { + println!("njord.toml already exists in the current directory. Skipping copy.") + } + + // get the migrations path + let migrations_path = current_dir.join("migrations/00000000000000_njord_initial_setup"); + + // check if the migration files already exist + if !migrations_path.exists() { + if let Err(err) = fs::create_dir_all(&migrations_path) { + eprintln!("Error creating migrations directory: {}", err); + return; + } + + write_migration_file(&migrations_path, "up.sql", up_sql_content); + write_migration_file(&migrations_path, "down.sql", down_sql_content); + } else { + println!("Initial migration files already exist. Skipping creation."); + } + } else { + eprintln!("Error determining the current directory.") + } +} + +/// Writes content to a migration file in the specified directory. +/// +/// Given a `Path` representing the directory where migration files are stored, a `file_name` for +/// the migration file, and the `content` to be written to the file, this function constructs the +/// full path for the file and writes the content to it. +/// +/// # Arguments +/// +/// * `migrations_path` - A reference to a `Path` representing the directory for migration files. +/// * `file_name` - A string slice representing the name of the migration file. +/// * `content` - A string slice containing the content to be written to the migration file. +/// +/// # Errors +/// +/// If there is an error writing to the file, an error message is printed to standard error +/// using `eprintln!`. The error message includes the file name and the specific error details. +/// +/// If the write operation is successful, a success message is printed to standard output using +/// `println!`. The success message includes the file name. +/// +/// # Panics +/// +/// This function does not panic. +fn write_migration_file(migrations_path: &Path, file_name: &str, content: &str) { + let file_path = migrations_path.join(file_name); + + if let Err(err) = fs::write(&file_path, content) { + eprintln!("Error writing {}: {}", file_name, err); + } else { + println!("{} successfully created.", file_name); + } +} + +/// Handles the "migration" subcommand based on the provided `ArgMatches`. +/// +/// # Arguments +/// +/// * `sub_matches` - The `ArgMatches` object containing subcommand-specific matches. +pub fn handle_migration_subcommand(sub_matches: &ArgMatches) { + match sub_matches.subcommand() { + Some(("generate", generate_matches)) => { + let name = generate_matches.get_one::("name"); + let env = generate_matches.get_one::("env"); + let dry_run = generate_matches.get_one::("dry-run"); + + generate(name, env, dry_run) + } + Some(("run", run_matches)) => { + let env = run_matches.get_one::("env"); + let log_level = run_matches.get_one::("log-level"); + + run(env, log_level) + } + Some(("rollback", rollback_matches)) => { + let env = rollback_matches.get_one::("env"); + let to = rollback_matches.get_one::("to"); + let log_level = rollback_matches.get_one::("log-level"); + + rollback(env, to, log_level) + } + _ => { + eprintln!("Invalid subcommand for 'migration'. Use 'njord migration --help' for usage information."); + std::process::exit(1); + } + } +} + +/// Handles the top-level command based on the provided command name and `ArgMatches`. +/// +/// # Arguments +/// +/// * `cmd` - The name of the command. +/// * `sub_matches` - The `ArgMatches` object containing command-specific matches. +pub fn handle_command(cmd: &str, sub_matches: &ArgMatches) { + match cmd { + "migration" => handle_migration_subcommand(sub_matches), + "setup" => handle_setup(), + _ => { + eprintln!("Invalid command. Use 'njord --help' for usage information."); + std::process::exit(1); + } + } +} diff --git a/njord_cli/src/main.rs b/njord_cli/src/main.rs new file mode 100644 index 00000000..01757af7 --- /dev/null +++ b/njord_cli/src/main.rs @@ -0,0 +1,75 @@ +mod migration; +mod command; +mod util; +use clap::Arg; +use command::handle_command; + +fn main() { + let cmd = clap::Command::new("njord") + .bin_name("njord") + .author("Marcus Cvjeticanin. ") + .about("Njord CLI ⛵ for handling migration changes.") + .subcommand_required(true) + .subcommand(clap::command!("setup").about("Initializes Njord with an empty migrations directory and a njord.toml config file.")) + .subcommand( + clap::command!("migration") + .subcommand( + clap::command!("generate") + .about("Generates a new migration file with the specified name.") + + .arg(Arg::new("name") + .help("Specifies the name of the schema change.") + .value_name("name")) + + .arg(Arg::new("env") + .help("Specifies the environment (e.g., development, test, staging, production).") + .value_name("env")) + + .arg(Arg::new("log-level") + .help("Sets the logging level (e.g., standard, debug).") + .value_name("log-level")) + + .arg(Arg::new("dry-run") + .help("Simulates the migration without applying changes.")) + + .arg(Arg::new("dir") + .help("Specifies the target directory for generated migration changes.") + .value_name("path")) + ) + .subcommand( + clap::command!("run") + .about("Applies all pending migrations to the database.") + + .arg(Arg::new("env") + .help("Target a specific environment.")) + + .arg(Arg::new("log-level") + .help("Sets the logging level (e.g., standard, debug).") + .value_name("log-level")), + ) + .subcommand( + clap::command!("rollback") + .about("Rolls back the last applied migration or to a specific version.") + + .arg(Arg::new("to") + .long("to") + .help("Sets a previous migration change to rollback to (e.g. --to=20231204120000") + .value_name("change")) + + .arg(Arg::new("env") + .help("Target a specific environment.")) + + .arg(Arg::new("log-level") + .help("Sets the logging level (e.g., standard, debug).") + .value_name("log-level")), + ) + ) + .get_matches(); + + if let Some((cmd, sub_matches)) = cmd.subcommand() { + handle_command(cmd, sub_matches); + } else { + eprintln!("Invalid command. Use 'njord --help' for usage information."); + std::process::exit(1); + } +} diff --git a/njord_cli/src/migration.rs b/njord_cli/src/migration.rs new file mode 100644 index 00000000..fbe84e99 --- /dev/null +++ b/njord_cli/src/migration.rs @@ -0,0 +1,287 @@ +use std::{fs, path::Path}; + +use njord::sqlite; +use rusqlite::{Connection, Error, ErrorCode}; + +use crate::util::{create_migration_files, get_local_migration_versions, get_migrations_directory_path, get_next_migration_version, MigrationHistory, read_config, version_not_in_database}; + +/// Generates migration files with the specified name, environment, and dry-run option. +/// +/// # Arguments +/// +/// * `name` - Optional parameter representing the name of the migration file. +/// * `env` - Optional parameter specifying the environment (e.g., development, test, staging, production). +/// * `dry_run` - Optional parameter indicating whether to simulate the migration without applying changes. +/// +/// # Example +/// +/// ```rust +/// generate(Some("example_name"), Some("development"), Some("true")); +/// ``` +pub fn generate(name: Option<&String>, env: Option<&String>, dry_run: Option<&String>) { + if let Ok(config) = read_config() { + if let Some(migrations_dir) = get_migrations_directory_path(&config) { + // get the next migration version based on existing ones + if let Ok(next_version) = get_next_migration_version(&migrations_dir) { + let migration_name = name.map(|s| s.as_str()).unwrap_or("example_name"); + + // create migration files + if let Err(err) = create_migration_files(&migrations_dir, &next_version, migration_name) + { + eprintln!("Error creating migration files: {}", err); + return; + } + + println!("Migration files generated successfully:"); + println!("Version: {}", next_version); + println!("Name: {}", migration_name); + println!("Environment: {:?}", env); + println!("Dry-run: {:?}", dry_run); + } else { + eprintln!("Error determining next migration version."); + } + } else { + eprintln!("Error determining migrations directory."); + } + } else { + eprintln!("Error reading configuration file."); + } +} + +/// Runs migration files with the specified environment and log level. +/// +/// # Arguments +/// +/// * `env` - Optional parameter specifying the target environment for applying migrations. +/// * `log_level` - Optional parameter setting the logging level (e.g., standard, debug). +/// +/// # Example +/// +/// ```rust +/// run(Some("production"), Some("debug")); +/// ``` +pub fn run(env: Option<&String>, log_level: Option<&String>) { + let db_relative_path = "./sqlite.db"; + let db_path = Path::new(&db_relative_path); + let conn = sqlite::open(db_path); + + if let Ok(config) = read_config() { + if let Some(migrations_dir) = get_migrations_directory_path(&config) { + + match conn { + Ok(conn) => { + println!("Database connection established successfully."); + + // obtain the latest version from the "migration_history" table + if let Ok(latest_db_version) = get_latest_migration_version(&conn) { + println!("latest_db_version: {}", latest_db_version); + + // get all local migration changes + let local_versions = match get_local_migration_versions(&migrations_dir) { + Ok(versions) => versions, + Err(err) => { + eprintln!("Error retrieving local migration versions: {}", err); + return; + } + }; + + for value in &local_versions { + println!("Value: {}", value); + } + + // go over each migration schema version changes if they should be executed + // TODO: need to check the ordering here + // for example it can be run with 00000000000001_init_tables first and + // then run 00000000000000_njord_initial_setup which is not incremental + for local_version in &local_versions { + match version_not_in_database(&conn, &local_version) { + Ok(_) => { + println!("Migration {} not found in database. Executing migration...", local_version); + + let migrations_dir = format!("migrations/{}", local_version); + println!("migrations_dir: {}", migrations_dir); + + let db_relative_path = "./sqlite.db"; + let db_path = Path::new(&db_relative_path); + let conn = sqlite::open(db_path); + + match conn { + Ok(c) => execute_pending_migration(c, &migrations_dir, &local_version).unwrap(), + Err(_) => {} + } + } + Err(_) => {} + } + } + } else { + eprintln!("Error obtaining latest migration version."); + } + } + Err(err) => eprintln!("Error establishing database connection: {}", err), + }; + + println!( + "Running migration with env '{:?}' and log-level '{:?}'", + env, log_level + ); + } + } +} + +/// Rolls back migration changes to a specific version, with optional environment and log level. +/// +/// # Arguments +/// +/// * `env` - Optional parameter specifying the target environment for rolling back migrations. +/// * `to` - Optional parameter setting a previous migration change to rollback to. +/// * `log_level` - Optional parameter setting the logging level (e.g., standard, debug). +/// +/// # Example +/// +/// ```rust +/// rollback(Some("development"), Some("20231204120000"), Some("info")); +/// ``` +pub fn rollback(env: Option<&String>, to: Option<&String>, log_level: Option<&String>) { + if let Some(target_version) = to { + let db_relative_path = "./sqlite.db"; + let db_path = Path::new(&db_relative_path); + let conn = sqlite::open(db_path); + + match conn { + Ok(conn) => { + println!("Database connection established successfully."); + + // construct paths to migration directories + let migrations_dir = format!("migrations/{}", target_version); + + // execute down.sql for the specified version + if let Err(down_err) = execute_sql_from_file(&conn, &migrations_dir, "down.sql") { + eprintln!("Error executing down.sql: {}", down_err); + } else { + println!("down.sql executed successfully."); + } + } + Err(err) => eprintln!("Error establishing database connection: {}", err), + }; + + println!( + "Rolling back migration with env '{:?}' to '{:?}' log_level '{:?}'", + env, target_version, log_level + ); + } else { + eprintln!("Error: Please provide a target version to rollback to."); + } +} + +/// Retrieves the latest migration version from the "migration_history" table. +/// +/// # Arguments +/// +/// * `conn` - A reference to a `rusqlite::Connection`. +/// +/// # Returns +/// +/// A `Result` containing the latest migration version as a `String` or a `rusqlite::Error` if an error occurs. +/// +/// # Note +/// +/// This function queries the "migration_history" table to obtain the latest version. +fn get_latest_migration_version(conn: &Connection) -> Result { + let query = "SELECT version FROM migration_history ORDER BY version DESC LIMIT 1;"; + let result: Result = conn.query_row(query, [], |row| row.get(0)); + + match result { + Ok(version) => Ok(version), + Err(err) => { + match err { + Error::SqliteFailure(error, _) if error.code == ErrorCode::Unknown => { + Ok("00000000000000_njord_initial_setup".to_string()) + } + _ => { + eprintln!("Error getting latest migration version: {}", err); + Err(err) + } + } + } + } +} + +/// Executes SQL content from a file on the provided database connection. +/// +/// # Arguments +/// +/// * `conn` - A reference to a `rusqlite::Connection`. +/// * `migrations_dir` - A string slice representing the path to the migrations directory. +/// * `file_name` - A string slice representing the name of the SQL file to execute. +/// +/// # Returns +/// +/// A `Result` indicating success (`Ok(())`) or a `rusqlite::Error` if an error occurs. +/// +/// # Note +/// +/// This function reads the SQL content from the specified file and executes it on the provided database connection. +fn execute_sql_from_file( + conn: &Connection, + migrations_dir: &str, + file_name: &str, +) -> Result<(), Error> { + let file_path = Path::new(migrations_dir).join(file_name); + + let sql_content = match fs::read_to_string(&file_path) { + Ok(content) => content, + Err(err) => { + eprintln!("Error reading {}: {}", file_path.display(), err); + return Err(Error::QueryReturnedNoRows); // placeholder error + } + }; + + match conn.execute_batch(&sql_content) { + Ok(_) => { + println!("{} executed successfully.", file_path.display()); + Ok(()) + } + Err(err) => { + eprintln!("Error executing {}: {}", file_path.display(), err); + Err(err) + } + } +} + +/// Executes migration changes in directories that have not been applied yet. +/// +/// # Arguments +/// +/// * `conn` - A reference to a `rusqlite::Connection`. +/// * `migrations_dir` - A string slice representing the path to the migrations directory. +/// * `next_version` - A string representing the next migration version. +/// +/// # Errors +/// +/// Returns a `rusqlite::Error` if there is an issue executing the SQL scripts or inserting into the database. +fn execute_pending_migration( + conn: Connection, + migrations_dir: &str, + next_version: &str, +) -> Result<(), Error> { + match execute_sql_from_file(&conn, &migrations_dir, "up.sql") { + Ok(_) => { + println!("up.sql executed successfully."); + // insert new row with the version into the database + let row = MigrationHistory { version: next_version.to_string() }; + sqlite::insert(&conn, vec![row])?; + } + Err(up_err) => { + eprintln!("Error executing up.sql: {}", up_err); + + // TODO: we need to make sure that this is only run on error when it's not "table already exists" kind of errors + if let Err(down_err) = execute_sql_from_file(&conn, &migrations_dir, "down.sql") { + eprintln!("Error executing down.sql: {}", down_err); + } else { + println!("down.sql executed successfully."); + } + } + } + Ok(()) +} + diff --git a/njord_cli/src/util.rs b/njord_cli/src/util.rs new file mode 100644 index 00000000..766b1fab --- /dev/null +++ b/njord_cli/src/util.rs @@ -0,0 +1,255 @@ +use core::fmt; +use serde::Deserialize; +use std::error::Error as StdError; +use std::path::{Path, PathBuf}; +use std::{env, fs}; +use std::collections::HashSet; +use rusqlite::{Connection, Error}; +use toml::Value as TomlConfig; +use njord_derive::Table; +use njord::table::Table; + +#[derive(Debug)] +pub enum ConfigError { + Io(std::io::Error), + Toml(toml::de::Error), +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +pub struct Config { + migrations_directory: Option, + schema_file: Option, +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +pub struct MigrationsDirectory { + dir: String, +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +pub struct SchemaFile { + file: String, +} + +#[derive(Table)] +#[table_name = "migration_history"] +pub struct MigrationHistory { + pub version: String, +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConfigError::Io(err) => write!(f, "IO error: {}", err), + ConfigError::Toml(err) => write!(f, "TOML error: {}", err), + } + } +} + +impl StdError for ConfigError {} + +impl From for ConfigError { + fn from(err: std::io::Error) -> Self { + ConfigError::Io(err) + } +} + +impl From for ConfigError { + fn from(err: toml::de::Error) -> Self { + ConfigError::Toml(err) + } +} + +/// Reads the configuration from the file. +/// +/// This function reads the content of the `njord.toml` file located in the root +/// of the repository, parses it into a `Config` struct, and returns the result. +/// If any error occurs during the file reading or parsing, it returns a +/// `ConfigError`. +/// +/// # Errors +/// +/// Returns a `ConfigError` if there is an issue with reading the file or +/// parsing its content. +/// +pub fn read_config() -> Result { + let current_dir = env::current_dir()?; + + // construct the path to njord.toml in the root of the repository + let config_path = current_dir.join("njord.toml"); + + // read the content of njord.toml + let config_content = match fs::read_to_string(&config_path) { + Ok(content) => content, + Err(err) => { + eprintln!("Error reading config.toml: {}", err); + return Err(err.into()); + } + }; + + // parse the content + let config: TomlConfig = match toml::from_str(&config_content) { + Ok(value) => value, + Err(err) => { + eprintln!("Error parsing config.toml: {}", err); + return Err(err.into()); + } + }; + + Ok(config) +} + +/// Gets the next migration version based on the existing ones in the migrations directory. +/// +/// This function reads the existing migration versions from the specified +/// `migrations_dir`, determines the maximum version, increments it, and +/// returns the next migration version as a string. +/// +/// # Arguments +/// +/// * `migrations_dir` - The path to the directory containing existing +/// migration versions. +/// +/// # Errors +/// +/// Returns a `std::io::Error` if there is an issue reading the existing +/// migration versions. +/// +pub fn get_next_migration_version(migrations_dir: &Path) -> Result { + let entries = fs::read_dir(migrations_dir)?; + let versions: Vec = entries + .filter_map(|entry| { + entry.ok() + .and_then(|e| e.file_name().to_str().map(String::from)) + }) + .filter_map(|version| { + version.split('_').next().unwrap_or_default().parse().ok() + }) + .collect(); + + let max_version = versions.into_iter().max(); + + match max_version { + Some(max_value) => { + let next_version = max_value + 1; + Ok(format!("{:014}", next_version)) + } + None => Ok("00000000000001_unknown".to_string()), // default version + } +} + +/// Retrieves local migration versions from the specified directory. +/// +/// This function reads the names of files in the specified `migrations_dir` +/// directory, filters out non-UTF-8 filenames and collects the valid filenames +/// into a `HashSet`. +/// +/// # Arguments +/// +/// * `migrations_dir` - The path to the directory containing migration versions. +/// +/// # Returns +/// +/// A `Result` containing a `HashSet` of local migration versions if +/// successful, or an `std::io::Error` if there is an issue reading the directory. +pub fn get_local_migration_versions(migrations_dir: &Path) -> Result, std::io::Error> { + let entries = fs::read_dir(migrations_dir)?; + let local_versions: HashSet = entries + .filter_map(|entry| { + entry.ok() + .and_then(|e| e.file_name().to_str().map(String::from)) + }) + .collect(); + + Ok(local_versions) +} + +/// Checks if a specific version exists in the migration history table. +/// +/// # Arguments +/// +/// * `conn` - A reference to a `rusqlite::Connection`. +/// * `version` - A string slice representing the version to check for. +/// +/// # Returns +/// +/// `true` if the version exists, `false` otherwise. +/// +/// # Errors +/// +/// Returns a `rusqlite::Error` if there is an issue with the database query. +pub fn version_not_in_database(conn: &Connection, version: &str) -> Result { + let query = "SELECT EXISTS(SELECT 1 FROM migration_history WHERE version = ?)"; + let result: Result = conn.query_row(query, &[&version], |row| row.get(0)); + + match result { + Ok(1) => Ok(false), + Ok(0) => Ok(true), + Err(err) => Err(err), + _ => Ok(false), + } +} + +/// Creates migration files in the specified directory. +/// +/// This function creates migration files in the specified directory based on +/// the provided `version` and `name`. It creates two files: `up.sql` and +/// `down.sql` within the migration directory. +/// +/// # Arguments +/// +/// * `migrations_dir` - The path to the directory where migration files will be created. +/// * `version` - The version of the migration. +/// * `name` - The name of the migration. +/// +/// # Errors +/// +/// Returns a `std::io::Error` if there is an issue creating the migration files. +/// +pub fn create_migration_files( + migrations_dir: &Path, + version: &str, + name: &str, +) -> Result<(), std::io::Error> { + let mut dir_path = PathBuf::from(migrations_dir); + dir_path.push(format!("{}_{}", version, name)); + + println!("{:?}", dir_path); + + fs::create_dir_all(&dir_path)?; + + let up_sql_path = dir_path.join("up.sql"); + let down_sql_path = dir_path.join("down.sql"); + + fs::File::create(up_sql_path)?; + fs::File::create(down_sql_path)?; + + Ok(()) +} + +/// Retrieves the path to the migrations directory from the configuration. +/// +/// This function extracts the path to the migrations directory from the provided `config`. +/// It expects the configuration to be in TOML format, and it assumes a specific structure +/// where the migrations directory is nested within the `migrations_directory` key. +/// +/// # Arguments +/// +/// * `config` - A reference to a `toml::Value` representing the configuration. +/// +/// # Returns +/// +/// An `Option` containing the path to the migrations directory if found in the configuration, +/// or `None` if the migrations directory is not present or cannot be extracted. +pub fn get_migrations_directory_path(config: &TomlConfig) -> Option { + let migrations_dir = config + .get("migrations_directory") // Adjust this based on the actual structure of your toml::Value + .and_then(|value| value.get("dir")) + .and_then(|dir| dir.as_str()) + .map(PathBuf::from); + + migrations_dir +} diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mariadb/down.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mariadb/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mariadb/up.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mariadb/up.sql new file mode 100644 index 00000000..e69de29b diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mssql/down.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mssql/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mssql/up.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mssql/up.sql new file mode 100644 index 00000000..e69de29b diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mysql/down.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mysql/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mysql/up.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/mysql/up.sql new file mode 100644 index 00000000..e69de29b diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/oracle/down.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/oracle/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/oracle/up.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/oracle/up.sql new file mode 100644 index 00000000..e69de29b diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/postgres/down.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/postgres/down.sql new file mode 100644 index 00000000..c371940d --- /dev/null +++ b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/postgres/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Njord to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS njord_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS njord_set_updated_at(); \ No newline at end of file diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/postgres/up.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/postgres/up.sql new file mode 100644 index 00000000..2af41f0c --- /dev/null +++ b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/postgres/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Njord to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT njord_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION njord_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE njord_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION njord_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/sqlite/down.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/sqlite/down.sql new file mode 100644 index 00000000..fe22bae1 --- /dev/null +++ b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/sqlite/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS migration_history; \ No newline at end of file diff --git a/njord_cli/templates/migrations/00000000000000_njord_initial_setup/sqlite/up.sql b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/sqlite/up.sql new file mode 100644 index 00000000..53d5fbba --- /dev/null +++ b/njord_cli/templates/migrations/00000000000000_njord_initial_setup/sqlite/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS migration_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version TEXT NOT NULL UNIQUE, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/njord_cli/templates/njord.toml b/njord_cli/templates/njord.toml new file mode 100644 index 00000000..1b8787d3 --- /dev/null +++ b/njord_cli/templates/njord.toml @@ -0,0 +1,8 @@ +# For documentation on how to configure this file, +# see https://njord.rs + +[schema] +file = "src/schema.rs" + +[migrations_directory] +dir = "migrations" diff --git a/njord_examples/generate.sh b/njord_examples/generate.sh new file mode 100755 index 00000000..6480f651 --- /dev/null +++ b/njord_examples/generate.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# ----------------------------------------------------------------------------- +# Script: generate.sh +# Description: Generate new migration files application with SQLite support. +# Author: Marcus Cvjeticanin +# Date: December 15, 2023 +# ----------------------------------------------------------------------------- +# cargo run --manifest-path=../../njord_cli/Cargo.toml -- migration generate -- --name=init_tables -- --env=development --dry-run # TODO: --dry-run doesnt work! +cargo run --manifest-path=../../njord_cli/Cargo.toml -- migration generate -- init_tables -- --env=development diff --git a/njord_examples/migrations/00000000000000_njord_initial_setup/down.sql b/njord_examples/migrations/00000000000000_njord_initial_setup/down.sql new file mode 100644 index 00000000..fe22bae1 --- /dev/null +++ b/njord_examples/migrations/00000000000000_njord_initial_setup/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS migration_history; \ No newline at end of file diff --git a/njord_examples/migrations/00000000000000_njord_initial_setup/up.sql b/njord_examples/migrations/00000000000000_njord_initial_setup/up.sql new file mode 100644 index 00000000..53d5fbba --- /dev/null +++ b/njord_examples/migrations/00000000000000_njord_initial_setup/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS migration_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version TEXT NOT NULL UNIQUE, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/njord_examples/migrations/00000000000001_init_tables/down.sql b/njord_examples/migrations/00000000000001_init_tables/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/njord_examples/migrations/00000000000001_init_tables/up.sql b/njord_examples/migrations/00000000000001_init_tables/up.sql new file mode 100644 index 00000000..b9f82c00 --- /dev/null +++ b/njord_examples/migrations/00000000000001_init_tables/up.sql @@ -0,0 +1,32 @@ +-- users table +CREATE TABLE users ( + user_id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + email TEXT NOT NULL, + address TEXT NOT NULL +); + +-- products table +CREATE TABLE products ( + product_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + description TEXT NOT NULL, + price REAL NOT NULL, + stock_quantity INTEGER NOT NULL, + category TEXT NOT NULL +); + +-- orders table +CREATE TABLE orders ( + order_id INTEGER PRIMARY KEY, + user_id INTEGER REFERENCES users(user_id), + total_cost REAL NOT NULL, + order_date TEXT NOT NULL +); + +-- order_products table +CREATE TABLE order_products ( + order_id INTEGER REFERENCES orders(order_id), + product_id INTEGER REFERENCES products(product_id), + PRIMARY KEY (order_id, product_id) +); \ No newline at end of file diff --git a/njord_examples/rollback.sh b/njord_examples/rollback.sh new file mode 100755 index 00000000..f32a5f24 --- /dev/null +++ b/njord_examples/rollback.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# ----------------------------------------------------------------------------- +# Script: rollback.sh +# Description: Rollback migration changes to specified change with SQLite support. +# Author: Marcus Cvjeticanin +# Date: December 15, 2023 +# ----------------------------------------------------------------------------- + +cargo run --manifest-path=../../njord_cli/Cargo.toml -- migration rollback --env=development --to=00000000000000 diff --git a/njord_examples/run.sh b/njord_examples/run.sh new file mode 100755 index 00000000..6f0fe518 --- /dev/null +++ b/njord_examples/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# ----------------------------------------------------------------------------- +# Script: run.sh +# Description: Run new migration schema changes with SQLite support. +# Author: Marcus Cvjeticanin +# Date: December 15, 2023 +# ----------------------------------------------------------------------------- + +cargo run --manifest-path=../../njord_cli/Cargo.toml -- migration run -- --env=development --log-level=debug diff --git a/njord_examples/setup.sh b/njord_examples/setup.sh new file mode 100755 index 00000000..5963a097 --- /dev/null +++ b/njord_examples/setup.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# ----------------------------------------------------------------------------- +# Script: setup.sh +# Description: Build and setup the njord project with SQLite support. +# Author: Marcus Cvjeticanin +# Date: December 15, 2023 +# ----------------------------------------------------------------------------- + +cargo build --manifest-path=../../njord_cli/Cargo.toml --no-default-features --features "sqlite" +cargo run --manifest-path=../../njord_cli/Cargo.toml -- setup diff --git a/njord_examples/sqlite_cli/Cargo.toml b/njord_examples/sqlite_cli/Cargo.toml new file mode 100644 index 00000000..7fa3a0f3 --- /dev/null +++ b/njord_examples/sqlite_cli/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "sqlite_cli" +version = "0.1.0" +edition = "2021" + +[dependencies] +njord = { version = "0.4.0", path = "../../njord" } +njord_derive = { version = "0.4.0", optional = true, path = "../../njord_derive" } diff --git a/njord_examples/sqlite_cli/generate.sh b/njord_examples/sqlite_cli/generate.sh new file mode 100755 index 00000000..6480f651 --- /dev/null +++ b/njord_examples/sqlite_cli/generate.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# ----------------------------------------------------------------------------- +# Script: generate.sh +# Description: Generate new migration files application with SQLite support. +# Author: Marcus Cvjeticanin +# Date: December 15, 2023 +# ----------------------------------------------------------------------------- +# cargo run --manifest-path=../../njord_cli/Cargo.toml -- migration generate -- --name=init_tables -- --env=development --dry-run # TODO: --dry-run doesnt work! +cargo run --manifest-path=../../njord_cli/Cargo.toml -- migration generate -- init_tables -- --env=development diff --git a/njord_examples/sqlite_cli/migrations/00000000000000_njord_initial_setup/down.sql b/njord_examples/sqlite_cli/migrations/00000000000000_njord_initial_setup/down.sql new file mode 100644 index 00000000..fe22bae1 --- /dev/null +++ b/njord_examples/sqlite_cli/migrations/00000000000000_njord_initial_setup/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS migration_history; \ No newline at end of file diff --git a/njord_examples/sqlite_cli/migrations/00000000000000_njord_initial_setup/up.sql b/njord_examples/sqlite_cli/migrations/00000000000000_njord_initial_setup/up.sql new file mode 100644 index 00000000..53d5fbba --- /dev/null +++ b/njord_examples/sqlite_cli/migrations/00000000000000_njord_initial_setup/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS migration_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version TEXT NOT NULL UNIQUE, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/njord_examples/sqlite_cli/migrations/00000000000001_init_tables/down.sql b/njord_examples/sqlite_cli/migrations/00000000000001_init_tables/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/njord_examples/sqlite_cli/migrations/00000000000001_init_tables/up.sql b/njord_examples/sqlite_cli/migrations/00000000000001_init_tables/up.sql new file mode 100644 index 00000000..b9f82c00 --- /dev/null +++ b/njord_examples/sqlite_cli/migrations/00000000000001_init_tables/up.sql @@ -0,0 +1,32 @@ +-- users table +CREATE TABLE users ( + user_id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + email TEXT NOT NULL, + address TEXT NOT NULL +); + +-- products table +CREATE TABLE products ( + product_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + description TEXT NOT NULL, + price REAL NOT NULL, + stock_quantity INTEGER NOT NULL, + category TEXT NOT NULL +); + +-- orders table +CREATE TABLE orders ( + order_id INTEGER PRIMARY KEY, + user_id INTEGER REFERENCES users(user_id), + total_cost REAL NOT NULL, + order_date TEXT NOT NULL +); + +-- order_products table +CREATE TABLE order_products ( + order_id INTEGER REFERENCES orders(order_id), + product_id INTEGER REFERENCES products(product_id), + PRIMARY KEY (order_id, product_id) +); \ No newline at end of file diff --git a/njord_examples/sqlite_cli/njord.toml b/njord_examples/sqlite_cli/njord.toml new file mode 100644 index 00000000..1b8787d3 --- /dev/null +++ b/njord_examples/sqlite_cli/njord.toml @@ -0,0 +1,8 @@ +# For documentation on how to configure this file, +# see https://njord.rs + +[schema] +file = "src/schema.rs" + +[migrations_directory] +dir = "migrations" diff --git a/njord_examples/sqlite_cli/rollback.sh b/njord_examples/sqlite_cli/rollback.sh new file mode 100755 index 00000000..f32a5f24 --- /dev/null +++ b/njord_examples/sqlite_cli/rollback.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# ----------------------------------------------------------------------------- +# Script: rollback.sh +# Description: Rollback migration changes to specified change with SQLite support. +# Author: Marcus Cvjeticanin +# Date: December 15, 2023 +# ----------------------------------------------------------------------------- + +cargo run --manifest-path=../../njord_cli/Cargo.toml -- migration rollback --env=development --to=00000000000000 diff --git a/njord_examples/sqlite_cli/run.sh b/njord_examples/sqlite_cli/run.sh new file mode 100755 index 00000000..6f0fe518 --- /dev/null +++ b/njord_examples/sqlite_cli/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# ----------------------------------------------------------------------------- +# Script: run.sh +# Description: Run new migration schema changes with SQLite support. +# Author: Marcus Cvjeticanin +# Date: December 15, 2023 +# ----------------------------------------------------------------------------- + +cargo run --manifest-path=../../njord_cli/Cargo.toml -- migration run -- --env=development --log-level=debug diff --git a/njord_examples/sqlite_cli/setup.sh b/njord_examples/sqlite_cli/setup.sh new file mode 100755 index 00000000..5963a097 --- /dev/null +++ b/njord_examples/sqlite_cli/setup.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# ----------------------------------------------------------------------------- +# Script: setup.sh +# Description: Build and setup the njord project with SQLite support. +# Author: Marcus Cvjeticanin +# Date: December 15, 2023 +# ----------------------------------------------------------------------------- + +cargo build --manifest-path=../../njord_cli/Cargo.toml --no-default-features --features "sqlite" +cargo run --manifest-path=../../njord_cli/Cargo.toml -- setup diff --git a/njord_examples/sqlite_cli/src/main.rs b/njord_examples/sqlite_cli/src/main.rs new file mode 100644 index 00000000..e7a11a96 --- /dev/null +++ b/njord_examples/sqlite_cli/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/njord_examples/sqlite_cli/src/schema.rs b/njord_examples/sqlite_cli/src/schema.rs new file mode 100644 index 00000000..0bea521f --- /dev/null +++ b/njord_examples/sqlite_cli/src/schema.rs @@ -0,0 +1,26 @@ +#[derive(Table, Default)] +pub struct User { + user_id: usize, + username: String, + email: String, + address: String, +} + +#[derive(Table, Default)] +pub struct Product { + product_id: usize, + name: String, + description: String, + price: f64, + stock_quantity: usize, + category: String, +} + +#[derive(Table, Default)] +pub struct Order { + order_id: usize, + user_id: usize, + products: Vec, + total_cost: f64, + order_date: NaiveDateTime, +}