feat(politics): update election scheduling logic to include all unoccupied positions and improve query efficiency

This commit is contained in:
Torsten Schulz (local)
2026-08-24 09:02:55 +02:00
parent 846ba5b3c4
commit d0f4e33fc0

View File

@@ -80,60 +80,86 @@ private:
)"; )";
// ------------------------------------------------------------ // ------------------------------------------------------------
// STEP 1: Erzeuge nur diejenigen Wahlen, bei denen noch keine Election // STEP 1: Erzeuge Wahlen für *alle* unbesetzten Soll-Stellen.
// für denselben Termin (NOW()+2 Tage) existiert. // 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"( static constexpr const char* QUERY_SELECT_NEEDED_ELECTIONS = R"(
WITH 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 ( 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, -- 2) Abgelaufene Ämter entfernen. Ein <= statt = heilt auch Lücken,
-- und merke deren (office_type_id, region_id) -- falls der Worker an einem Tag nicht gelaufen ist.
expired_today AS ( expired_offices AS (
DELETE FROM falukant_data.political_office AS po DELETE FROM falukant_data.political_office AS po
USING falukant_type.political_office_type AS pot USING falukant_type.political_office_type AS pot
WHERE po.office_type_id = pot.id 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 RETURNING
pot.id AS office_type_id, pot.id AS office_type_id,
po.region_id AS region_id po.region_id AS region_id
), ),
-- 3) Gruppiere nach Typ+Region und zähle, wie viele Sitze heute frei geworden sind -- 3) Alle Soll-Stellen pro politischem Amt und echter Spielregion.
gaps_per_region AS ( -- Bisher wurden nur gerade abgelaufene Ämter betrachtet. Deshalb
-- erhielten nie besetzte (z. B. neu hinzugefügte) Amtstypen nie eine
-- Ausschreibung.
required_seats AS (
SELECT SELECT
office_type_id, pot.id AS office_type_id,
region_id, r.id AS region_id,
COUNT(*) AS gaps pot.seats_per_region AS seats_total
FROM expired_today 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 GROUP BY office_type_id, region_id
), ),
gaps_per_region AS (
-- 4) Filtere nur diejenigen Typ+RegionKombinationen, für die noch **keine** Election
-- mit genau demselben Datum angelegt wurde
to_schedule AS (
SELECT SELECT
g.office_type_id, rs.office_type_id,
g.region_id, rs.region_id,
g.gaps, rs.seats_total
- COALESCE(os.seats_occupied, 0)
- COALESCE(es.seats_reserved, 0) AS gaps,
td.election_date td.election_date
FROM FROM required_seats AS rs
gaps_per_region AS g LEFT JOIN occupied_seats AS os
CROSS JOIN ON os.office_type_id = rs.office_type_id
target_date AS td AND os.region_id = rs.region_id
WHERE NOT EXISTS ( LEFT JOIN reserved_seats AS es
SELECT 1 ON es.office_type_id = rs.office_type_id
FROM falukant_data.election AS e AND es.region_id = rs.region_id
WHERE e.office_type_id = g.office_type_id CROSS JOIN target_date AS td
AND e.region_id = g.region_id WHERE rs.seats_total
AND e."date"::date = td.election_date - 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 ( new_elections AS (
INSERT INTO falukant_data.election INSERT INTO falukant_data.election
(office_type_id, "date", posts_to_fill, created_at, updated_at, region_id) (office_type_id, "date", posts_to_fill, created_at, updated_at, region_id)
@@ -145,7 +171,7 @@ private:
NOW() AS updated_at, NOW() AS updated_at,
ts.region_id ts.region_id
FROM FROM
to_schedule AS ts gaps_per_region AS ts
RETURNING RETURNING
id AS election_id, id AS election_id,
region_id, region_id,