Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Wheels Migration Generator

ASecurity

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.

393 stars
0 votes
0 copies
1 views
Added 12/19/2025
databasesbashsqldatabaseperformance

Works with

cli

Security Analysis

A100/100

Scanned 2/12/2026

$npx -y skills add Microck/ordinary-claude-skills --skill wheels-migration-generator --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Wheels Migration Generator?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Wheels Migration Generator
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/microck-wheels-migration-generator/badge)](https://www.skillsdirectory.com/skills/microck-wheels-migration-generator)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
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:**
```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!
```

### ❌ ANTI-PATTERN 2: timestamps() Includes deletedAt

**WRONG:**
```cfm
t.datetime(columnNames="deletedAt", allowNull=true);
t.timestamps();  // ❌ Creates duplicate deletedAt!
```

**CORRECT:**
```cfm
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:**
```cfm
execute("INSERT INTO posts (publishedAt) VALUES (DATE_SUB(NOW(), INTERVAL 1 DAY))");  ❌ MySQL only!
```

**CORRECT:**
```cfm
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

```cfm
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

```cfm
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

```cfm
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

```cfm
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

```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();
```

## 🚨 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:**
```cfm
// ❌ 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**
```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:**
- 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:**
```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)
```

### 2. Migration Development Workflow

**🔴 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:**
- 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!):**
```bash
# In production, NEVER reset!
wheels dbmigrate latest  # Only run new migrations
```

### 3. Composite Index Ordering (CRITICAL)

**❌ WRONG ORDER - Causes Index Conflicts:**
```cfm
addIndex(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.

### 2. Foreign Key Naming for Self-Referential Tables

**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)**
```cfm
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)**
```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
```

### 3. Migration Retry with force=true

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:**
- ✅ 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):

```cfm
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

```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");
```

## Foreign Key Management

```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");
```

## Join Table Migration

```cfm
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

```cfm
addColumn(
    table="posts",
    columnType="datetime",
    columnName="deletedAt",
    allowNull=true
);
addIndex(table="posts", columnNames="deletedAt");
```

### Adding Full Text Search

```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
```

### Adding Versioning

```cfm
addColumn(table="posts", columnType="integer", columnName="version", default=1);
addColumn(table="posts", columnType="integer", columnName="lockVersion", default=0);
```

## Migration Commands

```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
```

## 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

Attribution

MicrockMicrock
View sourceSee grades on GitHubMore from Microck →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Mysql Best Practices

MySQL development best practices for schema design, query optimization, and database administration

2481 votes

Jpa Patterns

Spring Boot中的JPA/Hibernate实体设计、关系、查询优化、事务、审计、索引、分页和连接池模式。

2456590 votes

Clickhouse Io

ClickHouse数据库模式、查询优化、分析和数据工程最佳实践,适用于高性能分析工作负载。

2456590 votes

Postgres Patterns

基于Supabase最佳实践的PostgreSQL数据库模式,用于查询优化、架构设计、索引和安全。

2456590 votes

Sql Pro

Master modern SQL with cloud-native databases, OLTP/OLAP

458250 votes
View all in databases →