From 9a4cc0bf41c316fcb82c7ebb4b045283d431ef9d Mon Sep 17 00:00:00 2001 From: "Dotan J. Nahum" Date: Sun, 15 Dec 2024 12:19:11 +0200 Subject: [PATCH 1/4] add: infer migration --- examples/demo/Cargo.lock | 283 ++++++------ loco-gen/Cargo.toml | 1 + loco-gen/src/infer.rs | 101 +++++ loco-gen/src/lib.rs | 25 +- loco-gen/src/migration.rs | 75 ++++ loco-gen/src/model.rs | 88 ++-- loco-gen/src/scaffold.rs | 2 +- .../src/templates/migration/add_columns.t | 59 +++ .../src/templates/migration/add_references.t | 83 ++++ .../migration/{migration.t => empty.t} | 0 loco-gen/src/templates/migration/join_table.t | 81 ++++ .../src/templates/migration/remove_columns.t | 59 +++ src/cli.rs | 24 +- src/schema.rs | 402 +----------------- 14 files changed, 657 insertions(+), 626 deletions(-) create mode 100644 loco-gen/src/infer.rs create mode 100644 loco-gen/src/migration.rs create mode 100644 loco-gen/src/templates/migration/add_columns.t create mode 100644 loco-gen/src/templates/migration/add_references.t rename loco-gen/src/templates/migration/{migration.t => empty.t} (100%) create mode 100644 loco-gen/src/templates/migration/join_table.t create mode 100644 loco-gen/src/templates/migration/remove_columns.t diff --git a/examples/demo/Cargo.lock b/examples/demo/Cargo.lock index 1c18600ef..6f4f3515c 100644 --- a/examples/demo/Cargo.lock +++ b/examples/demo/Cargo.lock @@ -559,7 +559,7 @@ dependencies = [ "rand", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "tower-layer", "tower-service", @@ -591,7 +591,7 @@ dependencies = [ "btparse-stable", "colored", "regex", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1141,6 +1141,16 @@ dependencies = [ "regex", ] +[[package]] +name = "cruet" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6132609543972496bc97b1e01f1ce6586768870aeb4cabeb3385f4e05b5caead" +dependencies = [ + "once_cell", + "regex", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1312,7 +1322,7 @@ dependencies = [ "console", "shell-words", "tempfile", - "thiserror", + "thiserror 1.0.69", "zeroize", ] @@ -1578,7 +1588,7 @@ dependencies = [ "fluent-syntax", "intl-memoizer", "intl_pluralrules", - "rustc-hash", + "rustc-hash 1.1.0", "self_cell 0.10.3", "smallvec", "unic-langid", @@ -1599,7 +1609,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" dependencies = [ - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1668,21 +1678,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2204,22 +2199,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", + "webpki-roots", ] [[package]] @@ -2716,13 +2696,14 @@ version = "0.13.2" dependencies = [ "chrono", "clap", + "cruet 0.14.0", "dialoguer", "duct", "regex", "rrgen", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "tracing", ] @@ -2772,7 +2753,7 @@ dependencies = [ "serde_yaml", "sqlx", "tera", - "thiserror", + "thiserror 1.0.69", "thousands", "tokio", "tokio-cron-scheduler", @@ -2915,7 +2896,7 @@ dependencies = [ "rustc_version", "smallvec", "tagptr", - "thiserror", + "thiserror 1.0.69", "triomphe", "uuid", ] @@ -2937,23 +2918,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -3112,50 +3076,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl" -version = "0.10.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "ordered-float" version = "3.9.2" @@ -3293,7 +3213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.69", "ucd-trie", ] @@ -3637,6 +3557,58 @@ dependencies = [ "winapi", ] +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.0", + "rustls", + "socket2", + "thiserror 2.0.4", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom", + "rand", + "ring", + "rustc-hash 2.1.0", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.4", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -3754,7 +3726,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3833,29 +3805,31 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", "system-configuration", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "windows-registry", ] @@ -3866,7 +3840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9838134a2bfaa8e1f40738fcc972ac799de6e0e06b5157acb95fc2b05a0ea283" dependencies = [ "lazy_static", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3919,7 +3893,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e40013551787f9f535e7dbc8dafc164591d941aeae48881a385d8b0393dd45f6" dependencies = [ - "cruet", + "cruet 0.13.3", "fs-err", "glob", "heck 0.4.1", @@ -3929,7 +3903,7 @@ dependencies = [ "serde_regex", "serde_yaml", "tera", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3995,7 +3969,7 @@ dependencies = [ "mime", "mime_guess", "rand", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -4026,6 +4000,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + [[package]] name = "rustc_version" version = "0.4.1" @@ -4077,6 +4057,9 @@ name = "rustls-pki-types" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -4116,7 +4099,7 @@ dependencies = [ "serial_test", "sha2", "slog-term", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-util", "tracing", @@ -4147,15 +4130,6 @@ dependencies = [ "sdd", ] -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -4218,7 +4192,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "url", @@ -4317,7 +4291,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.87", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -4349,29 +4323,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "selectors" version = "0.26.0" @@ -4639,7 +4590,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -4859,7 +4810,7 @@ dependencies = [ "sha2", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.69", "time", "tokio", "tokio-stream", @@ -4948,7 +4899,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "uuid", @@ -4992,7 +4943,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "uuid", @@ -5249,7 +5200,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490" +dependencies = [ + "thiserror-impl 2.0.4", ] [[package]] @@ -5263,6 +5223,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "thiserror-impl" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "thousands" version = "0.2.0" @@ -5379,16 +5350,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.0" @@ -5543,7 +5504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "thiserror", + "thiserror 1.0.69", "time", "tracing-subscriber", ] @@ -5645,7 +5606,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" dependencies = [ - "rustc-hash", + "rustc-hash 1.1.0", ] [[package]] diff --git a/loco-gen/Cargo.toml b/loco-gen/Cargo.toml index d59fe5681..56b7583e7 100644 --- a/loco-gen/Cargo.toml +++ b/loco-gen/Cargo.toml @@ -14,6 +14,7 @@ path = "src/lib.rs" [dependencies] +cruet = "0.14.0" rrgen = "0.5.3" serde = { workspace = true } serde_json = { workspace = true } diff --git a/loco-gen/src/infer.rs b/loco-gen/src/infer.rs new file mode 100644 index 000000000..128e926f3 --- /dev/null +++ b/loco-gen/src/infer.rs @@ -0,0 +1,101 @@ +use cruet::{case::snake::to_snake_case, Inflector}; // For pluralization and singularization + +#[derive(Debug, PartialEq, Eq)] +pub enum MigrationType { + CreateTable { table: String }, + AddColumns { table: String }, + RemoveColumns { table: String }, + AddReference { table: String }, + CreateJoinTable { table_a: String, table_b: String }, + Empty, +} + +pub fn guess_migration_type(migration_name: &str) -> MigrationType { + let normalized_name = to_snake_case(migration_name); + let parts: Vec<&str> = normalized_name.split('_').collect(); + + match parts.as_slice() { + ["create", table_name] => MigrationType::CreateTable { + table: table_name.to_plural(), + }, + ["add", _reference_name, "ref", "to", table_name] => MigrationType::AddReference { + table: table_name.to_plural(), + }, + ["add", _column_names @ .., "to", table_name] => MigrationType::AddColumns { + table: table_name.to_plural(), + }, + ["remove", _column_names @ .., "from", table_name] => MigrationType::RemoveColumns { + table: table_name.to_plural(), + }, + ["create", "join", "table", table_a, "and", table_b] => { + let table_a = table_a.to_singular(); + let table_b = table_b.to_singular(); + MigrationType::CreateJoinTable { table_a, table_b } + } + _ => MigrationType::Empty, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_infer_create_table() { + assert_eq!( + guess_migration_type("CreateUsers"), + MigrationType::CreateTable { + table: "users".to_string(), + } + ); + } + + #[test] + fn test_infer_add_columns() { + assert_eq!( + guess_migration_type("AddNameAndAgeToUsers"), + MigrationType::AddColumns { + table: "users".to_string(), + } + ); + } + + #[test] + fn test_infer_remove_columns() { + assert_eq!( + guess_migration_type("RemoveNameAndAgeFromUsers"), + MigrationType::RemoveColumns { + table: "users".to_string(), + } + ); + } + + #[test] + fn test_infer_add_reference() { + assert_eq!( + guess_migration_type("AddUserRefToPosts"), + MigrationType::AddReference { + table: "posts".to_string(), + } + ); + } + + #[test] + fn test_infer_create_join_table() { + assert_eq!( + guess_migration_type("CreateJoinTableUsersAndGroups"), + MigrationType::CreateJoinTable { + table_a: "user".to_string(), + table_b: "group".to_string() + } + ); + } + + #[test] + fn test_empty_migration() { + assert_eq!( + guess_migration_type("UnknownMigrationType"), + MigrationType::Empty + ); + } +} diff --git a/loco-gen/src/lib.rs b/loco-gen/src/lib.rs index 90ed56a93..908c3c4b2 100644 --- a/loco-gen/src/lib.rs +++ b/loco-gen/src/lib.rs @@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize}; use serde_json::json; mod controller; +mod infer; +mod migration; #[cfg(feature = "with-db")] mod model; #[cfg(feature = "with-db")] @@ -20,8 +22,6 @@ const MAILER_SUB_T: &str = include_str!("templates/mailer/subject.t"); const MAILER_TEXT_T: &str = include_str!("templates/mailer/text.t"); const MAILER_HTML_T: &str = include_str!("templates/mailer/html.t"); -const MIGRATION_T: &str = include_str!("templates/migration/migration.t"); - const TASK_T: &str = include_str!("templates/task/task.t"); const TASK_TEST_T: &str = include_str!("templates/task/test.t"); @@ -151,14 +151,14 @@ pub enum Component { /// Model fields, eg. title:string hits:int fields: Vec<(String, String)>, - - /// Generate migration code and stop, don't run the migration - migration_only: bool, }, #[cfg(feature = "with-db")] Migration { /// Name of the migration file name: String, + + /// Params fields, eg. title:string hits:int + fields: Vec<(String, String)>, }, #[cfg(feature = "with-db")] Scaffold { @@ -222,15 +222,10 @@ pub fn generate(component: Component, appinfo: &AppInfo) -> Result<()> { */ match component { #[cfg(feature = "with-db")] - Component::Model { - name, - link, - fields, - migration_only, - } => { + Component::Model { name, link, fields } => { println!( "{}", - model::generate(&rrgen, &name, link, migration_only, &fields, appinfo)? + model::generate(&rrgen, &name, link, &fields, appinfo)? ); } #[cfg(feature = "with-db")] @@ -241,10 +236,8 @@ pub fn generate(component: Component, appinfo: &AppInfo) -> Result<()> { ); } #[cfg(feature = "with-db")] - Component::Migration { name } => { - let vars = - json!({ "name": name, "ts": chrono::Utc::now(), "pkg_name": appinfo.app_name}); - rrgen.generate(MIGRATION_T, &vars)?; + Component::Migration { name, fields } => { + migration::generate(&rrgen, &name, &fields, appinfo)?; } Component::Controller { name, diff --git a/loco-gen/src/migration.rs b/loco-gen/src/migration.rs new file mode 100644 index 000000000..f53918c71 --- /dev/null +++ b/loco-gen/src/migration.rs @@ -0,0 +1,75 @@ +use chrono::Utc; +use rrgen::RRgen; +use serde_json::json; + +use super::Result; +use crate::{ + infer, + model::{get_columns_and_references, MODEL_T}, +}; + +const MIGRATION_T: &str = include_str!("templates/migration/empty.t"); +const ADD_COLS_T: &str = include_str!("templates/migration/add_columns.t"); +const ADD_REFS_T: &str = include_str!("templates/migration/add_references.t"); +const REMOVE_COLS_T: &str = include_str!("templates/migration/remove_columns.t"); +const JOIN_TABLE_T: &str = include_str!("templates/migration/join_table.t"); + +use super::{collect_messages, AppInfo}; + +/// skipping some fields from the generated models. +/// For example, the `created_at` and `updated_at` fields are automatically +/// generated by the Loco app and should be given +pub const IGNORE_FIELDS: &[&str] = &["created_at", "updated_at", "create_at", "update_at"]; + +pub fn generate( + rrgen: &RRgen, + name: &str, + fields: &[(String, String)], + appinfo: &AppInfo, +) -> Result { + let pkg_name: &str = &appinfo.app_name; + let ts = Utc::now(); + + let res = infer::guess_migration_type(name); + let migration_gen = match res { + // NOTE: re-uses the 'new model' migration template! + infer::MigrationType::CreateTable { table } => { + let (columns, references) = get_columns_and_references(fields)?; + let vars = json!({"name": table, "ts": ts, "pkg_name": pkg_name, "is_link": false, "columns": columns, "references": references}); + rrgen.generate(MODEL_T, &vars)? + } + infer::MigrationType::AddColumns { table } => { + let (columns, references) = get_columns_and_references(fields)?; + let vars = json!({"name": name, "table": table, "ts": ts, "pkg_name": pkg_name, "is_link": false, "columns": columns, "references": references}); + rrgen.generate(ADD_COLS_T, &vars)? + } + infer::MigrationType::RemoveColumns { table } => { + let (columns, _references) = get_columns_and_references(fields)?; + let vars = json!({"name": name, "table": table, "ts": ts, "pkg_name": pkg_name, "columns": columns}); + rrgen.generate(REMOVE_COLS_T, &vars)? + } + infer::MigrationType::AddReference { table } => { + let (columns, references) = get_columns_and_references(fields)?; + let vars = json!({"name": name, "table": table, "ts": ts, "pkg_name": pkg_name, "columns": columns, "references": references}); + rrgen.generate(ADD_REFS_T, &vars)? + } + infer::MigrationType::CreateJoinTable { table_a, table_b } => { + let mut tables = [table_a.clone(), table_b.clone()]; + tables.sort(); + let table = tables.join("_"); + let (columns, references) = get_columns_and_references(&[ + (table_a, "references".to_string()), + (table_b, "references".to_string()), + ])?; + let vars = json!({"name": name, "table": table, "ts": ts, "pkg_name": pkg_name, "columns": columns, "references": references}); + rrgen.generate(JOIN_TABLE_T, &vars)? + } + infer::MigrationType::Empty => { + let vars = json!({"name": name, "ts": ts, "pkg_name": pkg_name}); + rrgen.generate(MIGRATION_T, &vars)? + } + }; + + let messages = collect_messages(vec![migration_gen]); + Ok(messages) +} diff --git a/loco-gen/src/model.rs b/loco-gen/src/model.rs index 928f280be..238feef03 100644 --- a/loco-gen/src/model.rs +++ b/loco-gen/src/model.rs @@ -8,7 +8,7 @@ use serde_json::json; use super::{Error, Result}; use crate::get_mappings; -const MODEL_T: &str = include_str!("templates/model/model.t"); +pub const MODEL_T: &str = include_str!("templates/model/model.t"); const MODEL_TEST_T: &str = include_str!("templates/model/test.t"); use super::{collect_messages, AppInfo}; @@ -18,17 +18,14 @@ use super::{collect_messages, AppInfo}; /// generated by the Loco app and should be given pub const IGNORE_FIELDS: &[&str] = &["created_at", "updated_at", "create_at", "update_at"]; -pub fn generate( - rrgen: &RRgen, - name: &str, - is_link: bool, - migration_only: bool, +/// columns are , : ("content", "string") +/// references are : ("user", `user_id`) +/// parsed from e.g.: model article content:string user:references +/// puts a `user_id` in articles, then fk to users +#[allow(clippy::type_complexity)] +pub fn get_columns_and_references( fields: &[(String, String)], - appinfo: &AppInfo, -) -> Result { - let pkg_name: &str = &appinfo.app_name; - let ts = Utc::now(); - +) -> Result<(Vec<(String, String)>, Vec<(String, String)>)> { let mut columns = Vec::new(); let mut references = Vec::new(); for (fname, ftype) in fields { @@ -41,12 +38,12 @@ pub fn generate( } if ftype == "references" { let fkey = format!("{fname}_id"); - columns.push((fkey.clone(), "integer")); + columns.push((fkey.clone(), "integer".to_string())); // user, user_id references.push((fname.to_string(), fkey)); } else if let Some(refname) = ftype.strip_prefix("references:") { let fkey = format!("{fname}_id"); - columns.push((fkey.clone(), "integer")); + columns.push((fkey.clone(), "integer".to_string())); references.push((refname.to_string(), fkey)); } else { let mappings = get_mappings(); @@ -57,39 +54,51 @@ pub fn generate( mappings.schema_fields() )) })?; - columns.push((fname.to_string(), schema_type.as_str())); + columns.push((fname.to_string(), schema_type.to_string())); } } + Ok((columns, references)) +} +pub fn generate( + rrgen: &RRgen, + name: &str, + is_link: bool, + fields: &[(String, String)], + appinfo: &AppInfo, +) -> Result { + let pkg_name: &str = &appinfo.app_name; + let ts = Utc::now(); + + let (columns, references) = get_columns_and_references(fields)?; let vars = json!({"name": name, "ts": ts, "pkg_name": pkg_name, "is_link": is_link, "columns": columns, "references": references}); let res1 = rrgen.generate(MODEL_T, &vars)?; let res2 = rrgen.generate(MODEL_TEST_T, &vars)?; - if !migration_only { - let cwd = current_dir()?; - let env_map: HashMap<_, _> = std::env::vars().collect(); - - let _ = cmd!("cargo", "loco-tool", "db", "migrate",) - .stderr_to_stdout() - .dir(cwd.as_path()) - .full_env(&env_map) - .run() - .map_err(|err| { - Error::Message(format!( - "failed to run loco db migration. error details: `{err}`", - )) - })?; - let _ = cmd!("cargo", "loco-tool", "db", "entities",) - .stderr_to_stdout() - .dir(cwd.as_path()) - .full_env(&env_map) - .run() - .map_err(|err| { - Error::Message(format!( - "failed to run loco db entities. error details: `{err}`", - )) - })?; - } + // generate the model files by migrating and re-running seaorm + let cwd = current_dir()?; + let env_map: HashMap<_, _> = std::env::vars().collect(); + + let _ = cmd!("cargo", "loco-tool", "db", "migrate",) + .stderr_to_stdout() + .dir(cwd.as_path()) + .full_env(&env_map) + .run() + .map_err(|err| { + Error::Message(format!( + "failed to run loco db migration. error details: `{err}`", + )) + })?; + let _ = cmd!("cargo", "loco-tool", "db", "entities",) + .stderr_to_stdout() + .dir(cwd.as_path()) + .full_env(&env_map) + .run() + .map_err(|err| { + Error::Message(format!( + "failed to run loco db entities. error details: `{err}`", + )) + })?; let messages = collect_messages(vec![res1, res2]); Ok(messages) @@ -141,7 +150,6 @@ mod tests { &rrgen, "movies", false, - true, &[("title".to_string(), "string".to_string())], &AppInfo { app_name: "saas".to_string(), diff --git a/loco-gen/src/scaffold.rs b/loco-gen/src/scaffold.rs index b627b695e..b56f230d3 100644 --- a/loco-gen/src/scaffold.rs +++ b/loco-gen/src/scaffold.rs @@ -34,7 +34,7 @@ pub fn generate( // - scaffold is never a link table // - never run with migration_only, because the controllers will refer to the // models. the models only arrive after migration and entities sync. - let model_messages = model::generate(rrgen, name, false, false, fields, appinfo)?; + let model_messages = model::generate(rrgen, name, false, fields, appinfo)?; let mappings = get_mappings(); let mut columns = Vec::new(); diff --git a/loco-gen/src/templates/migration/add_columns.t b/loco-gen/src/templates/migration/add_columns.t new file mode 100644 index 000000000..4e97fdf81 --- /dev/null +++ b/loco-gen/src/templates/migration/add_columns.t @@ -0,0 +1,59 @@ +{% set mig_ts = ts | date(format="%Y%m%d_%H%M%S") -%} +{% set mig_name = name | snake_case -%} +{% set tbl_enum = table | plural | pascal_case -%} +{% set module_name = "m" ~ mig_ts ~ "_" ~ mig_name -%} +to: "migration/src/{{module_name}}.rs" +skip_glob: "migration/src/m????????_??????_{{mig_name}}.rs" +message: "Migration `{{mig_name}}` added! You can now apply it with `$ cargo loco db migrate`." +injections: +- into: "migration/src/lib.rs" + before: "inject-above" + content: " Box::new({{module_name}}::Migration)," +- into: "migration/src/lib.rs" + before: "pub struct Migrator" + content: "mod {{module_name}};" +--- +use loco_rs::schema::*; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + alter({{tbl_enum}}::Table) + {% for column in columns -%} + {% if column.1 == "decimal_len_null" or column.1 == "decimal_len" -%} + .add_column({{column.1}}({{tbl_enum}}::{{column.0 | pascal_case }}, 16, 4)) + {% else -%} + .add_column({{column.1}}({{tbl_enum}}::{{column.0 | pascal_case}})) + {% endif -%} + {% endfor -%} + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + alter({{tbl_enum}}::Table) + {% for column in columns -%} + .drop_column({{tbl_enum}}::{{column.0 | pascal_case}}) + {% endfor -%} + .to_owned() + ) + .await + } +} + +#[derive(DeriveIden)] +enum {{tbl_enum}} { + Table, + {% for column in columns -%} + {{column.0 | pascal_case}}, + {% endfor %} +} diff --git a/loco-gen/src/templates/migration/add_references.t b/loco-gen/src/templates/migration/add_references.t new file mode 100644 index 000000000..9357f4917 --- /dev/null +++ b/loco-gen/src/templates/migration/add_references.t @@ -0,0 +1,83 @@ +{% set mig_ts = ts | date(format="%Y%m%d_%H%M%S") -%} +{% set mig_name = name | snake_case -%} +{% set tbl_enum = table | plural | pascal_case -%} +{% set module_name = "m" ~ mig_ts ~ "_" ~ mig_name -%} +to: "migration/src/{{module_name}}.rs" +skip_glob: "migration/src/m????????_??????_{{mig_name}}.rs" +message: "Migration `{{mig_name}}` added! You can now apply it with `$ cargo loco db migrate`." +injections: +- into: "migration/src/lib.rs" + before: "inject-above" + content: " Box::new({{module_name}}::Migration)," +- into: "migration/src/lib.rs" + before: "pub struct Migrator" + content: "mod {{module_name}};" +--- +use loco_rs::schema::*; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + alter({{tbl_enum}}::Table) + {% for column in columns -%} + {% if column.1 == "decimal_len_null" or column.1 == "decimal_len" -%} + .add_column({{column.1}}({{tbl_enum}}::{{column.0 | pascal_case }}, 16, 4)) + {% else -%} + .add_column({{column.1}}({{tbl_enum}}::{{column.0 | pascal_case}})) + {% endif -%} + {% endfor -%} + {% for ref in references -%} + .add_foreign_key( + TableForeignKey::new() + .name("fk-{{table | plural | snake_case}}-{{ref.0 | plural| snake_case}}") + .from_tbl({{tbl_enum}}::Table) + .from_col({{tbl_enum}}::{{ref.1 | pascal_case}}) + .to_tbl({{ref.0 | plural | pascal_case}}::Table) + .to_col({{ref.0 | plural | pascal_case}}::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + {% endfor -%} + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + alter({{tbl_enum}}::Table) + {% for ref in references -%} + .drop_foreign_key(Alias::new("fk-{{table | plural | snake_case}}-{{ref.0 | plural| snake_case}}")) + {% endfor -%} + {% for column in columns -%} + .drop_column({{tbl_enum}}::{{column.0 | pascal_case}}) + {% endfor -%} + .to_owned() + ) + .await + } +} + +#[derive(DeriveIden)] +enum {{tbl_enum}} { + Table, + {% for column in columns -%} + {{column.0 | pascal_case}}, + {% endfor %} +} + +{% for ref in references | unique(attribute="0") -%} +#[derive(DeriveIden)] +enum {{ref.0 | plural | pascal_case}} { + Table, + Id, +} +{% endfor -%} + diff --git a/loco-gen/src/templates/migration/migration.t b/loco-gen/src/templates/migration/empty.t similarity index 100% rename from loco-gen/src/templates/migration/migration.t rename to loco-gen/src/templates/migration/empty.t diff --git a/loco-gen/src/templates/migration/join_table.t b/loco-gen/src/templates/migration/join_table.t new file mode 100644 index 000000000..5029a687d --- /dev/null +++ b/loco-gen/src/templates/migration/join_table.t @@ -0,0 +1,81 @@ +{% set mig_ts = ts | date(format="%Y%m%d_%H%M%S") -%} +{% set plural_snake = name | plural | snake_case -%} +{% set module_name = "m" ~ mig_ts ~ "_" ~ plural_snake -%} +{% set tbl_enum = table | plural | pascal_case -%} +to: "migration/src/{{module_name}}.rs" +skip_glob: "migration/src/m????????_??????_{{plural_snake}}.rs" +message: "Migration for `{{name}}` added! You can now apply it with `$ cargo loco db migrate`." +injections: +- into: "migration/src/lib.rs" + before: "inject-above" + content: " Box::new({{module_name}}::Migration)," +- into: "migration/src/lib.rs" + before: "pub struct Migrator" + content: "mod {{module_name}};" +--- +use loco_rs::schema::table_auto_tz; +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + table_auto_tz({{tbl_enum}}::Table) + .primary_key( + Index::create() + .name("idx-{{plural_snake}}-refs-pk") + .table({{tbl_enum}}::Table) + {% for ref in references -%} + .col({{tbl_enum}}::{{ref.1 | pascal_case}}) + {% endfor -%} + , + ) + {% for column in columns -%} + {% if column.1 == "decimal_len_null" or column.1 == "decimal_len" -%} + .col({{column.1}}({{tbl_enum}}::{{column.0 | pascal_case }}, 16, 4)) + {% else -%} + .col({{column.1}}({{tbl_enum}}::{{column.0 | pascal_case}})) + {% endif -%} + {% endfor -%} + {% for ref in references -%} + .foreign_key( + ForeignKey::create() + .name("fk-{{plural_snake}}-{{ref.1 | plural| snake_case}}") + .from({{tbl_enum}}::Table, {{tbl_enum}}::{{ref.1 | pascal_case}}) + .to({{ref.0 | plural | pascal_case}}::Table, {{ref.0 | plural | pascal_case}}::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + {% endfor -%} + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table({{tbl_enum}}::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum {{tbl_enum}} { + Table, + {% for column in columns -%} + {{column.0 | pascal_case}}, + {% endfor %} +} + +{% for ref in references | unique(attribute="0") -%} +#[derive(DeriveIden)] +enum {{ref.0 | plural | pascal_case}} { + Table, + Id, +} +{% endfor -%} + diff --git a/loco-gen/src/templates/migration/remove_columns.t b/loco-gen/src/templates/migration/remove_columns.t new file mode 100644 index 000000000..1f6f959f4 --- /dev/null +++ b/loco-gen/src/templates/migration/remove_columns.t @@ -0,0 +1,59 @@ +{% set mig_ts = ts | date(format="%Y%m%d_%H%M%S") -%} +{% set mig_name = name | snake_case -%} +{% set tbl_enum = table | plural | pascal_case -%} +{% set module_name = "m" ~ mig_ts ~ "_" ~ mig_name -%} +to: "migration/src/{{module_name}}.rs" +skip_glob: "migration/src/m????????_??????_{{mig_name}}.rs" +message: "Migration `{{mig_name}}` added! You can now apply it with `$ cargo loco db migrate`." +injections: +- into: "migration/src/lib.rs" + before: "inject-above" + content: " Box::new({{module_name}}::Migration)," +- into: "migration/src/lib.rs" + before: "pub struct Migrator" + content: "mod {{module_name}};" +--- +use loco_rs::schema::*; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + alter({{tbl_enum}}::Table) + {% for column in columns -%} + .drop_column({{tbl_enum}}::{{column.0 | pascal_case}}) + {% endfor -%} + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + alter({{tbl_enum}}::Table) + {% for column in columns -%} + {% if column.1 == "decimal_len_null" or column.1 == "decimal_len" -%} + .add_column({{column.1}}({{tbl_enum}}::{{column.0 | pascal_case }}, 16, 4)) + {% else -%} + .add_column({{column.1}}({{tbl_enum}}::{{column.0 | pascal_case}})) + {% endif -%} + {% endfor -%} + .to_owned(), + ) + .await + } +} + +#[derive(DeriveIden)] +enum {{tbl_enum}} { + Table, + {% for column in columns -%} + {{column.0 | pascal_case}}, + {% endfor %} +} diff --git a/src/cli.rs b/src/cli.rs index a179bc7c1..2e83812cf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -175,10 +175,6 @@ enum ComponentArg { #[arg(short, long, action)] link: bool, - /// Generate migration code only. Don't run the migration automatically. - #[arg(short, long, action)] - migration_only: bool, - /// Model fields, eg. title:string hits:int #[clap(value_parser = parse_key_val::)] fields: Vec<(String, String)>, @@ -188,6 +184,9 @@ enum ComponentArg { Migration { /// Name of the migration to generate name: String, + /// Table fields, eg. title:string hits:int + #[clap(value_parser = parse_key_val::)] + fields: Vec<(String, String)>, }, #[cfg(feature = "with-db")] /// Generates a CRUD scaffold, model and controller @@ -264,19 +263,9 @@ impl ComponentArg { fn into_gen_component(self, config: &Config) -> crate::Result { match self { #[cfg(feature = "with-db")] - Self::Model { - name, - link, - migration_only, - fields, - } => Ok(Component::Model { - name, - link, - migration_only, - fields, - }), + Self::Model { name, link, fields } => Ok(Component::Model { name, link, fields }), #[cfg(feature = "with-db")] - Self::Migration { name } => Ok(Component::Migration { name }), + Self::Migration { name, fields } => Ok(Component::Migration { name, fields }), #[cfg(feature = "with-db")] Self::Scaffold { name, @@ -440,7 +429,8 @@ enum JobsCommands { Tidy {}, /// Deletes jobs based on their age in days. Purge { - /// Deletes jobs with errors or cancelled, older than the specified maximum age in days. + /// Deletes jobs with errors or cancelled, older than the specified + /// maximum age in days. #[arg(long, default_value_t = 90)] max_age: i64, /// Limits the jobs being saved to those with specific criteria like diff --git a/src/schema.rs b/src/schema.rs index e093235db..bb68c2282 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,57 +1,8 @@ -//! # Database Table Schema Helpers -//! -//! This module defines functions and helpers for creating database table -//! schemas using the `sea-orm` and `sea-query` libraries. -//! -//! # Example -//! -//! The following example shows how the user migration file should be and using -//! the schema helpers to create the Db fields. -//! -//! ```rust -//! use sea_orm_migration::{prelude::*, schema::*}; -//! -//! #[derive(DeriveMigrationName)] -//! pub struct Migration; -//! -//! #[async_trait::async_trait] -//! impl MigrationTrait for Migration { -//! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { -//! let table = table_auto(Users::Table) -//! .col(pk_auto(Users::Id)) -//! .col(uuid(Users::Pid)) -//! .col(string_uniq(Users::Email)) -//! .col(string(Users::Password)) -//! .col(string(Users::Name)) -//! .col(string_null(Users::ResetToken)) -//! .col(timestamp_null(Users::ResetSentAt)) -//! .to_owned(); -//! manager.create_table(table).await?; -//! Ok(()) -//! } -//! -//! async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { -//! manager -//! .drop_table(Table::drop().table(Users::Table).to_owned()) -//! .await -//! } -//! } -//! -//! #[derive(Iden)] -//! pub enum Users { -//! Table, -//! Id, -//! Pid, -//! Email, -//! Name, -//! Password, -//! ResetToken, -//! ResetSentAt, -//! } -//! ``` - -use sea_orm::sea_query::{ColumnDef, Expr, IntoIden, Table, TableCreateStatement}; -use sea_orm_migration::{prelude::Iden, schema::timestamp_with_time_zone, sea_query}; +use sea_orm::sea_query::{ + ColumnDef, Expr, IntoIden, Table, TableAlterStatement, TableCreateStatement, +}; +pub use sea_orm_migration::schema::*; +use sea_orm_migration::{prelude::Iden, sea_query}; #[derive(Iden)] enum GeneralIds { @@ -59,6 +10,11 @@ enum GeneralIds { UpdatedAt, } +/// Alter table +pub fn alter(name: T) -> TableAlterStatement { + Table::alter().table(name).take() +} + /// Wrapping table schema creation. pub fn table_auto_tz(name: T) -> TableCreateStatement where @@ -67,27 +23,8 @@ where timestamps_tz(Table::create().table(name).if_not_exists().take()) } -pub fn table_auto(name: T) -> TableCreateStatement -where - T: IntoIden + 'static, -{ - timestamps(Table::create().table(name).if_not_exists().take()) -} +// these two are just aliases, original types exist in seaorm already. -/// Create a primary key column with auto-increment feature. -pub fn pk_auto(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name) - .integer() - .not_null() - .auto_increment() - .primary_key() - .take() -} - -/// Add timestamp columns (`CreatedAt` and `UpdatedAt`) to an existing table. #[must_use] pub fn timestamps_tz(t: TableCreateStatement) -> TableCreateStatement { let mut t = t; @@ -96,46 +33,6 @@ pub fn timestamps_tz(t: TableCreateStatement) -> TableCreateStatement { t.take() } -#[must_use] -pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement { - let mut t = t; - t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp())) - .col(timestamp(GeneralIds::UpdatedAt).default(Expr::current_timestamp())); - t.take() -} - -/// Create a UUID column definition with a unique constraint. -pub fn uuid(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).unique_key().uuid().not_null().take() -} - -/// Create a UUID type column definition. -pub fn uuid_col(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).uuid().not_null().take() -} - -/// Create a nullable UUID type column definition. -pub fn uuid_col_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).uuid().take() -} - -/// Create a nullable string column definition. -pub fn string_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).string().take() -} - /// Create a nullable timestamptz column definition. pub fn timestamptz_null(name: T) -> ColumnDef where @@ -154,280 +51,3 @@ where .not_null() .take() } - -/// Create a non-nullable string column definition. -pub fn string(name: T) -> ColumnDef -where - T: IntoIden, -{ - string_null(name).not_null().take() -} - -/// Create a unique string column definition. -pub fn string_uniq(name: T) -> ColumnDef -where - T: IntoIden, -{ - string(name).unique_key().take() -} - -/// Create a nullable text column definition. -pub fn text_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).text().take() -} - -/// Create a nullable text column definition. -pub fn text(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).text().take() -} - -/// Create a nullable tiny integer column definition. -pub fn tiny_integer_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).tiny_integer().take() -} - -/// Create a non-nullable tiny integer column definition. -pub fn tiny_integer(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).tiny_integer().not_null().take() -} - -/// Create a unique tiny integer column definition. -pub fn tiny_integer_uniq(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).tiny_integer().unique_key().take() -} - -/// Create a nullable small integer column definition. -pub fn small_integer_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).small_integer().take() -} - -/// Create a non-nullable small integer column definition. -pub fn small_integer(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).small_integer().not_null().take() -} - -/// Create a unique small integer column definition. -pub fn small_integer_uniq(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).small_integer().unique_key().take() -} - -/// Create a nullable integer column definition. -pub fn integer_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).integer().take() -} - -/// Create a non-nullable integer column definition. -pub fn integer(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).integer().not_null().take() -} - -/// Create a unique integer column definition. -pub fn integer_uniq(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).integer().unique_key().take() -} - -/// Create a nullable big integer column definition. -pub fn big_integer_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).big_integer().take() -} - -/// Create a non-nullable big integer column definition. -pub fn big_integer(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).big_integer().not_null().take() -} - -/// Create a unique big integer column definition. -pub fn big_integer_uniq(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).big_integer().unique_key().take() -} - -/// Create a nullable float column definition. -pub fn float_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).float().take() -} - -/// Create a non-nullable float column definition. -pub fn float(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).float().not_null().take() -} - -/// Create a nullable double column definition. -pub fn double_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).double().take() -} - -/// Create a non-nullable double column definition. -pub fn double(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).double().not_null().take() -} - -/// Create a nullable decimal column definition. -pub fn decimal_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).decimal().take() -} - -/// Create a non-nullable decimal column definition. -pub fn decimal(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).decimal().not_null().take() -} - -/// Create a nullable decimal length column definition with custom precision and -/// scale. -pub fn decimal_len_null(name: T, precision: u32, scale: u32) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).decimal_len(precision, scale).take() -} - -/// Create a non-nullable decimal length column definition with custom precision -/// and scale. -pub fn decimal_len(name: T, precision: u32, scale: u32) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name) - .decimal_len(precision, scale) - .not_null() - .take() -} - -/// Create a nullable boolean column definition. -pub fn bool_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).boolean().take() -} - -/// Create a non-nullable boolean column definition. -pub fn bool(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).boolean().not_null().take() -} - -/// Create a nullable date column definition. -pub fn date_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).date().take() -} - -/// Create a non-nullable date column definition. -pub fn date(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).date().not_null().take() -} - -/// Create a nullable timestamp column definition. -pub fn timestamp_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).date_time().take() -} - -/// Create a non-nullable timestamp column definition. -pub fn timestamp(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).date_time().not_null().take() -} - -/// Create a non-nullable json column definition. -pub fn json(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).json().not_null().take() -} - -/// Create a nullable json column definition. -pub fn json_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).json().take() -} - -/// Create a non-nullable json binary column definition. -pub fn jsonb(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).json_binary().not_null().take() -} - -/// Create a nullable json binary column definition. -pub fn jsonb_null(name: T) -> ColumnDef -where - T: IntoIden, -{ - ColumnDef::new(name).json_binary().take() -} From daa862eb09caa8fb7b34f57eccf2d0dca4402ff8 Mon Sep 17 00:00:00 2001 From: "Dotan J. Nahum" Date: Sun, 15 Dec 2024 13:33:43 +0200 Subject: [PATCH 2/4] docs --- CHANGELOG.md | 1 + docs-site/content/docs/the-app/models.md | 103 ++++++++++++++++------- 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c128ab926..d08a89ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* feat: smart migration generator. you can now generate migration based on naming them for creating a table, adding columns, references, join tables and more. [https://github.com/loco-rs/loco/pull/1086](https://github.com/loco-rs/loco/pull/1086) * feat: `cargo loco routes` will now pretty-print routes * fix: guard jwt error behind feature flag. [https://github.com/loco-rs/loco/pull/1032](https://github.com/loco-rs/loco/pull/1032) * fix: logger file_appender not using the seperated format setting. [https://github.com/loco-rs/loco/pull/1036](https://github.com/loco-rs/loco/pull/1036) diff --git a/docs-site/content/docs/the-app/models.md b/docs-site/content/docs/the-app/models.md index a8f66df10..94685cb2d 100644 --- a/docs-site/content/docs/the-app/models.md +++ b/docs-site/content/docs/the-app/models.md @@ -111,9 +111,9 @@ impl super::_entities::users::ActiveModel { # Crafting models -## Migrations +## The model generator -To add a new model _you have to use a migration_. +To add a new model the model generator creates a migration, runs it, and then triggers an entities sync from your database schema which will hydrate and create your model entities. ``` $ cargo loco generate model posts title:string! content:text user:references @@ -181,20 +181,24 @@ You can generate an empty model: $ cargo loco generate model posts ``` -You can generate an empty model **migration only** which means migrations will not run automatically: + +Or a data model, without any references: ``` -$ cargo loco generate model --migration-only posts +$ cargo loco generate model posts title:string! content:text ``` -Or a data model, without any references: +## Migrations + +Other than using the model generator, you drive your schema by *creating migrations*. ``` -$ cargo loco generate model posts title:string! content:text +$ cargo loco generate [name:type, name:type ...] ``` This creates a migration in the root of your project in `migration/`. -You can now apply it: + +You can apply it: ``` $ cargo loco db migrate @@ -210,6 +214,63 @@ Loco is a migration-first framework, similar to Rails. Which means that when you This enforces _everything-as-code_, _reproducibility_ and _atomicity_, where no knowledge of the schema goes missing. +**Naming the migration is important**, the type of migration that is being generated is inferred from the migration name. + +### Create a new table + +* Name template: `Create___` +* Example: `CreatePosts` + +``` +$ cargo loco g migration CreatePosts title:string content:string +``` + +### Add columns + +* Name template: `Add___To___` +* Example: `AddNameAndAgeToUsers` (the string `NameAndAge` does not matter, you specify columns individually, however `Users` does matter because this will be the name of the table) + +``` +$ cargo loco g migration AddNameAndAgeToUsers name:string age:int +``` + +### Remove columns + +* Name template: `Remove___From___` +* Example: `RemoveNameAndAgeFromUsers` (same note exists as in _add columns_) + +``` +$ cargo logo g migration RemoveNameAndAgeFromUsers name:string age:int +``` + +### Add references + +* Name template: `Add___RefTo___` +* Example: `AddUserRefToPosts` (`User` does not matter, as you specify one or many references individually, `Posts` does matter as it will be the table name in the migration) + +``` +$ cargo loco g migration AddUserRefToPosts user:references +``` + +### Create a join table + +* Name template: `CreateJoinTable___And___` (supported between 2 tables) +* Example: `CreateJoinTableUsersAndGroups` + +``` +$ cargo loco g migration CreateJoinTableUsersAndGroups count:int +``` + +You can also add some state columns regarding the relationship (such as `count` here). + +### Create an empty migration + +Use any descriptive name for a migration that does not fall into one of the above patterns to create an empty migration. + +``` +$ cargo loco g migration FixUsersTable +``` + ### Down Migrations If you realize that you made a mistake, you can always undo the migration. This will undo the changes made by the migration (assuming that you added the appropriate code for `down` in the migration). @@ -247,27 +308,11 @@ $ cargo loco generate model movies long_title:string added_by:references:users d * reference added_by is in singular, the referenced model is a model and is plural: `added_by:references:users` * column name in snake case: `long_title:string` -### Naming migrations - -There are no rules for how to name migrations, but here's a few guidelines to keep your migration stack readable as a list of files: - -* `` - create a table, plural, `movies` -* `add_
_` - add a column, `add_users_email` -* `index_
_` - add an index, `index_users_email` -* `alter_` - change a schema, `alter_users` -* `delete_
_` - remove a column, `delete_users_email` -* `data_fix_` - fix some data, using entity queries or raw SQL, `data_fix_users_timezone_issue_315` -Example: - -```sh -$ cargo loco generate migration add_users_email -``` +### Migration Definition -### Add or remove a column - -Adding a column: +**Add a column** ```rust manager @@ -280,7 +325,7 @@ Adding a column: .await ``` -Dropping a column: +**Drop a column** ```rust manager @@ -293,8 +338,7 @@ Dropping a column: .await ``` -### Add index - +**Add index** You can copy some of this code for adding an index @@ -310,8 +354,7 @@ You can copy some of this code for adding an index .await; ``` -### Create a data fix - +**Create a data fix** Creating a data fix in a migration is easy - just use SQL statements as you like: From 9ecb5580afe1e28d06caee9396bd643121210030 Mon Sep 17 00:00:00 2001 From: "Dotan J. Nahum" Date: Sun, 15 Dec 2024 18:58:59 +0200 Subject: [PATCH 3/4] typo --- docs-site/content/docs/the-app/models.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-site/content/docs/the-app/models.md b/docs-site/content/docs/the-app/models.md index 94685cb2d..e0d139154 100644 --- a/docs-site/content/docs/the-app/models.md +++ b/docs-site/content/docs/the-app/models.md @@ -193,7 +193,7 @@ $ cargo loco generate model posts title:string! content:text Other than using the model generator, you drive your schema by *creating migrations*. ``` -$ cargo loco generate [name:type, name:type ...] +$ cargo loco generate migration [name:type, name:type ...] ``` This creates a migration in the root of your project in `migration/`. From fe394dbae504ad078e218cb09939f166b31e618c Mon Sep 17 00:00:00 2001 From: "Dotan J. Nahum" Date: Sun, 15 Dec 2024 19:01:56 +0200 Subject: [PATCH 4/4] flag with-db --- loco-gen/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/loco-gen/src/lib.rs b/loco-gen/src/lib.rs index 908c3c4b2..0a1f20cf3 100644 --- a/loco-gen/src/lib.rs +++ b/loco-gen/src/lib.rs @@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize}; use serde_json::json; mod controller; +#[cfg(feature = "with-db")] mod infer; +#[cfg(feature = "with-db")] mod migration; #[cfg(feature = "with-db")] mod model;