|
| 1 | +--- |
| 2 | +title: 'Wheels 4.0.2: shared-database migration reconciliation and native apt/yum repos' |
| 3 | +slug: wheels-4-0-2-released |
| 4 | +publishedAt: '2026-05-27T20:13:01.000Z' |
| 5 | +updatedAt: '2026-05-28T03:20:32.000Z' |
| 6 | +author: Peter Amiri |
| 7 | +tags: |
| 8 | + - wheels-4 |
| 9 | + - release-notes |
| 10 | + - frameworks |
| 11 | +categories: |
| 12 | + - Releases |
| 13 | +excerpt: >- |
| 14 | + Wheels 4.0.2 is the second patch on the 4.0 line. It teaches the migrator |
| 15 | + how to cope with a database that more than one developer shares — |
| 16 | + orphan-version detection, a `migrate doctor` health report, and `forget` / |
| 17 | + `pretend` reconciliation commands — fixes a class of silent migration |
| 18 | + rollbacks on MSSQL, makes the migrator's column-name helpers consistent, and |
| 19 | + ships native signed apt.wheels.dev / yum.wheels.dev package repositories so |
| 20 | + Linux installs and upgrades are a one-liner. |
| 21 | +coverImage: null |
| 22 | +--- |
| 23 | + |
| 24 | +Wheels 4.0.2 ships today, a week after [4.0.1](https://github.com/wheels-dev/wheels/releases/tag/v4.0.1). Like 4.0.1 it is a patch release in the SemVer sense — no breaking changes, no new public APIs you have to learn — but where 4.0.1 was a broad post-GA shakeout, 4.0.2 has a center of gravity: the migrator, and specifically what happens to migrations when more than one developer points at the same development database. |
| 25 | + |
| 26 | +If you work solo against your own database, most of this release is invisible to you (the apt/yum repos and a handful of fixes aside). If you're on a team that shares a dev database — or you've ever pulled a branch and watched `wheels migrate latest` quietly do nothing — this one is for you. |
| 27 | + |
| 28 | +## Migrations when your team shares a dev database |
| 29 | + |
| 30 | +The `wheels_migrator_versions` tracking table records which migrations have run. On a shared dev database it can drift out of sync with the migration files in your checkout: a teammate applies a migration, the tracking table records its version, but the file that produced it isn't on your branch yet. We call that an **orphan version** — a tracked version with no matching local file. |
| 31 | + |
| 32 | +Before 4.0.2, an orphan at the top of the table sent `wheels migrate latest` down a misleading path: it saw a tracked version "ahead" of your latest local file, assumed you were rolling *back*, and silently no-op'd. You'd run the command, see nothing happen, and have no idea why ([#2798](https://github.com/wheels-dev/wheels/pull/2798)). |
| 33 | + |
| 34 | +4.0.2 detects orphans explicitly and does the sensible thing instead — it warns you, names the orphan versions, and then applies your pending local migrations rather than no-op'ing: |
| 35 | + |
| 36 | +```bash |
| 37 | +wheels migrate latest |
| 38 | +# [warning] These tracked versions have no migration file on this branch: |
| 39 | +# 20260522101500 (applied by a peer?) |
| 40 | +# Applying 1 pending local migration... |
| 41 | +``` |
| 42 | + |
| 43 | +`wheels migrate info` now renders orphan rows with a `[?]` marker so a drifted table is obvious at a glance, Rails-style: |
| 44 | + |
| 45 | +``` |
| 46 | +[x] 20260520090000 create_users |
| 47 | +[?] 20260522101500 ********** NO FILE ********** |
| 48 | +[ ] 20260526140000 add_index_to_orders (pending) |
| 49 | +``` |
| 50 | + |
| 51 | +### A health report, and two reconciliation commands |
| 52 | + |
| 53 | +Three new `wheels migrate` subcommands give you a way to *act* on drift — the Flyway `validate` / `repair` analogues for Wheels ([#2799](https://github.com/wheels-dev/wheels/pull/2799)): |
| 54 | + |
| 55 | +```bash |
| 56 | +wheels migrate doctor # health report: orphans + pending + applied count. Pure read; never mutates. |
| 57 | +wheels migrate forget <version> --yes # drop a stale tracking row WITHOUT running down() |
| 58 | +wheels migrate pretend <version> --yes # mark a version applied WITHOUT running up() |
| 59 | +``` |
| 60 | + |
| 61 | +- **`doctor`** is a single-command, read-only health check. It lists orphan versions, pending local migrations, and the applied count — and prints in yellow when the migrator is unhealthy so a "succeeded but needs attention" result doesn't read as all-clear. |
| 62 | +- **`forget`** removes a single row from `wheels_migrator_versions` without running `down()` — for when an orphan's table changes don't actually exist in your database and you just need the bookkeeping cleaned up. It refuses if a matching local file exists (use `migrate down` for that) or if the version isn't in the table. |
| 63 | +- **`pretend`** inserts a tracking row without running `up()` — for when the schema change is already present (a peer applied it) and you only need to record that fact. It refuses if the version is already applied or if no local file matches. |
| 64 | + |
| 65 | +Both `forget` and `pretend` are **dry-run by default** — without `--yes` they print exactly what they would do and exit without touching the table. |
| 66 | + |
| 67 | +### The tracking table knows more now |
| 68 | + |
| 69 | +`wheels_migrator_versions` gained two columns — `name` and `applied_at` ([#2800](https://github.com/wheels-dev/wheels/pull/2800)). They're additive and nullable, added automatically on the first migrator call after you upgrade, so existing rows keep working and simply display version-only. New migrations record their name and the time they ran, which is what lets `migrate info` show you *what* an orphan was and *when* a peer applied it — not just a bare version number. |
| 70 | + |
| 71 | +The full walkthrough — what an orphan is, the three resolution paths, and the recommendation to avoid sharing a dev database in the first place — is in the new [Shared Development Databases](https://guides.wheels.dev/v4-0-0/basics/shared-development-databases/) guide under Basics. |
| 72 | + |
| 73 | +## Two migrator correctness fixes worth calling out |
| 74 | + |
| 75 | +**Model writes inside a migration no longer silently roll back on MSSQL** ([#2810](https://github.com/wheels-dev/wheels/pull/2810)). If your `up()` or `down()` called `model("Tag").create(...)` (or `update()` / `deleteAll()`), the row could vanish. The migrator wraps every `up()`/`down()` in its own outer transaction, and Model's default `transaction="commit"` opened a *nested* transaction on top — and nested-transaction semantics differ per JDBC driver. On MSSQL most acutely, the inner commit didn't release the row and the outer commit dropped it. The migrator now signals "I own the outer transaction" via a request-scoped flag, and Model skips the nested transaction when it sees it. Engine-agnostic, and the flag is cleared on both the success and error paths so it can't leak past the migration. |
| 76 | + |
| 77 | +**No more spurious commit after a rollback** ([#2813](https://github.com/wheels-dev/wheels/pull/2813)). `migrateIndividual()` issued a `transaction action="commit"` unconditionally after its try/catch — including on the error path, where the catch had already rolled back. On Lucee that second action is a silent no-op, but on Adobe CF 2023/2025 the driver can throw "transaction not active" and *mask the real migration failure*, making the underlying problem much harder to diagnose. The commit is now skipped when the rollback fired. |
| 78 | + |
| 79 | +## Consistent migrator helpers: `columnNames` everywhere |
| 80 | + |
| 81 | +In 4.0.1 most `TableDefinition` column helpers already accepted `columnNames` / `columnName`, but `t.references()` insisted on `referenceNames` and `t.primaryKey()` insisted on `name` — the last two outliers. Both humans and AI agents kept reaching for the consistent form and hitting "argument required" errors. 4.0.2 closes the gap ([#2802](https://github.com/wheels-dev/wheels/pull/2802), [#2812](https://github.com/wheels-dev/wheels/pull/2812)), with a broader `Migration.cfc` command-consistency sweep alongside ([#2804](https://github.com/wheels-dev/wheels/pull/2804)): |
| 82 | + |
| 83 | +```cfm |
| 84 | +// 4.0.2 — matches every other column helper |
| 85 | +t.references(columnNames="user"); |
| 86 | +t.primaryKey(columnNames="userId", autoIncrement=true); |
| 87 | +
|
| 88 | +// still works — the legacy forms aren't going away |
| 89 | +t.references(referenceNames="user"); |
| 90 | +t.primaryKey(name="userId", autoIncrement=true); |
| 91 | +``` |
| 92 | + |
| 93 | +`t.references()` also respects `useUnderscoreReferenceColumns` — when set, it produces `<name>_id` / `<name>_type` columns matching Wheels' `belongsTo` defaults. (The framework default is `false`; `wheels new` scaffolds new apps with it `true`.) |
| 94 | + |
| 95 | +## `wheels upgrade check` learns to advise |
| 96 | + |
| 97 | +The upgrade scanner only knew how to report *breaking* changes. 4.0.2 adds an **advisory tier** — opt-in recommendations that surface in a separate cyan "Recommended Improvements" section and never affect your exit code ([#2805](https://github.com/wheels-dev/wheels/pull/2805)). Advisory checks run on point-release upgrades too, not just major-version jumps. |
| 98 | + |
| 99 | +The first concrete advisories pair with the helper work above ([#2807](https://github.com/wheels-dev/wheels/pull/2807)): if your migrations use `t.references(` the scanner suggests opting into `useUnderscoreReferenceColumns` to match `belongsTo` naming — and it's careful to note that *already-applied* migrations are unaffected, so you're not alarmed about your existing schema. It also warns about the mixed-convention trap of flipping that flag mid-project. The advisory is suppressed when the flag is already set (new apps ship with it on), and — like every check in the scanner now — it strips CFML comments before pattern-matching so a commented-out `// t.references(...)` doesn't trip a false positive. The pre-check that reads your settings was also widened to scan all of `config/`, not just one file ([#2809](https://github.com/wheels-dev/wheels/pull/2809)). |
| 100 | + |
| 101 | +## Native apt and yum repositories |
| 102 | + |
| 103 | +This is the second headline, and it's the one that touches every Linux user. **`apt.wheels.dev` and `yum.wheels.dev` are live, GPG-signed, and serving real package repositories** ([#2814](https://github.com/wheels-dev/wheels/pull/2814)). Installing and upgrading Wheels on Linux is now a normal package-manager operation — no GitHub-release download step, no manual `dpkg -i ./file.deb`: |
| 104 | + |
| 105 | +```bash |
| 106 | +# Debian / Ubuntu |
| 107 | +curl -fsSL https://apt.wheels.dev/wheels.gpg \ |
| 108 | + | sudo tee /usr/share/keyrings/wheels.gpg >/dev/null |
| 109 | +echo "deb [signed-by=/usr/share/keyrings/wheels.gpg] https://apt.wheels.dev stable main" \ |
| 110 | + | sudo tee /etc/apt/sources.list.d/wheels.list |
| 111 | +sudo apt update && sudo apt install wheels |
| 112 | + |
| 113 | +# Fedora / RHEL / Rocky |
| 114 | +sudo dnf config-manager --add-repo https://yum.wheels.dev/wheels.repo |
| 115 | +sudo dnf install wheels |
| 116 | +``` |
| 117 | + |
| 118 | +Upgrades collapse to `sudo apt upgrade wheels` / `sudo dnf upgrade wheels` — one command, no version pinning. The repositories are signed with a dedicated `Wheels Distribution <hello@wheels.dev>` GPG key (fingerprint `6872 16C9 32B4 9F03 94E0 9AED 5D89 AF8F 9C9B 8CFB`), and both the apt `InRelease` index and the yum `repomd.xml.asc` verify against the published key. Under the hood they're served from Cloudflare R2 rather than Pages — Pages caps files at 25 MiB and the `.deb`/`.rpm` are ~80 MB — but the URL experience is identical to what was promised. |
| 119 | + |
| 120 | +While we were in the Linux packaging code, 4.0.2 also fixes the regression where the `.deb`/`.rpm` double-nested the framework one directory too deep, crashing every fresh `wheels new` install on Ubuntu/Fedora with `could not find component or class with name [wheels.Injector]` ([#2776](https://github.com/wheels-dev/wheels/pull/2776)). The Linux packages now stage the framework at the same depth the Homebrew formula does. |
| 121 | + |
| 122 | +## Compatibility matrix restored: BoxLang and Adobe CF 2023/2025 |
| 123 | + |
| 124 | +4.0.2 also greens the compatibility matrix for two engines that had been red since 4.0.0 ([#2817](https://github.com/wheels-dev/wheels/pull/2817)). **BoxLang** had been reporting 17 fail / 72 error on every database — traced to a single line in `Global.cfc`'s pseudo-constructor (`local.varKey = ""`), which BoxLang materializes as `variables.local` and which then shadows the function-local `local` scope of every mixed-in `$`-helper, so `local.appKey = $appKey()` resolved against `{varKey}` and threw `KeyNotFoundException`. Lucee and Adobe both keep `local` reserved to the function scope, so neither saw it; the loop now lives in a real function. **Adobe CF 2023/2025** had been crashing the entire suite (HTTP 404 with a ~1 MB HTML prefix corrupting the result JSON) ever since 4.0.1's `cfheader` fix uncovered a deeper response-already-committed cascade — `InvokeMethodSpec` was invoking `Public.index()` and flushing the congratulations welcome page into the test-runner response buffer, which Adobe then commits mid-run. The render is now captured with `cfsavecontent`, and six further Adobe-specific traps were fixed alongside (`request`-scope parameter shadowing in middleware, empty-body `cfhttp` POSTs in `TestClient`, array-by-value mutation in `ParallelRunner.$collectFailures`, double-`include` in `$reincludeGlobals`, a `fileWrite`/`fileRead` newline roundtrip on Adobe 2025, and `cf_sql_integer` overflow on CockroachDB's `unique_rowid()` PKs). Both engines now report zero failures across the full matrix CI — if you were holding off on 4.0 because your target engine was red, this is the release that closes that gap. |
| 125 | + |
| 126 | +## Smaller fixes |
| 127 | + |
| 128 | +- **Reserved-word column names work in `SELECT`** ([#2787](https://github.com/wheels-dev/wheels/pull/2787)). The `WHERE` and `ORDER BY` builders already quoted identifiers, but the `SELECT`/`GROUP BY` builder appended them raw — so a model backed by a table with a `key`, `order`, or `group` column blew up on `findAll`/`findOne` with a cryptic SQL syntax error the moment the select list mentioned it. Identifiers are now quoted there too. |
| 129 | +- **`wheels packages install` aliases `add` on the paths LuCLI doesn't intercept** ([#2786](https://github.com/wheels-dev/wheels/pull/2786)). For MCP and programmatic callers, `install` now does exactly what `add` does instead of printing a warning and returning nothing. (At the shell, `wheels packages install` is still swallowed by LuCLI's built-in extension installer upstream of module dispatch, so shell users keep using `wheels packages add` — that's documented in the command's own `--help`.) |
| 130 | +- **Clearer routing errors for redundant namespace prefixes** ([#2794](https://github.com/wheels-dev/wheels/pull/2794)). The mapper now rejects a redundant namespace prefix in `to=` / `controller=` instead of silently producing a route that points nowhere. |
| 131 | +- **`?reload=true` re-includes changed `app/global/*.cfm` files** ([#2795](https://github.com/wheels-dev/wheels/pull/2795)), so edits to your global helpers take effect on a bare reload without a full server restart. |
| 132 | +- **A friendlier fresh-install failure** ([#2774](https://github.com/wheels-dev/wheels/pull/2774)). When the Injector fails to construct during application start (a stale `/wheels` mapping under Lucee Express, say), the generated app's `onError` now guards `application.wo` and preserves the *original* error behind a minimal HTML fallback — instead of cascading into the opaque `The key [WO] does not exist` exception that tripped up "Your First 15 Minutes" tutorial readers. |
| 133 | +- A cluster of test-harness fixes: `BrowserTest` resolves its base URL through a layered lookup at instance time ([#2783](https://github.com/wheels-dev/wheels/pull/2783)) and gives a clearer hint when `this.browser` is unwired ([#2782](https://github.com/wheels-dev/wheels/pull/2782)); `WheelsTest` auto-binds include-injected globals into the spec scope ([#2793](https://github.com/wheels-dev/wheels/pull/2793)); and `test-local.sh` no longer dies silently when `~/.lucli/express` is missing ([#2796](https://github.com/wheels-dev/wheels/pull/2796)). |
| 134 | + |
| 135 | +## Upgrading |
| 136 | + |
| 137 | +If you're on 4.0.0 or 4.0.1, upgrading is a one-liner and requires no code changes: |
| 138 | + |
| 139 | +```bash |
| 140 | +brew upgrade wheels # macOS |
| 141 | +scoop update wheels # Windows |
| 142 | +sudo apt upgrade wheels # Debian / Ubuntu (or add the repo above first) |
| 143 | +sudo dnf upgrade wheels # Fedora / RHEL / Rocky |
| 144 | +``` |
| 145 | + |
| 146 | +The migrator's new tracking-table columns are added automatically the first time you run any `wheels migrate` command after upgrading — there's nothing to run by hand. |
| 147 | + |
| 148 | +The [4.0.2 release notes](https://github.com/wheels-dev/wheels/releases/tag/v4.0.2) on GitHub have the full PR list, and the [CHANGELOG](https://github.com/wheels-dev/wheels/blob/develop/CHANGELOG.md) carries the longer-form rationale for each entry. |
| 149 | + |
| 150 | +Thank you to everyone running a shared dev database who filed an issue describing exactly how `migrate latest` confused them — the reconciliation tooling in this release exists because you told us what the silent no-op felt like from the other side. As always, the bleeding-edge channel (`brew install wheels-dev/wheels/wheels-be`, the Scoop `wheels-be` manifest, or the `bleeding-edge` suite on apt/yum) tracks `develop` if you want to ride ahead of the next patch. |
| 151 | + |
| 152 | +Onward to 4.0.3. |
| 153 | + |
0 commit comments