Skip to content

Commit 7724a87

Browse files
bpamiriclaude
andcommitted
docs(web/guides): correct migrations and seeding guides to verified 4.0.x behavior
Audit batch 2 (p1-14-migrations) corrections, all behavior-verified on CLI 4.0.3 + develop source: - migrate info/doctor have no common-port fallback — they require the project-bound port config like the write commands (#3080) - failed migrations currently exit 0; warn against gating CI on $? (#3081) - generator filename is <ts>_<NameAsTyped>.cfc verbatim from the local clock — the snake_case _table shape belongs to the model generator - columnName (singular) is an accepted alias; null is never accepted - remove the nonexistent 'limit=8 maps to BIGINT on MySQL' mapping; point at t.bigInteger() - NOW() fails on SQLite (default DB) and SQL Server; the portable spelling is CURRENT_TIMESTAMP; execute() has no parameters argument - replace nonexistent 'wheels generate seed [--all]' with 'wheels generate snippets seed-data' - mark 'wheels seed --generate' as non-functional (#3082) - document the seedOnce validation-failure outcome (rollback + non-zero exit as of 4.0.4; silent on 4.0.3) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
1 parent 840274b commit 7724a87

2 files changed

Lines changed: 14 additions & 11 deletions

File tree

web/sites/guides/src/content/docs/v4-0-0/basics/migrations.mdx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ wheels --version
3939
<Aside type="caution" title="Write commands require a project-bound server">
4040
`wheels migrate latest`, `migrate up`, `migrate down`, `migrate forget`, `migrate pretend`, `migrate rename-system-tables`, `wheels seed`, and `wheels db reset` (which runs migrate + seed) only connect to a server whose port is explicitly configured for this project — via the `port` field in `lucee.json` (created by `wheels new`, or set manually) or the `PORT` variable in `.env`. They refuse the common-port fallback (8080, etc.) to prevent silently targeting a sibling app's server when you work on multiple projects. If you see `Wheels.ServerNotRunning`, run `wheels start` in this project's directory first.
4141

42-
`wheels migrate info` and `wheels migrate doctor` are read-only and still probe common ports as a fallback, so they work without an explicit port config.
42+
`wheels migrate info` and `wheels migrate doctor` are read-only, but they currently go through the same project-bound check — there is no common-port fallback for them either, so they too need the explicit port config ([#3080](https://github.com/wheels-dev/wheels/issues/3080) tracks whether the read-side fallback comes back).
4343
</Aside>
4444

45-
The runner wraps each migration in a transaction so a failing `up()` or `down()` rolls back cleanly on databases that support transactional DDL — but that support varies, so read the caution below before relying on it.
45+
The runner wraps each migration in a transaction so a failing `up()` or `down()` rolls back cleanly on databases that support transactional DDL — but that support varies, so read the caution below before relying on it. One more caveat for scripts and CI: a failed migration is loud in the command output but the CLI currently still exits `0`, so don't gate a pipeline on the exit code alone ([#3081](https://github.com/wheels-dev/wheels/issues/3081)).
4646

4747
<Aside type="caution" title="DDL auto-commit on MySQL and Oracle">
4848
On MySQL and Oracle, DDL statements (`CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`, …) implicitly commit the surrounding transaction. A multi-statement migration that fails midway on those databases leaves the statements that already ran committed — partial schema the transaction wrapper cannot undo. PostgreSQL, SQL Server, and SQLite support transactional DDL, so the all-or-nothing guarantee holds there. On MySQL/Oracle, prefer small single-purpose migrations, and if one does die halfway, use `wheels migrate doctor` and the reconciliation subcommands below to get the tracking table and schema back in line.
@@ -66,7 +66,7 @@ The generator scaffolds a CFC with `up()` and `down()` stubs and drops it in `ap
6666
wheels generate migration CreatePosts
6767
```
6868

69-
That produces a file named like `20260420143000_create_posts_table.cfc`. The prefix is `YYYYMMDDHHMMSS` — generated from the clock at creation time — and the runner applies migrations in filename order, so the timestamp is what puts your change after everyone else's. Never rename or reorder migration files after they've been committed: the history in `wheels_migrator_versions` keys on the timestamp prefix, and a renamed file looks like a brand-new migration to the runner.
69+
That produces a file named like `20260420143000_CreatePosts.cfc` — the name you typed is used verbatim. The prefix is `YYYYMMDDHHMMSS` — generated from your local clock at creation time — and the runner applies migrations in filename order, so the timestamp is what puts your change after everyone else's. Never rename or reorder migration files after they've been committed: the history in `wheels_migrator_versions` keys on the timestamp prefix, and a renamed file looks like a brand-new migration to the runner.
7070

7171
## Writing a migration — full example
7272

@@ -94,7 +94,7 @@ Three things to note:
9494

9595
1. `createTable("posts")` returns a table-builder object. Every column method — `t.string`, `t.integer`, `t.datetime` — mutates that builder. Nothing hits the database until `t.create()`.
9696
2. `t.timestamps()` creates `createdAt`, `updatedAt`, **and** a soft-delete column (`deletedAt`). Don't also add those columns by hand — you'll get duplicate-column errors. See [Models and the ORM](/v4-0-0/basics/models-and-the-orm/) for how Wheels populates them automatically on save.
97-
3. The column-builder methods use `columnNames` (plural) and `allowNull` — not `columnName` and `null`. A single call can create several same-typed columns at once: `t.string(columnNames="firstName,lastName", limit=60)`.
97+
3. The column-builder methods use `columnNames` (plural) and `allowNull`. Every helper also accepts `columnName` (singular) as an alias, but `columnNames` is the preferred form; the nullable flag is always `allowNull``null` is never accepted. A single call can create several same-typed columns at once: `t.string(columnNames="firstName,lastName", limit=60)`.
9898

9999
## Column types
100100

@@ -105,7 +105,7 @@ These are the column-builder methods defined on `TableDefinition`. Every one tak
105105
| `t.string(columnNames=, limit=, allowNull=, default=)` | VARCHAR | `limit` defaults to 255 |
106106
| `t.char(columnNames=, limit=)` | CHAR | fixed-width string |
107107
| `t.text(columnNames=, size=)` | TEXT / MEDIUMTEXT / LONGTEXT | `size` is MySQL-only |
108-
| `t.integer(columnNames=, limit=, allowNull=, default=)` | INT | `limit=8` maps to BIGINT on MySQL |
108+
| `t.integer(columnNames=, limit=, allowNull=, default=)` | INT | `limit` is a display width, not a size bump — for BIGINT use `t.bigInteger()` |
109109
| `t.bigInteger(columnNames=)` | BIGINT | |
110110
| `t.float(columnNames=)` | FLOAT | |
111111
| `t.decimal(columnNames=, precision=, scale=)` | DECIMAL | use for money |
@@ -232,12 +232,12 @@ component extends="wheels.migrator.Migration" hint="Remove deprecated teaser" {
232232

233233
Real seed data — users, roles, reference lookups — belongs in `app/db/seeds.cfm` where it runs idempotently via `seedOnce()`. See [Seeding](/v4-0-0/basics/seeding/) for that path. Don't use migrations for seed data in long-lived projects: a `INSERT` in a migration runs exactly once, so editing the inserted row later means writing another migration to update it, and fresh clones of the repo end up with whatever the most recent migration wrote.
234234

235-
That said, you'll occasionally want a migration to backfill data for a schema change — for example, populating a new column from an existing one. When you do, use inline SQL with `NOW()` for timestamps. Parameter binding in `execute()` is unreliable across databases, and database-specific date functions (`CURRENT_TIMESTAMP`, `GETDATE()`, `SYSDATETIME()`) don't port. `NOW()` works across MySQL, PostgreSQL, SQL Server, H2, and SQLite.
235+
That said, you'll occasionally want a migration to backfill data for a schema change — for example, populating a new column from an existing one. When you do, use inline SQL: `execute()` takes a single SQL string and nothing else — there is no parameters argument, so there's no binding to reach for. For timestamps, use `CURRENT_TIMESTAMP`, which works across MySQL, PostgreSQL, SQL Server, H2, and SQLite. `NOW()` does not port: SQLite — the default database for `wheels new` apps — fails with `no such function: NOW`, and SQL Server has no native `NOW()` either. Other database-specific date functions (`GETDATE()`, `SYSDATETIME()`) don't port at all.
236236

237237
```cfm {test:compile}
238238
component extends="wheels.migrator.Migration" hint="Seed admin role" {
239239
function up() {
240-
execute("INSERT INTO roles (name, createdAt, updatedAt) VALUES ('admin', NOW(), NOW())");
240+
execute("INSERT INTO roles (name, createdAt, updatedAt) VALUES ('admin', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)");
241241
}
242242
243243
function down() {
@@ -273,7 +273,7 @@ The throw is the honest signal: "you can't walk past this point backward." If yo
273273

274274
## Filename format
275275

276-
Generated migration files are named `<YYYYMMDDHHMMSS>_<snake_case_name>.cfc` — a 14-digit UTC-ish timestamp, an underscore, then the name you passed to `wheels generate migration`. The timestamp determines the order the runner applies them. Don't hand-edit the prefix; if you need to reorder after a merge conflict, regenerate the file and move your body into it.
276+
Generated migration files are named `<YYYYMMDDHHMMSS>_<NameAsTyped>.cfc` — a 14-digit timestamp from your local clock, an underscore, then the name you passed to `wheels generate migration`, written exactly as you typed it (no snake_casing, no added suffix). The snake_case shape you may also see in `app/migrator/migrations/``20260420143000_create_posts_table.cfc` — comes from the *model* generator: `wheels g model Post` scaffolds a `create_posts_table` migration alongside the model. The timestamp determines the order the runner applies them. Don't hand-edit the prefix; if you need to reorder after a merge conflict, regenerate the file and move your body into it.
277277

278278
## Related guides
279279

web/sites/guides/src/content/docs/v4-0-0/basics/seeding.mdx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ Run it with `wheels seed`. The seeder opens a transaction, includes `app/db/seed
5858

5959
Under the hood, it calls `model("<ModelName>").findOne(where="<uniqueProperty>='<value>'")` first. If the record exists, the call increments `totalSkipped` and returns silently. If not, it calls `model(...).new(properties).save()` and increments `totalCreated`. Re-running is always safe: no duplicates, no errors, no surprises.
6060

61+
There's a third outcome: when `save()` fails validation, the entry is recorded as failed and the whole run rolls back — as of 4.0.4, with `success=false` and a non-zero exit so the failure is visible. (On 4.0.3 the run also rolled back, but silently, while still reporting the created counts.)
62+
6163
The `properties` struct is what gets persisted on creation, so include every non-nullable column the model needs — not just the unique ones. The `uniqueProperties` list is only the subset used for the lookup.
6264

6365
## Environment-specific seeds
@@ -93,10 +95,11 @@ The commands you'll actually use day-to-day:
9395

9496
- `wheels seed` — run convention seeds, auto-detecting the current environment
9597
- `wheels seed --environment=production` — run seeds for a specific environment regardless of where the app thinks it's running
96-
- `wheels generate seed` — scaffold `app/db/seeds.cfm` if it doesn't already exist
97-
- `wheels generate seed --all` — scaffold `seeds.cfm` plus `seeds/development.cfm` and `seeds/production.cfm` stubs
98+
- `wheels generate snippets seed-data` — write `seeds.cfm` and `seeds-development.cfm` starter templates to `app/snippets/`; copy them into place (`app/db/seeds.cfm` and `app/db/seeds/development.cfm`) to activate them
99+
100+
There is no `wheels generate seed` command — it errors with `Unknown generator type: seed`. The snippets generator above is the scaffold path.
98101

99-
There's also `wheels seed --generate`, which bypasses your seed files and generates random fake records for every model. It's legacy behaviour from before `seedOnce()` existed and rarely what you want — prefer explicit `seedOnce()` calls where you control the data.
102+
You may also see references to `wheels seed --generate`, a legacy flag from before `seedOnce()` existed that bypasses your seed files and generates random fake records for every model. It's currently non-functional: every model errors internally, zero rows are created, and the command still reports success ([#3082](https://github.com/wheels-dev/wheels/issues/3082)). Don't use it — write explicit `seedOnce()` calls where you control the data.
100103

101104
## Multiple `seedOnce` calls in one file
102105

0 commit comments

Comments
 (0)