wheels-migration-generator
Generate database-agnostic Wheels migrations for creating tables, altering schemas, and managing database changes. Use when creating or modifying database schema, adding tables, columns, indexes, or foreign keys. Prevents database-specific SQL and ensures cross-database compatibility.
About wheels-migration-generator
wheels-migration-generator is a Claude AI skill developed by wheels-dev. Generate database-agnostic Wheels migrations for creating tables, altering schemas, and managing database changes. Use when creating or modifying database schema, adding tables, columns, indexes, or foreign keys. Prevents database-specific SQL and ensures cross-database compatibility. This powerful Claude Code plugin helps developers automate workflows and enhance productivity with intelligent AI assistance.
Why use wheels-migration-generator? With 0 stars on GitHub, this skill has been trusted by developers worldwide. Install this Claude skill instantly to enhance your development workflow with AI-powered automation.
| name | Wheels Migration Generator |
| description | Generate database-agnostic Wheels migrations for creating tables, altering schemas, and managing database changes. Use when creating or modifying database schema, adding tables, columns, indexes, or foreign keys. Prevents database-specific SQL and ensures cross-database compatibility. |
Wheels Migration Generator
When to Use This Skill
Activate automatically when:
- User requests to create a migration (e.g., "create posts table")
- User wants to add/modify/remove columns
- User needs to add indexes or foreign keys
- User is changing database schema
- User mentions: migration, database, table, column, index, schema
šØ CRITICAL: Migration File Location
Migrations MUST be in: app/migrator/migrations/
NOT: db/migrate/ or any other location
After creating migration files, reload Wheels: curl -s "http://localhost:PORT/?reload=true&password="
Critical Anti-Patterns to Prevent
ā ANTI-PATTERN 1: Wrong Migration Directory
WRONG:
# Creating migration in wrong location db/migrate/20251022072809_CreateUsers.cfc ā Won't be found!
CORRECT:
# Wheels looks for migrations here app/migrator/migrations/20251022072809_CreateUsers.cfc ā Correct!
ā ANTI-PATTERN 2: timestamps() Includes deletedAt
WRONG:
t.datetime(columnNames="deletedAt", allowNull=true); t.timestamps(); // ā Creates duplicate deletedAt!
CORRECT:
t.timestamps(); // ā Includes createdAt, updatedAt, AND deletedAt
Note: Wheels t.timestamps() automatically adds:
createdAt(datetime, NOT NULL)updatedAt(datetime, NOT NULL)deletedAt(datetime, NULL) - for soft delete support
ā ANTI-PATTERN 3: Database-Specific Date Functions
NEVER use database-specific functions like DATE_SUB(), NOW(), CURDATE()!
WRONG:
execute("INSERT INTO posts (publishedAt) VALUES (DATE_SUB(NOW(), INTERVAL 1 DAY))"); ā MySQL only!
CORRECT:
var pastDate = DateAdd("d", -1, Now()); execute("INSERT INTO posts (publishedAt) VALUES (TIMESTAMP '#DateFormat(pastDate, "yyyy-mm-dd")# #TimeFormat(pastDate, "HH:mm:ss")#')"); ā Cross-database!
Migration Structure
Basic Migration Template
component extends="wheels.migrator.Migration" { function up() { transaction { try { // Your migration code here } catch (any e) { local.exception = e; } if (StructKeyExists(local, "exception")) { transaction action="rollback"; Throw( errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any" ); } else { transaction action="commit"; } } } function down() { // Rollback code here } }
Create Table Migration
component extends="wheels.migrator.Migration" { function up() { transaction { try { // Create table t = createTable(name="posts", force=false); // String columns t.string(columnNames="title", allowNull=false, limit=200); t.string(columnNames="slug", allowNull=false, limit=200); // Text columns t.text(columnNames="content", allowNull=false); t.text(columnNames="excerpt", allowNull=true); // Integer columns t.integer(columnNames="viewCount", default=0); t.integer(columnNames="userId", allowNull=false); // Boolean columns t.boolean(columnNames="published", default=false); // DateTime columns t.datetime(columnNames="publishedAt", allowNull=true); // Timestamps (createdAt, updatedAt) t.timestamps(); // Create the table t.create(); // Add indexes addIndex(table="posts", columnNames="slug", unique=true); addIndex(table="posts", columnNames="userId"); addIndex(table="posts", columnNames="published,publishedAt"); // Add foreign key addForeignKey( table="posts", referenceTable="users", column="userId", referenceColumn="id", onDelete="cascade" ); } catch (any e) { local.exception = e; } if (StructKeyExists(local, "exception")) { transaction action="rollback"; Throw( errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any" ); } else { transaction action="commit"; } } } function down() { dropTable("posts"); } }
Alter Table Migration
component extends="wheels.migrator.Migration" { function up() { transaction { try { // Add column addColumn( table="posts", columnType="string", columnName="metaDescription", limit=300, allowNull=true ); // Change column changeColumn( table="posts", columnName="title", columnType="string", limit=255, // Changed from 200 allowNull=false ); // Rename column renameColumn( table="posts", oldColumnName="summary", newColumnName="excerpt" ); // Remove column removeColumn(table="posts", columnName="oldField"); // Add index addIndex(table="posts", columnNames="metaDescription"); } catch (any e) { local.exception = e; } if (StructKeyExists(local, "exception")) { transaction action="rollback"; Throw( errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any" ); } else { transaction action="commit"; } } } function down() { removeColumn(table="posts", columnName="metaDescription"); // Reverse other changes... } }
Data Migration (Seed Data)
Database-Agnostic Date Formatting
component extends="wheels.migrator.Migration" { function up() { transaction { try { // CORRECT: Use CFML date functions var now = Now(); var day1 = DateAdd("d", -7, now); var day2 = DateAdd("d", -6, now); var day3 = DateAdd("d", -5, now); // Format dates for SQL var nowFormatted = "TIMESTAMP '#DateFormat(now, "yyyy-mm-dd")# #TimeFormat(now, "HH:mm:ss")#'"; var day1Formatted = "TIMESTAMP '#DateFormat(day1, "yyyy-mm-dd")# #TimeFormat(day1, "HH:mm:ss")#'"; var day2Formatted = "TIMESTAMP '#DateFormat(day2, "yyyy-mm-dd")# #TimeFormat(day2, "HH:mm:ss")#'"; // Insert data execute(" INSERT INTO posts (title, slug, content, published, publishedAt, createdAt, updatedAt) VALUES ( 'Getting Started with HTMX', 'getting-started-with-htmx', '<p>HTMX is a modern approach to building web applications...</p>', 1, #day1Formatted#, #day1Formatted#, #day1Formatted# ) "); execute(" INSERT INTO posts (title, slug, content, published, publishedAt, createdAt, updatedAt) VALUES ( 'Tailwind CSS Best Practices', 'tailwind-css-best-practices', '<p>Tailwind provides utility-first CSS...</p>', 1, #day2Formatted#, #day2Formatted#, #day2Formatted# ) "); } catch (any e) { local.exception = e; } if (StructKeyExists(local, "exception")) { transaction action="rollback"; Throw( errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any" ); } else { transaction action="commit"; } } } function down() { execute("DELETE FROM posts WHERE slug IN ('getting-started-with-htmx', 'tailwind-css-best-practices')"); } }
Column Types
Available Column Types
// String (VARCHAR) t.string(columnNames="name", limit=255, allowNull=false, default=""); // Text (TEXT/CLOB) t.text(columnNames="description", allowNull=true); // Integer t.integer(columnNames="count", default=0, allowNull=false); // Big Integer t.biginteger(columnNames="largeNumber"); // Float t.float(columnNames="rating", default=0.0); // Decimal t.decimal(columnNames="price", precision=10, scale=2); // Boolean t.boolean(columnNames="active", default=true); // Date t.date(columnNames="birthDate"); // DateTime t.datetime(columnNames="publishedAt"); // Time t.time(columnNames="startTime"); // Binary t.binary(columnNames="fileData"); // UUID t.string(columnNames="uuid", limit=36); // Timestamps (adds createdAt and updatedAt) t.timestamps();
šØ Production-Tested Critical Fixes
1. CLI Generator Boolean Parameter Bug (CRITICAL)
š“ CRITICAL DISCOVERY: The CLI generator wheels g migration creates migrations with string boolean values instead of actual booleans, causing silent failures.
Problem Generated by CLI:
// ā CLI generates this - STRING values that don't work! t = createTable(name='users', force='false', id='true', primaryKey='id');
Symptoms:
- Migration reports success but table isn't created correctly
- "NoPrimaryKey" errors even though migration succeeded
- Primary key not properly configured in database
- Wheels ORM can't find primary key column
ā SOLUTION: Simplify to Use Defaults
// Remove all explicit boolean parameters - let Wheels use defaults t = createTable(name='users'); // That's it! t.string(columnNames='username', allowNull=false, limit='50'); t.timestamps(); t.create();
Why This Works:
- Wheels
createTable()has correct default behavior - Explicit string booleans (
'false','true') break the logic - Omitting parameters lets Wheels handle it correctly
- Default: creates 'id' as primary key automatically
MANDATORY Post-CLI-Generation Fix:
// 1. Find this pattern in generated migration: t = createTable(name='tablename', force='false', id='true', primaryKey='id'); // 2. Replace with: t = createTable(name='tablename');
Rule:
ā
MANDATORY: After CLI generation, remove force/id/primaryKey parameters from createTable()
ā NEVER use string boolean values: 'false', 'true'
ā
Use actual booleans IF needed: false, true (but defaults are better)
2. Migration Development Workflow
š“ LESSON LEARNED: When migrations fail or you need to iterate, always reset before running latest.
Standard Development Workflow:
# 1. Generate migration wheels g migration CreateUsersTable # 2. Edit migration file (fix CLI-generated issues!) # 3. ALWAYS reset before running during development wheels dbmigrate reset # Drops all tables, clean slate wheels dbmigrate latest # Run all migrations fresh # 4. If migration fails, fix it then: wheels dbmigrate reset # Reset again wheels dbmigrate latest # Try again
Why Reset is Important:
- Failed migrations may leave partial tables
- Partial tables prevent subsequent migrations from running
- Reset ensures clean database state
- Catches migration errors early
Production Workflow (Different!):
# In production, NEVER reset! wheels dbmigrate latest # Only run new migrations
3. Composite Index Ordering (CRITICAL)
ā WRONG ORDER - Causes Index Conflicts:
addIndex(table="likes", columnNames="userId"); // ā Creates duplicate addIndex(table="likes", columnNames="tweetId"); addIndex(table="likes", columnNames="userId,tweetId", unique=true);
ā CORRECT ORDER - Composite First:
// Composite index FIRST - it covers queries on the first column too! addIndex(table="likes", columnNames="userId,tweetId", unique=true); // Then add index for second column only addIndex(table="likes", columnNames="tweetId");
Why: A composite index on (userId, tweetId) can be used for queries filtering by userId alone, making a separate userId index redundant.
2. Foreign Key Naming for Self-Referential Tables
Problem: Multiple foreign keys to the same table generate duplicate constraint names in H2:
// ā Both try to create "FK_FOLLOWS_USERS" - conflict! addForeignKey(table="follows", referenceTable="users", column="followerId") addForeignKey(table="follows", referenceTable="users", column="followingId")
Solution A: Explicit Key Names (Preferred for Production)
addForeignKey( table="follows", referenceTable="users", column="followerId", referenceColumn="id", keyName="FK_follows_follower", // Explicit unique name onDelete="cascade" ); addForeignKey( table="follows", referenceTable="users", column="followingId", referenceColumn="id", keyName="FK_follows_following", // Different unique name onDelete="cascade" );
Solution B: Skip Foreign Keys (Acceptable for Development)
// Rely on application-layer validation instead // Indexes provide query performance, foreign keys are optional addIndex(table="follows", columnNames="followerId,followingId", unique=true); addIndex(table="follows", columnNames="followingId"); // Note: Foreign keys omitted to avoid H2 naming conflicts // Application validates referential integrity
3. Migration Retry with force=true
When migrations fail mid-transaction (common during development):
// Use force=true to drop and recreate if table exists t = createTable(name="likes", force=true); // Drops existing table first
When to use:
- ā After failed migration leaves partial tables
- ā During development when iterating on schema
- ā NOT recommended for production (use proper versioning)
4. Join Table Pattern
For many-to-many relationships (e.g., likes, follows):
t = createTable(name="likes", force=true); t.integer(columnNames="userId", allowNull=false); t.integer(columnNames="tweetId", allowNull=false); t.datetime(columnNames="createdAt", allowNull=false); // Track when relationship created t.create(); // IMPORTANT: Composite unique index FIRST addIndex(table="likes", columnNames="userId,tweetId", unique=true); addIndex(table="likes", columnNames="tweetId"); // For reverse lookups
Index Management
// Simple index addIndex(table="posts", columnNames="title"); // Unique index addIndex(table="posts", columnNames="slug", unique=true); // Composite index addIndex(table="posts", columnNames="published,publishedAt"); // Remove index removeIndex(table="posts", indexName="idx_posts_title");
Foreign Key Management
// Add foreign key addForeignKey( table="posts", referenceTable="users", column="userId", referenceColumn="id", onDelete="cascade", // Options: cascade, setNull, setDefault, restrict onUpdate="cascade" ); // Remove foreign key removeForeignKey(table="posts", keyName="fk_posts_userId");
Join Table Migration
component extends="wheels.migrator.Migration" { function up() { transaction { try { // Create join table for many-to-many t = createTable(name="postTags", force=false); t.integer(columnNames="postId", allowNull=false); t.integer(columnNames="tagId", allowNull=false); t.timestamps(); t.create(); // Add indexes addIndex(table="postTags", columnNames="postId"); addIndex(table="postTags", columnNames="tagId"); addIndex(table="postTags", columnNames="postId,tagId", unique=true); // Add foreign keys addForeignKey( table="postTags", referenceTable="posts", column="postId", referenceColumn="id", onDelete="cascade" ); addForeignKey( table="postTags", referenceTable="tags", column="tagId", referenceColumn="id", onDelete="cascade" ); } catch (any e) { local.exception = e; } if (StructKeyExists(local, "exception")) { transaction action="rollback"; Throw( errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any" ); } else { transaction action="commit"; } } } function down() { dropTable("postTags"); } }
Implementation Checklist
When generating a migration:
- Extends wheels.migrator.Migration
- Wrapped in transaction block
- Try/catch for error handling
- Rollback on exception
- Commit on success
- Use CFML date functions (NOT SQL date functions)
- Format dates with DateFormat/TimeFormat
- Include down() method for rollback
- Add appropriate indexes
- Add foreign keys where needed
- Use database-agnostic column types
Common Patterns
Adding Soft Delete
addColumn( table="posts", columnType="datetime", columnName="deletedAt", allowNull=true ); addIndex(table="posts", columnNames="deletedAt");
Adding Full Text Search
// Add column for search addColumn( table="posts", columnType="text", columnName="searchContent", allowNull=true ); // Create search index (database-specific, document it) // For PostgreSQL: CREATE INDEX ... USING GIN // For MySQL: CREATE FULLTEXT INDEX
Adding Versioning
addColumn(table="posts", columnType="integer", columnName="version", default=1); addColumn(table="posts", columnType="integer", columnName="lockVersion", default=0);
Migration Commands
# Create new migration wheels g migration CreatePostsTable # Run pending migrations wheels dbmigrate latest # Run single migration wheels dbmigrate up # Rollback last migration wheels dbmigrate down # Show migration status wheels dbmigrate info
Related Skills
- wheels-model-generator: Creates models for tables
- wheels-anti-pattern-detector: Validates migration code
Generated by: Wheels Migration Generator Skill v1.0 Framework: CFWheels 3.0+ Last Updated: 2025-10-20

wheels-dev
wheels
Download Skill Files
View Installation GuideDownload the complete skill directory including SKILL.md and all related files