Skip to content

[BUG] Minigame ranking entry FK blocks character deletion #796

@nolt

Description

@nolt

Characters that have a MiniGameRankingEntry cannot be deleted (FK has no cascade)

Description

Deleting a character fails when that character has any row in MiniGameRankingEntry.
It happens both via the Admin Panel and in‑game (the save on
Player.RemoveFromGameAsyncSaveProgressAsync throws), with:

Npgsql.PostgresException 23503: update or delete on table "Character"
violates foreign key constraint "FK_MiniGameRankingEntry_Character_CharacterId"
on table "MiniGameRankingEntry"

Root cause

MiniGameRankingEntry.Character is an optional navigation:

// src/DataModel/Statistics/MiniGameRankingEntry.cs
public virtual Character? Character { get; set; }

and the entity is registered without any relationship/delete configuration:

// src/Persistence/EntityFramework/EntityDataContext.cs
modelBuilder.Entity<MiniGameRankingEntry>();

For an optional relationship EF Core defaults the FK to NO ACTION, so the database
rejects the character delete. Every other character‑owned relationship is explicitly
cascaded (see CharacterExtensions.cs: letters, skills, stat attributes, quest states,
drop‑item‑groups, guild membership all use OnDelete(DeleteBehavior.Cascade)).
MiniGameRankingEntry was simply missed — so it is the only one of the seven
Character foreign keys that blocks deletion.

Impact

Any character that ever placed in a mini‑game ranking (Blood Castle / Devil Square /
Chaos Castle, etc.) can no longer be deleted, by admins or players.

Reproduce

  1. Play a mini‑game with a character so it gets a MiniGameRankingEntry.
  2. Try to delete that character (Admin Panel or in‑game) → FK violation 23503.

Two possible fixes (design decision)

  • A — Cascade (simplest, consistent). Configure the relationship with
    OnDelete(DeleteBehavior.Cascade) so ranking entries are removed together with the
    character. Matches how all other character‑owned data behaves. Ranking entries are
    scoped per game instance (GameInstanceId), i.e. transient, so dropping them on
    character deletion is reasonable.

  • B — Keep history (SET NULL + snapshot). If ranking entries should survive
    character deletion (a persistent leaderboard), change the FK to SetNull (the
    navigation is already nullable) and store a denormalized character‑name snapshot on
    the entry, updating the ranking view to use it. More involved; only worth it if
    persistent leaderboards are intended. The current NO ACTION implements neither
    intent — it just blocks deletion.

Note on existing databases

A model/config change only fixes new schemas. Already‑created databases also need a
migration (or a manual ALTER TABLE data."MiniGameRankingEntry" ... ON DELETE CASCADE/SET NULL), since EF won't retroactively alter an existing FK.

Immediate workaround

Delete the character's ranking rows first, then delete the character:

DELETE FROM data."MiniGameRankingEntry" WHERE "CharacterId" = '<character-id>';

or apply the cascade once at the DB level:

ALTER TABLE data."MiniGameRankingEntry" DROP CONSTRAINT "FK_MiniGameRankingEntry_Character_CharacterId";
ALTER TABLE data."MiniGameRankingEntry" ADD CONSTRAINT "FK_MiniGameRankingEntry_Character_CharacterId"
  FOREIGN KEY ("CharacterId") REFERENCES data."Character"("Id") ON DELETE CASCADE;

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions