Migrations
Migrations are the rockstar way to evolve your database schema over time. AstrolaDB automatically generates migration files that track every change to your schema, making it easy to apply updates, roll back changes, and keep your team in sync.
Overview
Section titled “Overview”Migrations are JavaScript files that live in your migrations/ directory. Each
migration contains two functions:
up(m)- Applies the schema changes (create tables, add columns, etc.)down(m)- Reverses those changes (for rollbacks)
Migrations are auto-generated by comparing your current schema files against the state captured in previous migrations. You never write migrations by hand — but you can edit them after generation for fine-tuning.
Upgrade (Migrate)
Section titled “Upgrade (Migrate)”export default migration({ up(m) { m.create_table("auth.user", (t) => {}); m.create_table("blog.post", (t) => {}); m.create_index("blog.post", ["author_id"]); },});Downgrade (Rollback)
Section titled “Downgrade (Rollback)”export default migration({ down(m) { m.drop_table("auth.user"); m.drop_table("blog.post"); },});Generating Migrations
Section titled “Generating Migrations”Use the alab new command to generate a migration:
# Generate a migration with a descriptive namealab new add_user_auth
# Generate a migration for any schema changesalab new update_blog_schemaAstrolaDB compares your current schema files against the last migration’s state,
computes the diff, and generates a sequentially numbered migration file like
001_add_user_auth.js.
Migration File Structure
Section titled “Migration File Structure”Here’s what a real migration looks like:
// Migration: start
export default migration({ up(m) { // Table: auth.role m.create_table("auth.role", (t) => { t.id(); t.string("name", 100).unique(); t.timestamps(); });
// Table: auth.user m.create_table("auth.user", (t) => { t.id(); t.string("email", 255).unique(); t.boolean("is_active").default(true); t.string("password", 255); t.string("username", 50).unique(); t.timestamps(); });
// Table: auth_role_auth_user m.create_table("auth_role_auth_user", (t) => { t.belongs_to("auth.role"); t.belongs_to("auth.user"); }); // Index: auth_role_auth_user m.create_index("auth_role_auth_user", ["role_id"], { name: "idx_auth_role_auth_user_role_id", }); // Index: auth_role_auth_user m.create_index("auth_role_auth_user", ["user_id"], { name: "idx_auth_role_auth_user_user_id", }); // Index: auth_role_auth_user m.create_index("auth_role_auth_user", ["role_id", "user_id"], { unique: true, name: "idx_auth_role_auth_user_unique", });
// Table: blog.post m.create_table("blog.post", (t) => { t.id(); t.belongs_to("auth.user").as("author"); t.text("body"); t.datetime("published_at").optional(); t.string("slug", 255).unique(); t.enum("status", ["draft", "published", "archived"]).default("draft"); t.string("title", 200); t.datetime("deleted_at").optional(); t.timestamps(); }); // Index: blog.post m.create_index("blog.post", ["author_id"]); },
down(m) { m.drop_table("blog.post"); m.drop_table("auth_role_auth_user"); m.drop_table("auth.user"); m.drop_table("auth.role"); },});Key points:
- The
mparameter is the migration builder with methods for schema operations - The
tparameter (insidecreate_table) is the table builder for defining columns - Comments help organize operations by table
- Tables are created in dependency order (referenced tables first)
- The
down()function reverses everything inup()
Migration DSL Reference
Section titled “Migration DSL Reference”These are the operations available in generated migrations. Use this reference when reviewing or editing migrations.
The migration builder m provides these operations:
| Method | Description |
|---|---|
m.create_table(ref, fn) | Create a new table. Pass a callback that receives the table builder. |
m.drop_table(ref) | Drop an existing table. |
m.rename_table(oldRef, newName) | Rename a table. |
m.add_column(ref, fn) | Add a column to an existing table. |
m.drop_column(ref, name) | Drop a column from a table. |
m.rename_column(ref, oldName, newName) | Rename a column. |
m.alter_column(ref, name, fn) | Alter a column’s type or constraints. |
m.create_index(ref, columns, opts?) | Create an index. Options: name, unique. |
m.drop_index(name) | Drop an index by name. |
m.add_foreign_key(ref, cols, refTable, refCols, opts?) | Add a foreign key constraint. Options: name, on_delete, on_update. |
m.drop_foreign_key(ref, name) | Drop a foreign key constraint. |
m.add_check(ref, name, expression) | Add a CHECK constraint. |
m.drop_check(ref, name) | Drop a CHECK constraint. |
m.sql(statement) | Execute raw SQL. |
Example usage:
up(m) { // Add a new column m.add_column("auth.user", (t) => { t.string("phone", 20).optional(); });
// Create an index m.create_index("auth.user", ["email"], { name: "idx_user_email" });
// Add a CHECK constraint m.add_check("auth.user", "chk_age", "age >= 18");
// Raw SQL for complex operations m.sql("CREATE EXTENSION IF NOT EXISTS pgcrypto;");}Table Builder Reference
Section titled “Table Builder Reference”Inside create_table, the table builder t provides:
| Method | Description |
|---|---|
t.id() | UUID primary key column (named id). |
t.timestamps() | Adds created_at and updated_at columns. |
t.soft_delete() | Adds a nullable deleted_at datetime column. |
t.sortable() | Adds a position integer column (default 0). |
t.belongs_to(ref) | Foreign key to another table. Chainable with .as(alias), .optional(), .on_delete(action), .on_update(action). |
t.one_to_one(ref) | Unique foreign key (same chainable API as belongs_to). |
t.many_to_many(ref) | Many-to-many relationship (generates a junction table automatically). |
t.belongs_to_any(refs, opts?) | Polymorphic relationship. Adds {as}_type and {as}_id columns. Options: { as: "name" }. |
t.string(name, length) | Variable-length string column. |
t.text(name) | Text column (unlimited length). |
t.integer(name) | Integer column. |
t.float(name) | Floating-point number. |
t.decimal(name, precision, scale) | Decimal column for precise numbers. |
t.boolean(name) | Boolean column. |
t.date(name) | Date column (no time). |
t.time(name) | Time column (no date). |
t.datetime(name) | Timestamp column. |
t.uuid(name) | UUID column. |
t.json(name) | JSON column. |
t.base64(name) | Base64-encoded binary column. |
t.enum(name, values) | Enum column with predefined values. |
Column modifiers (chainable):
.optional()- Allow NULL values.unique()- Add unique constraint.default(value)- Set default value.backfill(value)- Set backfill value for existing rows when adding a NOT NULL column
Example usage:
m.create_table("products.item", (t) => { t.id(); t.string("sku", 50).unique(); t.string("name", 200); t.text("description").optional(); t.decimal("price", 10, 2); t.integer("stock").default(0); t.boolean("available").default(true); t.enum("category", ["electronics", "clothing", "food"]); t.belongs_to("products.brand").optional().on_delete("SET NULL"); t.timestamps();});Running Migrations
Section titled “Running Migrations”Once you’ve generated migrations, use these commands to apply or rollback changes:
# Apply all pending migrationsalab migrate
# Preview SQL without applyingalab migrate --dry
# Apply migrations and create a git commitalab migrate --commit
# Rollback the last migrationalab rollback
# Check which migrations are pendingalab statusHow Generation Works
Section titled “How Generation Works”AstrolaDB’s migration generator is deterministic and smart:
- Diff calculation - Compares your current schema files against the state stored in the last migration
- Dependency ordering - Tables are created in dependency order (referenced tables first, referencing tables after)
- Column sorting - Columns appear in a predictable order:
idfirst, then alphabetically, thentimestampslast - Automatic reversals - The
down()function is auto-generated as the reverse ofup()
Editing Migrations
Section titled “Editing Migrations”Migrations are always generated — never written from scratch. However, after generation you can edit them to add custom logic like raw SQL, data migrations, or tweaks that the generator can’t infer.
Typical workflow:
- Update your schema files (in
schemas/) - Run
alab new <name>to generate a new migration - Review and optionally edit the generated migration
- Run
alab migrateto apply it
This keeps your schema definitions as the single source of truth while giving you full control over the migration logic when needed.