Skip to content

Commit 53f9255

Browse files
committed
bug: fix tournament bugs in swiss , slot saftey , seeding order scheduling eerrors and orpahed brackets
1 parent 562d210 commit 53f9255

5 files changed

Lines changed: 77 additions & 13 deletions

File tree

hasura/functions/tournaments/assign_team_to_bracket_slot.sql

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ DECLARE
99
target_bracket tournament_brackets%ROWTYPE;
1010
slot_position int;
1111
BEGIN
12+
-- Lock the target row so concurrent callers (e.g. two feeder matches
13+
-- finishing simultaneously) serialize on the same bracket. Combined with
14+
-- the IS NULL guards on the UPDATEs below, this prevents either caller
15+
-- from overwriting a team the other just placed.
1216
SELECT * INTO target_bracket
1317
FROM tournament_brackets
1418
WHERE id = _target_bracket_id
15-
LIMIT 1;
19+
FOR UPDATE;
1620

1721
IF target_bracket IS NULL OR _team_id IS NULL THEN
1822
RETURN;
@@ -44,36 +48,45 @@ BEGIN
4448
WHERE ranked.id = _source_bracket_id;
4549
END IF;
4650

51+
-- Every UPDATE carries an IS NULL guard as defense-in-depth alongside the
52+
-- row lock, so a slot can never be overwritten even if a caller bypasses
53+
-- the lock (e.g. direct mutation outside this function).
4754
IF slot_position = 1 THEN
4855
IF target_bracket.tournament_team_id_1 IS NULL THEN
4956
UPDATE tournament_brackets
5057
SET tournament_team_id_1 = _team_id
51-
WHERE id = _target_bracket_id;
58+
WHERE id = _target_bracket_id
59+
AND tournament_team_id_1 IS NULL;
5260
ELSIF target_bracket.tournament_team_id_2 IS NULL THEN
5361
UPDATE tournament_brackets
5462
SET tournament_team_id_2 = _team_id
55-
WHERE id = _target_bracket_id;
63+
WHERE id = _target_bracket_id
64+
AND tournament_team_id_2 IS NULL;
5665
END IF;
5766
ELSIF slot_position = 2 THEN
5867
IF target_bracket.tournament_team_id_2 IS NULL THEN
5968
UPDATE tournament_brackets
6069
SET tournament_team_id_2 = _team_id
61-
WHERE id = _target_bracket_id;
70+
WHERE id = _target_bracket_id
71+
AND tournament_team_id_2 IS NULL;
6272
ELSIF target_bracket.tournament_team_id_1 IS NULL THEN
6373
UPDATE tournament_brackets
6474
SET tournament_team_id_1 = _team_id
65-
WHERE id = _target_bracket_id;
75+
WHERE id = _target_bracket_id
76+
AND tournament_team_id_1 IS NULL;
6677
END IF;
6778
ELSE
6879
-- Fallback: first empty slot (for callers without source bracket)
6980
IF target_bracket.tournament_team_id_1 IS NULL THEN
7081
UPDATE tournament_brackets
7182
SET tournament_team_id_1 = _team_id
72-
WHERE id = _target_bracket_id;
83+
WHERE id = _target_bracket_id
84+
AND tournament_team_id_1 IS NULL;
7385
ELSIF target_bracket.tournament_team_id_2 IS NULL THEN
7486
UPDATE tournament_brackets
7587
SET tournament_team_id_2 = _team_id
76-
WHERE id = _target_bracket_id;
88+
WHERE id = _target_bracket_id
89+
AND tournament_team_id_2 IS NULL;
7790
END IF;
7891
END IF;
7992
END;

hasura/functions/tournaments/assign_teams_to_swiss_pools.sql

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,23 @@ BEGIN
4141
RAISE NOTICE ' Pool %-% (group %): % teams',
4242
pool_record.wins, pool_record.losses, pool_group, team_count;
4343

44+
-- Filter out any teams already used by an earlier pool iteration.
45+
-- pool_record.team_ids is a stale snapshot from when the outer
46+
-- FOR...SELECT was materialized (used_teams was empty then), so a
47+
-- team borrowed by an earlier pool could otherwise be paired twice.
48+
SELECT COALESCE(array_agg(t), ARRAY[]::uuid[]) INTO teams_to_pair
49+
FROM unnest(pool_record.team_ids) AS t
50+
WHERE NOT (t = ANY(used_teams));
51+
52+
team_count := COALESCE(array_length(teams_to_pair, 1), 0);
53+
54+
IF team_count = 0 THEN
55+
CONTINUE;
56+
END IF;
57+
4458
-- Handle odd number of teams
4559
adjacent_team_id := NULL;
46-
teams_to_pair := pool_record.team_ids;
47-
60+
4861
IF team_count % 2 != 0 THEN
4962
-- Find a team from an adjacent pool
5063
adjacent_team_id := find_adjacent_swiss_team(_stage_id, pool_record.wins, pool_record.losses, used_teams);

hasura/functions/tournaments/check_tournament_finished.sql

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,37 @@ AS $$
55
DECLARE
66
total_brackets int;
77
unfinished_brackets int;
8+
orphan tournament_brackets%ROWTYPE;
89
BEGIN
10+
-- Sweep any orphaned brackets (no teams, no pending feeders, bye=false,
11+
-- finished=false) and let resolve_bracket_bye mark them bye/finished.
12+
-- Without this, an orphaned bracket would be counted as "unfinished"
13+
-- below and the tournament would hang indefinitely.
14+
FOR orphan IN
15+
SELECT tb.*
16+
FROM tournament_brackets tb
17+
INNER JOIN tournament_stages ts ON ts.id = tb.tournament_stage_id
18+
WHERE ts.tournament_id = _tournament_id
19+
AND tb.finished = false
20+
AND tb.bye = false
21+
AND tb.tournament_team_id_1 IS NULL
22+
AND tb.tournament_team_id_2 IS NULL
23+
AND NOT EXISTS (
24+
SELECT 1 FROM tournament_brackets child
25+
WHERE (child.parent_bracket_id = tb.id
26+
OR child.loser_parent_bracket_id = tb.id)
27+
AND child.finished = false
28+
)
29+
AND EXISTS (
30+
SELECT 1 FROM tournament_brackets child
31+
WHERE child.parent_bracket_id = tb.id
32+
OR child.loser_parent_bracket_id = tb.id
33+
)
34+
ORDER BY tb.round, tb.match_number
35+
LOOP
36+
PERFORM resolve_bracket_bye(orphan);
37+
END LOOP;
38+
939
select count(*) into total_brackets
1040
from tournament_brackets tb
1141
inner join tournament_stages ts on ts.id = tb.tournament_stage_id

hasura/functions/tournaments/schedule_tournament_match.sql

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn
2121

2222
-- If bracket is already finished, don't try to schedule it
2323
IF bracket.finished = true THEN
24+
RAISE NOTICE 'schedule_tournament_match: bracket % already finished, skipping', bracket.id;
2425
RETURN NULL;
2526
END IF;
26-
27+
2728
IF bracket.tournament_team_id_1 IS NULL AND bracket.tournament_team_id_2 IS NULL THEN
29+
RAISE NOTICE 'schedule_tournament_match: bracket % has no teams, skipping', bracket.id;
2830
RETURN NULL;
2931
END IF;
3032

3133
-- For all other cases, we require two teams to schedule a match
3234
IF bracket.tournament_team_id_1 IS NULL OR bracket.tournament_team_id_2 IS NULL THEN
35+
RAISE NOTICE 'schedule_tournament_match: bracket % missing one team (t1=%, t2=%), skipping',
36+
bracket.id, bracket.tournament_team_id_1, bracket.tournament_team_id_2;
3337
RETURN NULL;
3438
END IF;
3539

@@ -63,6 +67,7 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn
6367
FROM match_options mo WHERE mo.id = _match_options_id;
6468

6569
IF _match_mode = 'admin' THEN
70+
RAISE NOTICE 'schedule_tournament_match: bracket % is admin-mode, skipping auto-schedule', bracket.id;
6671
RETURN NULL;
6772
END IF;
6873
END;

hasura/views/v_team_stage_results.sql

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,11 @@ ORDER BY
270270
THEN (COALESCE(ass.rounds_won, 0)::float / ass.rounds_lost::float)
271271
ELSE COALESCE(ass.rounds_won, 0)::float
272272
END DESC,
273-
CASE
274-
WHEN COALESCE(tkd.total_deaths, 0) > 0
273+
CASE
274+
WHEN COALESCE(tkd.total_deaths, 0) > 0
275275
THEN (COALESCE(tkd.total_kills, 0)::float / tkd.total_deaths::float)
276276
ELSE COALESCE(tkd.total_kills, 0)::float
277-
END DESC;
277+
END DESC,
278+
-- Deterministic final tiebreaker so identical stats produce a stable
279+
-- ordering across calls (e.g. for OFFSET-based seeding in seed_stage).
280+
ass.team_id ASC;

0 commit comments

Comments
 (0)