From 05c81e2995c0d757aafce9260aa562972ee578a1 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 16 Jan 2026 12:29:11 +0900 Subject: [PATCH 1/2] Remove jsonb --- .../changepack_log_9bKjgJSyjqvAncyixbIuf.json | 1 + Cargo.lock | 20 +++++----- SKILL.md | 38 ++++++++++++++++--- crates/vespertide-core/src/schema/column.rs | 9 +++-- crates/vespertide-exporter/src/seaorm/mod.rs | 27 ++++++++++++- ...ty_snapshots@params_jsonb_custom_type.snap | 21 ++++++++++ .../vespertide-exporter/src/sqlalchemy/mod.rs | 14 ++----- .../vespertide-exporter/src/sqlmodel/mod.rs | 4 +- crates/vespertide-query/src/sql/helpers.rs | 4 -- 9 files changed, 99 insertions(+), 39 deletions(-) create mode 100644 .changepacks/changepack_log_9bKjgJSyjqvAncyixbIuf.json create mode 100644 crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_jsonb_custom_type.snap diff --git a/.changepacks/changepack_log_9bKjgJSyjqvAncyixbIuf.json b/.changepacks/changepack_log_9bKjgJSyjqvAncyixbIuf.json new file mode 100644 index 0000000..59072fe --- /dev/null +++ b/.changepacks/changepack_log_9bKjgJSyjqvAncyixbIuf.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch"},"note":"Remove jsonb and update SKILL","date":"2026-01-16T03:28:50.936265200Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 60a0526..6c525c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2995,7 +2995,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespertide" -version = "0.1.29" +version = "0.1.30" dependencies = [ "vespertide-core", "vespertide-macro", @@ -3003,7 +3003,7 @@ dependencies = [ [[package]] name = "vespertide-cli" -version = "0.1.29" +version = "0.1.30" dependencies = [ "anyhow", "assert_cmd", @@ -3028,7 +3028,7 @@ dependencies = [ [[package]] name = "vespertide-config" -version = "0.1.29" +version = "0.1.30" dependencies = [ "clap", "schemars", @@ -3038,7 +3038,7 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.29" +version = "0.1.30" dependencies = [ "rstest", "schemars", @@ -3050,7 +3050,7 @@ dependencies = [ [[package]] name = "vespertide-exporter" -version = "0.1.29" +version = "0.1.30" dependencies = [ "insta", "rstest", @@ -3061,7 +3061,7 @@ dependencies = [ [[package]] name = "vespertide-loader" -version = "0.1.29" +version = "0.1.30" dependencies = [ "anyhow", "rstest", @@ -3076,7 +3076,7 @@ dependencies = [ [[package]] name = "vespertide-macro" -version = "0.1.29" +version = "0.1.30" dependencies = [ "proc-macro2", "quote", @@ -3093,11 +3093,11 @@ dependencies = [ [[package]] name = "vespertide-naming" -version = "0.1.29" +version = "0.1.30" [[package]] name = "vespertide-planner" -version = "0.1.29" +version = "0.1.30" dependencies = [ "insta", "rstest", @@ -3108,7 +3108,7 @@ dependencies = [ [[package]] name = "vespertide-query" -version = "0.1.29" +version = "0.1.30" dependencies = [ "insta", "rstest", diff --git a/SKILL.md b/SKILL.md index e491602..c1ff8b8 100644 --- a/SKILL.md +++ b/SKILL.md @@ -34,6 +34,28 @@ The schema URL provides: --- +## Post-Edit Validation (MANDATORY) + +**After EVERY edit to a model file, ALWAYS run these checks:** + +```bash +# 1. Check for parsing errors and schema violations +vespertide diff + +# 2. Preview generated SQL to verify correctness +vespertide sql +``` + +**Verify the output:** +- `vespertide diff` shows expected changes (no unexpected additions/removals) +- `vespertide sql` generates valid SQL for your target database +- IDE shows no red squiggles (schema validation errors) +- All required fields (`name`, `type`, `nullable`) are present + +**Only proceed to `vespertide revision` after verification passes.** + +--- + ## Installation ```bash @@ -201,8 +223,7 @@ vespertide revision -m "add status column" | `"interval"` | INTERVAL | Time duration | | `"bytea"` | BYTEA | Binary data | | `"uuid"` | UUID | UUIDs | -| `"json"` | JSON | JSON data | -| `"jsonb"` | JSONB | Binary JSON (indexable, recommended) | +| `"json"` | JSON | JSON data (cross-database compatible) | | `"inet"` | INET | IPv4/IPv6 address | | `"cidr"` | CIDR | Network address | | `"macaddr"` | MACADDR | MAC address | @@ -261,7 +282,9 @@ vespertide revision -m "add status column" > - Better for frequently-changing value sets > - Works identically across PostgreSQL, MySQL, SQLite -#### Custom Type (fallback) +#### Custom Type (AVOID - last resort only) + +> **WARNING**: Avoid custom types. They break cross-database compatibility. Use built-in types or redesign your schema. ```json { "kind": "custom", "custom_type": "POINT" } @@ -508,7 +531,7 @@ Both columns with `"primary_key": true` creates a **single composite primary key "nullable": false, "default": "'pending'" }, - { "name": "metadata", "type": "jsonb", "nullable": true }, + { "name": "metadata", "type": "json", "nullable": true }, { "name": "created_at", "type": "timestamptz", "nullable": false, "default": "NOW()" }, { "name": "updated_at", "type": "timestamptz", "nullable": true } ] @@ -638,7 +661,7 @@ Both columns with `"primary_key": true` creates a **single composite primary key 1. **Use enums for status/category fields** - Prefer over text + CHECK 2. **Use integer enums for expandable sets** - No migration needed for new values 3. **Use `timestamptz` over `timestamp`** - Timezone-aware is safer -4. **Use `jsonb` over `json`** - Indexable and faster +4. **Use `json` type for JSON data** - Works across all backends (PostgreSQL, MySQL, SQLite) ### MUST NOT DO @@ -648,6 +671,9 @@ Both columns with `"primary_key": true` creates a **single composite primary key 4. **Never use table-level constraints** - Except for CHECK expressions only 5. **Never manually create/edit migration files** - Only `fill_with` exception 6. **Never manually edit exported ORM files** - Use `vespertide export` to regenerate +7. **Never use `jsonb` type** - Use `json` instead (JSONB not supported in SQLite) +8. **Never use custom types** - Use built-in types only for cross-database compatibility +9. **Never use array types** - Use a separate join table instead (arrays not supported in SQLite) ### Naming Conventions @@ -675,7 +701,7 @@ boolean Flags date, time, timestamp, timestamptz Time interval Duration uuid UUIDs -json, jsonb JSON +json JSON bytea Binary inet, cidr, macaddr Network xml XML diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs index e7b4bb3..29edf4f 100644 --- a/crates/vespertide-core/src/schema/column.rs +++ b/crates/vespertide-core/src/schema/column.rs @@ -87,7 +87,8 @@ impl ColumnType { SimpleColumnType::Interval => "String".to_string(), SimpleColumnType::Bytea => "Vec".to_string(), SimpleColumnType::Uuid => "Uuid".to_string(), - SimpleColumnType::Json | SimpleColumnType::Jsonb => "Json".to_string(), + SimpleColumnType::Json => "Json".to_string(), + // SimpleColumnType::Jsonb => "Json".to_string(), SimpleColumnType::Inet | SimpleColumnType::Cidr => "String".to_string(), SimpleColumnType::Macaddr => "String".to_string(), SimpleColumnType::Xml => "String".to_string(), @@ -139,7 +140,7 @@ pub enum SimpleColumnType { // JSON types Json, - Jsonb, + // Jsonb, // Network types Inet, @@ -263,7 +264,7 @@ mod tests { #[case(SimpleColumnType::Bytea, "Vec")] #[case(SimpleColumnType::Uuid, "Uuid")] #[case(SimpleColumnType::Json, "Json")] - #[case(SimpleColumnType::Jsonb, "Json")] + // #[case(SimpleColumnType::Jsonb, "Json")] #[case(SimpleColumnType::Inet, "String")] #[case(SimpleColumnType::Cidr, "String")] #[case(SimpleColumnType::Macaddr, "String")] @@ -294,7 +295,7 @@ mod tests { #[case(SimpleColumnType::Bytea, "Option>")] #[case(SimpleColumnType::Uuid, "Option")] #[case(SimpleColumnType::Json, "Option")] - #[case(SimpleColumnType::Jsonb, "Option")] + // #[case(SimpleColumnType::Jsonb, "Option")] #[case(SimpleColumnType::Inet, "Option")] #[case(SimpleColumnType::Cidr, "Option")] #[case(SimpleColumnType::Macaddr, "Option")] diff --git a/crates/vespertide-exporter/src/seaorm/mod.rs b/crates/vespertide-exporter/src/seaorm/mod.rs index 42699ec..66a1d3b 100644 --- a/crates/vespertide-exporter/src/seaorm/mod.rs +++ b/crates/vespertide-exporter/src/seaorm/mod.rs @@ -219,6 +219,11 @@ fn render_column( attrs.push(formatted); } + // For custom types, add column_type attribute with the custom type value + if let ColumnType::Complex(ComplexColumnType::Custom { custom_type }) = &column.r#type { + attrs.push(format!("column_type = \"{}\"", custom_type)); + } + // Output attribute if any if !attrs.is_empty() { lines.push(format!(" #[sea_orm({})]", attrs.join(", "))); @@ -235,6 +240,16 @@ fn render_column( enum_type } } + // JSONB custom type should use Json rust type + ColumnType::Complex(ComplexColumnType::Custom { custom_type }) + if custom_type.to_uppercase() == "JSONB" => + { + if column.nullable { + "Option".to_string() + } else { + "Json".to_string() + } + } _ => column.r#type.to_rust_type(column.nullable), }; @@ -1035,7 +1050,6 @@ mod helper_tests { #[case(ColumnType::Simple(SimpleColumnType::Bytea), false, "Vec")] #[case(ColumnType::Simple(SimpleColumnType::Uuid), false, "Uuid")] #[case(ColumnType::Simple(SimpleColumnType::Json), false, "Json")] - #[case(ColumnType::Simple(SimpleColumnType::Jsonb), false, "Json")] #[case(ColumnType::Simple(SimpleColumnType::Inet), false, "String")] #[case(ColumnType::Simple(SimpleColumnType::Cidr), false, "String")] #[case(ColumnType::Simple(SimpleColumnType::Macaddr), false, "String")] @@ -2244,6 +2258,17 @@ mod tests { TableConstraint::PrimaryKey { columns: vec!["id".into()], auto_increment: false }, ], })] + #[case("jsonb_custom_type", TableDef { + name: "json_struct".into(), + description: None, + columns: vec![ + ColumnDef { name: "id".into(), r#type: ColumnType::Simple(SimpleColumnType::Integer), nullable: false, default: None, comment: None, primary_key: Some(PrimaryKeySyntax::Bool(true)), unique: None, index: None, foreign_key: None }, + ColumnDef { name: "json_data".into(), r#type: ColumnType::Simple(SimpleColumnType::Json), nullable: false, default: None, comment: None, primary_key: None, unique: None, index: None, foreign_key: None }, + ColumnDef { name: "jsonb_data".into(), r#type: ColumnType::Complex(ComplexColumnType::Custom { custom_type: "JSONB".into() }), nullable: false, default: None, comment: None, primary_key: None, unique: None, index: None, foreign_key: None }, + ColumnDef { name: "jsonb_nullable".into(), r#type: ColumnType::Complex(ComplexColumnType::Custom { custom_type: "jsonb".into() }), nullable: true, default: None, comment: None, primary_key: None, unique: None, index: None, foreign_key: None }, + ], + constraints: vec![], + })] fn render_entity_snapshots(#[case] name: &str, #[case] table: TableDef) { let rendered = render_entity(&table); with_settings!({ snapshot_suffix => format!("params_{}", name) }, { diff --git a/crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_jsonb_custom_type.snap b/crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_jsonb_custom_type.snap new file mode 100644 index 0000000..a73958e --- /dev/null +++ b/crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__render_entity_snapshots@params_jsonb_custom_type.snap @@ -0,0 +1,21 @@ +--- +source: crates/vespertide-exporter/src/seaorm/mod.rs +expression: rendered +--- +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "json_struct")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub json_data: Json, + #[sea_orm(column_type = "JSONB")] + pub jsonb_data: Json, + #[sea_orm(column_type = "jsonb")] + pub jsonb_nullable: Option, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/vespertide-exporter/src/sqlalchemy/mod.rs b/crates/vespertide-exporter/src/sqlalchemy/mod.rs index ec13683..9e65e7a 100644 --- a/crates/vespertide-exporter/src/sqlalchemy/mod.rs +++ b/crates/vespertide-exporter/src/sqlalchemy/mod.rs @@ -65,7 +65,7 @@ impl<'a> UsedTypes<'a> { self.sa_types.insert("Uuid"); self.needs_uuid = true; } - SimpleColumnType::Json | SimpleColumnType::Jsonb => { + SimpleColumnType::Json => { self.sa_types.insert("JSON"); } SimpleColumnType::Inet | SimpleColumnType::Cidr | SimpleColumnType::Macaddr => { @@ -461,7 +461,7 @@ fn column_type_to_python(col_type: &ColumnType, nullable: bool) -> String { SimpleColumnType::Interval => "str", SimpleColumnType::Bytea => "bytes", SimpleColumnType::Uuid => "UUID", - SimpleColumnType::Json | SimpleColumnType::Jsonb => "dict", + SimpleColumnType::Json => "dict", SimpleColumnType::Inet | SimpleColumnType::Cidr => "str", SimpleColumnType::Macaddr => "str", SimpleColumnType::Xml => "str", @@ -505,7 +505,7 @@ fn column_type_to_sqlalchemy(col_type: &ColumnType) -> String { SimpleColumnType::Interval => "Interval".into(), SimpleColumnType::Bytea => "LargeBinary".into(), SimpleColumnType::Uuid => "Uuid".into(), - SimpleColumnType::Json | SimpleColumnType::Jsonb => "JSON".into(), + SimpleColumnType::Json => "JSON".into(), SimpleColumnType::Inet | SimpleColumnType::Cidr | SimpleColumnType::Macaddr => { "String(255)".into() } @@ -829,7 +829,6 @@ mod tests { col("bytea_col", ColumnType::Simple(SimpleColumnType::Bytea)), col("uuid_col", ColumnType::Simple(SimpleColumnType::Uuid)), col("json_col", ColumnType::Simple(SimpleColumnType::Json)), - col("jsonb_col", ColumnType::Simple(SimpleColumnType::Jsonb)), col("inet_col", ColumnType::Simple(SimpleColumnType::Inet)), col("cidr_col", ColumnType::Simple(SimpleColumnType::Cidr)), col("macaddr_col", ColumnType::Simple(SimpleColumnType::Macaddr)), @@ -1241,13 +1240,6 @@ mod tests { assert!(used.sa_types.contains("JSON")); } - #[test] - fn test_used_types_jsonb() { - let mut used = UsedTypes::default(); - used.add_column_type(&ColumnType::Simple(SimpleColumnType::Jsonb), false); - assert!(used.sa_types.contains("JSON")); - } - #[test] fn test_used_types_inet() { let mut used = UsedTypes::default(); diff --git a/crates/vespertide-exporter/src/sqlmodel/mod.rs b/crates/vespertide-exporter/src/sqlmodel/mod.rs index 41cd99d..006b092 100644 --- a/crates/vespertide-exporter/src/sqlmodel/mod.rs +++ b/crates/vespertide-exporter/src/sqlmodel/mod.rs @@ -448,7 +448,7 @@ fn column_type_to_python(col_type: &ColumnType, nullable: bool) -> String { SimpleColumnType::Interval => "str", SimpleColumnType::Bytea => "bytes", SimpleColumnType::Uuid => "UUID", - SimpleColumnType::Json | SimpleColumnType::Jsonb => "dict", + SimpleColumnType::Json => "dict", SimpleColumnType::Inet | SimpleColumnType::Cidr => "str", SimpleColumnType::Macaddr => "str", SimpleColumnType::Xml => "str", @@ -826,7 +826,6 @@ mod tests { col("bytea_col", ColumnType::Simple(SimpleColumnType::Bytea)), col("uuid_col", ColumnType::Simple(SimpleColumnType::Uuid)), col("json_col", ColumnType::Simple(SimpleColumnType::Json)), - col("jsonb_col", ColumnType::Simple(SimpleColumnType::Jsonb)), col("inet_col", ColumnType::Simple(SimpleColumnType::Inet)), col("cidr_col", ColumnType::Simple(SimpleColumnType::Cidr)), col("macaddr_col", ColumnType::Simple(SimpleColumnType::Macaddr)), @@ -854,7 +853,6 @@ mod tests { assert!(result.contains("bytea_col: bytes")); assert!(result.contains("uuid_col: UUID")); assert!(result.contains("json_col: dict")); - assert!(result.contains("jsonb_col: dict")); assert!(result.contains("inet_col: str")); assert!(result.contains("cidr_col: str")); assert!(result.contains("macaddr_col: str")); diff --git a/crates/vespertide-query/src/sql/helpers.rs b/crates/vespertide-query/src/sql/helpers.rs index 1de8b4f..84d3d16 100644 --- a/crates/vespertide-query/src/sql/helpers.rs +++ b/crates/vespertide-query/src/sql/helpers.rs @@ -93,9 +93,6 @@ pub fn apply_column_type_with_table(col: &mut SeaColumnDef, ty: &ColumnType, tab SimpleColumnType::Json => { col.json(); } - SimpleColumnType::Jsonb => { - col.json_binary(); - } SimpleColumnType::Inet => { col.custom(Alias::new("INET")); } @@ -420,7 +417,6 @@ mod tests { #[case(SimpleColumnType::Bytea)] #[case(SimpleColumnType::Uuid)] #[case(SimpleColumnType::Json)] - #[case(SimpleColumnType::Jsonb)] #[case(SimpleColumnType::Inet)] #[case(SimpleColumnType::Cidr)] #[case(SimpleColumnType::Macaddr)] From b37f0bc9e78af7eb87c4b1e1283df327e6f3042a Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 16 Jan 2026 13:32:22 +0900 Subject: [PATCH 2/2] Fix test --- crates/vespertide-loader/src/migrations.rs | 8 +++++--- crates/vespertide-loader/src/models.rs | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/vespertide-loader/src/migrations.rs b/crates/vespertide-loader/src/migrations.rs index 9cab2bc..762bee9 100644 --- a/crates/vespertide-loader/src/migrations.rs +++ b/crates/vespertide-loader/src/migrations.rs @@ -324,9 +324,11 @@ actions: assert!(err_msg.contains("CARGO_MANIFEST_DIR environment variable not set")); // Restore the original value if it existed - // Note: In a test environment, we don't restore to avoid affecting other tests - // The serial_test ensures tests run sequentially - drop(original); + if let Some(val) = original { + unsafe { + env::set_var("CARGO_MANIFEST_DIR", val); + } + } } #[test] diff --git a/crates/vespertide-loader/src/models.rs b/crates/vespertide-loader/src/models.rs index 1e60f82..d142174 100644 --- a/crates/vespertide-loader/src/models.rs +++ b/crates/vespertide-loader/src/models.rs @@ -374,7 +374,12 @@ mod tests { let err_msg = result.unwrap_err().to_string(); assert!(err_msg.contains("CARGO_MANIFEST_DIR environment variable not set")); - drop(original); + // Restore the original value if it existed + if let Some(val) = original { + unsafe { + env::set_var("CARGO_MANIFEST_DIR", val); + } + } } #[test]