Chitragupt: declarative migrations

Chitragupt is the ORM and migration engine — the orm chitragupt line in the project manifest selects it. Its defining idea is that you never write a migration. You describe the schema you want — implicitly, by authoring entities — and Chitragupt figures out the SQL to get the database there. It is a desired-state engine (the Atlas/Terraform model), not a replay-a-fixed-log one.

The desired state comes from your entities

You already authored the schema without knowing it. Every entity, its fields, its relationships, its indexes, and its persistence-layer invariants are the desired schema. When you build the project, that intent is compiled into one artifact — a machine-readable desired schema (gen/schema.json) — the single source of truth for what the database should look like.

Take Chowk's OrderLine:

sub-domains/ordering/entities/order_line.vishwakarma
entity OrderLine aggregate_root global {
  audit   timestamps
  indexes [
    { name "uk_order_line_id"    kind unique   fields [id] }
    { name "ix_order_line_order" kind standard fields [order_id] }
  ]

  id         uuid  pk filterable[eq]
  order_id   uuid  required immutable filterable[eq] relates Order many_to_one on_delete cascade
  product_id uuid  required immutable filterable[eq]
  quantity   int32 required
  unit_price Money required
  created_at timestamp immutable sortable
  updated_at timestamp
}

From this alone, Chitragupt knows the table it wants: an order_line table with typed columns, a primary key on id, a foreign key order_id → order(id) that is ON DELETE CASCADE (from on_delete cascade), a unique index and a standard index, and the audit timestamp columns. You wrote no DDL — the table is derived.

generate diffs desired vs. actual; migrate applies the delta

Two commands carry the model:

  • chitragupt generate connects to the live database, reads its actual schema, compares it against the desired schema (gen/schema.json), and writes a migration containing only the difference. If the database already matches, the diff is empty — "Schema is up to date. No migration needed." — and nothing is written. This is what makes it idempotent: re-running against an up-to-date database is a no-op.
  • chitragupt migrate applies the pending migration to the database.

Because the migration is computed from the current state of the target, the same desired schema produces different SQL against different databases — a fresh database gets CREATE TABLE; an existing one missing a single column gets just ALTER TABLE … ADD COLUMN. You never think about "which migration number am I on"; you think about "what should the schema be," and the delta to get there is derived.

This is why migrations aren't committed. The repo's source of truth is the desired schema (gen/schema.json), authored via entities. The SQL is derived at deploy time against whatever the database currently is. There is no hand-maintained migrations/*.sql log to drift from the model.

What the diff covers

Chitragupt's diff manages the full shape of a relational schema, not just tables:

ObjectDerived from
Schemas (one per sub-domain)the sub-domain
Tables & typed columnsthe entity and its fields
Nullability, defaults, immutabilityfield flags (required, set, immutable)
Primary keyspk
Foreign keys + ON DELETE actionrelates … on_delete cascade | restrict | set null
Unique & standard indexesthe indexes [...] block
Enum typesenum declarations
CHECK constraintspersistence-layer invariants (below)
Triggerslifecycle/consistency rules that need them

Persistence invariants become CHECK constraints

An invariant whose rule is over the row's own fields, targeting persistence, is enforced in the database as a CHECK constraint. Chowk's stock ledger can never over-reserve:

sub-domains/inventory/invariants/stock_covers_reservations.vishwakarma
invariant StockCoversReservations "Reserved stock can never exceed available stock." {
  expr       "this.reserved <= this.available"
  enforce_on [create, update]
  target     persistence
  error      "InsufficientStock"
}

Chitragupt emits this as a CHECK (reserved <= available) on the stock_level table. The database itself refuses a violating row — a guarantee no application bug can bypass — and the framework maps the constraint violation back to the declared InsufficientStock error (a typed 409). You declared a business rule; it materialized as a database constraint.

Verifying an apply landed

After migrate, confirm the schema is really there rather than trusting the exit code — query the new tables (\dt <sub-domain>.*), check a constraint exists, insert a violating row and watch the CHECK reject it. A clean generate afterward (empty diff) is the proof the database now matches the desired schema exactly.

The migration workflow page covers the full CLI lifecycle — status, rollback, validation, and the deploy-time story — and Row-Level Security & roles covers how Chitragupt secures the tables it creates.