Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

L12 - Migrations

Authenticated estate graph and history-free push — one differ, one compiler, dialect SQL in the bound migration driver

Location: src/migrations/

Why This Layer Exists

Your TypeScript schema is the source of truth:

const user = s.model({
  id: s.string().id().ulid(),
  email: s.string().unique(),
  name: s.string().nullable(),  // Add this scalar
});

The migrations layer serializes that schema, diffs it, and compiles dialect SQL. File history and live sync share that path. They do not share storage.

Domain Model

Term Meaning
Estate One storage root: immutable target descriptor, snapshots, SQL blobs, state manifests.
Snapshot Canonical description of the VibORM-managed schema.
SQL blob Plain review SQL. Production slices authenticated byte ranges from it.
State One graph node. Identity is the manifest hash, not a filename or index.
Marker Database row for the last confirmed state and arrival path.
Ledger Append-only attempt and outcome evidence. Not reconstructed from the marker.
Push plan Ephemeral live program. Never inserted into the estate.

Two declared facts at generation time: the desired snapshot and the parent transitions. Pairing, apply order, and “latest file” are not stored.

How It Works

1. Schema snapshot

The current TypeScript schema is serialized with the exact resolved relation index:

{
  tables: [{
    name: "user",
    columns: [{
      name: "email",
      type: "varchar",
      nullable: false,
      unique: true
    }],
    indexes: [...],
    foreignKeys: [...]
  }],
  enums: []
}

2. Comparison baseline

push introspects the live target through the bound migration driver.

migrate generate compares against the selected parent state’s snapshot. No live database is required. The first state uses the virtual empty root.

3. Diffing

The one differ emits operations. The one resolver settles destructive and ambiguous changes.

[
  { type: "addColumn", table: "user", column: { name: "name", ... } },
  { type: "dropIndex", table: "user", indexName: "email_idx" }
]

4. Compilation

compile.ts turns resolved operations or complete manual Sql values into ordered operations, checks, typed parameters, and one SQL blob. Dialect text, probes, and transactional classification stay on the bound MigrationDriver. Query-engine SQL is not used here.

Production never reparses display SQL and never splits on ; or comments. One manual Sql value is one opaque provider dispatch.

Estate on Disk

migrations/
├── estate.json
├── snapshots/<snapshot-hash>.json
├── sql/<sql-hash>.sql
└── states/<state-id>.json

There is no journal, no numbered 0001_*.sql, no latest snapshot, and no $migrations table. Control tables are _viborm_migration_state (marker) and _viborm_migration_log (ledger).

A state becomes visible only after snapshot and SQL bytes are durable and the manifest is published through an atomic no-replace write.

Public Surface

createMigrationClient is the one composition root. createFsStorageWriter is the filesystem estate factory.

generate
check / list / show / graph
status / verify / log
apply / down / baseline / resolve / reset
push

apply({ to }) targets a full state id, unambiguous prefix, or unambiguous name. Numeric indexes have no meaning.

push never receives estate storage and never writes the marker. A non-empty push against a valid marker is refused.

Removed and not aliased: MigrationContext, journal(), pending(), squash(), parseStatements, createFsStorageDriver, path-level get/put/delete.

Operation Types

The DiffOperation union (see src/migrations/types.ts):

Operation Description
createTable New table
dropTable Remove table
renameTable Rename table
addColumn Add column to table
dropColumn Remove column
renameColumn Rename column
alterColumn Change column type/constraints
createIndex Add index
dropIndex Remove index
addForeignKey Add foreign key constraint
dropForeignKey Remove foreign key constraint
addUniqueConstraint Add unique constraint
dropUniqueConstraint Remove unique constraint
addPrimaryKey Add primary key
dropPrimaryKey Remove primary key
createEnum Create enum type
dropEnum Remove enum type
alterEnum Add/remove enum values

Ambiguous Changes

Some diffs cannot be decided from names alone:

// Did you rename "name" to "fullName"?
// Or delete "name" and create "fullName"?

Generation and push take a resolve callback. The CLI prompts during generate when a structural rename is ambiguous. Those decisions are closed into the published state or into push consent. They are not replayed from a journal.

Push vs Migrate

viborm push

History-free live sync. Preview is inert. Effectful apply requires the consent from that preview for destructive or force-reset work. Dry-run has zero database and storage effects. Push never writes estate storage or the marker.

Best for: local development, disposable databases.

viborm migrate

Authenticated state graph. Generate publishes immutable artifacts. Apply executes authenticated slices and advances the marker. down follows the recorded arrival path. Rollback restores managed schema shape; it does not restore discarded data.

Best for: production, review, branches that must converge.

See src/cli/commands/migrate.ts and viborm/migrations (createMigrationClient, createFsStorageWriter, previewPush).

Provider Limits

Effectful work is admitted only when lock and marker CAS are proven.

Provider Effectful migrate / push
PGlite, pg, postgres.js, Bun SQL Effectful when lock/CAS is proven. Transactional when every statement permits it.
SQLite3, Bun SQLite Effectful when lock/CAS is proven.
MySQL2 Stepwise. DDL implicitly commits; nominal transactions do not roll back.
Neon HTTP, PlanetScale Read-only / offline for effectful work. Generation, check, status, and push dry-run remain.
D1, D1 HTTP, LibSQL Effectful refused until proven.

No provider inherits a guarantee from its dialect name.

Connection to Other Layers

  • L2 (Scalars): Scalar state determines column definitions
  • L4 (Relations): The resolved relation index determines foreign keys and junctions
  • L5 (Schema validation): Definition-time topology; migrations do not rescan inverses
  • L7 (Adapters): Not the DDL owner. Dialect migration SQL lives on MigrationDriver
  • L8 (Drivers): Connection, pinned session, execution. Neon HTTP and PlanetScale have no effectful lock

Was this page helpful?