From 0892e2db8b52c7f16e202e65723e4b41e5d95d2a Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 17 Apr 2026 17:30:40 +0200 Subject: [PATCH] Implement political office history logging: Added SQL logic to archive political office records upon expiration and deletion, ensuring historical tracking of office terms. Updated relevant queries to insert records into `falukant_log.political_office_history` for better compliance with data retention policies. --- ..._falukant_log_political_office_history.sql | 28 ++++++ migrations/README.md | 4 + src/worker/sql.rs | 97 ++++++++++++++----- 3 files changed, 105 insertions(+), 24 deletions(-) create mode 100644 migrations/016_falukant_log_political_office_history.sql diff --git a/migrations/016_falukant_log_political_office_history.sql b/migrations/016_falukant_log_political_office_history.sql new file mode 100644 index 0000000..e9bada8 --- /dev/null +++ b/migrations/016_falukant_log_political_office_history.sql @@ -0,0 +1,28 @@ +-- Abgeschlossene politische Ämter (Amtsende, Neubesetzung, Entfernung) für Auswertung „Karrierehöchstwert“ / UI. +-- Wird vom YpDaemon vor Löschen aus falukant_data.political_office befüllt. + +CREATE TABLE IF NOT EXISTS falukant_log.political_office_history ( + id BIGSERIAL PRIMARY KEY, + character_id INTEGER NOT NULL, + office_type_id INTEGER NOT NULL, + region_id INTEGER, + start_date TIMESTAMPTZ NOT NULL, + end_date TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tabelle kann bereits vom Backend existieren (ohne region_id o. Ä.): CREATE TABLE IF NOT EXISTS ergänzt keine Spalten. +ALTER TABLE falukant_log.political_office_history + ADD COLUMN IF NOT EXISTS region_id INTEGER; + +CREATE INDEX IF NOT EXISTS idx_pol_office_hist_character + ON falukant_log.political_office_history (character_id); + +CREATE INDEX IF NOT EXISTS idx_pol_office_hist_office_type + ON falukant_log.political_office_history (office_type_id); + +CREATE INDEX IF NOT EXISTS idx_pol_office_hist_region + ON falukant_log.political_office_history (region_id); + +COMMENT ON TABLE falukant_log.political_office_history IS + 'Politische Amtszeiten nach Ende; start_date/end_date aus Amtszeile bzw. NOW() bei vorzeitigem Ende (YpDaemon).'; diff --git a/migrations/README.md b/migrations/README.md index 8bb444b..d03e458 100644 --- a/migrations/README.md +++ b/migrations/README.md @@ -41,3 +41,7 @@ Spalte **`falukant_data.falukant_user.certificate_productions_count_since`**: Ze ## `015_falukant_log_production_completion_count.sql` Spalte **`falukant_log.production.completion_count`**: zählt **abgeschlossene Produktionen** pro aggregierter Log-Zeile (bei gleichem Tag/Produkt/Region wird die Menge per UPSERT summiert; ohne `completion_count` bliebe `COUNT(*)` über die Zeilen fälschlich niedrig). Zertifikatsabfrage nutzt **`SUM(completion_count)`** (Migration **`015`** vor Deploy des aktualisierten Produce-Workers ausführen). + +## `016_falukant_log_political_office_history.sql` + +Tabelle **`falukant_log.political_office_history`**: Archiv abgeschlossener politischer Amtszeiten (`character_id`, `office_type_id`, `region_id`, `start_date`, `end_date`). Der Daemon schreibt **vor** jedem relevanten `DELETE` auf **`falukant_data.political_office`** (Amtsende/Neuwahl-Pfad, Übersitz-Trim, Charaktertod). **`falukant_data.process_elections()`** (PostgreSQL) liegt außerhalb des Rust-Repos — falls dort Zeilen gelöscht werden, analog **`INSERT` in diese Historie** in der DB-Funktion ergänzen. diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 6354193..4af548a 100644 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -1040,15 +1040,28 @@ pub const QUERY_SELECT_NEEDED_ELECTIONS: &str = r#" target_date AS ( SELECT NOW()::date AS election_date ), + offices_ending_today AS ( + SELECT po.id, + po.character_id, + po.office_type_id, + po.region_id, + po.created_at AS start_date, + (po.created_at + (pot.term_length * INTERVAL '1 day')) AS end_date + FROM falukant_data.political_office po + JOIN falukant_type.political_office_type pot ON po.office_type_id = pot.id + CROSS JOIN target_date td + WHERE (po.created_at + (pot.term_length * INTERVAL '1 day'))::date = td.election_date + ), + archived_expiring AS ( + INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date) + SELECT character_id, office_type_id, region_id, start_date, end_date + FROM offices_ending_today + ), expired_today AS ( - DELETE FROM falukant_data.political_office AS po - USING falukant_type.political_office_type AS pot - WHERE po.office_type_id = pot.id - AND (po.created_at + (pot.term_length * INTERVAL '1 day'))::date - = (SELECT election_date FROM target_date) - RETURNING - pot.id AS office_type_id, - po.region_id AS region_id + DELETE FROM falukant_data.political_office po + WHERE po.id IN (SELECT id FROM offices_ending_today) + RETURNING po.office_type_id AS office_type_id, + po.region_id AS region_id ), gaps_per_region AS ( SELECT @@ -1151,14 +1164,27 @@ pub const QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES: &str = r#" pub const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#" WITH + doomed AS ( + SELECT po.id, + po.character_id, + po.office_type_id, + po.region_id, + po.created_at AS start_date, + (po.created_at + (pot.term_length * INTERVAL '1 day')) AS term_end + FROM falukant_data.political_office po + JOIN falukant_type.political_office_type pot ON po.office_type_id = pot.id + WHERE (po.created_at + (pot.term_length * INTERVAL '1 day')) <= NOW() + ), + archived_expired AS ( + INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date) + SELECT character_id, office_type_id, region_id, start_date, term_end + FROM doomed + ), expired_offices AS ( - DELETE FROM falukant_data.political_office AS po - USING falukant_type.political_office_type AS pot - WHERE po.office_type_id = pot.id - AND (po.created_at + (pot.term_length * INTERVAL '1 day')) <= NOW() - RETURNING - pot.id AS office_type_id, - po.region_id AS region_id + DELETE FROM falukant_data.political_office po + WHERE po.id IN (SELECT id FROM doomed) + RETURNING po.office_type_id AS office_type_id, + po.region_id AS region_id ), distinct_types AS ( SELECT DISTINCT office_type_id, region_id FROM expired_offices @@ -1617,8 +1643,10 @@ pub const QUERY_TRIM_EXCESS_OFFICES_GLOBAL: &str = r#" ranked AS ( SELECT po.id, + po.character_id, po.office_type_id, po.region_id, + po.created_at, s.seats_total, ROW_NUMBER() OVER ( PARTITION BY po.office_type_id, po.region_id @@ -1630,9 +1658,14 @@ pub const QUERY_TRIM_EXCESS_OFFICES_GLOBAL: &str = r#" AND s.region_id = po.region_id ), to_delete AS ( - SELECT id + SELECT id, character_id, office_type_id, region_id, created_at FROM ranked WHERE rn > seats_total + ), + archived_trim AS ( + INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date) + SELECT character_id, office_type_id, region_id, created_at, NOW() + FROM to_delete ) DELETE FROM falukant_data.political_office WHERE id IN (SELECT id FROM to_delete); @@ -2485,7 +2518,12 @@ pub const QUERY_DELETE_POLITICAL_OFFICE: &str = r#" WITH removed AS ( DELETE FROM falukant_data.political_office WHERE character_id = $1 - RETURNING office_type_id, region_id + RETURNING character_id, office_type_id, region_id, created_at + ), + archived_removed AS ( + INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date) + SELECT character_id, office_type_id, region_id, created_at, NOW() + FROM removed ), affected AS ( SELECT DISTINCT office_type_id, region_id @@ -2506,8 +2544,10 @@ pub const QUERY_DELETE_POLITICAL_OFFICE: &str = r#" ranked AS ( SELECT po.id, + po.character_id, po.office_type_id, po.region_id, + po.created_at, s.seats_total, ROW_NUMBER() OVER ( PARTITION BY po.office_type_id, po.region_id @@ -2519,9 +2559,14 @@ pub const QUERY_DELETE_POLITICAL_OFFICE: &str = r#" AND s.region_id = po.region_id ), to_delete AS ( - SELECT id + SELECT id, character_id, office_type_id, region_id, created_at FROM ranked WHERE rn > seats_total + ), + archived_trim AS ( + INSERT INTO falukant_log.political_office_history (character_id, office_type_id, region_id, start_date, end_date) + SELECT character_id, office_type_id, region_id, created_at, NOW() + FROM to_delete ) DELETE FROM falukant_data.political_office WHERE id IN (SELECT id FROM to_delete); @@ -3903,6 +3948,7 @@ pub const QUERY_HAS_MOTHER_BIRTH_TODAY: &str = r#" /// Ein Spielercharakter pro Falukant-User (bei mehreren lebenden: **höchste** `character.id`, /// typischerweise zuletzt aktiver Slot — konsistent mit UI, das oft den Hauptcharakter nutzt). /// `completed_production_count`: Summe `completion_count` in `falukant_log.production` seit `certificate_productions_count_since` (Migration `014`; Spalte `completion_count` Migration `015`); **NULL** = alle passenden Log-Zeilen (Bestand vor erstem Aufstieg nach Migration). +/// `max_church_hierarchy`: `GREATEST(character.highest_church_hierarchy_ever, MAX aktuelles kirchliches Amt)` (Migration `007` / Kirchen-Daemon). pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#" SELECT DISTINCT ON (fu.id) fu.id AS falukant_user_id, @@ -3929,12 +3975,15 @@ pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#" ) >= fu.certificate_productions_count_since ) ), 0) AS completed_production_count, - COALESCE(( - SELECT MAX(cot.hierarchy_level)::int - FROM falukant_data.church_office co - JOIN falukant_type.church_office_type cot ON cot.id = co.office_type_id - WHERE co.character_id = c.id - ), 0) AS max_church_hierarchy, + GREATEST( + COALESCE(c.highest_church_hierarchy_ever, 0)::int, + COALESCE(( + SELECT MAX(cot.hierarchy_level)::int + FROM falukant_data.church_office co + JOIN falukant_type.church_office_type cot ON cot.id = co.office_type_id + WHERE co.character_id = c.id + ), 0) + ) AS max_church_hierarchy, COALESCE(( SELECT STRING_AGG(DISTINCT pot.name, '|') FROM falukant_data.political_office po