Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions hasura/triggers/tournament_stages.sql
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ DECLARE
current_order INTEGER;
next_stage_record RECORD;
BEGIN
-- Resequencing after a delete only shifts "order"; the delete trigger
-- regenerates the brackets once for the whole tournament afterwards.
BEGIN
PERFORM 1 FROM pg_temp.resequencing_tournament_stages LIMIT 1;
RETURN NEW;
EXCEPTION
WHEN undefined_table THEN
NULL;
END;

-- Format-only tweaks (per-round best-of settings, default best-of) don't
-- affect bracket structure — they are read at match materialization — so
-- they must not trigger a bracket regeneration.
Expand Down Expand Up @@ -203,6 +213,14 @@ DECLARE
tournament_status text;
stage_has_matches boolean;
BEGIN
BEGIN
PERFORM 1 FROM pg_temp.resequencing_tournament_stages LIMIT 1;
RETURN NEW;
EXCEPTION
WHEN undefined_table THEN
NULL;
END;

-- Format-only tweaks (per-round best-of settings, default best-of) are
-- allowed at any point: they only apply to matches that have not been
-- created yet, so a live tournament/league can adjust its series format.
Expand Down Expand Up @@ -284,6 +302,28 @@ CREATE OR REPLACE FUNCTION public.tad_tournament_stages() RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
-- Stage order must stay 1..N with no gaps: stages are linked by
-- "order" + 1, so a hole leaves the stages after it orphaned.
CREATE TEMP TABLE IF NOT EXISTS resequencing_tournament_stages (dummy int);

BEGIN
UPDATE tournament_stages ts
SET "order" = resequenced.new_order
FROM (
SELECT id, (ROW_NUMBER() OVER (ORDER BY "order" ASC, id ASC))::int AS new_order
FROM tournament_stages
WHERE tournament_id = OLD.tournament_id
) resequenced
WHERE ts.id = resequenced.id
AND ts."order" IS DISTINCT FROM resequenced.new_order;
EXCEPTION
WHEN OTHERS THEN
DROP TABLE IF EXISTS pg_temp.resequencing_tournament_stages;
RAISE;
END;

DROP TABLE IF EXISTS pg_temp.resequencing_tournament_stages;

PERFORM update_tournament_stages(OLD.tournament_id);
PERFORM cleanup_orphaned_match_options(OLD.match_options_id);
RETURN OLD;
Expand Down
46 changes: 46 additions & 0 deletions test/tournament-stages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,52 @@ describe("tournament stages: Swiss and RoundRobin (SQL-driven)", () => {
expect(Number(stage1.min_teams)).toBe(8);
});

const THREE_STAGES = [
{ type: "SingleElimination", order: 1, minTeams: 16, maxTeams: 16 },
{ type: "SingleElimination", order: 2, minTeams: 8, maxTeams: 8 },
{ type: "SingleElimination", order: 3, minTeams: 4, maxTeams: 4 },
];

it("deleting a stage closes the gap in the remaining stage order", async () => {
const t = await tfx.createTournament(THREE_STAGES);

await postgres.query("DELETE FROM tournament_stages WHERE id = $1", [
t.stageIds[0],
]);

const stages = await postgres.query<Array<{ id: string; order: number }>>(
`SELECT id, "order" FROM tournament_stages WHERE tournament_id = $1 ORDER BY "order"`,
[t.id],
);
expect(stages.map((stage) => Number(stage.order))).toEqual([1, 2]);
expect(stages.map((stage) => stage.id)).toEqual([
t.stageIds[1],
t.stageIds[2],
]);
});

it("deleting a middle stage keeps the surviving stages linked", async () => {
const t = await tfx.createTournament(THREE_STAGES);

await postgres.query("DELETE FROM tournament_stages WHERE id = $1", [
t.stageIds[1],
]);

const stages = await postgres.query<Array<{ order: number }>>(
`SELECT "order" FROM tournament_stages WHERE tournament_id = $1 ORDER BY "order"`,
[t.id],
);
expect(stages.map((stage) => Number(stage.order))).toEqual([1, 2]);

// Stages link by "order" + 1, so the gap used to leave stage 3 orphaned.
const [linked] = await postgres.query<Array<{ count: string }>>(
`SELECT COUNT(*) FROM tournament_brackets
WHERE tournament_stage_id = $1 AND parent_bracket_id IS NOT NULL`,
[t.stageIds[0]],
);
expect(Number(linked.count)).toBeGreaterThan(0);
});

it("a RoundRobin stage feeds its top teams into the elimination stage", async () => {
const t = await tfx.launch(
[
Expand Down
Loading