Run migrations
You’ll change the schema, generate the migration, apply it, roll it back and apply it again, then wire migrations into deployment.
Prerequisites
Section titled “Prerequisites”- A project with a schema and at least one applied migration (Set up a project).
- The
.gisila/snapshot directory committed with the code.
1. Change the schema
Section titled “1. Change the schema”Add a nullable slug column to Post in lib/models/blog.gisila.yaml:
slug: { type: varchar, max_length: 220, is_unique: true }2. Generate the migration
Section titled “2. Generate the migration”dart run gisila_orm:generateGenerated migration files: Up: lib/models/migrations/1730460000000_auto_project_changes.up.sql Down: lib/models/migrations/1730460000000_auto_project_changes.down.sqlEach run that sees a change writes a new <millis>_auto_project_changes pair.
Rename the files if you like a descriptive name (keep the timestamp prefix and
the .up.sql / .down.sql suffixes), but do it before the migration is
applied anywhere: the file name is the id recorded in the database.
3. Review it
Section titled “3. Review it”-- Migration: auto_project_changes-- Generated on: 2026-11-01T10:00:00.000
BEGIN;
-- Add column: posts.slugALTER TABLE "posts" ADD COLUMN "slug" VARCHAR(220) UNIQUE;
COMMIT;Look for -- DESTRUCTIVE and -- GUESSED RENAME comments. Edit the SQL freely
at this stage, for example to add a backfill.
4. Check status and apply
Section titled “4. Check status and apply”dart run gisila_orm:migrate statusdart run gisila_orm:migrate upDiscovered: 2, applied: 1 [x] 1730000000000_initial_schema [ ] 1730460000000_auto_project_changes
Applied 1 migration(s) in batch 2: - 1730460000000_auto_project_changesBy default migrate searches lib/ recursively. Pass --dir lib/models/migrations
to be explicit.
5. Roll back and re-apply
Section titled “5. Roll back and re-apply”dart run gisila_orm:migrate down # last batchdart run gisila_orm:migrate down --steps 2dart run gisila_orm:migrate upRolling back runs each migration’s .down.sql. Don’t roll back in production
as a habit: the down script of a migration that added data-bearing columns
drops that data.
6. When generate refuses a change
Section titled “6. When generate refuses a change”Removing a column prints an error and writes nothing:
The schema change would lose data, so no migration was written: - Drop column: posts.slugCheck these are intended (back up the data first if needed), thenre-run with --allow-destructive.Once you’re sure:
dart run gisila_orm:generate --allow-destructive7. Add a hand-written migration
Section titled “7. Add a hand-written migration”For SQL the schema can’t express (a view, a partial index, a data fix), add your own pair in the same directory with a later timestamp:
lib/models/migrations/1730470000000_partial_index_published.up.sql:
-- gisila:no-transactionCREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_posts_published_recent" ON "posts" ("published_at") WHERE "status" = 'published';lib/models/migrations/1730470000000_partial_index_published.down.sql:
DROP INDEX IF EXISTS "idx_posts_published_recent";8. Run migrations in deployment
Section titled “8. Run migrations in deployment”Run migrate up before starting the new version of your app:
export DATABASE_URL='postgres://app:secret@db.internal:5432/blog?sslmode=verify-full'dart run gisila_orm:migrate upConcurrent runs are safe: they take an advisory lock and run one at a time. If you ship a compiled binary without the Dart SDK, run migrations from code at startup instead:
import 'package:gisila_orm/gisila.dart';
Future<void> migrate(Database db) async { final manager = MigrationManager(db); final result = await manager.up(await manager.discoverIn('lib/models/migrations')); for (final m in result.applied) { print('applied ${m.id}'); }}discoverIn reads files at runtime, so ship the migrations directory with the
binary, or build Migration objects
from embedded strings.
Verify it works
Section titled “Verify it works”dart run gisila_orm:migrate statusEvery migration shows [x] and none is marked (edited after it was applied).