Skip to content

Commit 064819e

Browse files
committed
fix: auto-complete bye matches in losers bracket
Runtime byes in LB (brackets that receive one team from a WB loser drop but have no LB feeder due to R1 pruning) were stuck indefinitely because the trigger required both teams to schedule a match. Add resolve_bracket_bye() that detects brackets with one team and no pending feeders, marks them as bye/finished, and advances the lone team to the parent bracket. Called from the bracket trigger and from tournament resume logic.
1 parent 4ea14ad commit 064819e

3 files changed

Lines changed: 92 additions & 0 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
-- Detects and resolves "runtime byes" in elimination brackets.
2+
-- A runtime bye occurs when a bracket receives exactly one team but has
3+
-- no remaining feeder brackets that could provide the second team.
4+
-- This happens in double-elimination losers brackets after LB R1 pruning:
5+
-- the pruned LB R1 match can no longer feed its parent LB R2 bracket,
6+
-- so the WB loser that drops into LB R2 has no opponent.
7+
CREATE OR REPLACE FUNCTION public.resolve_bracket_bye(
8+
_bracket tournament_brackets
9+
) RETURNS boolean
10+
LANGUAGE plpgsql
11+
AS $$
12+
DECLARE
13+
pending_feeders int;
14+
lone_team_id uuid;
15+
tournament_id uuid;
16+
BEGIN
17+
-- Only applies when exactly one team is present
18+
IF (_bracket.tournament_team_id_1 IS NOT NULL AND _bracket.tournament_team_id_2 IS NOT NULL) THEN
19+
RETURN false;
20+
END IF;
21+
22+
IF (_bracket.tournament_team_id_1 IS NULL AND _bracket.tournament_team_id_2 IS NULL) THEN
23+
RETURN false;
24+
END IF;
25+
26+
IF _bracket.finished = true OR _bracket.match_id IS NOT NULL THEN
27+
RETURN false;
28+
END IF;
29+
30+
-- Count unfinished, non-bye feeder brackets that could still send a team
31+
SELECT COUNT(*) INTO pending_feeders
32+
FROM tournament_brackets child
33+
WHERE (child.parent_bracket_id = _bracket.id
34+
OR child.loser_parent_bracket_id = _bracket.id)
35+
AND child.finished = false
36+
AND child.bye = false;
37+
38+
IF pending_feeders > 0 THEN
39+
RETURN false;
40+
END IF;
41+
42+
-- Runtime bye confirmed: one team, no pending feeders
43+
lone_team_id := COALESCE(_bracket.tournament_team_id_1, _bracket.tournament_team_id_2);
44+
45+
RAISE NOTICE 'Resolving runtime bye: bracket %, team % advanced to parent %',
46+
_bracket.id, lone_team_id, _bracket.parent_bracket_id;
47+
48+
-- Mark as bye and finished
49+
UPDATE tournament_brackets
50+
SET bye = true, finished = true
51+
WHERE id = _bracket.id;
52+
53+
-- Advance the lone team to the parent bracket
54+
IF _bracket.parent_bracket_id IS NOT NULL THEN
55+
PERFORM public.assign_team_to_bracket_slot(_bracket.parent_bracket_id, lone_team_id);
56+
END IF;
57+
58+
-- Check if tournament is now complete
59+
SELECT ts.tournament_id INTO tournament_id
60+
FROM tournament_stages ts
61+
WHERE ts.id = _bracket.tournament_stage_id;
62+
63+
IF tournament_id IS NOT NULL THEN
64+
PERFORM check_tournament_finished(tournament_id);
65+
END IF;
66+
67+
RETURN true;
68+
END;
69+
$$;

‎hasura/triggers/tournament_brackets.sql‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ BEGIN
3232
IF should_auto_schedule(NEW.tournament_stage_id) THEN
3333
PERFORM schedule_tournament_match(NEW);
3434
END IF;
35+
-- One team present but not the other: check for runtime bye
36+
ELSIF (NEW.tournament_team_id_1 IS NOT NULL) != (NEW.tournament_team_id_2 IS NOT NULL) THEN
37+
IF should_auto_schedule(NEW.tournament_stage_id) THEN
38+
PERFORM resolve_bracket_bye(NEW);
39+
END IF;
3540
END IF;
3641
END IF;
3742

‎hasura/triggers/tournaments.sql‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ BEGIN
3737
OLD.status = 'Paused' AND NEW.status = 'Live'
3838
AND NEW.auto_start
3939
) THEN
40+
-- Resolve runtime byes first (one team, no pending feeders)
41+
-- Process lower rounds first so cascading byes propagate correctly
42+
FOR bracket_row IN
43+
SELECT tb.*
44+
FROM tournament_brackets tb
45+
INNER JOIN tournament_stages ts ON ts.id = tb.tournament_stage_id
46+
WHERE ts.tournament_id = NEW.id
47+
AND tb.match_id IS NULL
48+
AND tb.finished = false
49+
AND tb.bye = false
50+
AND ((tb.tournament_team_id_1 IS NOT NULL AND tb.tournament_team_id_2 IS NULL)
51+
OR (tb.tournament_team_id_1 IS NULL AND tb.tournament_team_id_2 IS NOT NULL))
52+
ORDER BY tb.round, tb.match_number
53+
LOOP
54+
PERFORM resolve_bracket_bye(bracket_row);
55+
END LOOP;
56+
57+
-- Then schedule matches with both teams present
4058
FOR bracket_row IN
4159
SELECT tb.*
4260
FROM tournament_brackets tb

0 commit comments

Comments
 (0)