You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: web/sites/guides/src/content/docs/v4-0-0/basics/migrations.mdx
+8-8Lines changed: 8 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -39,10 +39,10 @@ wheels --version
39
39
<Asidetype="caution"title="Write commands require a project-bound server">
40
40
`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.
41
41
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).
43
43
</Aside>
44
44
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)).
46
46
47
47
<Asidetype="caution"title="DDL auto-commit on MySQL and Oracle">
48
48
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
66
66
wheels generate migration CreatePosts
67
67
```
68
68
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.
70
70
71
71
## Writing a migration — full example
72
72
@@ -94,7 +94,7 @@ Three things to note:
94
94
95
95
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()`.
96
96
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)`.
98
98
99
99
## Column types
100
100
@@ -105,7 +105,7 @@ These are the column-builder methods defined on `TableDefinition`. Every one tak
105
105
|`t.string(columnNames=, limit=, allowNull=, default=)`| VARCHAR |`limit` defaults to 255 |
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.
234
234
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()` fortimestamps. 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.
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)");
241
241
}
242
242
243
243
function down() {
@@ -273,7 +273,7 @@ The throw is the honest signal: "you can't walk past this point backward." If yo
273
273
274
274
## Filename format
275
275
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.
Copy file name to clipboardExpand all lines: web/sites/guides/src/content/docs/v4-0-0/basics/seeding.mdx
+6-3Lines changed: 6 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -58,6 +58,8 @@ Run it with `wheels seed`. The seeder opens a transaction, includes `app/db/seed
58
58
59
59
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.
60
60
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
+
61
63
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.
62
64
63
65
## Environment-specific seeds
@@ -93,10 +95,11 @@ The commands you'll actually use day-to-day:
93
95
94
96
-`wheels seed` — run convention seeds, auto-detecting the current environment
95
97
-`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.
98
101
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.
0 commit comments