Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_9bKjgJSyjqvAncyixbIuf.json
Original file line number Diff line number Diff line change
@@ -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"}
20 changes: 10 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 32 additions & 6 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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" }
Expand Down Expand Up @@ -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 }
]
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions crates/vespertide-core/src/schema/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ impl ColumnType {
SimpleColumnType::Interval => "String".to_string(),
SimpleColumnType::Bytea => "Vec<u8>".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(),
Expand Down Expand Up @@ -139,7 +140,7 @@ pub enum SimpleColumnType {

// JSON types
Json,
Jsonb,
// Jsonb,

// Network types
Inet,
Expand Down Expand Up @@ -263,7 +264,7 @@ mod tests {
#[case(SimpleColumnType::Bytea, "Vec<u8>")]
#[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")]
Expand Down Expand Up @@ -294,7 +295,7 @@ mod tests {
#[case(SimpleColumnType::Bytea, "Option<Vec<u8>>")]
#[case(SimpleColumnType::Uuid, "Option<Uuid>")]
#[case(SimpleColumnType::Json, "Option<Json>")]
#[case(SimpleColumnType::Jsonb, "Option<Json>")]
// #[case(SimpleColumnType::Jsonb, "Option<Json>")]
#[case(SimpleColumnType::Inet, "Option<String>")]
#[case(SimpleColumnType::Cidr, "Option<String>")]
#[case(SimpleColumnType::Macaddr, "Option<String>")]
Expand Down
27 changes: 26 additions & 1 deletion crates/vespertide-exporter/src/seaorm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(", ")));
Expand All @@ -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<Json>".to_string()
} else {
"Json".to_string()
}
}
_ => column.r#type.to_rust_type(column.nullable),
};

Expand Down Expand Up @@ -1035,7 +1050,6 @@ mod helper_tests {
#[case(ColumnType::Simple(SimpleColumnType::Bytea), false, "Vec<u8>")]
#[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")]
Expand Down Expand Up @@ -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) }, {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Json>,
}

impl ActiveModelBehavior for ActiveModel {}
14 changes: 3 additions & 11 deletions crates/vespertide-exporter/src/sqlalchemy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 1 addition & 3 deletions crates/vespertide-exporter/src/sqlmodel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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"));
Expand Down
8 changes: 5 additions & 3 deletions crates/vespertide-loader/src/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
7 changes: 6 additions & 1 deletion crates/vespertide-loader/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading