Skip to content

Run migrations

You’ll change the schema, generate the migration, apply it, roll it back and apply it again, then wire migrations into deployment.

  • A project with a schema and at least one applied migration (Set up a project).
  • The .gisila/ snapshot directory committed with the code.

Add a nullable slug column to Post in lib/models/blog.gisila.yaml:

slug: { type: varchar, max_length: 220, is_unique: true }
Terminal window
dart run gisila_orm:generate
Generated migration files:
Up: lib/models/migrations/1730460000000_auto_project_changes.up.sql
Down: lib/models/migrations/1730460000000_auto_project_changes.down.sql

Each 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.

-- Migration: auto_project_changes
-- Generated on: 2026-11-01T10:00:00.000
BEGIN;
-- Add column: posts.slug
ALTER 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.

Terminal window
dart run gisila_orm:migrate status
dart run gisila_orm:migrate up
Discovered: 2, applied: 1
[x] 1730000000000_initial_schema
[ ] 1730460000000_auto_project_changes
Applied 1 migration(s) in batch 2:
- 1730460000000_auto_project_changes

By default migrate searches lib/ recursively. Pass --dir lib/models/migrations to be explicit.

Terminal window
dart run gisila_orm:migrate down # last batch
dart run gisila_orm:migrate down --steps 2
dart run gisila_orm:migrate up

Rolling 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.

Removing a column prints an error and writes nothing:

The schema change would lose data, so no migration was written:
- Drop column: posts.slug
Check these are intended (back up the data first if needed), then
re-run with --allow-destructive.

Once you’re sure:

Terminal window
dart run gisila_orm:generate --allow-destructive

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-transaction
CREATE 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";

Run migrate up before starting the new version of your app:

Terminal window
export DATABASE_URL='postgres://app:secret@db.internal:5432/blog?sslmode=verify-full'
dart run gisila_orm:migrate up

Concurrent 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.

Terminal window
dart run gisila_orm:migrate status

Every migration shows [x] and none is marked (edited after it was applied).