Delete the members of an aggregate which are referenced by their owner - #934
Open
sven-n wants to merge 2 commits into
Open
Delete the members of an aggregate which are referenced by their owner#934sven-n wants to merge 2 commits into
sven-n wants to merge 2 commits into
Conversation
A member of an aggregate which is a collection holds its foreign key at the child, so "on delete cascade" removes it together with its owner. A member which is referenced by the owner (one-to-one) holds the foreign key at the owner instead, and a cascade only ever runs from the referenced row to the referencing one - so deleting the owner leaves the referenced row behind, unreachable. The generated cascade of these 28 relationships therefore says "deleting the item storage deletes the character", which is the opposite of what's meant. The most visible case is an item storage: every deleted character left its inventory behind, and with it every item lying in it. That happens in the normal game path (DeleteCharacterAction), not just in the bot purge, which worked around it by deleting the storages itself. The source generator now emits the delete triggers for these members along with the cascades it already generates, so the invariant is maintained by the same annotation: mark a member of an aggregate and its delete behaviour is correct, whichever side holds the foreign key. Members which reference a shared type are skipped - deleting them would take data away from their other owners. - EfCoreModelGenerator generates AggregateDeleteTriggers.CreateScript, a trigger function plus one trigger per member (26 of 28; Buff. MagicEffectDefinition and Skill.MasterDefinition reference a shared type and need an annotation review first). - A migration applies the script and removes the item storages which were orphaned before the triggers existed. - A test fails when a model change makes the generated script differ from what the migrations apply, so a new member can't be forgotten. - BotGenerator no longer deletes the storages itself: the trigger has already removed the rows when EF saves, which would fail the save. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9QAbEmp9rhnSWqZvkVaeN
#927 moved the deletion of a single bot account into TryDeleteBotAccountAsync. Kept that structure and removed the manual deletion of the item storages there instead: the delete triggers remove them now, and EF's own delete of the storage would find the row already gone and fail the save. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9QAbEmp9rhnSWqZvkVaeN
Deploying openmudocs with
|
| Latest commit: |
8154d96
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://48613988.openmudocs.pages.dev |
| Branch Preview URL: | https://claude-pr927-cascade-delete.openmudocs.pages.dev |
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.
Fixes #933.
A member of an aggregate which is a collection holds its foreign key at the child, so the generated
ON DELETE CASCADEremoves it together with its owner. A member which is referenced by the owner (one-to-one) holds the foreign key at the owner instead, and a cascade only ever runs from the referenced row to the referencing one — so deleting the owner leaves the referenced row behind, unreachable. The cascade we generate for those 28 relationships today says "deleting the item storage deletes the character", which is the opposite of what's meant.The most visible case is an item storage: every deleted character leaves its inventory behind, and with it every item lying in it. That happens in the normal game path (
DeleteCharacterAction), not only in the bot purge, which worked around it by deleting the storages itself.Since no foreign key can express the other direction, this uses a trigger — generated from the very annotation that states the intent, so it stays correct by construction: mark a member as
[MemberOfAggregate]and its delete behaviour is right, whichever side holds the foreign key.What's in here
EfCoreModelGeneratorgeneratesAggregateDeleteTriggers.CreateScriptnext to the cascades it already emits: onedelete_owned_row()function per schema, plus oneafter deletetrigger per one-to-one member. The list is sorted, so the generated file is stable.BotGeneratorno longer deletes the storages itself. It has to stop: EF orders the character delete before the storage delete, so the trigger has already removed the row when EF's ownDELETEruns, and the save would fail with aDbUpdateConcurrencyException.Triggers fire for rows deleted by a foreign key cascade as well, so
delete from data."Account"alone now cleans up the characters and each of their inventories — a shallow or bulk delete is covered, which the application-side workaround could never do without loading the whole graph.Two members are skipped
[MemberOfAggregate]is not a clean "owned exclusively" list today, and a trigger would make that dangerous rather than merely useless. Members whose target type is shared — it appears in aGameConfigurationcollection or in the generator'sStandaloneTypes— get no trigger and keep today's behaviour:Buff.MagicEffectDefinition—MagicEffectDefinitionis inGameConfiguration.MagicEffectsand also referenced bySkill.MagicEffectDefandItemDefinition.ConsumeEffect.Skill.MasterDefinition—MasterSkillDefinitionis inStandaloneTypes.Two more are included but worth a second opinion from someone who knows the data better than the code shows:
MiniGameChangeEvent.SpawnArea(aMonsterSpawnArea, which also appears inGameMapDefinition.MonsterSpawns) andQuestReward.ItemReward. Say the word and I'll exclude either.Not in here
Flipping those 28
DeleteBehavior.CascadetoNoAction, which would also be right — the database currently carries a rule meaning the opposite of the aggregate semantics, harmless only because EF happens to order deletes dependent-first. It changes the model, so it needs a scaffolded migration with 28 foreign keys dropped and re-added plus a snapshot update; better as its own change.Testing
The new unit test covers the generator/migration drift. What I could not verify here: this environment has no .NET SDK and no PostgreSQL, so the solution was not built and the migration was not executed against a database. The SQL was derived from
EntityDataContextModelSnapshot.csand every schema, table and column name in it was checked against that snapshot, and both script copies (the generated constant and the migration) were verified byte-identical. Still worth one run of the migration against a real database before merging.Note this touches
BotGenerator.DeleteAllBotsAsync, which #927 also changes — whichever lands first, I'm happy to rebase the other.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q9QAbEmp9rhnSWqZvkVaeN
Generated by Claude Code