Skip to content

fix(model): dependent deletes follow callbacks = false; check dependent when declared - #4394

Merged
bpamiri merged 4 commits into
developfrom
peter/dependent-callbacks
Oct 6, 2026
Merged

bpamiri merged 4 commits into
developfrom
peter/dependent-callbacks

Conversation

@bpamiri

@bpamiri bpamiri commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #4373.

delete(callbacks = false) covers dependent records

hasMany() / hasOne() with dependent = "delete" or "remove" instantiate the child records and delete or update them. delete(callbacks = false) on the parent didn't pass callbacks on, so the children's callbacks still ran. $deleteDependents() now takes callbacks and passes it to every dependent call, and $delete() passes its own.

The records those calls load were also found without callbacks. deleteAll(instantiate = true), deleteOne() and updateOne() called findAll() / findOne() without it, so afterFind / afterInitialization still ran with callbacks = false. They now pass it on, as updateAll(instantiate = true) already did.

For hasMany, dependent = "deleteAll" / "removeAll" work in bulk and run no callbacks. For hasOne, they go through deleteOne() / updateOne(), which load the record, and got no callbacks argument at all. They now get one too.

An invalid dependent value fails when declared

An unsupported value, such as dependent = "destroy", was accepted by hasMany() / hasOne() and only threw Wheels.InvalidArgument at the first delete. It now throws the same error, with the same message, when the association is declared. Valid values are unchanged: delete, deleteAll, remove, removeAll and false.

An app that declared an invalid value and never deleted through that association used to load; it now fails at model load, in every environment.

Specs

vendor/wheels/tests/specs/model/dependentCallbacksSpec.cfc uses dedicated test models, so no shared class changes. They are DepPost, with beforeDelete / beforeSave logging to the request, and three parent models: dependent = "delete", "remove" and "destroy". DepPost logs beforeDelete, beforeSave, afterFind and afterRollback. The parent fixtures cover hasMany (delete, remove, destroy) and hasOne (all four values). Each delete runs with transaction = "rollback".

  • callbacks = false with hasMany dependent = "delete" / "remove": no child callback runs (find, delete, save or rollback).
  • callbacks = false with hasOne and each of delete, deleteAll, remove, removeAll: no child callback runs.
  • callbacks = true: the children's afterFind, write and afterRollback callbacks run, for hasMany and for a hasOne with deleteAll.
  • dependent = "destroy" throws Wheels.InvalidArgument when the model loads.
  • Every documented value is accepted.

RED on develop: the two callbacks = false cases (3 child callbacks ran), the declaration case and the accepted-values case fail. The callbacks = true control passes.

GREEN, Lucee 7 + SQLite: full suite 6943/6943; complexity gate passes. The hasOne and finder-callback cases were RED before the second commit (afterFind ran).

🤖 Generated with Claude Code

bpamiri and others added 2 commits October 5, 2026 10:02
…nt when declared

delete(callbacks = false) passed nothing on to the dependent records it deleted or
removed, so their callbacks still ran. An invalid dependent value was accepted by
hasMany() / hasOne() and only threw at the first delete; it now throws
Wheels.InvalidArgument when the association is declared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri

bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

REQUEST CHANGES at 4a8ee44:

  • MEDIUM — vendor/wheels/model/associations.cfm:295: hasOne uses deleteObject/removeObject, which resolve to deleteOne/updateOne even for dependent="deleteAll"/"removeAll". These branches therefore instantiate a child, but still omit the parent's callbacks flag. Pass it through and cover hasOne write and transaction callbacks for false/true. The PR body's statement that these modes never instantiate needs qualification.
  • MEDIUM — vendor/wheels/model/delete.cfm:39, :147, and vendor/wheels/model/update.cfm:206: the internal findAll/findOne calls omit callbacks, so child afterFind/afterInitialization can still run during a dependent operation with callbacks=false. Forward the flag through child loading and add finder-callback regressions alongside the existing write-callback checks.

The new declaration validator preserves case-insensitive operation names and boolean-false values by inspection. The current regression suite covers hasMany beforeDelete/beforeSave in rollback mode; it does not yet prove the single-record or transaction-callback paths above. Final gate remains the revised-head matrix plus PR CI.

deleteAll(instantiate = true), deleteOne() and updateOne() loaded their records
without passing callbacks on, so afterFind / afterInitialization still ran with
callbacks = false. A hasOne with dependent = "deleteAll" / "removeAll" loads its
record through deleteOne() / updateOne() and got no callbacks argument at all.
Both now follow the parent's callbacks; specs cover hasOne for every dependent
value and the finder and transaction callbacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri

bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Source APPROVE at ab06ed7; final gate pending.

Both propagation gaps are resolved: all dependent branches now pass callbacks, including the single-record paths behind hasOne bulk-named modes, and the three internal child-loading queries pass it into their finders. The new fixtures cover all four hasOne dependent modes with callbacks disabled; positive controls verify finder, delete and rollback callbacks still run when enabled.

Evidence qualification: the suite directly tests afterRollback, not afterCommit; suppression of both follows the shared transaction-callback enqueue guard. Likewise, afterFind is directly logged while initialization suppression follows the same forwarded finder flag. No remaining source blocker. Final approval awaits the revised-head matrix and PR CI.

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Wheels Test Results

28 744 tests   28 164 ✅  9m 31s ⏱️
 2 276 suites     580 💤
     4 files         0 ❌

Results for commit a845fc6.

♻️ This comment has been updated with latest results.

…llbacks

Signed-off-by: Peter Amiri <peter@alurium.com>

# Conflicts:
#	vendor/wheels/model/associations.cfm
#	vendor/wheels/model/delete.cfm
@bpamiri

bpamiri commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

rev1-r1, taking the final over from rev1-r2. Delta check of the develop merge with #4397: head a845fc6. The conflict resolution is correct.

I compared this PR's own diff against its merge base, before (ab06ed7, which rev1-r2 source-approved) and after (a845fc6). Every difference is in the expected place:

Full matrix 37379675857 (on ab06ed7): 32/32 legs green, dependentCallbacksSpec 6/6 on every leg. The final follows the narrowed runs on this head (37397511929, Lucee 7 × all 7 databases; 37397521096, 4 engines × SQLite) and PR CI.

@bpamiri

bpamiri commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

FINAL: APPROVE. Head a845fc6 (ab06ed7 + the develop merge with #4397). rev1-r1, taking over from rev1-r2.

@bpamiri
bpamiri merged commit 32b1bdd into develop Oct 6, 2026
44 checks passed
@bpamiri
bpamiri deleted the peter/dependent-callbacks branch October 6, 2026 01:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dependent= deletes ignore callbacks=false; invalid dependent= fails late

1 participant