The GitHub Actions job "CI" on 
grails-core.git/fix/dirty-checking-collection-tracking-8.0.x has failed.
Run started by GitHub user codeconsole (triggered by codeconsole).

Head commit for run:
e88cb0beb97d90a04aa7de6fc52781f5768cc7f5 / Scott Murphy Heiberg 
<[email protected]>
Keep collection properties dirty-checked after reassignment; track 
iterator-based removals

Interception-based stores (MongoDB) rely exclusively on the
DirtyChecking* wrappers — there is no flush-time snapshot comparison —
so mutations that escape them are silently lost: save() sees a clean
entity and persists nothing.

Two real-world escapes:

1. Reassignment through a generated setter stored the raw value, so
   'entity.items = []' over a tracked list replaced the wrapper with a
   plain untracked ArrayList. The common defensive re-init
   'if (!entity.items) entity.items = []' triggers this on every load
   (an empty tracked collection is falsy in Groovy), and because
   [] == [] the equality-suppressed markDirty never flagged the
   assignment either. The in-place add() that followed was lost.

2. DirtyCheckingCollection never overrode iterator(), so every
   iterator-based removal — including Groovy's removeAll(Closure) and
   retainAll(Closure) and Java's removeIf — bypassed tracking, along
   with retainAll(Collection), List.sort and List.replaceAll.

The fix stays interception-only (no snapshots, no flush-time diffing):

- Wrappers override iterator()/listIterator() with dirty-marking
  iterators plus the missing direct mutators — the same approach as
  Hibernate's PersistentCollection.
- Generated setters for Collection/List/Set/Map-typed properties assign
  through DirtyCheckingSupport.rewrap, which wraps the incoming value
  ONLY when the value being replaced was itself a tracked wrapper. A
  never-tracked property (transient instance, or a store like Hibernate
  with its own dirty checking) stores the raw value as before, and
  non-collection properties compile to identical bytecode.
- A replacement wrapper is flagged isAssigned() so
  PersistentEntityCodec takes the full-rewrite path rather than
  per-element diffing — a replacement's layout need not match the
  stored array (a same-size replacement of clean elements previously
  emitted no update at all once wrapped).

Specs reproduce each escape before the fix: DirtyCheckingCollectionSpec
(wrapper mutation paths), DirtyCheckCollectionReassignmentSpec
(setter reassignment), and EmbeddedCollectionDirtyTrackingSpec
(end-to-end against MongoDB, replicating the production shape where an
auto-timestamped entity dropped an embedded-collection add).

Report URL: https://github.com/apache/grails-core/actions/runs/33448845373

With regards,
GitHub Actions via GitBox

Reply via email to