diff --git a/docs/FALUKANT_DEATH_SUCCESSION_SMOKE_TEST.md b/docs/FALUKANT_DEATH_SUCCESSION_SMOKE_TEST.md index 3c2debb..2e976ff 100755 --- a/docs/FALUKANT_DEATH_SUCCESSION_SMOKE_TEST.md +++ b/docs/FALUKANT_DEATH_SUCCESSION_SMOKE_TEST.md @@ -15,6 +15,7 @@ Verhindern, dass nach Tod/Erbe ein Mischzustand entsteht ## Voraussetzungen - Daemon läuft mit aktuellen Änderungen. +- Migration `020_falukant_character_relation_cleanup.sql` ist eingespielt. - Zugriff auf DB (psql). - Zugriff auf WebSocket-Eventstream oder Daemon-Log. - Ein Test-User mit: @@ -63,6 +64,15 @@ WHERE c.user_id = ; ```sql -- Todes-Cleanup (sollte leer sein) +SELECT COUNT(*) FROM falukant_data.relationship +WHERE character1_id = OR character2_id = ; +SELECT COUNT(*) FROM falukant_data.child_relation +WHERE child_character_id = + OR father_character_id = + OR mother_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 = ; SELECT COUNT(*) FROM falukant_data.debtors_prism WHERE character_id = ; SELECT COUNT(*) FROM falukant_data.political_office WHERE character_id = ; @@ -106,5 +116,8 @@ Nach Eintreffen von `falukantUpdateStatus`: - [ ] `CharacterDeath` kommt genau für den verstorbenen Charakter. - [ ] `falukantUpdateStatus` kommt nach Erbwechsel für den betroffenen User. - [ ] `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). - [ ] UI bleibt konsistent (Bild + Status gehören zum selben Charakter). diff --git a/migrations/020_falukant_character_relation_cleanup.sql b/migrations/020_falukant_character_relation_cleanup.sql new file mode 100644 index 0000000..d77bd10 --- /dev/null +++ b/migrations/020_falukant_character_relation_cleanup.sql @@ -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; diff --git a/migrations/README.md b/migrations/README.md index 8f4ad07..26f69d1 100755 --- a/migrations/README.md +++ b/migrations/README.md @@ -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 `parameters` `goal` (`expose`, Standard; oder `blackmail`) sowie `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 +``` diff --git a/src/worker/character_creation.rs b/src/worker/character_creation.rs index b9ebb64..84c42b7 100755 --- a/src/worker/character_creation.rs +++ b/src/worker/character_creation.rs @@ -420,12 +420,14 @@ impl CharacterCreationWorker { let tr = death_ctx.as_ref().map(|c| { death_log::wrap_death_notification(c, "director_death", user_id) }); - Self::notify_user( + if let Err(e) = Self::notify_user( pool, broker, user_id, tr.as_deref().unwrap_or("director_death"), - )?; + ) { + eprintln!("[CharacterCreationWorker] Todesbenachrichtigung: {e}"); + } } // 2) Relationships löschen und betroffene User benachrichtigen @@ -439,12 +441,14 @@ impl CharacterCreationWorker { let tr = death_ctx.as_ref().map(|c| { death_log::wrap_death_notification(c, "relationship_death", related_user_id) }); - Self::notify_user( + if let Err(e) = Self::notify_user( pool, broker, related_user_id, 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| { death_log::wrap_death_notification(c, "child_death", father_user_id) }); - Self::notify_user( + if let Err(e) = Self::notify_user( pool, broker, father_user_id, tr.as_deref().unwrap_or("child_death"), - )?; + ) { + eprintln!("[CharacterCreationWorker] Todesbenachrichtigung: {e}"); + } } if let Some(mother_user_id) = row .get("mother_user_id") @@ -475,12 +481,14 @@ impl CharacterCreationWorker { let tr = death_ctx.as_ref().map(|c| { death_log::wrap_death_notification(c, "child_death", mother_user_id) }); - Self::notify_user( + if let Err(e) = Self::notify_user( pool, broker, mother_user_id, tr.as_deref().unwrap_or("child_death"), - )?; + ) { + eprintln!("[CharacterCreationWorker] Todesbenachrichtigung: {e}"); + } } } diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 68a2166..4a0210f 100755 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -983,7 +983,7 @@ pub const QUERY_DELETE_CHILD_RELATION: &str = r#" WITH deleted AS ( 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).