cleanup at death
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 4m44s

This commit is contained in:
Torsten Schulz (local)
2026-11-04 15:14:08 +01:00
parent 30f5c22974
commit da2b32e877
5 changed files with 124 additions and 9 deletions

View File

@@ -15,6 +15,7 @@ Verhindern, dass nach Tod/Erbe ein Mischzustand entsteht
## Voraussetzungen ## Voraussetzungen
- Daemon läuft mit aktuellen Änderungen. - Daemon läuft mit aktuellen Änderungen.
- Migration `020_falukant_character_relation_cleanup.sql` ist eingespielt.
- Zugriff auf DB (psql). - Zugriff auf DB (psql).
- Zugriff auf WebSocket-Eventstream oder Daemon-Log. - Zugriff auf WebSocket-Eventstream oder Daemon-Log.
- Ein Test-User mit: - Ein Test-User mit:
@@ -63,6 +64,15 @@ WHERE c.user_id = <USER_ID>;
```sql ```sql
-- Todes-Cleanup (sollte leer sein) -- Todes-Cleanup (sollte leer sein)
SELECT COUNT(*) FROM falukant_data.relationship
WHERE character1_id = <CHARACTER_ID> OR character2_id = <CHARACTER_ID>;
SELECT COUNT(*) FROM falukant_data.child_relation
WHERE child_character_id = <CHARACTER_ID>
OR father_character_id = <CHARACTER_ID>
OR mother_character_id = <CHARACTER_ID>;
-- Es dürfen auch keine Zustände gelöschter Beziehungen zurückbleiben.
SELECT COUNT(*) FROM falukant_data.relationship_state rs
WHERE NOT EXISTS (SELECT 1 FROM falukant_data.relationship r WHERE r.id = rs.relationship_id);
SELECT COUNT(*) FROM falukant_data.knowledge WHERE character_id = <CHARACTER_ID>; SELECT COUNT(*) FROM falukant_data.knowledge WHERE character_id = <CHARACTER_ID>;
SELECT COUNT(*) FROM falukant_data.debtors_prism WHERE character_id = <CHARACTER_ID>; SELECT COUNT(*) FROM falukant_data.debtors_prism WHERE character_id = <CHARACTER_ID>;
SELECT COUNT(*) FROM falukant_data.political_office WHERE character_id = <CHARACTER_ID>; SELECT COUNT(*) FROM falukant_data.political_office WHERE character_id = <CHARACTER_ID>;
@@ -106,5 +116,8 @@ Nach Eintreffen von `falukantUpdateStatus`:
- [ ] `CharacterDeath` kommt genau für den verstorbenen Charakter. - [ ] `CharacterDeath` kommt genau für den verstorbenen Charakter.
- [ ] `falukantUpdateStatus` kommt nach Erbwechsel für den betroffenen User. - [ ] `falukantUpdateStatus` kommt nach Erbwechsel für den betroffenen User.
- [ ] `knowledge`/`debtors_prism`/`political_office`/`election_candidate` sind bereinigt. - [ ] `knowledge`/`debtors_prism`/`political_office`/`election_candidate` sind bereinigt.
- [ ] `relationship`/`relationship_state`/`child_relation` sind bereinigt (beide Partner, alle drei Kind-/Elternrollen).
- [ ] Kindstod benachrichtigt auch einen einzelnen bekannten Elternteil (`NULL` beim anderen Elternteil).
- [ ] Ein Benachrichtigungsfehler unterbricht den NPC-Todes-Cleanup nicht.
- [ ] `political_office_history` enthält entfernte Ämter (falls vorhanden). - [ ] `political_office_history` enthält entfernte Ämter (falls vorhanden).
- [ ] UI bleibt konsistent (Bild + Status gehören zum selben Charakter). - [ ] UI bleibt konsistent (Bild + Status gehören zum selben Charakter).

View File

@@ -0,0 +1,77 @@
-- Charakter-Verknüpfungen auch bei DELETEs außerhalb der Worker bereinigen.
-- Löscht bestehende verwaiste Beziehungen; vorher auf der Ziel-DB sichern.
BEGIN;
LOCK TABLE falukant_data.character, falukant_data.relationship,
falukant_data.relationship_state, falukant_data.child_relation
IN SHARE ROW EXCLUSIVE MODE;
DELETE FROM falukant_data.relationship_state rs
WHERE NOT EXISTS (
SELECT 1 FROM falukant_data.relationship r WHERE r.id = rs.relationship_id
)
OR EXISTS (
SELECT 1 FROM falukant_data.relationship r
WHERE r.id = rs.relationship_id
AND ((r.character1_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM falukant_data.character c WHERE c.id = r.character1_id))
OR (r.character2_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM falukant_data.character c WHERE c.id = r.character2_id)))
);
DELETE FROM falukant_data.relationship r
WHERE (r.character1_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM falukant_data.character c WHERE c.id = r.character1_id
)) OR (r.character2_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM falukant_data.character c WHERE c.id = r.character2_id
));
DELETE FROM falukant_data.child_relation cr
WHERE (cr.child_character_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM falukant_data.character c WHERE c.id = cr.child_character_id
)) OR (cr.father_character_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM falukant_data.character c WHERE c.id = cr.father_character_id
)) OR (cr.mother_character_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM falukant_data.character c WHERE c.id = cr.mother_character_id
));
-- Vorhandene einspaltige FKs ersetzen (deren Namen sind je nach Backend verschieden).
-- Auch relationship_state absichern, damit es die Kaskade nicht blockieren kann.
DO $$
DECLARE
target record;
existing_fk record;
BEGIN
FOR target IN
SELECT * FROM (VALUES
('relationship', 'character1_id', 'character'),
('relationship', 'character2_id', 'character'),
('child_relation', 'child_character_id', 'character'),
('child_relation', 'father_character_id', 'character'),
('child_relation', 'mother_character_id', 'character'),
('relationship_state', 'relationship_id', 'relationship')
) AS targets(table_name, column_name, referenced_table)
LOOP
FOR existing_fk IN
SELECT con.conname
FROM pg_constraint con
JOIN pg_attribute att ON att.attrelid = con.conrelid
AND con.conkey = ARRAY[att.attnum]
WHERE con.contype = 'f'
AND con.conrelid = format('falukant_data.%I', target.table_name)::regclass
AND con.confrelid = format('falukant_data.%I', target.referenced_table)::regclass
AND att.attname = target.column_name
LOOP
EXECUTE format('ALTER TABLE falukant_data.%I DROP CONSTRAINT %I',
target.table_name, existing_fk.conname);
END LOOP;
EXECUTE format(
'ALTER TABLE falukant_data.%I ADD CONSTRAINT %I FOREIGN KEY (%I) REFERENCES falukant_data.%I (id) ON DELETE CASCADE',
target.table_name, target.table_name || '_' || target.column_name || '_fkey',
target.column_name, target.referenced_table);
END LOOP;
END;
$$;
COMMIT;

View File

@@ -65,3 +65,20 @@ negativen Ruf- und Eheeffekte für niedrige und mittlere Stände).
Der Backend-Auftrag verwendet `performer_id`, `victim_id` und optional in Der Backend-Auftrag verwendet `performer_id`, `victim_id` und optional in
`parameters` `goal` (`expose`, Standard; oder `blackmail`) sowie `parameters` `goal` (`expose`, Standard; oder `blackmail`) sowie
`relationship_id`. Details: `docs/FALUKANT_UNDERGROUND_INVESTIGATE_AFFAIR.md`. `relationship_id`. Details: `docs/FALUKANT_UNDERGROUND_INVESTIGATE_AFFAIR.md`.
## `020_falukant_character_relation_cleanup.sql`
Entfernt bestehende verwaiste Einträge aus `relationship`, `relationship_state`
und `child_relation`. Sichert anschließend beide Beziehungspartner sowie Kind,
Vater und Mutter mit Fremdschlüsseln und `ON DELETE CASCADE` ab. Damit werden
die Verknüpfungen bei jeder Charakter-Löschung entfernt, auch außerhalb des
Daemons. `relationship_state` wird zusammen mit der Beziehung gelöscht.
Zeilen mit unbekanntem Elternteil (`NULL`) bleiben gültig.
Voraussetzung: Migration `001` ist eingespielt. Die Migration läuft atomar,
ist wiederholbar und sperrt Schreibzugriffe auf die betroffenen Tabellen für
ihre Laufzeit. Nach Backup manuell auf der Ziel-DB ausführen:
```bash
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f migrations/020_falukant_character_relation_cleanup.sql
```

View File

@@ -420,12 +420,14 @@ impl CharacterCreationWorker {
let tr = death_ctx.as_ref().map(|c| { let tr = death_ctx.as_ref().map(|c| {
death_log::wrap_death_notification(c, "director_death", user_id) death_log::wrap_death_notification(c, "director_death", user_id)
}); });
Self::notify_user( if let Err(e) = Self::notify_user(
pool, pool,
broker, broker,
user_id, user_id,
tr.as_deref().unwrap_or("director_death"), tr.as_deref().unwrap_or("director_death"),
)?; ) {
eprintln!("[CharacterCreationWorker] Todesbenachrichtigung: {e}");
}
} }
// 2) Relationships löschen und betroffene User benachrichtigen // 2) Relationships löschen und betroffene User benachrichtigen
@@ -439,12 +441,14 @@ impl CharacterCreationWorker {
let tr = death_ctx.as_ref().map(|c| { let tr = death_ctx.as_ref().map(|c| {
death_log::wrap_death_notification(c, "relationship_death", related_user_id) death_log::wrap_death_notification(c, "relationship_death", related_user_id)
}); });
Self::notify_user( if let Err(e) = Self::notify_user(
pool, pool,
broker, broker,
related_user_id, related_user_id,
tr.as_deref().unwrap_or("relationship_death"), tr.as_deref().unwrap_or("relationship_death"),
)?; ) {
eprintln!("[CharacterCreationWorker] Todesbenachrichtigung: {e}");
}
} }
} }
@@ -461,12 +465,14 @@ impl CharacterCreationWorker {
let tr = death_ctx.as_ref().map(|c| { let tr = death_ctx.as_ref().map(|c| {
death_log::wrap_death_notification(c, "child_death", father_user_id) death_log::wrap_death_notification(c, "child_death", father_user_id)
}); });
Self::notify_user( if let Err(e) = Self::notify_user(
pool, pool,
broker, broker,
father_user_id, father_user_id,
tr.as_deref().unwrap_or("child_death"), tr.as_deref().unwrap_or("child_death"),
)?; ) {
eprintln!("[CharacterCreationWorker] Todesbenachrichtigung: {e}");
}
} }
if let Some(mother_user_id) = row if let Some(mother_user_id) = row
.get("mother_user_id") .get("mother_user_id")
@@ -475,12 +481,14 @@ impl CharacterCreationWorker {
let tr = death_ctx.as_ref().map(|c| { let tr = death_ctx.as_ref().map(|c| {
death_log::wrap_death_notification(c, "child_death", mother_user_id) death_log::wrap_death_notification(c, "child_death", mother_user_id)
}); });
Self::notify_user( if let Err(e) = Self::notify_user(
pool, pool,
broker, broker,
mother_user_id, mother_user_id,
tr.as_deref().unwrap_or("child_death"), tr.as_deref().unwrap_or("child_death"),
)?; ) {
eprintln!("[CharacterCreationWorker] Todesbenachrichtigung: {e}");
}
} }
} }

View File

@@ -983,7 +983,7 @@ pub const QUERY_DELETE_CHILD_RELATION: &str = r#"
WITH deleted AS ( WITH deleted AS (
DELETE FROM falukant_data.child_relation WHERE child_character_id = $1 RETURNING father_character_id, mother_character_id DELETE FROM falukant_data.child_relation WHERE child_character_id = $1 RETURNING father_character_id, mother_character_id
) )
SELECT cf.user_id AS father_user_id, cm.user_id AS mother_user_id FROM deleted d JOIN falukant_data.character cf ON cf.id = d.father_character_id JOIN falukant_data.character cm ON cm.id = d.mother_character_id; SELECT cf.user_id AS father_user_id, cm.user_id AS mother_user_id FROM deleted d LEFT JOIN falukant_data.character cf ON cf.id = d.father_character_id LEFT JOIN falukant_data.character cm ON cm.id = d.mother_character_id;
"#; "#;
/// Entfernt alle child_relation-Zeilen, in denen der Charakter Vater oder Mutter ist (nötig vor Charakter-Löschung). /// Entfernt alle child_relation-Zeilen, in denen der Charakter Vater oder Mutter ist (nötig vor Charakter-Löschung).