Database migrations are version control for your database schema. They allow your team to define, share, and track changes to the database structure over time. With migrations, you can:
Track Changes - Every schema modification is recorded and versioned
Collaborate Safely - Team members can pull schema changes alongside code changes
Rollback Errors - Undo problematic migrations when issues arise
Environment Parity - Ensure consistent database structure across development, staging, and production
Automate Deployment - Run migrations as part of your deployment pipeline
Elegant's migration system provides a database-agnostic API that works consistently across MySQL, MariaDB, PostgreSQL, and SQLite.
Generating Migrations
Generate Migration Files
Use the make:migration command to create a new migration file:
elegant make:migration CreateUsersTable
This generates a timestamped migration file in your resources/database/migrations directory:
By default, migrations execute against your primary database connection. However, you can specify an alternative connection for migrations that need to run against a different database:
Setting the connection property directs Elegant to run this migration against the named connection defined in your elegant.config.js file. This is particularly useful when managing multiple databases or when certain tables need to reside in separate database instances. Conditional Migration Execution Elegant allows you to control whether a migration should execute based on runtime conditions. Implement the shouldRun() method to define custom logic that determines if the migration proceeds:
When shouldRun() returns false, Elegant skips the migration entirely. This feature enables environment-specific migrations, feature flag integration, or conditional schema changes based on application configuration. The method evaluates each time you run migrations, providing dynamic control over which schema changes apply to your database.
// VARCHAR with default length (255)
table.string('name');
// VARCHAR with custom length
table.string('code', 50);
// CHAR with specified length
table.char('country_code', 2);
// TEXT column for long content
table.text('description');
// LONGTEXT for very large content
table.longText('article_content');
// Boolean (stored as TINYINT(1) in MySQL/MariaDB)
table.boolean('active');
table.boolean('is_admin').default(false);
Date and Time Columns
// Date only
table.date('birth_date');
// Time only
table.time('opening_time');
// DateTime
table.dateTime('event_start');
// Timestamp
table.timestamp('created_at');
// Timestamp with current time default
table.timestamp('updated_at').default('CURRENT_TIMESTAMP');
// Convenience method for created_at and updated_at
table.timestamps();
When working with composite primary keys, Elegant supports foreign key constraints that reference multiple columns. The constraint name automatically follows the fk_ prefix pattern, with each column name joined by underscores.
Defining Composite Foreign Keys
Create a foreign key constraint that references multiple columns in another table:
Both shorthand approaches automatically infer the referenced table by converting the column name to its plural form (removing the _ id suffix). For example, company_id references the companies table.
export class DropOldTables extends Migration {
async up() {
await this.schema.dropTableIfExists('old_users');
await this.schema.dropTableIfExists('deprecated_logs');
}
async down() {
// Usually no down action needed for cleanup migrations
}
}
Creating Functions
Create stored functions within your migrations using the table.fn() method. Functions are reusable database routines that return a single value and can be called from queries.
Basic Function Definition
await this.schema.fn('get_user_email', (fn) => {
fn('get_user_email', (fn) => {
fn.params.int('user_id_in');
fn.returns.string('email_out');
fn.body = `
SELECT email INTO email_out FROM users WHERE id = user_id_in;
RETURN email_out;
`;
});
})
Return Type - Specify what the function returns using fn.returns:
Once created, you can call these functions in your queries:
// Using the query builder
const email = await db.table('users')
.selectRaw('get_user_email(id) as email')
.where('id', userId)
.first();
// Using raw queries
const postCount = await db.raw( 'SELECT calculate_user_posts_count(?) as count', [userId] );
Database Support
Function creation is currently supported for:
MySQL - Full support
MariaDB - Full support
PostgreSQL - Syntax may differ, test thoroughly
SQLite - Not supported (SQLite uses a different approach for functions)
Best Practices
Keep Functions Simple - Complex logic is often better in application code
Handle NULL Values - Use COALESCE() or NULL checks
Name Descriptively - Use clear names like calculate_* or get_*
Document Logic - Add comments in complex function bodies
Test Thoroughly - Functions are harder to debug than application code
Dropping Functions
To remove a function in a migration's down() method:
async down() {
await this.schema.dropFn('users')
// postgres requires you specify the parameters to drop function
await this.schema.dropFn('users', fn => {
fn.params.string('user_id')
})
// you can also run a raw query to drop the function
await this.db.raw('DROP FUNCTION IF EXISTS get_user_email');
}
Running Migrations
Execute All Pending Migrations
Run all migrations that haven't been executed yet:
elegant migrate
This command:
Checks which migrations have already run
Executes pending migrations in chronological order
Records successful migrations in the migrations tracking table
Check Migration Status
View which migrations have run and which are pending:
elegant migrate:status
Output example:
Migration Date
Migration Name
Status
2025-10-25T21:06:01.524Z
CreateContactsTable
pending
2025-10-25T22:47:22.000
CreateElegantMigrationTable
success
Rolling Back Migrations
Rollback Last Batch
Undo the most recent migration batch:
elegant migrate:rollback
This executes the down() method of the last batch of migrations.
Rollback Multiple Batches
Rollback a specific number of migration batches:
# Rollback last 3 batches
elegant migrate:rollback --step=3
Rollback All Migrations
Reset the database to its initial state:
elegant migrate:reset
Generating SQL Statements
Elegant can generate SQL statements from your migration files without executing them against the database. This is useful for review, version control, or integration with external database management tools.
Output Options
Save to Specific File Path
Generate SQL statements for a migration and save them to a designated file location. Use the -o or --outputFile option to specify the exact file path:
elegant make:statement --migration User -o resources/statements/User.sql
If the target directory doesn't exist, add the -f or --force flag to create it automatically:
elegant make:statement --force --migration User -o resources/statements/User.sql
Save to Specific Directory
Generate SQL statements and save them to a directory without specifying the filename. Use the -p or --outputDir option to define the target directory. The file will be named after the model class with a .sql extension:
elegant make:statement --migration User -p resources/statements
This creates resources/statements/User.sql. If the directory doesn't exist, include the -f or --force flag:
elegant make:statement --force --migration User -p resources/statements
Output to Console
Print the generated SQL directly to standard output by omitting both file and directory options:
elegant make:statement --migration User
This allows you to:
Review SQL statements before execution
Pipe output to other database tools or scripts
Integrate with version control workflows
Process statements through custom pipelines
Example with piping:
elegant make:statement --migration User | mysql -u root -p database_name
Migration Best Practices
Always Provide Down Methods Every migration should have a working down() method:
// Good - single purpose
export class AddEmailVerificationToUsers extends Migration {
// Only adds email verification fields
}
// Bad - does too much
export class UpdateUsersAndPosts extends Migration {
// Modifies multiple unrelated tables
}
3. Test Migrations Both Ways
Always test both up() and down() methods:
# Run migration
elegant migrate
# Test rollback
elegant migrate:rollback
# Re-run migration
elegant migrate
4. Use Descriptive Names
Migration names should clearly describe what they do:
// Good
CreateUsersTable
AddEmailVerificationToUsers
AddIndexToProductsSku
CreateOrdersTable
// Bad
UpdateDatabase
Changes
Migration1
Fix
5. Handle Production Carefully
In production environments:
Backup first - Always backup before running migrations
Review changes - Understand what each migration does
Plan downtime - Some migrations may require brief downtime
Test in staging - Run migrations in a staging environment first
Monitor performance - Large table alterations can be slow
6. Version Control
Always commit migration files to version control
Never modify migrations that have run in production
Create new migrations to fix issues instead of editing old ones
Database-Specific Considerations
MySQL / MariaDB
await this.schema.create('users', (table) => {
table.id();
table.string('name');
// MySQL specific options
table.engine('InnoDB');
table.charset('utf8mb4');
table.collation('utf8mb4_unicode_ci');
});