From 2c31bce77985f901467a82fa44ab1b961cb6db02 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 24 Aug 2026 09:02:55 +0200 Subject: [PATCH] feat(politics): update election scheduling logic to include all unoccupied positions and improve query efficiency --- src/politics_worker.h | 96 +++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/src/politics_worker.h b/src/politics_worker.h index cf35575..193cc10 100755 --- a/src/politics_worker.h +++ b/src/politics_worker.h @@ -80,72 +80,98 @@ private: )"; // ------------------------------------------------------------ - // STEP 1: Erzeuge nur diejenigen Wahlen, bei denen noch keine Election - // für denselben Termin (NOW()+2 Tage) existiert. + // STEP 1: Erzeuge Wahlen für *alle* unbesetzten Soll-Stellen. + // Dabei zählen auch bereits angelegte Wahlen als Reservierung, + // damit der tägliche Lauf keine doppelten Ausschreibungen erzeugt. // ------------------------------------------------------------ static constexpr const char* QUERY_SELECT_NEEDED_ELECTIONS = R"( WITH - -- 1) Definiere das heutige Datum einmal als Referenz + -- 1) Definiere den tatsächlichen Wahltermin einmal als Referenz. + -- Die Bewerbungsphase dauert drei Tage (vgl. process_elections()). target_date AS ( - SELECT NOW()::date AS election_date + SELECT NOW() + INTERVAL '3 days' AS election_date ), - -- 2) Lösche nur diejenigen Ämter, deren Ablaufdatum heute erreicht ist, - -- und merke deren (office_type_id, region_id) - expired_today AS ( + -- 2) Abgelaufene Ämter entfernen. Ein <= statt = heilt auch Lücken, + -- falls der Worker an einem Tag nicht gelaufen ist. + 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'))::date = (SELECT election_date FROM target_date) + AND po.created_at + (pot.term_length * INTERVAL '1 day') <= NOW() RETURNING pot.id AS office_type_id, po.region_id AS region_id ), - -- 3) Gruppiere nach Typ+Region und zähle, wie viele Sitze heute frei geworden sind - gaps_per_region AS ( + -- 3) Alle Soll-Stellen pro politischem Amt und echter Spielregion. + -- Bisher wurden nur gerade abgelaufene Ämter betrachtet. Deshalb + -- erhielten nie besetzte (z. B. neu hinzugefügte) Amtstypen nie eine + -- Ausschreibung. + required_seats AS ( SELECT - office_type_id, - region_id, - COUNT(*) AS gaps - FROM expired_today + pot.id AS office_type_id, + r.id AS region_id, + pot.seats_per_region AS seats_total + FROM falukant_type.political_office_type AS pot + JOIN falukant_data.region AS r + ON TRUE + JOIN falukant_type.region AS rt + ON rt.id = r.region_type_id + AND rt.label_tr = pot.region_type + ), + + -- 4) Aktuell besetzte und durch laufende Wahlen bereits reservierte + -- Sitze zählen. + occupied_seats AS ( + SELECT po.office_type_id, po.region_id, COUNT(*) AS seats_occupied + FROM falukant_data.political_office AS po + JOIN falukant_type.political_office_type AS pot + ON pot.id = po.office_type_id + -- Datenändernde CTEs teilen sich denselben Statement-Snapshot. + -- Deshalb abgelaufene Zeilen hier ausdrücklich nicht mitzählen. + WHERE po.created_at + (pot.term_length * INTERVAL '1 day') > NOW() + GROUP BY po.office_type_id, po.region_id + ), + reserved_seats AS ( + SELECT office_type_id, region_id, SUM(posts_to_fill) AS seats_reserved + FROM falukant_data.election GROUP BY office_type_id, region_id ), - - -- 4) Filtere nur diejenigen Typ+Region‐Kombinationen, für die noch **keine** Election - -- mit genau demselben Datum angelegt wurde - to_schedule AS ( + gaps_per_region AS ( SELECT - g.office_type_id, - g.region_id, - g.gaps, + rs.office_type_id, + rs.region_id, + rs.seats_total + - COALESCE(os.seats_occupied, 0) + - COALESCE(es.seats_reserved, 0) AS gaps, td.election_date - FROM - gaps_per_region AS g - CROSS JOIN - target_date AS td - WHERE NOT EXISTS ( - SELECT 1 - FROM falukant_data.election AS e - WHERE e.office_type_id = g.office_type_id - AND e.region_id = g.region_id - AND e."date"::date = td.election_date - ) + FROM required_seats AS rs + LEFT JOIN occupied_seats AS os + ON os.office_type_id = rs.office_type_id + AND os.region_id = rs.region_id + LEFT JOIN reserved_seats AS es + ON es.office_type_id = rs.office_type_id + AND es.region_id = rs.region_id + CROSS JOIN target_date AS td + WHERE rs.seats_total + - COALESCE(os.seats_occupied, 0) + - COALESCE(es.seats_reserved, 0) > 0 ), - -- 5) Lege für jede so gefilterte Kombination genau eine Election an + -- 5) Lege für jede vakante Kombination genau eine Election an. new_elections AS ( INSERT INTO falukant_data.election (office_type_id, "date", posts_to_fill, created_at, updated_at, region_id) SELECT ts.office_type_id, - ts.election_date AS "date", + ts.election_date AS "date", ts.gaps AS posts_to_fill, NOW() AS created_at, NOW() AS updated_at, ts.region_id FROM - to_schedule AS ts + gaps_per_region AS ts RETURNING id AS election_id, region_id,