From 6a5a7891f1f4f0c6a98590497da1515ca918ae44 Mon Sep 17 00:00:00 2001 From: hz <1766264+zeeshaun@users.noreply.github.com> Date: Sat, 30 May 2026 03:40:33 -0500 Subject: [PATCH 1/4] Fix tournament match substitute lineups --- .../tournaments/schedule_tournament_match.sql | 48 +++- .../down.sql | 188 +++++++++++++++ .../up.sql | 226 ++++++++++++++++++ 3 files changed, 457 insertions(+), 5 deletions(-) create mode 100644 hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/down.sql create mode 100644 hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/up.sql diff --git a/hasura/functions/tournaments/schedule_tournament_match.sql b/hasura/functions/tournaments/schedule_tournament_match.sql index 8b833f65..51103248 100644 --- a/hasura/functions/tournaments/schedule_tournament_match.sql +++ b/hasura/functions/tournaments/schedule_tournament_match.sql @@ -17,6 +17,7 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn _match_options_id UUID; _round_best_of int; _swiss_match_type text; + _min_players_per_lineup int; BEGIN IF bracket.match_id IS NOT NULL THEN RETURN bracket.match_id; @@ -138,6 +139,11 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn RETURNING lineup_1_id, lineup_2_id INTO _lineup_1_id, _lineup_2_id; + SELECT match_min_players_per_lineup(m) + INTO _min_players_per_lineup + FROM matches m + WHERE m.id = _match_id; + SELECT tt.captain_steam_id INTO _captain_steam_id_1 FROM tournament_teams tt @@ -149,16 +155,48 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn WHERE tt.id = bracket.tournament_team_id_2; FOR member IN - SELECT * FROM tournament_team_roster - WHERE tournament_team_id = bracket.tournament_team_id_1 + SELECT ttr.* + FROM tournament_team_roster ttr + INNER JOIN tournament_teams tt + ON tt.id = ttr.tournament_team_id + LEFT JOIN team_roster tr + ON tr.team_id = tt.team_id + AND tr.player_steam_id = ttr.player_steam_id + WHERE ttr.tournament_team_id = bracket.tournament_team_id_1 + ORDER BY + CASE WHEN ttr.player_steam_id = _captain_steam_id_1 THEN 0 ELSE 1 END, + CASE tr.status + WHEN 'Starter' THEN 1 + WHEN 'Substitute' THEN 2 + WHEN 'Benched' THEN 3 + ELSE 4 + END, + ttr.player_steam_id + LIMIT _min_players_per_lineup LOOP INSERT INTO match_lineup_players (match_lineup_id, steam_id) VALUES (_lineup_1_id, member.player_steam_id); END LOOP; FOR member IN - SELECT * FROM tournament_team_roster - WHERE tournament_team_id = bracket.tournament_team_id_2 + SELECT ttr.* + FROM tournament_team_roster ttr + INNER JOIN tournament_teams tt + ON tt.id = ttr.tournament_team_id + LEFT JOIN team_roster tr + ON tr.team_id = tt.team_id + AND tr.player_steam_id = ttr.player_steam_id + WHERE ttr.tournament_team_id = bracket.tournament_team_id_2 + ORDER BY + CASE WHEN ttr.player_steam_id = _captain_steam_id_2 THEN 0 ELSE 1 END, + CASE tr.status + WHEN 'Starter' THEN 1 + WHEN 'Substitute' THEN 2 + WHEN 'Benched' THEN 3 + ELSE 4 + END, + ttr.player_steam_id + LIMIT _min_players_per_lineup LOOP INSERT INTO match_lineup_players (match_lineup_id, steam_id) VALUES (_lineup_2_id, member.player_steam_id); @@ -200,4 +238,4 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn RETURN _match_id; END; - $$; \ No newline at end of file + $$; diff --git a/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/down.sql b/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/down.sql new file mode 100644 index 00000000..373dc3b8 --- /dev/null +++ b/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/down.sql @@ -0,0 +1,188 @@ +CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tournament_brackets) RETURNS uuid + LANGUAGE plpgsql + AS $$ + DECLARE + tournament tournaments; + stage tournament_stages; + member RECORD; + _lineup_1_id UUID; + _lineup_2_id UUID; + _captain_steam_id_1 bigint; + _captain_steam_id_2 bigint; + _match_id UUID; + feeder RECORD; + feeders_with_team int := 0; + winner_id UUID; + _template_match_options_id UUID; + _match_options_id UUID; + _round_best_of int; + _swiss_match_type text; + BEGIN + IF bracket.match_id IS NOT NULL THEN + RETURN bracket.match_id; + END IF; + + IF bracket.finished = true THEN + RAISE NOTICE 'schedule_tournament_match: bracket % already finished, skipping', bracket.id; + RETURN NULL; + END IF; + + IF bracket.tournament_team_id_1 IS NULL AND bracket.tournament_team_id_2 IS NULL THEN + RAISE NOTICE 'schedule_tournament_match: bracket % has no teams, skipping', bracket.id; + RETURN NULL; + END IF; + + IF bracket.tournament_team_id_1 IS NULL OR bracket.tournament_team_id_2 IS NULL THEN + RAISE NOTICE 'schedule_tournament_match: bracket % missing one team (t1=%, t2=%), skipping', + bracket.id, bracket.tournament_team_id_1, bracket.tournament_team_id_2; + RETURN NULL; + END IF; + + SELECT ts.* INTO stage + FROM tournament_brackets tb + INNER JOIN tournament_stages ts ON ts.id = tb.tournament_stage_id + WHERE tb.id = bracket.id; + + SELECT t.* INTO tournament + FROM tournament_brackets tb + INNER JOIN tournament_stages ts ON ts.id = tb.tournament_stage_id + INNER JOIN tournaments t ON t.id = ts.tournament_id + WHERE tb.id = bracket.id; + + IF bracket.match_options_id IS NOT NULL THEN + _template_match_options_id := bracket.match_options_id; + ELSIF stage.match_options_id IS NOT NULL THEN + _template_match_options_id := stage.match_options_id; + ELSE + _template_match_options_id := tournament.match_options_id; + END IF; + + DECLARE + _match_mode text; + BEGIN + SELECT mo.match_mode INTO _match_mode + FROM match_options mo WHERE mo.id = _template_match_options_id; + + IF _match_mode = 'admin' AND bracket.scheduled_at IS NULL THEN + RAISE NOTICE 'schedule_tournament_match: bracket % is admin-mode without schedule, skipping auto-schedule', bracket.id; + RETURN NULL; + END IF; + END; + + IF bracket.match_options_id IS NULL THEN + IF stage.type = 'Swiss' THEN + DECLARE + _wins int; + _losses int; + _wins_needed int := 3; + BEGIN + _wins := (bracket."group" / 100)::int; + _losses := (bracket."group" % 100)::int; + IF _wins = _wins_needed - 1 THEN + _swiss_match_type := 'advancement'; + ELSIF _losses = _wins_needed - 1 THEN + _swiss_match_type := 'elimination'; + ELSE + _swiss_match_type := 'regular'; + END IF; + _round_best_of := get_bracket_best_of(stage.id, _swiss_match_type, bracket.round); + END; + ELSE + _round_best_of := get_bracket_best_of(stage.id, bracket.path, bracket.round); + END IF; + + IF _round_best_of IS NOT NULL THEN + _match_options_id := clone_match_options_with_best_of(_template_match_options_id, _round_best_of); + END IF; + END IF; + + IF _match_options_id IS NULL THEN + _match_options_id := clone_match_options(_template_match_options_id); + END IF; + + _match_id := gen_random_uuid(); + + UPDATE tournament_brackets + SET match_id = _match_id + WHERE id = bracket.id; + + INSERT INTO matches ( + id, + status, + organizer_steam_id, + match_options_id, + scheduled_at + ) + VALUES ( + _match_id, + 'PickingPlayers', + tournament.organizer_steam_id, + _match_options_id, + GREATEST(COALESCE(bracket.scheduled_at, now()), now()) + ) + RETURNING lineup_1_id, lineup_2_id + INTO _lineup_1_id, _lineup_2_id; + + SELECT tt.captain_steam_id + INTO _captain_steam_id_1 + FROM tournament_teams tt + WHERE tt.id = bracket.tournament_team_id_1; + + SELECT tt.captain_steam_id + INTO _captain_steam_id_2 + FROM tournament_teams tt + WHERE tt.id = bracket.tournament_team_id_2; + + FOR member IN + SELECT * FROM tournament_team_roster + WHERE tournament_team_id = bracket.tournament_team_id_1 + LOOP + INSERT INTO match_lineup_players (match_lineup_id, steam_id) + VALUES (_lineup_1_id, member.player_steam_id); + END LOOP; + + FOR member IN + SELECT * FROM tournament_team_roster + WHERE tournament_team_id = bracket.tournament_team_id_2 + LOOP + INSERT INTO match_lineup_players (match_lineup_id, steam_id) + VALUES (_lineup_2_id, member.player_steam_id); + END LOOP; + + IF _captain_steam_id_1 IS NOT NULL THEN + UPDATE match_lineup_players + SET captain = true + WHERE match_lineup_id = _lineup_1_id + AND steam_id = _captain_steam_id_1; + END IF; + + IF _captain_steam_id_2 IS NOT NULL THEN + UPDATE match_lineup_players + SET captain = true + WHERE match_lineup_id = _lineup_2_id + AND steam_id = _captain_steam_id_2; + END IF; + + UPDATE match_lineups + SET team_id = tt.team_id + FROM tournament_teams tt + WHERE match_lineups.id = _lineup_1_id + AND tt.id = bracket.tournament_team_id_1 + AND tt.team_id IS NOT NULL; + + UPDATE match_lineups + SET team_id = tt.team_id + FROM tournament_teams tt + WHERE match_lineups.id = _lineup_2_id + AND tt.id = bracket.tournament_team_id_2 + AND tt.team_id IS NOT NULL; + + UPDATE matches + SET status = 'WaitingForCheckIn' + WHERE id = _match_id; + + PERFORM calculate_tournament_bracket_start_times(tournament.id); + + RETURN _match_id; + END; + $$; diff --git a/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/up.sql b/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/up.sql new file mode 100644 index 00000000..232a1537 --- /dev/null +++ b/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/up.sql @@ -0,0 +1,226 @@ +CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tournament_brackets) RETURNS uuid + LANGUAGE plpgsql + AS $$ + DECLARE + tournament tournaments; + stage tournament_stages; + member RECORD; + _lineup_1_id UUID; + _lineup_2_id UUID; + _captain_steam_id_1 bigint; + _captain_steam_id_2 bigint; + _match_id UUID; + feeder RECORD; + feeders_with_team int := 0; + winner_id UUID; + _template_match_options_id UUID; + _match_options_id UUID; + _round_best_of int; + _swiss_match_type text; + _min_players_per_lineup int; + BEGIN + IF bracket.match_id IS NOT NULL THEN + RETURN bracket.match_id; + END IF; + + IF bracket.finished = true THEN + RAISE NOTICE 'schedule_tournament_match: bracket % already finished, skipping', bracket.id; + RETURN NULL; + END IF; + + IF bracket.tournament_team_id_1 IS NULL AND bracket.tournament_team_id_2 IS NULL THEN + RAISE NOTICE 'schedule_tournament_match: bracket % has no teams, skipping', bracket.id; + RETURN NULL; + END IF; + + IF bracket.tournament_team_id_1 IS NULL OR bracket.tournament_team_id_2 IS NULL THEN + RAISE NOTICE 'schedule_tournament_match: bracket % missing one team (t1=%, t2=%), skipping', + bracket.id, bracket.tournament_team_id_1, bracket.tournament_team_id_2; + RETURN NULL; + END IF; + + SELECT ts.* INTO stage + FROM tournament_brackets tb + INNER JOIN tournament_stages ts ON ts.id = tb.tournament_stage_id + WHERE tb.id = bracket.id; + + SELECT t.* INTO tournament + FROM tournament_brackets tb + INNER JOIN tournament_stages ts ON ts.id = tb.tournament_stage_id + INNER JOIN tournaments t ON t.id = ts.tournament_id + WHERE tb.id = bracket.id; + + IF bracket.match_options_id IS NOT NULL THEN + _template_match_options_id := bracket.match_options_id; + ELSIF stage.match_options_id IS NOT NULL THEN + _template_match_options_id := stage.match_options_id; + ELSE + _template_match_options_id := tournament.match_options_id; + END IF; + + DECLARE + _match_mode text; + BEGIN + SELECT mo.match_mode INTO _match_mode + FROM match_options mo WHERE mo.id = _template_match_options_id; + + IF _match_mode = 'admin' AND bracket.scheduled_at IS NULL THEN + RAISE NOTICE 'schedule_tournament_match: bracket % is admin-mode without schedule, skipping auto-schedule', bracket.id; + RETURN NULL; + END IF; + END; + + IF bracket.match_options_id IS NULL THEN + IF stage.type = 'Swiss' THEN + DECLARE + _wins int; + _losses int; + _wins_needed int := 3; + BEGIN + _wins := (bracket."group" / 100)::int; + _losses := (bracket."group" % 100)::int; + IF _wins = _wins_needed - 1 THEN + _swiss_match_type := 'advancement'; + ELSIF _losses = _wins_needed - 1 THEN + _swiss_match_type := 'elimination'; + ELSE + _swiss_match_type := 'regular'; + END IF; + _round_best_of := get_bracket_best_of(stage.id, _swiss_match_type, bracket.round); + END; + ELSE + _round_best_of := get_bracket_best_of(stage.id, bracket.path, bracket.round); + END IF; + + IF _round_best_of IS NOT NULL THEN + _match_options_id := clone_match_options_with_best_of(_template_match_options_id, _round_best_of); + END IF; + END IF; + + IF _match_options_id IS NULL THEN + _match_options_id := clone_match_options(_template_match_options_id); + END IF; + + _match_id := gen_random_uuid(); + + UPDATE tournament_brackets + SET match_id = _match_id + WHERE id = bracket.id; + + INSERT INTO matches ( + id, + status, + organizer_steam_id, + match_options_id, + scheduled_at + ) + VALUES ( + _match_id, + 'PickingPlayers', + tournament.organizer_steam_id, + _match_options_id, + GREATEST(COALESCE(bracket.scheduled_at, now()), now()) + ) + RETURNING lineup_1_id, lineup_2_id + INTO _lineup_1_id, _lineup_2_id; + + SELECT match_min_players_per_lineup(m) + INTO _min_players_per_lineup + FROM matches m + WHERE m.id = _match_id; + + SELECT tt.captain_steam_id + INTO _captain_steam_id_1 + FROM tournament_teams tt + WHERE tt.id = bracket.tournament_team_id_1; + + SELECT tt.captain_steam_id + INTO _captain_steam_id_2 + FROM tournament_teams tt + WHERE tt.id = bracket.tournament_team_id_2; + + FOR member IN + SELECT ttr.* + FROM tournament_team_roster ttr + INNER JOIN tournament_teams tt + ON tt.id = ttr.tournament_team_id + LEFT JOIN team_roster tr + ON tr.team_id = tt.team_id + AND tr.player_steam_id = ttr.player_steam_id + WHERE ttr.tournament_team_id = bracket.tournament_team_id_1 + ORDER BY + CASE WHEN ttr.player_steam_id = _captain_steam_id_1 THEN 0 ELSE 1 END, + CASE tr.status + WHEN 'Starter' THEN 1 + WHEN 'Substitute' THEN 2 + WHEN 'Benched' THEN 3 + ELSE 4 + END, + ttr.player_steam_id + LIMIT _min_players_per_lineup + LOOP + INSERT INTO match_lineup_players (match_lineup_id, steam_id) + VALUES (_lineup_1_id, member.player_steam_id); + END LOOP; + + FOR member IN + SELECT ttr.* + FROM tournament_team_roster ttr + INNER JOIN tournament_teams tt + ON tt.id = ttr.tournament_team_id + LEFT JOIN team_roster tr + ON tr.team_id = tt.team_id + AND tr.player_steam_id = ttr.player_steam_id + WHERE ttr.tournament_team_id = bracket.tournament_team_id_2 + ORDER BY + CASE WHEN ttr.player_steam_id = _captain_steam_id_2 THEN 0 ELSE 1 END, + CASE tr.status + WHEN 'Starter' THEN 1 + WHEN 'Substitute' THEN 2 + WHEN 'Benched' THEN 3 + ELSE 4 + END, + ttr.player_steam_id + LIMIT _min_players_per_lineup + LOOP + INSERT INTO match_lineup_players (match_lineup_id, steam_id) + VALUES (_lineup_2_id, member.player_steam_id); + END LOOP; + + IF _captain_steam_id_1 IS NOT NULL THEN + UPDATE match_lineup_players + SET captain = true + WHERE match_lineup_id = _lineup_1_id + AND steam_id = _captain_steam_id_1; + END IF; + + IF _captain_steam_id_2 IS NOT NULL THEN + UPDATE match_lineup_players + SET captain = true + WHERE match_lineup_id = _lineup_2_id + AND steam_id = _captain_steam_id_2; + END IF; + + UPDATE match_lineups + SET team_id = tt.team_id + FROM tournament_teams tt + WHERE match_lineups.id = _lineup_1_id + AND tt.id = bracket.tournament_team_id_1 + AND tt.team_id IS NOT NULL; + + UPDATE match_lineups + SET team_id = tt.team_id + FROM tournament_teams tt + WHERE match_lineups.id = _lineup_2_id + AND tt.id = bracket.tournament_team_id_2 + AND tt.team_id IS NOT NULL; + + UPDATE matches + SET status = 'WaitingForCheckIn' + WHERE id = _match_id; + + PERFORM calculate_tournament_bracket_start_times(tournament.id); + + RETURN _match_id; + END; + $$; From 4c76be55017a36880e148b4ecfe4d08219bd9a4e Mon Sep 17 00:00:00 2001 From: hz <1766264+zeeshaun@users.noreply.github.com> Date: Sat, 30 May 2026 22:41:18 -0500 Subject: [PATCH 2/4] Keep tournament scheduler fix in Hasura functions --- .../down.sql | 188 --------------- .../up.sql | 226 ------------------ 2 files changed, 414 deletions(-) delete mode 100644 hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/down.sql delete mode 100644 hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/up.sql diff --git a/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/down.sql b/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/down.sql deleted file mode 100644 index 373dc3b8..00000000 --- a/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/down.sql +++ /dev/null @@ -1,188 +0,0 @@ -CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tournament_brackets) RETURNS uuid - LANGUAGE plpgsql - AS $$ - DECLARE - tournament tournaments; - stage tournament_stages; - member RECORD; - _lineup_1_id UUID; - _lineup_2_id UUID; - _captain_steam_id_1 bigint; - _captain_steam_id_2 bigint; - _match_id UUID; - feeder RECORD; - feeders_with_team int := 0; - winner_id UUID; - _template_match_options_id UUID; - _match_options_id UUID; - _round_best_of int; - _swiss_match_type text; - BEGIN - IF bracket.match_id IS NOT NULL THEN - RETURN bracket.match_id; - END IF; - - IF bracket.finished = true THEN - RAISE NOTICE 'schedule_tournament_match: bracket % already finished, skipping', bracket.id; - RETURN NULL; - END IF; - - IF bracket.tournament_team_id_1 IS NULL AND bracket.tournament_team_id_2 IS NULL THEN - RAISE NOTICE 'schedule_tournament_match: bracket % has no teams, skipping', bracket.id; - RETURN NULL; - END IF; - - IF bracket.tournament_team_id_1 IS NULL OR bracket.tournament_team_id_2 IS NULL THEN - RAISE NOTICE 'schedule_tournament_match: bracket % missing one team (t1=%, t2=%), skipping', - bracket.id, bracket.tournament_team_id_1, bracket.tournament_team_id_2; - RETURN NULL; - END IF; - - SELECT ts.* INTO stage - FROM tournament_brackets tb - INNER JOIN tournament_stages ts ON ts.id = tb.tournament_stage_id - WHERE tb.id = bracket.id; - - SELECT t.* INTO tournament - FROM tournament_brackets tb - INNER JOIN tournament_stages ts ON ts.id = tb.tournament_stage_id - INNER JOIN tournaments t ON t.id = ts.tournament_id - WHERE tb.id = bracket.id; - - IF bracket.match_options_id IS NOT NULL THEN - _template_match_options_id := bracket.match_options_id; - ELSIF stage.match_options_id IS NOT NULL THEN - _template_match_options_id := stage.match_options_id; - ELSE - _template_match_options_id := tournament.match_options_id; - END IF; - - DECLARE - _match_mode text; - BEGIN - SELECT mo.match_mode INTO _match_mode - FROM match_options mo WHERE mo.id = _template_match_options_id; - - IF _match_mode = 'admin' AND bracket.scheduled_at IS NULL THEN - RAISE NOTICE 'schedule_tournament_match: bracket % is admin-mode without schedule, skipping auto-schedule', bracket.id; - RETURN NULL; - END IF; - END; - - IF bracket.match_options_id IS NULL THEN - IF stage.type = 'Swiss' THEN - DECLARE - _wins int; - _losses int; - _wins_needed int := 3; - BEGIN - _wins := (bracket."group" / 100)::int; - _losses := (bracket."group" % 100)::int; - IF _wins = _wins_needed - 1 THEN - _swiss_match_type := 'advancement'; - ELSIF _losses = _wins_needed - 1 THEN - _swiss_match_type := 'elimination'; - ELSE - _swiss_match_type := 'regular'; - END IF; - _round_best_of := get_bracket_best_of(stage.id, _swiss_match_type, bracket.round); - END; - ELSE - _round_best_of := get_bracket_best_of(stage.id, bracket.path, bracket.round); - END IF; - - IF _round_best_of IS NOT NULL THEN - _match_options_id := clone_match_options_with_best_of(_template_match_options_id, _round_best_of); - END IF; - END IF; - - IF _match_options_id IS NULL THEN - _match_options_id := clone_match_options(_template_match_options_id); - END IF; - - _match_id := gen_random_uuid(); - - UPDATE tournament_brackets - SET match_id = _match_id - WHERE id = bracket.id; - - INSERT INTO matches ( - id, - status, - organizer_steam_id, - match_options_id, - scheduled_at - ) - VALUES ( - _match_id, - 'PickingPlayers', - tournament.organizer_steam_id, - _match_options_id, - GREATEST(COALESCE(bracket.scheduled_at, now()), now()) - ) - RETURNING lineup_1_id, lineup_2_id - INTO _lineup_1_id, _lineup_2_id; - - SELECT tt.captain_steam_id - INTO _captain_steam_id_1 - FROM tournament_teams tt - WHERE tt.id = bracket.tournament_team_id_1; - - SELECT tt.captain_steam_id - INTO _captain_steam_id_2 - FROM tournament_teams tt - WHERE tt.id = bracket.tournament_team_id_2; - - FOR member IN - SELECT * FROM tournament_team_roster - WHERE tournament_team_id = bracket.tournament_team_id_1 - LOOP - INSERT INTO match_lineup_players (match_lineup_id, steam_id) - VALUES (_lineup_1_id, member.player_steam_id); - END LOOP; - - FOR member IN - SELECT * FROM tournament_team_roster - WHERE tournament_team_id = bracket.tournament_team_id_2 - LOOP - INSERT INTO match_lineup_players (match_lineup_id, steam_id) - VALUES (_lineup_2_id, member.player_steam_id); - END LOOP; - - IF _captain_steam_id_1 IS NOT NULL THEN - UPDATE match_lineup_players - SET captain = true - WHERE match_lineup_id = _lineup_1_id - AND steam_id = _captain_steam_id_1; - END IF; - - IF _captain_steam_id_2 IS NOT NULL THEN - UPDATE match_lineup_players - SET captain = true - WHERE match_lineup_id = _lineup_2_id - AND steam_id = _captain_steam_id_2; - END IF; - - UPDATE match_lineups - SET team_id = tt.team_id - FROM tournament_teams tt - WHERE match_lineups.id = _lineup_1_id - AND tt.id = bracket.tournament_team_id_1 - AND tt.team_id IS NOT NULL; - - UPDATE match_lineups - SET team_id = tt.team_id - FROM tournament_teams tt - WHERE match_lineups.id = _lineup_2_id - AND tt.id = bracket.tournament_team_id_2 - AND tt.team_id IS NOT NULL; - - UPDATE matches - SET status = 'WaitingForCheckIn' - WHERE id = _match_id; - - PERFORM calculate_tournament_bracket_start_times(tournament.id); - - RETURN _match_id; - END; - $$; diff --git a/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/up.sql b/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/up.sql deleted file mode 100644 index 232a1537..00000000 --- a/hasura/migrations/default/1831000000000_schedule_tournament_matches_without_substitutes/up.sql +++ /dev/null @@ -1,226 +0,0 @@ -CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tournament_brackets) RETURNS uuid - LANGUAGE plpgsql - AS $$ - DECLARE - tournament tournaments; - stage tournament_stages; - member RECORD; - _lineup_1_id UUID; - _lineup_2_id UUID; - _captain_steam_id_1 bigint; - _captain_steam_id_2 bigint; - _match_id UUID; - feeder RECORD; - feeders_with_team int := 0; - winner_id UUID; - _template_match_options_id UUID; - _match_options_id UUID; - _round_best_of int; - _swiss_match_type text; - _min_players_per_lineup int; - BEGIN - IF bracket.match_id IS NOT NULL THEN - RETURN bracket.match_id; - END IF; - - IF bracket.finished = true THEN - RAISE NOTICE 'schedule_tournament_match: bracket % already finished, skipping', bracket.id; - RETURN NULL; - END IF; - - IF bracket.tournament_team_id_1 IS NULL AND bracket.tournament_team_id_2 IS NULL THEN - RAISE NOTICE 'schedule_tournament_match: bracket % has no teams, skipping', bracket.id; - RETURN NULL; - END IF; - - IF bracket.tournament_team_id_1 IS NULL OR bracket.tournament_team_id_2 IS NULL THEN - RAISE NOTICE 'schedule_tournament_match: bracket % missing one team (t1=%, t2=%), skipping', - bracket.id, bracket.tournament_team_id_1, bracket.tournament_team_id_2; - RETURN NULL; - END IF; - - SELECT ts.* INTO stage - FROM tournament_brackets tb - INNER JOIN tournament_stages ts ON ts.id = tb.tournament_stage_id - WHERE tb.id = bracket.id; - - SELECT t.* INTO tournament - FROM tournament_brackets tb - INNER JOIN tournament_stages ts ON ts.id = tb.tournament_stage_id - INNER JOIN tournaments t ON t.id = ts.tournament_id - WHERE tb.id = bracket.id; - - IF bracket.match_options_id IS NOT NULL THEN - _template_match_options_id := bracket.match_options_id; - ELSIF stage.match_options_id IS NOT NULL THEN - _template_match_options_id := stage.match_options_id; - ELSE - _template_match_options_id := tournament.match_options_id; - END IF; - - DECLARE - _match_mode text; - BEGIN - SELECT mo.match_mode INTO _match_mode - FROM match_options mo WHERE mo.id = _template_match_options_id; - - IF _match_mode = 'admin' AND bracket.scheduled_at IS NULL THEN - RAISE NOTICE 'schedule_tournament_match: bracket % is admin-mode without schedule, skipping auto-schedule', bracket.id; - RETURN NULL; - END IF; - END; - - IF bracket.match_options_id IS NULL THEN - IF stage.type = 'Swiss' THEN - DECLARE - _wins int; - _losses int; - _wins_needed int := 3; - BEGIN - _wins := (bracket."group" / 100)::int; - _losses := (bracket."group" % 100)::int; - IF _wins = _wins_needed - 1 THEN - _swiss_match_type := 'advancement'; - ELSIF _losses = _wins_needed - 1 THEN - _swiss_match_type := 'elimination'; - ELSE - _swiss_match_type := 'regular'; - END IF; - _round_best_of := get_bracket_best_of(stage.id, _swiss_match_type, bracket.round); - END; - ELSE - _round_best_of := get_bracket_best_of(stage.id, bracket.path, bracket.round); - END IF; - - IF _round_best_of IS NOT NULL THEN - _match_options_id := clone_match_options_with_best_of(_template_match_options_id, _round_best_of); - END IF; - END IF; - - IF _match_options_id IS NULL THEN - _match_options_id := clone_match_options(_template_match_options_id); - END IF; - - _match_id := gen_random_uuid(); - - UPDATE tournament_brackets - SET match_id = _match_id - WHERE id = bracket.id; - - INSERT INTO matches ( - id, - status, - organizer_steam_id, - match_options_id, - scheduled_at - ) - VALUES ( - _match_id, - 'PickingPlayers', - tournament.organizer_steam_id, - _match_options_id, - GREATEST(COALESCE(bracket.scheduled_at, now()), now()) - ) - RETURNING lineup_1_id, lineup_2_id - INTO _lineup_1_id, _lineup_2_id; - - SELECT match_min_players_per_lineup(m) - INTO _min_players_per_lineup - FROM matches m - WHERE m.id = _match_id; - - SELECT tt.captain_steam_id - INTO _captain_steam_id_1 - FROM tournament_teams tt - WHERE tt.id = bracket.tournament_team_id_1; - - SELECT tt.captain_steam_id - INTO _captain_steam_id_2 - FROM tournament_teams tt - WHERE tt.id = bracket.tournament_team_id_2; - - FOR member IN - SELECT ttr.* - FROM tournament_team_roster ttr - INNER JOIN tournament_teams tt - ON tt.id = ttr.tournament_team_id - LEFT JOIN team_roster tr - ON tr.team_id = tt.team_id - AND tr.player_steam_id = ttr.player_steam_id - WHERE ttr.tournament_team_id = bracket.tournament_team_id_1 - ORDER BY - CASE WHEN ttr.player_steam_id = _captain_steam_id_1 THEN 0 ELSE 1 END, - CASE tr.status - WHEN 'Starter' THEN 1 - WHEN 'Substitute' THEN 2 - WHEN 'Benched' THEN 3 - ELSE 4 - END, - ttr.player_steam_id - LIMIT _min_players_per_lineup - LOOP - INSERT INTO match_lineup_players (match_lineup_id, steam_id) - VALUES (_lineup_1_id, member.player_steam_id); - END LOOP; - - FOR member IN - SELECT ttr.* - FROM tournament_team_roster ttr - INNER JOIN tournament_teams tt - ON tt.id = ttr.tournament_team_id - LEFT JOIN team_roster tr - ON tr.team_id = tt.team_id - AND tr.player_steam_id = ttr.player_steam_id - WHERE ttr.tournament_team_id = bracket.tournament_team_id_2 - ORDER BY - CASE WHEN ttr.player_steam_id = _captain_steam_id_2 THEN 0 ELSE 1 END, - CASE tr.status - WHEN 'Starter' THEN 1 - WHEN 'Substitute' THEN 2 - WHEN 'Benched' THEN 3 - ELSE 4 - END, - ttr.player_steam_id - LIMIT _min_players_per_lineup - LOOP - INSERT INTO match_lineup_players (match_lineup_id, steam_id) - VALUES (_lineup_2_id, member.player_steam_id); - END LOOP; - - IF _captain_steam_id_1 IS NOT NULL THEN - UPDATE match_lineup_players - SET captain = true - WHERE match_lineup_id = _lineup_1_id - AND steam_id = _captain_steam_id_1; - END IF; - - IF _captain_steam_id_2 IS NOT NULL THEN - UPDATE match_lineup_players - SET captain = true - WHERE match_lineup_id = _lineup_2_id - AND steam_id = _captain_steam_id_2; - END IF; - - UPDATE match_lineups - SET team_id = tt.team_id - FROM tournament_teams tt - WHERE match_lineups.id = _lineup_1_id - AND tt.id = bracket.tournament_team_id_1 - AND tt.team_id IS NOT NULL; - - UPDATE match_lineups - SET team_id = tt.team_id - FROM tournament_teams tt - WHERE match_lineups.id = _lineup_2_id - AND tt.id = bracket.tournament_team_id_2 - AND tt.team_id IS NOT NULL; - - UPDATE matches - SET status = 'WaitingForCheckIn' - WHERE id = _match_id; - - PERFORM calculate_tournament_bracket_start_times(tournament.id); - - RETURN _match_id; - END; - $$; From 91dadfc4c7e44cab1e55b354c609c656f33954b3 Mon Sep 17 00:00:00 2001 From: hz <1766264+zeeshaun@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:24:22 -0500 Subject: [PATCH 3/4] Use tournament max lineup size when scheduling --- .../tournaments/schedule_tournament_match.sql | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/hasura/functions/tournaments/schedule_tournament_match.sql b/hasura/functions/tournaments/schedule_tournament_match.sql index 51103248..6d25272d 100644 --- a/hasura/functions/tournaments/schedule_tournament_match.sql +++ b/hasura/functions/tournaments/schedule_tournament_match.sql @@ -17,7 +17,7 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn _match_options_id UUID; _round_best_of int; _swiss_match_type text; - _min_players_per_lineup int; + _max_players_per_lineup int; BEGIN IF bracket.match_id IS NOT NULL THEN RETURN bracket.match_id; @@ -139,10 +139,8 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn RETURNING lineup_1_id, lineup_2_id INTO _lineup_1_id, _lineup_2_id; - SELECT match_min_players_per_lineup(m) - INTO _min_players_per_lineup - FROM matches m - WHERE m.id = _match_id; + SELECT tournament_max_players_per_lineup(tournament) + INTO _max_players_per_lineup; SELECT tt.captain_steam_id INTO _captain_steam_id_1 @@ -172,7 +170,7 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn ELSE 4 END, ttr.player_steam_id - LIMIT _min_players_per_lineup + LIMIT _max_players_per_lineup LOOP INSERT INTO match_lineup_players (match_lineup_id, steam_id) VALUES (_lineup_1_id, member.player_steam_id); @@ -196,7 +194,7 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn ELSE 4 END, ttr.player_steam_id - LIMIT _min_players_per_lineup + LIMIT _max_players_per_lineup LOOP INSERT INTO match_lineup_players (match_lineup_id, steam_id) VALUES (_lineup_2_id, member.player_steam_id); From 93394acef4302cf9fc5a51d4f6f9bd874bfc713f Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Tue, 2 Jun 2026 11:13:28 -0400 Subject: [PATCH 4/4] fix: cap tournament lineup by match options; dedupe lineup loop --- .../tournaments/schedule_tournament_match.sql | 81 ++++++++----------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/hasura/functions/tournaments/schedule_tournament_match.sql b/hasura/functions/tournaments/schedule_tournament_match.sql index 6d25272d..83b09960 100644 --- a/hasura/functions/tournaments/schedule_tournament_match.sql +++ b/hasura/functions/tournaments/schedule_tournament_match.sql @@ -5,6 +5,7 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn tournament tournaments; stage tournament_stages; member RECORD; + _lineup RECORD; _lineup_1_id UUID; _lineup_2_id UUID; _captain_steam_id_1 bigint; @@ -139,8 +140,11 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn RETURNING lineup_1_id, lineup_2_id INTO _lineup_1_id, _lineup_2_id; - SELECT tournament_max_players_per_lineup(tournament) - INTO _max_players_per_lineup; + -- Cap by the match's own options (a clone), not the tournament default. + SELECT match_max_players_per_lineup(m) + INTO _max_players_per_lineup + FROM matches m + WHERE m.id = _match_id; SELECT tt.captain_steam_id INTO _captain_steam_id_1 @@ -152,52 +156,35 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn FROM tournament_teams tt WHERE tt.id = bracket.tournament_team_id_2; - FOR member IN - SELECT ttr.* - FROM tournament_team_roster ttr - INNER JOIN tournament_teams tt - ON tt.id = ttr.tournament_team_id - LEFT JOIN team_roster tr - ON tr.team_id = tt.team_id - AND tr.player_steam_id = ttr.player_steam_id - WHERE ttr.tournament_team_id = bracket.tournament_team_id_1 - ORDER BY - CASE WHEN ttr.player_steam_id = _captain_steam_id_1 THEN 0 ELSE 1 END, - CASE tr.status - WHEN 'Starter' THEN 1 - WHEN 'Substitute' THEN 2 - WHEN 'Benched' THEN 3 - ELSE 4 - END, - ttr.player_steam_id - LIMIT _max_players_per_lineup + FOR _lineup IN + SELECT * FROM (VALUES + (_lineup_1_id, bracket.tournament_team_id_1, _captain_steam_id_1), + (_lineup_2_id, bracket.tournament_team_id_2, _captain_steam_id_2) + ) AS l(match_lineup_id, tournament_team_id, captain_steam_id) LOOP - INSERT INTO match_lineup_players (match_lineup_id, steam_id) - VALUES (_lineup_1_id, member.player_steam_id); - END LOOP; - - FOR member IN - SELECT ttr.* - FROM tournament_team_roster ttr - INNER JOIN tournament_teams tt - ON tt.id = ttr.tournament_team_id - LEFT JOIN team_roster tr - ON tr.team_id = tt.team_id - AND tr.player_steam_id = ttr.player_steam_id - WHERE ttr.tournament_team_id = bracket.tournament_team_id_2 - ORDER BY - CASE WHEN ttr.player_steam_id = _captain_steam_id_2 THEN 0 ELSE 1 END, - CASE tr.status - WHEN 'Starter' THEN 1 - WHEN 'Substitute' THEN 2 - WHEN 'Benched' THEN 3 - ELSE 4 - END, - ttr.player_steam_id - LIMIT _max_players_per_lineup - LOOP - INSERT INTO match_lineup_players (match_lineup_id, steam_id) - VALUES (_lineup_2_id, member.player_steam_id); + FOR member IN + SELECT ttr.* + FROM tournament_team_roster ttr + INNER JOIN tournament_teams tt + ON tt.id = ttr.tournament_team_id + LEFT JOIN team_roster tr + ON tr.team_id = tt.team_id + AND tr.player_steam_id = ttr.player_steam_id + WHERE ttr.tournament_team_id = _lineup.tournament_team_id + ORDER BY + CASE WHEN ttr.player_steam_id = _lineup.captain_steam_id THEN 0 ELSE 1 END, + CASE tr.status + WHEN 'Starter' THEN 1 + WHEN 'Substitute' THEN 2 + WHEN 'Benched' THEN 3 + ELSE 4 + END, + ttr.player_steam_id + LIMIT _max_players_per_lineup + LOOP + INSERT INTO match_lineup_players (match_lineup_id, steam_id) + VALUES (_lineup.match_lineup_id, member.player_steam_id); + END LOOP; END LOOP; IF _captain_steam_id_1 IS NOT NULL THEN