Persist: index new entity first so circular references don't loop#15
Open
Spamercz wants to merge 1 commit into
Open
Persist: index new entity first so circular references don't loop#15Spamercz wants to merge 1 commit into
Spamercz wants to merge 1 commit into
Conversation
Persisting a new entity (EmptyElasticId) with a nested elastic entity that
references it back looped: Insert prepared the parent first, which persisted
the child, whose prepare persisted the parent again, and so on. New entities
are not tracked in the IdentityMap (empty-string id), so isChanged() always
reported them as changed and they were re-persisted.
Insert::execute now saves a new entity in two phases:
1. index it once with a shallow (all-null) body to obtain its id and register
it as "persisting" in the IdentityMap,
2. prepare/persist its nested entities - a back-reference to the parent now
resolves to the parent's id instead of re-persisting it,
3. re-index it with the full body.
Entities that already have an id are still indexed once.
IdentityMap gains a persistingList with markPersisting/unmarkPersisting/
isPersisting. PrepareEntityArray short-circuits to the id (instead of
recursing) whenever a referenced entity is currently being persisted, for the
nested AbstractElasticEntity, ElasticEntityCollection item and STIElasticEntity
cases. The "persisting" check is needed in addition to isChanged() because the
parent's hash changes as its children receive ids.
Adds PersistCircularTest, which persists a Title whose backdrop is an Image
whose title points back at the Title, and asserts it terminates, both ids are
assigned and each stored document references the other.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56a5a10 to
2d801f4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Persisting a new entity (
EmptyElasticId) whose nested elastic entity references it back loops forever.Insert::executecallsPrepareEntityArray::prepare()first, which persists the child; the child'sprepare()persists the parent again, and so on. New entities aren't tracked inIdentityMap(their id is the empty string), soisChanged()always reports them as changed and they get re-persisted.The v2.0
Title/Imagefixtures (Title.backdrop → Image,Image.title → Title) are exactly this circular shape.Change
Insert::executenow saves a new entity in two phases:IdentityMap.Entities that already have an id are still indexed once (unchanged).
Details
IdentityMapgainspersistingList+markPersisting()/unmarkPersisting()/isPersisting()(andclear()resets it).PrepareEntityArrayshort-circuits to the referenced entity's id (instead of recursing) whenever itisPersisting(), for the nestedAbstractElasticEntity,ElasticEntityCollectionitem, andSTIElasticEntitycases. This guard is needed in addition toisChanged()because the parent's serialized hash changes as its children receive ids — soisChanged()alone would still re-enter the loop.id), which is a valid non-empty document for any mapping; its content is irrelevant because phase 3 overwrites it. Only the second index refreshes, so there is one refresh per entity as before (one extra cheap index call for new entities).Test
tests/SpameriTests/Elastic/EntityManager/PersistCircularTest.phptpersists aTitlewhosebackdropis anImagewhosetitlepoints back at theTitle, and asserts: it terminates (no loop), both ids are assigned, the in-memory graph resolves both ids, and each stored document references the other.PHPStan (level 6) and CS are clean on the changed files.
🤖 Generated with Claude Code