diff --git a/.changepacks/changepack_log_VBxNuIzZ8oAhXU1OCJ7ow.json b/.changepacks/changepack_log_VBxNuIzZ8oAhXU1OCJ7ow.json new file mode 100644 index 0000000..25614e2 --- /dev/null +++ b/.changepacks/changepack_log_VBxNuIzZ8oAhXU1OCJ7ow.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch"},"note":"Add column validation","date":"2025-12-18T15:48:46.157103400Z"} \ No newline at end of file diff --git a/.changepacks/changepack_log_YlSdZ6-kG9UnxW5iK-l9X.json b/.changepacks/changepack_log_YlSdZ6-kG9UnxW5iK-l9X.json new file mode 100644 index 0000000..8cd8679 --- /dev/null +++ b/.changepacks/changepack_log_YlSdZ6-kG9UnxW5iK-l9X.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch"},"note":"Implement num_enum","date":"2025-12-18T15:48:35.861293400Z"} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c742731..7656a1a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -/target -local.db -settings.local.json +/target +local.db +settings.local.json +coverage diff --git a/Cargo.lock b/Cargo.lock index 3a3e48d..a3d455d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1857,6 +1857,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "runtime-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34bef7b9430b9f9e666d930202e1344765b623203affe2f779bcd1f269384248" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "rust_decimal" version = "1.39.0" @@ -2938,7 +2949,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespertide" -version = "0.1.11" +version = "0.1.12" dependencies = [ "vespertide-core", "vespertide-macro", @@ -2946,7 +2957,7 @@ dependencies = [ [[package]] name = "vespertide-cli" -version = "0.1.11" +version = "0.1.12" dependencies = [ "anyhow", "assert_cmd", @@ -2970,7 +2981,7 @@ dependencies = [ [[package]] name = "vespertide-config" -version = "0.1.11" +version = "0.1.12" dependencies = [ "clap", "serde", @@ -2978,7 +2989,7 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.11" +version = "0.1.12" dependencies = [ "rstest", "schemars", @@ -2988,7 +2999,7 @@ dependencies = [ [[package]] name = "vespertide-exporter" -version = "0.1.11" +version = "0.1.12" dependencies = [ "insta", "rstest", @@ -2998,7 +3009,7 @@ dependencies = [ [[package]] name = "vespertide-loader" -version = "0.1.11" +version = "0.1.12" dependencies = [ "anyhow", "rstest", @@ -3013,10 +3024,11 @@ dependencies = [ [[package]] name = "vespertide-macro" -version = "0.1.11" +version = "0.1.12" dependencies = [ "proc-macro2", "quote", + "runtime-macros", "syn 2.0.111", "tempfile", "thiserror", @@ -3029,7 +3041,7 @@ dependencies = [ [[package]] name = "vespertide-planner" -version = "0.1.11" +version = "0.1.12" dependencies = [ "rstest", "thiserror", @@ -3038,7 +3050,7 @@ dependencies = [ [[package]] name = "vespertide-query" -version = "0.1.11" +version = "0.1.12" dependencies = [ "insta", "rstest", diff --git a/crates/vespertide-cli/src/commands/diff.rs b/crates/vespertide-cli/src/commands/diff.rs index 7146a0a..cb55dc6 100644 --- a/crates/vespertide-cli/src/commands/diff.rs +++ b/crates/vespertide-cli/src/commands/diff.rs @@ -248,7 +248,7 @@ mod tests { #[case( MigrationAction::AddColumn { table: "users".into(), - column: ColumnDef { + column: Box::new(ColumnDef { name: "name".into(), r#type: ColumnType::Simple(SimpleColumnType::Text), nullable: true, @@ -258,7 +258,7 @@ mod tests { unique: None, index: None, foreign_key: None, - }, + }), fill_with: None, }, format!("{} {}.{}", "Add column:".bright_green(), "users".bright_cyan(), "name".bright_cyan().bold()) diff --git a/crates/vespertide-cli/src/commands/export.rs b/crates/vespertide-cli/src/commands/export.rs index 9900492..d497b50 100644 --- a/crates/vespertide-cli/src/commands/export.rs +++ b/crates/vespertide-cli/src/commands/export.rs @@ -7,7 +7,7 @@ use anyhow::{Context, Result}; use clap::ValueEnum; use vespertide_config::VespertideConfig; use vespertide_core::TableDef; -use vespertide_exporter::{Orm, render_entity}; +use vespertide_exporter::{Orm, render_entity_with_schema}; use crate::utils::load_config; @@ -51,8 +51,12 @@ pub fn cmd_export(orm: OrmArg, export_dir: Option) -> Result<()> { let orm_kind: Orm = orm.into(); + // Extract all tables for schema context (used for FK chain resolution) + let all_tables: Vec = normalized_models.iter().map(|(t, _)| t.clone()).collect(); + for (table, rel_path) in &normalized_models { - let code = render_entity(orm_kind, table).map_err(|e| anyhow::anyhow!(e))?; + let code = render_entity_with_schema(orm_kind, table, &all_tables) + .map_err(|e| anyhow::anyhow!(e))?; let out_path = build_output_path(&target_root, rel_path, orm_kind); if let Some(parent) = out_path.parent() { fs::create_dir_all(parent) diff --git a/crates/vespertide-cli/src/commands/sql.rs b/crates/vespertide-cli/src/commands/sql.rs index 465e339..51341e7 100644 --- a/crates/vespertide-cli/src/commands/sql.rs +++ b/crates/vespertide-cli/src/commands/sql.rs @@ -431,7 +431,7 @@ mod tests { version: 1, actions: vec![MigrationAction::AddColumn { table: "users".into(), - column: ColumnDef { + column: Box::new(ColumnDef { name: "nickname".into(), r#type: ColumnType::Simple(SimpleColumnType::Text), nullable: false, @@ -441,7 +441,7 @@ mod tests { unique: None, index: None, foreign_key: None, - }, + }), fill_with: Some("default".into()), }], }; diff --git a/crates/vespertide-core/src/action.rs b/crates/vespertide-core/src/action.rs index 6c3b0b7..7d362cf 100644 --- a/crates/vespertide-core/src/action.rs +++ b/crates/vespertide-core/src/action.rs @@ -28,7 +28,7 @@ pub enum MigrationAction { }, AddColumn { table: TableName, - column: ColumnDef, + column: Box, /// Optional fill value to backfill existing rows when adding NOT NULL without default. fill_with: Option, }, @@ -194,7 +194,7 @@ mod tests { #[case::add_column( MigrationAction::AddColumn { table: "users".into(), - column: default_column(), + column: Box::new(default_column()), fill_with: None, }, "AddColumn: users.email" diff --git a/crates/vespertide-core/src/lib.rs b/crates/vespertide-core/src/lib.rs index a4364da..1f07d2f 100644 --- a/crates/vespertide-core/src/lib.rs +++ b/crates/vespertide-core/src/lib.rs @@ -1,10 +1,11 @@ -pub mod action; -pub mod migration; -pub mod schema; - -pub use action::{MigrationAction, MigrationPlan}; -pub use migration::{MigrationError, MigrationOptions}; -pub use schema::{ - ColumnDef, ColumnName, ColumnType, ComplexColumnType, IndexDef, IndexName, ReferenceAction, - SimpleColumnType, StrOrBoolOrArray, TableConstraint, TableDef, TableName, TableValidationError, -}; +pub mod action; +pub mod migration; +pub mod schema; + +pub use action::{MigrationAction, MigrationPlan}; +pub use migration::{MigrationError, MigrationOptions}; +pub use schema::{ + ColumnDef, ColumnName, ColumnType, ComplexColumnType, EnumValues, IndexDef, IndexName, + NumValue, ReferenceAction, SimpleColumnType, StrOrBoolOrArray, TableConstraint, TableDef, + TableName, TableValidationError, +}; diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs index 23fd5a3..a6d45f5 100644 --- a/crates/vespertide-core/src/schema/column.rs +++ b/crates/vespertide-core/src/schema/column.rs @@ -1,226 +1,576 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::schema::{ - foreign_key::ForeignKeySyntax, names::ColumnName, primary_key::PrimaryKeySyntax, - str_or_bool::StrOrBoolOrArray, -}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct ColumnDef { - pub name: ColumnName, - pub r#type: ColumnType, - pub nullable: bool, - pub default: Option, - pub comment: Option, - pub primary_key: Option, - pub unique: Option, - pub index: Option, - pub foreign_key: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case", untagged)] -pub enum ColumnType { - Simple(SimpleColumnType), - Complex(ComplexColumnType), -} - -impl ColumnType { - /// Convert column type to Rust type string (for SeaORM entity generation) - pub fn to_rust_type(&self, nullable: bool) -> String { - let base = match self { - ColumnType::Simple(ty) => match ty { - SimpleColumnType::SmallInt => "i16".to_string(), - SimpleColumnType::Integer => "i32".to_string(), - SimpleColumnType::BigInt => "i64".to_string(), - SimpleColumnType::Real => "f32".to_string(), - SimpleColumnType::DoublePrecision => "f64".to_string(), - SimpleColumnType::Text => "String".to_string(), - SimpleColumnType::Boolean => "bool".to_string(), - SimpleColumnType::Date => "Date".to_string(), - SimpleColumnType::Time => "Time".to_string(), - SimpleColumnType::Timestamp => "DateTime".to_string(), - SimpleColumnType::Timestamptz => "DateTimeWithTimeZone".to_string(), - SimpleColumnType::Interval => "String".to_string(), - SimpleColumnType::Bytea => "Vec".to_string(), - SimpleColumnType::Uuid => "Uuid".to_string(), - SimpleColumnType::Json | SimpleColumnType::Jsonb => "Json".to_string(), - SimpleColumnType::Inet | SimpleColumnType::Cidr => "String".to_string(), - SimpleColumnType::Macaddr => "String".to_string(), - SimpleColumnType::Xml => "String".to_string(), - }, - ColumnType::Complex(ty) => match ty { - ComplexColumnType::Varchar { .. } => "String".to_string(), - ComplexColumnType::Numeric { .. } => "Decimal".to_string(), - ComplexColumnType::Char { .. } => "String".to_string(), - ComplexColumnType::Custom { .. } => "String".to_string(), // Default for custom types - ComplexColumnType::Enum { .. } => "String".to_string(), - }, - }; - - if nullable { - format!("Option<{}>", base) - } else { - base - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SimpleColumnType { - SmallInt, - Integer, - BigInt, - Real, - DoublePrecision, - - // Text types - Text, - - // Boolean type - Boolean, - - // Date/Time types - Date, - Time, - Timestamp, - Timestamptz, - Interval, - - // Binary type - Bytea, - - // UUID type - Uuid, - - // JSON types - Json, - Jsonb, - - // Network types - Inet, - Cidr, - Macaddr, - - // XML type - Xml, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case", tag = "kind")] -pub enum ComplexColumnType { - Varchar { length: u32 }, - Numeric { precision: u32, scale: u32 }, - Char { length: u32 }, - Custom { custom_type: String }, - Enum { name: String, values: Vec }, -} - -#[cfg(test)] -mod tests { - use super::*; - use rstest::rstest; - - #[rstest] - #[case(SimpleColumnType::SmallInt, "i16")] - #[case(SimpleColumnType::Integer, "i32")] - #[case(SimpleColumnType::BigInt, "i64")] - #[case(SimpleColumnType::Real, "f32")] - #[case(SimpleColumnType::DoublePrecision, "f64")] - #[case(SimpleColumnType::Text, "String")] - #[case(SimpleColumnType::Boolean, "bool")] - #[case(SimpleColumnType::Date, "Date")] - #[case(SimpleColumnType::Time, "Time")] - #[case(SimpleColumnType::Timestamp, "DateTime")] - #[case(SimpleColumnType::Timestamptz, "DateTimeWithTimeZone")] - #[case(SimpleColumnType::Interval, "String")] - #[case(SimpleColumnType::Bytea, "Vec")] - #[case(SimpleColumnType::Uuid, "Uuid")] - #[case(SimpleColumnType::Json, "Json")] - #[case(SimpleColumnType::Jsonb, "Json")] - #[case(SimpleColumnType::Inet, "String")] - #[case(SimpleColumnType::Cidr, "String")] - #[case(SimpleColumnType::Macaddr, "String")] - #[case(SimpleColumnType::Xml, "String")] - fn test_simple_column_type_to_rust_type_not_nullable( - #[case] column_type: SimpleColumnType, - #[case] expected: &str, - ) { - assert_eq!( - ColumnType::Simple(column_type).to_rust_type(false), - expected - ); - } - - #[rstest] - #[case(SimpleColumnType::SmallInt, "Option")] - #[case(SimpleColumnType::Integer, "Option")] - #[case(SimpleColumnType::BigInt, "Option")] - #[case(SimpleColumnType::Real, "Option")] - #[case(SimpleColumnType::DoublePrecision, "Option")] - #[case(SimpleColumnType::Text, "Option")] - #[case(SimpleColumnType::Boolean, "Option")] - #[case(SimpleColumnType::Date, "Option")] - #[case(SimpleColumnType::Time, "Option