Migrations Guide
Learn how to manage database schema changes effectively using Wheels CLI migrations.
Overview
Database migrations provide version control for your database schema. They allow you to:
Track schema changes over time
Share database changes with your team
Deploy schema updates safely
Roll back changes if needed
Keep database and code in sync
Migration Basics
What is a Migration?
A migration is a CFC file that describes a database change. Each migration has:
A timestamp-based version number
An
up()
method to apply changesAn optional
down()
method to reverse changes
Migration Files
Migrations are stored in /db/migrate/
with this naming convention:
[YYYYMMDDHHmmss]_[description].cfc
Example:
20240125143022_create_users_table.cfc
20240125143523_add_email_to_users.cfc
Creating Migrations
Generate Migration Commands
# Create blank migration
wheels dbmigrate create blank add_status_to_orders
# Create table migration
wheels dbmigrate create table products
# Add column migration
wheels dbmigrate create column users email
Migration Structure
Basic migration template:
component extends="wheels.migrator.Migration" {
function up() {
transaction {
// Apply changes
}
}
function down() {
transaction {
// Reverse changes
}
}
}
Table Operations
Creating Tables
function up() {
transaction {
t = createTable("products");
// Primary key (auto-created as 'id' by default)
t.primaryKey("productId"); // Custom primary key
// Column types
t.string("name", limit=100);
t.text("description");
t.integer("quantity");
t.bigInteger("views");
t.float("weight");
t.decimal("price", precision=10, scale=2);
t.boolean("active", default=true);
t.date("releaseDate");
t.datetime("publishedAt");
t.timestamp("lastModified");
t.time("openingTime");
t.binary("data");
t.uuid("uniqueId");
// Special columns
t.timestamps(); // Creates createdAt and updatedAt
t.references("user"); // Creates userId foreign key
// Create the table
t.create();
}
}
Table Options
function up() {
transaction {
t = createTable("products",
id=false, // Don't create auto-increment id
force=true, // Drop if exists
options="ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
// Composite primary key
t.primaryKey(["orderId", "productId"]);
t.create();
}
}
Dropping Tables
function down() {
transaction {
dropTable("products");
}
}
Column Operations
Adding Columns
function up() {
transaction {
addColumn(
table="users",
column="phoneNumber",
type="string",
limit=20,
null=true
);
// Multiple columns
t = changeTable("users");
t.string("address");
t.string("city");
t.string("postalCode", limit=10);
t.update();
}
}
Modifying Columns
function up() {
transaction {
changeColumn(
table="products",
column="price",
type="decimal",
precision=12,
scale=2,
null=false,
default=0
);
}
}
Renaming Columns
function up() {
transaction {
renameColumn(
table="users",
column="email_address",
newName="email"
);
}
}
Removing Columns
function up() {
transaction {
removeColumn(table="users", column="deprecated_field");
// Multiple columns
t = changeTable("products");
t.removeColumn("oldPrice");
t.removeColumn("legacyCode");
t.update();
}
}
Index Operations
Creating Indexes
function up() {
transaction {
// Simple index
addIndex(table="users", column="email");
// Unique index
addIndex(
table="users",
column="username",
unique=true
);
// Composite index
addIndex(
table="products",
columns="category,status",
name="idx_category_status"
);
// In table creation
t = createTable("orders");
t.string("orderNumber");
t.index("orderNumber", unique=true);
t.create();
}
}
Removing Indexes
function down() {
transaction {
removeIndex(table="users", name="idx_users_email");
// Or by column
removeIndex(table="products", column="sku");
}
}
Foreign Keys
Adding Foreign Keys
function up() {
transaction {
// Simple foreign key
addForeignKey(
table="orders",
column="userId",
referenceTable="users",
referenceColumn="id"
);
// With options
addForeignKey(
table="orderItems",
column="orderId",
referenceTable="orders",
referenceColumn="id",
onDelete="CASCADE",
onUpdate="CASCADE"
);
// In table creation
t = createTable("posts");
t.references("user", onDelete="SET NULL");
t.references("category", foreignKey=true);
t.create();
}
}
Removing Foreign Keys
function down() {
transaction {
removeForeignKey(
table="orders",
name="fk_orders_users"
);
}
}
Data Migrations
Inserting Data
function up() {
transaction {
// Single record
sql("
INSERT INTO roles (name, description, createdAt)
VALUES ('admin', 'Administrator', NOW())
");
// Multiple records
addRecord(table="permissions", name="users.create");
addRecord(table="permissions", name="users.read");
addRecord(table="permissions", name="users.update");
addRecord(table="permissions", name="users.delete");
}
}
Updating Data
function up() {
transaction {
updateRecord(
table="products",
where="status IS NULL",
values={status: "active"}
);
// Complex updates
sql("
UPDATE users
SET fullName = CONCAT(firstName, ' ', lastName)
WHERE fullName IS NULL
");
}
}
Removing Data
function down() {
transaction {
removeRecord(
table="roles",
where="name = 'temp_role'"
);
}
}
Advanced Migrations
Conditional Migrations
function up() {
transaction {
// Check if column exists
if (!hasColumn("users", "avatar")) {
addColumn(table="users", column="avatar", type="string");
}
// Check if table exists
if (!hasTable("analytics")) {
t = createTable("analytics");
t.integer("views");
t.timestamps();
t.create();
}
// Database-specific
if (getDatabaseType() == "mysql") {
sql("ALTER TABLE users ENGINE=InnoDB");
}
}
}
Using Raw SQL
function up() {
transaction {
// Complex operations
sql("
CREATE VIEW active_products AS
SELECT * FROM products
WHERE active = 1 AND deletedAt IS NULL
");
// Stored procedures
sql("
CREATE PROCEDURE CleanupOldData()
BEGIN
DELETE FROM logs WHERE createdAt < DATE_SUB(NOW(), INTERVAL 90 DAY);
END
");
}
}
Environment-Specific
function up() {
transaction {
// Always run
addColumn(table="users", column="lastLoginAt", type="datetime");
// Development only
if (getEnvironment() == "development") {
// Add test data
for (var i = 1; i <= 100; i++) {
addRecord(
table="users",
email="test#i#@example.com",
password="hashed_password"
);
}
}
}
}
Running Migrations
Basic Commands
# Check migration status
wheels dbmigrate info
# Run all pending migrations
wheels dbmigrate latest
# Run next migration only
wheels dbmigrate up
# Rollback last migration
wheels dbmigrate down
# Run specific version
wheels dbmigrate exec 20240125143022
# Reset all migrations
wheels dbmigrate reset
Migration Workflow
Create migration
wheels dbmigrate create table orders
Edit migration file
// Edit /db/migrate/[timestamp]_create_orders_table.cfc
Test migration
# Run migration wheels dbmigrate latest # Verify wheels dbmigrate info # Test rollback wheels dbmigrate down
Commit and share
git add db/migrate/ git commit -m "Add orders table migration"
Best Practices
1. Always Use Transactions
function up() {
transaction {
// All operations in transaction
// Rollback on any error
}
}
2. Make Migrations Reversible
function up() {
transaction {
addColumn(table="users", column="nickname", type="string");
}
}
function down() {
transaction {
removeColumn(table="users", column="nickname");
}
}
3. One Change Per Migration
# Good: Separate migrations
wheels dbmigrate create blank add_status_to_orders
wheels dbmigrate create blank add_priority_to_orders
# Bad: Multiple unrelated changes
wheels dbmigrate create blank update_orders_and_users
4. Test Migrations Thoroughly
# Test up
wheels dbmigrate latest
# Test down
wheels dbmigrate down
# Test up again
wheels dbmigrate up
5. Never Modify Completed Migrations
# Bad: Editing existing migration
# Good: Create new migration to fix issues
wheels dbmigrate create blank fix_orders_status_column
Common Patterns
Adding Non-Nullable Column
function up() {
transaction {
// Add nullable first
addColumn(table="users", column="role", type="string", null=true);
// Set default values
updateRecord(table="users", where="1=1", values={role: "member"});
// Make non-nullable
changeColumn(table="users", column="role", null=false);
}
}
Renaming Table with Foreign Keys
function up() {
transaction {
// Drop foreign keys first
removeForeignKey(table="posts", name="fk_posts_users");
// Rename table
renameTable(oldName="posts", newName="articles");
// Recreate foreign keys
addForeignKey(
table="articles",
column="userId",
referenceTable="users",
referenceColumn="id"
);
}
}
Safe Column Removal
function up() {
transaction {
// First migration: deprecate column
if (getEnvironment() != "production") {
announce("Column 'users.oldField' is deprecated and will be removed");
}
}
}
// Later migration (after code deployment)
function up() {
transaction {
removeColumn(table="users", column="oldField");
}
}
Troubleshooting
Migration Failed
# Check error
wheels dbmigrate info
# Fix migration file
# Retry
wheels dbmigrate latest
Stuck Migration
-- Manually fix schema_migrations table
DELETE FROM schema_migrations WHERE version = '20240125143022';
Performance Issues
function up() {
// Increase timeout for large tables
setting requestTimeout="300";
transaction {
// Add index without locking (MySQL)
sql("ALTER TABLE large_table ADD INDEX idx_column (column)");
}
}
Integration with CI/CD
Pre-deployment Check
#!/bin/bash
# Check for pending migrations
if wheels dbmigrate info | grep -q "pending"; then
echo "⚠️ Pending migrations detected!"
wheels dbmigrate info
exit 1
fi
Automated Deployment
# .github/workflows/deploy.yml
- name: Run migrations
run: |
wheels dbmigrate latest
wheels dbmigrate info
See Also
Last updated
Was this helpful?