vespertide
dev-five-git/vespertideDefine database schemas in JSON and generate migration plans. Use this skill when creating or modifying database models, defining tables with columns, constraints, and ENUM types for Vespertide-based projects.
SKILL.md
name: vespertide description: Define database schemas in JSON and generate migration plans. Use this skill when creating or modifying database models, defining tables with columns, constraints, and ENUM types for Vespertide-based projects.
Vespertide Database Schema Definition
Declarative database schema management. Define tables in JSON, generate typed migrations and SQL.
Schema Validation (MANDATORY)
Every model file MUST include $schema. Before saving: ensure no IDE validation errors, then run vespertide diff.
{
"$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json",
"name": "table_name",
"columns": []
}
Post-Edit Validation (MANDATORY)
After EVERY model edit, run:
vespertide diff # Check for parsing errors and expected changes
vespertide sql # Preview generated SQL for correctness
Only proceed to vespertide revision after both pass cleanly.
Installation
cargo install vespertide-cli
CLI Commands
| Command | Description |
|---|---|
vespertide init |
Initialize vespertide.json with defaults |
vespertide new <name> |
Create model template with $schema |
vespertide new <name> -f yaml |
Create model in specific format (json/yaml/yml) |
vespertide diff |
Show pending changes between migrations and models |
vespertide sql |
Preview SQL for next migration (default: postgres) |
vespertide sql -b mysql |
SQL for specific backend (postgres/mysql/sqlite) |
vespertide log |
Show SQL per applied migration |
vespertide log -b mysql |
Migration log for specific backend |
vespertide status |
Show config and sync overview |
vespertide revision -m "msg" |
Create migration file |
vespertide revision -m "msg" --fill-with table.col=value |
Create migration with fill values (repeatable) |
vespertide export --orm seaorm |
Export to ORM code (seaorm/sqlalchemy/sqlmodel) |
vespertide export --orm seaorm -d out/ |
Export to custom directory |
Configuration (vespertide.json)
{
"modelsDir": "models",
"migrationsDir": "migrations",
"tableNamingCase": "snake",
"columnNamingCase": "snake",
"modelFormat": "json",
"migrationFormat": "json",
"migrationFilenamePattern": "%04v_%m",
"modelExportDir": "src/models",
"prefix": "",
"seaorm": {
"extraEnumDerives": ["vespera::Schema"],
"extraModelDerives": [],
"enumNamingCase": "camel",
"vesperaSchemaType": true
}
}
| Field | Type | Default | Description |
|---|---|---|---|
modelsDir |
string | "models" |
Model JSON files directory |
migrationsDir |
string | "migrations" |
Migration files directory |
tableNamingCase |
string | "snake" |
snake / camel / pascal |
columnNamingCase |
string | "snake" |
snake / camel / pascal |
modelFormat |
string | "json" |
json / yaml / yml |
migrationFormat |
string | "json" |
json / yaml / yml |
migrationFilenamePattern |
string | "%04v_%m" |
%v=version, %m=message |
modelExportDir |
string | "src/models" |
ORM export output directory |
prefix |
string | "" |
Prefix for all table names |
SeaORM Config (seaorm nested object):
| Field | Type | Default | Description |
|---|---|---|---|
extraEnumDerives |
string[] | ["vespera::Schema"] |
Extra derives for generated enums |
extraModelDerives |
string[] | [] |
Extra derives for entity models |
enumNamingCase |
string | "camel" |
Serde rename_all for enums |
vesperaSchemaType |
bool | true |
Generate vespera::schema_type! calls |
Exported ORM Files (DO NOT EDIT)
CRITICAL: Files in
src/models/(e.g.src/models/*.rs,src/models/*.py) are AUTO-GENERATED byvespertide export. NEVER manually create or edit these files. Always edit source models inmodels/*.json, then regenerate:
# Edit models/*.json -> regenerate
vespertide export --orm seaorm
Migration / Revision Files (DO NOT EDIT)
CRITICAL: Migration files in the
migrations/directory are AUTO-GENERATED byvespertide revision. NEVER manually create, edit, or modify revision files. Always edit source models inmodels/*.json, then create a new revision:
# Edit models/*.json -> create revision
vespertide revision -m "describe your change"
Only exception: Adding fill_with values when prompted (NOT NULL columns without defaults).
{
"type": "add_column",
"table": "user",
"column": {
"name": "status",
"type": "text",
"nullable": false
},
"fill_with": "'active'"
}
Model Structure
{
"$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json",
"name": "table_name",
"description": "Optional table description",
"columns": [ /* ColumnDef[] */ ],
"constraints": [ /* Optional: CHECK constraints only */ ]
}
| Field | Required | Type | Description |
|---|---|---|---|
name |
yes | string | Table name (snake_case) |
columns |
yes | array | Column definitions |
description |
no | string | Table documentation |
constraints |
no | array | Table-level constraints (CHECK only) |
Column Definition
Required Fields
{ "name": "column_name", "type": "ColumnType", "nullable": false }
Optional Fields
| Field | Type | Description |
|---|---|---|
default |
string | boolean | number | Default value |
comment |
string | Column documentation |
primary_key |
boolean | object | Inline primary key |
unique |
boolean | string | string[] | Inline unique constraint |
index |
boolean | string | string[] | Inline index |
foreign_key |
string | object | Inline foreign key |
Column Types
Simple Types (string values)
| Type | SQL | Type | SQL |
|---|---|---|---|
"small_int" |
SMALLINT | "text" |
TEXT |
"integer" |
INTEGER | "boolean" |
BOOLEAN |
"big_int" |
BIGINT | "uuid" |
UUID |
"real" |
REAL | "json" |
JSON |
"double_precision" |
DOUBLE PRECISION | "bytea" |
BYTEA |
"date" |
DATE | "inet" |
INET |
"time" |
TIME | "cidr" |
CIDR |
"timestamp" |
TIMESTAMP | "macaddr" |
MACADDR |
"timestamptz" |
TIMESTAMPTZ | "xml" |
XML |
"interval" |
INTERVAL |
Complex Types (object values)
{ "kind": "varchar", "length": 255 }
{ "kind": "char", "length": 2 }
{ "kind": "numeric", "precision": 10, "scale": 2 }
{ "kind": "enum", "name": "...", "values": [...] }
{ "kind": "custom", "custom_type": "..." }
Enum Types (RECOMMENDED for status/category fields)
String Enum (PostgreSQL native enum):
{
"name": "status",
"type": {
"kind": "enum",
"name": "article_status",
"values": ["draft", "review", "published", "archived"]
},
"nullable": false,
"default": "'draft'"
}
Integer Enum (stored as INTEGER -- no DB migration needed for new values):
{
"name": "role",
"type": {
"kind": "enum",
"name": "user_role",
"values": [
{ "name": "guest", "value": 0 },
{ "name": "user", "value": 10 },
{ "name": "moderator", "value": 50 },
{ "name": "admin", "value": 100 }
]
},
"nullable": false,
"default": 0
}
Tip: Leave gaps in integer values (0, 10, 50, 100) to allow inserting new values without renumbering.
| Scenario | Recommended Type |
|---|---|
| Status fields (order_status, user_status) | String or Integer enum |
| Categories with fixed values | String enum |
| Priority/severity levels | Integer enum |
| Roles with potential expansion | Integer enum |
Inline Constraints (PREFERRED)
Always define constraints on columns. Use table-level
constraintsONLY for CHECK expressions.
Primary Key
{ "name": "id", "type": "integer", "nullable": false, "primary_key": true }
{ "name": "id", "type": "integer", "nullable": false, "primary_key": { "auto_increment": true } }
Unique
{ "name": "email", "type": "text", "nullable": false, "unique": true }
Named composite unique:
{ "name": "tenant_id", "type": "integer", "nullable": false, "unique": ["uq_tenant_user"] },
{ "name": "username", "type": "text", "nullable": false, "unique": ["uq_tenant_user"] }
Index
{ "name": "email", "type": "text", "nullable": false, "index": true }
Composite index:
{ "name": "user_id", "type": "integer", "nullable": false, "index": ["idx_user_created"] },
{ "name": "created_at", "type": "timestamptz", "nullable": false, "index": ["idx_user_created"] }
Foreign Key
Object syntax (recommended):
{
"name": "user_id",
"type": "integer",
"nullable": false,
"foreign_key": {
"ref_table": "user",
"ref_columns": ["id"],
"on_delete": "cascade",
"on_update": null
},
"index": true
}
Shorthand syntax:
{ "name": "user_id", "type": "integer", "nullable": false, "foreign_key": "user.id", "index": true }
Reference Actions (snake_case): "cascade", "restrict", "set_null", "set_default", "no_action"
Always add
"index": trueon foreign key columns for query performance.
Composite Primary Key (Inline)
Both columns with "primary_key": true create a single composite primary key:
{
"columns": [
{ "name": "user_id", "type": "integer", "nullable": false, "primary_key": true },
{ "name": "role_id", "type": "integer", "nullable": false, "primary_key": true }
]
}
Table-Level Constraints (CHECK only)
Use inline constraints for everything else (PK, unique, index, FK).
"constraints": [
{ "type": "check", "name": "check_positive_amount", "expr": "amount > 0" },
{ "type": "check", "name": "check_dates", "expr": "end_date > start_date" }
]
Default Values
| Type | Example | Notes |
|---|---|---|
| String literal | "'pending'" |
Single quotes inside string |
| Boolean | true / false |
Native JSON boolean |
| Integer | 0 |
Native JSON number |
| Float | 0.0 |
Native JSON number |
| SQL function | "NOW()" |
No quotes around function |
| UUID generation | "gen_random_uuid()" |
PostgreSQL |
Runtime Migration Macro
Use vespertide_migration! to run migrations at application startup:
[dependencies]
vespertide = "0.1"
sea-orm = { version = "2.0.0-rc", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"] }
use sea_orm::Database;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let db = Database::connect("postgres://user:pass@localhost/mydb").await?;
vespertide::vespertide_migration!(db).await?;
Ok(())
}
The macro generates database-specific SQL at compile time for zero-runtime overhead.
Complete Examples
User Table with Enum Status
{
"$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json",
"name": "user",
"columns": [
{ "name": "id", "type": "integer", "nullable": false, "primary_key": { "auto_increment": true } },
{ "name": "email", "type": "text", "nullable": false, "unique": true, "index": true },
{ "name": "name", "type": { "kind": "varchar", "length": 100 }, "nullable": false },
{
"name": "status",
"type": { "kind": "enum", "name": "user_status", "values": ["pending", "active", "suspended", "deleted"] },
"nullable": false,
"default": "'pending'"
},
{ "name": "metadata", "type": "json", "nullable": true },
{ "name": "created_at", "type": "timestamptz", "nullable": false, "default": "NOW()" },
{ "name": "updated_at", "type": "timestamptz", "nullable": true }
]
}
Order Table with Integer Enum and CHECK
{
"$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json",
"name": "order",
"columns": [
{ "name": "id", "type": "uuid", "nullable": false, "primary_key": true, "default": "gen_random_uuid()" },
{
"name": "customer_id",
"type": "integer",
"nullable": false,
"foreign_key": { "ref_table": "customer", "ref_columns": ["id"], "on_delete": "restrict" },
"index": true
},
{ "name": "total", "type": { "kind": "numeric", "precision": 10, "scale": 2 }, "nullable": false },
{
"name": "priority",
"type": {
"kind": "enum",
"name": "order_priority",
"values": [
{ "name": "low", "value": 0 },
{ "name": "normal", "value": 10 },
{ "name": "high", "value": 20 },
{ "name": "urgent", "value": 30 }
]
},
"nullable": false,
"default": 10
},
{
"name": "status",
"type": { "kind": "enum", "name": "order_status", "values": ["pending", "confirmed", "shipped", "delivered", "cancelled"] },
"nullable": false,
"default": "'pending'"
},
{ "name": "notes", "type": "text", "nullable": true },
{ "name": "created_at", "type": "timestamptz", "nullable": false, "default": "NOW()" }
],
"constraints": [
{ "type": "check", "name": "check_total_positive", "expr": "total >= 0" }
]
}
Many-to-Many Join Table
{
"$schema": "https://raw.githubusercontent.com/dev-five-git/vespertide/refs/heads/main/schemas/model.schema.json",
"name": "user_role",
"columns": [
{
"name": "user_id",
"type": "integer",
"nullable": false,
"primary_key": true,
"foreign_key": { "ref_table": "user", "ref_columns": ["id"], "on_delete": "cascade" }
},
{
"name": "role_id",
"type": "integer",
"nullable": false,
"primary_key": true,
"foreign_key": { "ref_table": "role", "ref_columns": ["id"], "on_delete": "cascade" },
"index": true
},
{ "name": "granted_at", "type": "timestamptz", "nullable": false, "default": "NOW()" },
{ "name": "granted_by", "type": "integer", "nullable": true, "foreign_key": "user.id" }
]
}
Workflow Summary
# 1. Create model
vespertide new user
# 2. Edit models/user.json
# 3. Validate
vespertide diff # Check changes
vespertide sql # Preview SQL
# 4. Create migration
vespertide revision -m "create user table"
# 5. Export ORM code (if needed)
vespertide export --orm seaorm
Guidelines Summary
MUST DO
- Always include
$schemain every model file - Always specify
nullableon every column - Run
vespertide diff+vespertide sqlafter every model edit - Index foreign key columns (
"index": true) - Use inline constraints (
primary_key,unique,index,foreign_keyon columns)
SHOULD DO
- Use enums for status/category fields (prefer over text + CHECK)
- Use integer enums for expandable value sets (no migration needed)
- Use
timestamptzovertimestamp(timezone-aware) - Use
jsontype for JSON data (cross-backend compatible)
MUST NOT DO
- Manually create/edit/modify revision (migration) files -- use
vespertide revisiononly - Manually create/edit
src/models/*.rs(or*.py) files -- usevespertide exportto regenerate - Use PascalCase for reference actions -- use
"cascade"not"Cascade" - Skip schema validation
- Add NOT NULL columns without
defaultorfill_with - Use table-level constraints for anything except CHECK
- Use
jsonbtype -- usejsoninstead (not supported cross-backend) - Use
customtypes -- breaks cross-database compatibility - Use array types -- use a join table instead
Naming Conventions
| Item | Convention | Example |
|---|---|---|
| Tables | snake_case | user_role |
| Columns | snake_case | created_at |
| Indexes | ix_{table}__{columns} |
ix_user__email |
| Unique | uq_{table}__{columns} |
uq_user__email |
| Foreign Key | fk_{table}__{columns} |
fk_post__author_id |
| Check | check_{description} |
check_positive_amount |
| Enums | snake_case | order_status |
Note: Auto-generated constraint names use double underscore
__as separator.
Quick Reference
SIMPLE TYPES COMPLEX TYPES
---------------------------------------- ----------------------------------------
integer, big_int, small_int Numbers { "kind": "varchar", "length": N }
real, double_precision Floats { "kind": "char", "length": N }
text Strings { "kind": "numeric", "precision": P, "scale": S }
boolean Flags { "kind": "enum", "name": "...", "values": [...] }
date, time, timestamp Time { "kind": "custom", "custom_type": "..." }
timestamptz, interval Time+
uuid UUIDs REFERENCE ACTIONS (snake_case!)
json JSON ----------------------------------------
bytea Binary cascade, restrict, set_null,
inet, cidr, macaddr Network set_default, no_action
xml XML
CONSTRAINT TYPES (inline preferred) DATABASE BACKENDS
---------------------------------------- ----------------------------------------
primary_key, unique, index, postgres (default), mysql, sqlite
foreign_key, check
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
Invalid enum in on_delete |
PascalCase used | Use "cascade" not "Cascade" |
| Missing required property | nullable omitted |
Add "nullable": true/false |
| Unknown column type | Typo in type name | Check column types table above |
| FK validation failed | Referenced table missing | Create referenced table first |
| NOT NULL without default | Adding column to existing table | Add default or use fill_with in revision |