From f09d89c1912493c8e5e0f6f1772bdf6a612aa2df Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Mon, 5 Oct 2026 09:47:19 -0700 Subject: [PATCH 1/3] fix(model): skip commit callbacks when callbacks = false save(callbacks = false) and delete(callbacks = false) skipped every other callback but still queued afterCommit / afterRollback. They now skip those too. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Peter Amiri --- .../callbacks-false-commit-callbacks.fixed.md | 1 + vendor/wheels/model/create.cfm | 10 ++- vendor/wheels/model/delete.cfm | 5 +- .../model/callbacksFalseAfterCommitSpec.cfc | 73 +++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 changelog.d/callbacks-false-commit-callbacks.fixed.md create mode 100644 vendor/wheels/tests/specs/model/callbacksFalseAfterCommitSpec.cfc diff --git a/changelog.d/callbacks-false-commit-callbacks.fixed.md b/changelog.d/callbacks-false-commit-callbacks.fixed.md new file mode 100644 index 0000000000..7ae58bb539 --- /dev/null +++ b/changelog.d/callbacks-false-commit-callbacks.fixed.md @@ -0,0 +1 @@ +- `save(callbacks=false)` and `delete(callbacks=false)` now skip `afterCommit` and `afterRollback` too, like every other callback. They used to queue them. A save that changes nothing still queues `afterCommit` diff --git a/vendor/wheels/model/create.cfm b/vendor/wheels/model/create.cfm index a506d4776f..d1e072651e 100644 --- a/vendor/wheels/model/create.cfm +++ b/vendor/wheels/model/create.cfm @@ -178,7 +178,10 @@ local.rv = true; // v4.2.0: queue afterCommit/afterRollback (fires at the outermost // transaction resolve, or immediately in none/false mode). - $enqueueTransactionCallbacks(operation = "create"); + // callbacks = false skips the commit callbacks too, like every other callback. + if (arguments.callbacks) { + $enqueueTransactionCallbacks(operation = "create"); + } } else if (local.rollback) { $resetToNew(); } @@ -206,7 +209,10 @@ } local.rv = true; // v4.2.0: queue afterCommit/afterRollback (see create branch). - $enqueueTransactionCallbacks(operation = "update"); + // callbacks = false skips the commit callbacks too, like every other callback. + if (arguments.callbacks) { + $enqueueTransactionCallbacks(operation = "update"); + } } } else { $validateAssociations(callbacks = arguments.callbacks); diff --git a/vendor/wheels/model/delete.cfm b/vendor/wheels/model/delete.cfm index b7557a11f8..74aef5d91a 100644 --- a/vendor/wheels/model/delete.cfm +++ b/vendor/wheels/model/delete.cfm @@ -226,7 +226,10 @@ local.rv = true; // v4.2.0: queue afterCommit/afterRollback (fires at the outermost // transaction resolve, or immediately in none/false mode). - $enqueueTransactionCallbacks(operation = "delete"); + // callbacks = false skips the commit callbacks too, like every other callback. + if (arguments.callbacks) { + $enqueueTransactionCallbacks(operation = "delete"); + } } } return local.rv; diff --git a/vendor/wheels/tests/specs/model/callbacksFalseAfterCommitSpec.cfc b/vendor/wheels/tests/specs/model/callbacksFalseAfterCommitSpec.cfc new file mode 100644 index 0000000000..5de53aa0a2 --- /dev/null +++ b/vendor/wheels/tests/specs/model/callbacksFalseAfterCommitSpec.cfc @@ -0,0 +1,73 @@ +/** + * `callbacks = false` on save() / delete() also skips afterCommit and afterRollback, like every other + * callback. A save that changes nothing still queues afterCommit (unchanged, documented). The test + * runner uses transactionMode = "none", so specs pass `transaction` explicitly. + */ +component extends="wheels.WheelsTest" { + + function beforeAll() { + variables.g = application.wo; + } + + function run() { + + describe("callbacks = false and the commit callbacks", () => { + + beforeEach(() => { + request.$acLog = []; + variables.g.model("tag").$registerCallback(type = "afterCommit", methods = "recordAfterCommit"); + variables.g.model("tag").$registerCallback(type = "afterRollback", methods = "recordAfterRollback"); + }); + + afterEach(() => { + variables.g.model("tag").$clearCallbacks(type = "afterCommit"); + variables.g.model("tag").$clearCallbacks(type = "afterRollback"); + variables.g.model("tag").deleteAll(where = "name LIKE 'cbfalse-%'", instantiate = false, callbacks = false, transaction = "commit"); + }); + + it("skips afterCommit on a create with callbacks = false", () => { + variables.g.model("tag").new(name = "cbfalse-create").save(callbacks = false, transaction = "commit"); + expect(ArrayLen(request.$acLog)).toBe(0); + }); + + it("skips afterCommit on an update with callbacks = false", () => { + var t = variables.g.model("tag").new(name = "cbfalse-update"); + t.save(callbacks = false, transaction = "commit"); + t.name = "cbfalse-update-2"; + t.save(callbacks = false, transaction = "commit"); + expect(ArrayLen(request.$acLog)).toBe(0); + }); + + it("skips afterCommit on a delete with callbacks = false", () => { + var t = variables.g.model("tag").new(name = "cbfalse-delete"); + t.save(callbacks = false, transaction = "commit"); + t.delete(callbacks = false, transaction = "commit"); + expect(ArrayLen(request.$acLog)).toBe(0); + }); + + it("skips afterRollback on a rolled-back save with callbacks = false", () => { + variables.g.model("tag").new(name = "cbfalse-rollback").save(callbacks = false, transaction = "rollback"); + expect(ArrayLen(request.$acLog)).toBe(0); + }); + + it("still runs them with callbacks = true", () => { + var t = variables.g.model("tag").new(name = "cbfalse-on"); + t.save(transaction = "commit"); + t.delete(transaction = "commit"); + variables.g.model("tag").new(name = "cbfalse-on-rb").save(transaction = "rollback"); + expect(ArrayToList(request.$acLog)).toBe("commit:cbfalse-on,commit:cbfalse-on,rollback:cbfalse-on-rb"); + }); + + it("still queues afterCommit for a save that changes nothing", () => { + var t = variables.g.model("tag").new(name = "cbfalse-nochange"); + t.save(transaction = "commit"); + request.$acLog = []; + t.save(transaction = "commit"); + expect(ArrayLen(request.$acLog)).toBe(1); + }); + + }); + + } + +} From 8d15191577047a567743eaf28e369edd44aefe7b Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Mon, 5 Oct 2026 09:54:20 -0700 Subject: [PATCH 2/3] test(model): savedChanges() is put back after a callbacks = false save rolls back Co-Authored-By: Claude Opus 5.5 Signed-off-by: Peter Amiri --- .../specs/model/callbacksFalseAfterCommitSpec.cfc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/vendor/wheels/tests/specs/model/callbacksFalseAfterCommitSpec.cfc b/vendor/wheels/tests/specs/model/callbacksFalseAfterCommitSpec.cfc index 5de53aa0a2..9bcee2fbec 100644 --- a/vendor/wheels/tests/specs/model/callbacksFalseAfterCommitSpec.cfc +++ b/vendor/wheels/tests/specs/model/callbacksFalseAfterCommitSpec.cfc @@ -50,6 +50,16 @@ component extends="wheels.WheelsTest" { expect(ArrayLen(request.$acLog)).toBe(0); }); + it("still puts back savedChanges() when a callbacks = false save rolls back", () => { + var t = variables.g.model("tag").new(name = "cbfalse-saved"); + t.save(callbacks = false, transaction = "commit"); + var before = Duplicate(t.savedChanges()); + t.name = "cbfalse-saved-undone"; + t.save(callbacks = false, transaction = "rollback"); + expect(t.savedChanges()).toBe(before); + expect(ArrayLen(request.$acLog)).toBe(0); + }); + it("still runs them with callbacks = true", () => { var t = variables.g.model("tag").new(name = "cbfalse-on"); t.save(transaction = "commit"); From c1c50b8035fe982daa4c24bf99e7e6e4757b4b86 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Mon, 5 Oct 2026 09:58:06 -0700 Subject: [PATCH 3/3] docs(model): the no-change afterCommit note applies with callbacks on Co-Authored-By: Claude Opus 5.5 Signed-off-by: Peter Amiri --- changelog.d/callbacks-false-commit-callbacks.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/callbacks-false-commit-callbacks.fixed.md b/changelog.d/callbacks-false-commit-callbacks.fixed.md index 7ae58bb539..1ec7f37235 100644 --- a/changelog.d/callbacks-false-commit-callbacks.fixed.md +++ b/changelog.d/callbacks-false-commit-callbacks.fixed.md @@ -1 +1 @@ -- `save(callbacks=false)` and `delete(callbacks=false)` now skip `afterCommit` and `afterRollback` too, like every other callback. They used to queue them. A save that changes nothing still queues `afterCommit` +- `save(callbacks=false)` and `delete(callbacks=false)` now skip `afterCommit` and `afterRollback` too, like every other callback. They used to queue them. With callbacks on, a save that changes nothing still queues `afterCommit`