Skip to content

Commit 0a6aa5e

Browse files
fix(migrator): skip transaction commit after rollback in migrateIndividual() (#2811) (#2813)
* fix(migrator): skip transaction commit after rollback in migrateIndividual() (#2811) `migrateIndividual()` issued `transaction action="commit"` unconditionally after its try/catch — including on the error path, where the catch block had already issued `transaction action="rollback"`. Unlike `migrateTo()`'s loops, there is no enclosing `for` to `break` out of, so control always fell through to the spurious commit. On Lucee the second action against a closed transaction is a silent no-op. On Adobe CF 2023/2025 (and potentially BoxLang) the JDBC driver may throw a "transaction not active" error that masks the real migration failure, making the underlying problem harder to diagnose. Fix: return `local.rv` from inside the catch after the rollback. Mirrors the early-exit pattern in `migrateTo()` (which uses `break` to exit its for loop and skip the commit); here we use `return` because the `transaction { }` block is not enclosed in a loop. Adds a regression spec under an isolated fixture directory (_assets/migrator-error-path/) so the synthetic always-throw migration does not pollute the shared 001/002/003 fixtures used by every other migrator spec. Diagnosed by both round-1 reviewers on #2810 and explicitly recommended as a follow-up. See issue #2811 for the full diagnosis and pattern comparison against migrateTo(). Signed-off-by: Peter Amiri <peter@alurium.com> * fix(migrator): address Reviewer A/B consensus findings (round 1) - Strip issue-number prefix from the commit-skip comment in Migrator.cfc; preserve the WHY (rollback already closed the transaction; mirrors migrateTo()'s break) per CLAUDE.md "Don't reference the current task". - Strip the trailing "Issue #2811." from the synthetic fixture's hint attribute in 004_synthetic_failing_migration.cfc. - Strip the issue prefix from the spec's intro comment and from the describe label in MigrateIndividualErrorPathSpec.cfc. - Remove the duplicate afterEach block in MigrateIndividualErrorPathSpec.cfc — beforeEach already guarantees a clean state before every it(), so the afterEach was a source of confusion rather than additional safety. The var _isCockroachDB → struct-form consistency nit (Reviewer A finding 3 / Reviewer B convergence item) was explicitly described by both reviewers as lowest-priority "can be addressed separately if desired" and is left for a follow-up to keep this commit scoped to consensus-only changes. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent 39a2bbf commit 0a6aa5e

3 files changed

Lines changed: 93 additions & 0 deletions

File tree

vendor/wheels/Migrator.cfc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,10 @@ component output="false" extends="wheels.Global"{
242242
local.rv = local.rv & "Error migrating #local.migration.version#.#Chr(13) & Chr(10)##e.message##Chr(13) & Chr(10)##e.detail##Chr(13) & Chr(10)#";
243243
transaction action="rollback";
244244
StructDelete(request, "$wheelsTransactionWrapper");
245+
// Skip the commit below — rollback already closed the transaction.
246+
// Mirrors the `break` in migrateTo()'s catch (no enclosing loop
247+
// here, so we return instead).
248+
return local.rv;
245249
}
246250
StructDelete(request, "$wheelsTransactionWrapper");
247251
transaction action="commit";
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
component extends="wheels.migrator.Migration" hint="Synthetic migration whose up() throws — exercises the migrateIndividual() rollback path." {
2+
3+
function up() {
4+
throw(
5+
type = "Wheels.Test.SyntheticFailure",
6+
message = "synthetic failure",
7+
detail = "intentional failure to exercise the rollback path in migrateIndividual()"
8+
);
9+
}
10+
11+
function down() {
12+
}
13+
14+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
component extends="wheels.WheelsTest" {
2+
3+
include "helperFunctions.cfm";
4+
5+
function beforeAll() {
6+
migration = CreateObject("component", "wheels.migrator.Migration").init();
7+
migrator = CreateObject("component", "wheels.Migrator").init(
8+
migratePath = "/wheels/tests/_assets/migrator-error-path/migrations/",
9+
sqlPath = "/wheels/tests/_assets/migrator-error-path/sql/"
10+
);
11+
}
12+
13+
function run() {
14+
15+
var _isCockroachDB = CreateObject("component", "wheels.migrator.Migration").init().adapter.adapterName() == "CockroachDB";
16+
17+
// When migrateIndividual()'s up() throws, the catch block must not fall
18+
// through to `transaction action="commit"` after the rollback. On Lucee
19+
// the spurious commit is a silent no-op; on Adobe CF 2023/2025 (and
20+
// potentially BoxLang) it can throw a JDBC "transaction not active"
21+
// error that masks the real migration failure. This spec asserts the
22+
// post-fix contract on every engine.
23+
describe("migrateIndividual() error path", () => {
24+
25+
beforeEach(() => {
26+
deleteMigratorVersions(2);
27+
try {
28+
queryExecute(
29+
"DELETE FROM #application.wheels.migratorTableName# WHERE version = '004'",
30+
{},
31+
{ datasource = application.wheels.dataSourceName }
32+
);
33+
} catch (any e) {}
34+
$cleanSqlDirectory();
35+
});
36+
37+
it("returns normally when the migration's up() throws", () => {
38+
if (_isCockroachDB) return;
39+
var rv = "";
40+
var threw = false;
41+
var thrownMessage = "";
42+
try {
43+
rv = migrator.migrateIndividual("004");
44+
} catch (any e) {
45+
threw = true;
46+
thrownMessage = e.message;
47+
}
48+
expect(threw).toBeFalse(
49+
"migrateIndividual() should catch the migration error and return its message, "
50+
& "not propagate an exception. Got: " & thrownMessage
51+
);
52+
expect(rv).toInclude("Error migrating 004");
53+
expect(rv).toInclude("synthetic failure");
54+
});
55+
56+
it("does not record a tracking row when the migration throws", () => {
57+
if (_isCockroachDB) return;
58+
migrator.migrateIndividual("004");
59+
var versions = queryExecute(
60+
"SELECT version FROM #application.wheels.migratorTableName# WHERE version = '004'",
61+
{},
62+
{ datasource = application.wheels.dataSourceName }
63+
);
64+
expect(versions.recordCount).toBe(
65+
0,
66+
"The migration's up() threw, so the catch should have rolled back "
67+
& "and returned early. No tracking row should exist for version 004."
68+
);
69+
});
70+
71+
});
72+
73+
}
74+
75+
}

0 commit comments

Comments
 (0)