Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/microck-wheels-migration-generator/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 158% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 192% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 271% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 219% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 157% | 0% |
Activate automatically when:
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="
WRONG:
bash# Creating migration in wrong location db/migrate/20251022072809_CreateUsers.cfc ❌ Won't be found!
CORRECT:
bash# Wheels looks for migrations here app/migrator/migrations/20251022072809_CreateUsers.cfc ✅ Correct!
WRONG:
cfmt.datetime(columnNames="deletedAt", allowNull=true); t.timestamps(); // ❌ Creates duplicate deletedAt!
CORRECT:
cfmt.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 supportNEVER use database-specific functions like DATE_SUB(), NOW(), CURDATE()!
WRONG:
cfmexecute("INSERT INTO posts (publishedAt) VALUES (DATE_SUB(NOW(), INTERVAL 1 DAY))"); ❌ MySQL only!
CORRECT:
cfmvar pastDate = DateAdd("d", -1, Now()); execute("INSERT INTO posts (publishedAt) VALUES (TIMESTAMP '#DateFormat(pastDate, "yyyy-mm-dd")# #TimeFormat(pastDate, "HH:mm:ss")#')"); ✅ Cross-database!
cfmcomponent 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 } }
cfmcomponent 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"); } }
cfmcomponent 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... } }
cfmcomponent 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')"); } }
cfm// 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();
🔴 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:
cfm// ❌ CLI generates this - STRING values that don't work! t = createTable(name='users', force='false', id='true', primaryKey='id');
Symptoms:
✅ SOLUTION: Simplify to Use Defaults
cfm// 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:
createTable() has correct default behavior'false', 'true') break the logicMANDATORY Post-CLI-Generation Fix:
cfm// 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)🔴 LESSON LEARNED: When migrations fail or you need to iterate, always reset before running latest.
Standard Development Workflow:
bash# 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:
Production Workflow (Different!):
bash# In production, NEVER reset! wheels dbmigrate latest # Only run new migrations
❌ WRONG ORDER - Causes Index Conflicts:
cfmaddIndex(table="likes", columnNames="userId"); // ❌ Creates duplicate addIndex(table="likes", columnNames="tweetId"); addIndex(table="likes", columnNames="userId,tweetId", unique=true);
✅ CORRECT ORDER - Composite First:
cfm// 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.
Problem: Multiple foreign keys to the same table generate duplicate constraint names in H2:
cfm// ❌ 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)
cfmaddForeignKey( 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)
cfm// 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
When migrations fail mid-transaction (common during development):
cfm// Use force=true to drop and recreate if table exists t = createTable(name="likes", force=true); // Drops existing table first
When to use:
For many-to-many relationships (e.g., likes, follows):
cfmt = 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
cfm// 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");
cfm// 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");
cfmcomponent 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"); } }
When generating a migration:
cfmaddColumn( table="posts", columnType="datetime", columnName="deletedAt", allowNull=true ); addIndex(table="posts", columnNames="deletedAt");
cfm// 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
cfmaddColumn(table="posts", columnType="integer", columnName="version", default=1); addColumn(table="posts", columnType="integer", columnName="lockVersion", default=0);
bash# 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
Generated by: Wheels Migration Generator Skill v1.0 Framework: CFWheels 3.0+ Last Updated: 2025-10-20
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | pass→pass | 8,842 | 7,280 | -18% | 1 | 1 | 0% | 1,582 | 5,998 | +279% | 0 | 0 | — |
case-01 | fail→pass | 16,113 | 8,400 | -48% | 1 | 1 | 0% | 2,566 | 6,622 | +158% | 0 | 0 | — |
case-02 | fail→pass | 12,211 | 9,747 | -20% | 1 | 1 | 0% | 2,326 | 6,790 | +192% | 0 | 0 | — |
case-03 | fail→pass | 9,831 | 10,974 | +12% | 1 | 1 | 0% | 1,915 | 7,099 | +271% | 0 | 0 | — |
case-05 | pass→pass | 12,365 | 9,850 | -20% | 1 | 1 | 0% | 2,371 | 6,422 | +171% | 0 | 0 | — |
case-06 | pass→pass | 10,870 | 7,835 | -28% | 1 | 1 | 0% | 1,968 | 6,151 | +213% | 0 | 0 | — |
case-07 | pass→pass | 11,936 | 6,487 | -46% | 1 | 1 | 0% | 2,018 | 5,867 | +191% | 0 | 0 | — |
case-08 | pass→pass | 11,619 | 9,540 | -18% | 1 | 1 | 0% | 2,111 | 6,431 | +205% | 0 | 0 | — |
case-09 | fail→pass | 10,603 | 6,705 | -37% | 1 | 1 | 0% | 1,888 | 6,030 | +219% | 0 | 0 | — |
case-10 | pass→pass | 8,225 | 5,542 | -33% | 1 | 1 | 0% | 1,572 | 5,857 | +273% | 0 | 0 | — |
case-11 | pass→pass | 8,650 | 5,610 | -35% | 1 | 1 | 0% | 1,664 | 5,672 | +241% | 0 | 0 | — |
case-12 | fail→pass | 27,979 | 7,792 | -72% | 1 | 1 | 0% | 2,299 | 5,907 | +157% | 0 | 0 | — |
case-13 | fail→fail | 12,842 | 6,692 | -48% | 1 | 1 | 0% | 2,159 | 6,009 | +178% | 0 | 0 | — |
case-14 | pass→pass | 6,402 | 6,860 | +7% | 1 | 1 | 0% | 1,054 | 6,024 | +472% | 0 | 0 | — |
case-15 | pass→pass | 11,711 | 10,105 | -14% | 1 | 1 | 0% | 2,189 | 6,689 | +206% | 0 | 0 | — |
case-16 | pass→pass | 8,974 | 3,876 | -57% | 1 | 1 | 0% | 1,532 | 5,279 | +245% | 0 | 0 | — |
case-17 | fail→pass | 7,546 | 5,578 | -26% | 1 | 1 | 0% | 1,318 | 5,703 | +333% | 0 | 0 | — |
case-18 | pass→pass | 7,759 | 6,054 | -22% | 1 | 1 | 0% | 1,371 | 5,820 | +325% | 0 | 0 | — |
case-19 | fail→pass | 9,512 | 4,514 | -53% | 1 | 1 | 0% | 1,455 | 5,369 | +269% | 0 | 0 | — |
case-20 | pass→pass | 4,379 | 3,542 | -19% | 1 | 1 | 0% | 691 | 5,326 | +671% | 0 | 0 | — |
case-21 | pass→pass | 6,088 | 4,123 | -32% | 1 | 1 | 0% | 985 | 5,446 | +453% | 0 | 0 | — |
case-22 | pass→pass | 9,873 | 6,351 | -36% | 1 | 1 | 0% | 1,694 | 5,908 | +249% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.