Skip to content

Commit

Permalink
Handle teams foreign key errors (#543)
Browse files Browse the repository at this point in the history
  • Loading branch information
evroon committed Feb 26, 2024
1 parent 566c0aa commit 1d763fe
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 54 deletions.
10 changes: 0 additions & 10 deletions backend/bracket/models/db/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
from bracket.models.db.stage import Stage
from bracket.models.db.stage_item import StageItem, StageType
from bracket.models.db.stage_item_inputs import StageItemInput
from bracket.utils.id_types import TeamId
from bracket.utils.types import assert_some


class RoundWithMatches(Round):
Expand All @@ -24,14 +22,6 @@ def handle_matches(values: list[Match]) -> list[Match]: # type: ignore[misc]
return []
return values

def get_team_ids(self) -> set[TeamId]:
return {
assert_some(team.id)
for match in self.matches
if isinstance(match, MatchWithDetailsDefinitive)
for team in match.teams
}


class StageItemWithRounds(StageItem):
rounds: list[RoundWithMatches]
Expand Down
5 changes: 1 addition & 4 deletions backend/bracket/routes/clubs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncpg # type: ignore[import-untyped]
from fastapi import APIRouter, Depends

from bracket.logic.subscriptions import check_requirement
Expand Down Expand Up @@ -32,10 +31,8 @@ async def create_new_club(
async def delete_club(
club_id: ClubId, _: UserPublic = Depends(user_authenticated_for_club)
) -> SuccessResponse:
try:
with check_foreign_key_violation({ForeignKey.tournaments_club_id_fkey}):
await sql_delete_club(club_id)
except asyncpg.exceptions.ForeignKeyViolationError as exc:
check_foreign_key_violation(exc, {ForeignKey.tournaments_club_id_fkey})

return SuccessResponse()

Expand Down
29 changes: 10 additions & 19 deletions backend/bracket/routes/teams.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends
from heliclockter import datetime_utc
from starlette import status

from bracket.database import database
from bracket.logic.ranking.elo import recalculate_ranking_for_tournament_id
Expand All @@ -19,7 +18,6 @@
)
from bracket.routes.util import team_dependency, team_with_players_dependency
from bracket.schema import players_x_teams, teams
from bracket.sql.stages import get_full_tournament_details
from bracket.sql.teams import (
get_team_by_id,
get_team_count,
Expand All @@ -28,6 +26,7 @@
)
from bracket.sql.validation import check_foreign_keys_belong_to_tournament
from bracket.utils.db import fetch_one_parsed
from bracket.utils.errors import ForeignKey, check_foreign_key_violation
from bracket.utils.id_types import PlayerId, TeamId, TournamentId
from bracket.utils.pagination import PaginationTeams
from bracket.utils.types import assert_some
Expand Down Expand Up @@ -109,23 +108,15 @@ async def delete_team(
_: UserPublic = Depends(user_authenticated_for_tournament),
team: FullTeamWithPlayers = Depends(team_with_players_dependency),
) -> SuccessResponse:
stages = await get_full_tournament_details(tournament_id, no_draft_rounds=False)
for stage in stages:
for stage_item in stage.stage_items:
for round_ in stage_item.rounds:
if team.id in round_.get_team_ids():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Could not delete team that participates in matches",
)

if len(team.players):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Could not delete team that still has players in it",
)
with check_foreign_key_violation(
{
ForeignKey.stage_item_inputs_team_id_fkey,
ForeignKey.matches_team1_id_fkey,
ForeignKey.matches_team2_id_fkey,
}
):
await sql_delete_team(tournament_id, assert_some(team.id))

await sql_delete_team(tournament_id, assert_some(team.id))
await recalculate_ranking_for_tournament_id(tournament_id)
return SuccessResponse()

Expand Down
4 changes: 1 addition & 3 deletions backend/bracket/routes/tournaments.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,8 @@ async def update_tournament_by_id(
async def delete_tournament(
tournament_id: TournamentId, _: UserPublic = Depends(user_authenticated_for_tournament)
) -> SuccessResponse:
try:
with check_foreign_key_violation({ForeignKey.stages_tournament_id_fkey}):
await sql_delete_tournament(tournament_id)
except asyncpg.exceptions.ForeignKeyViolationError as exc:
check_foreign_key_violation(exc, {ForeignKey.stages_tournament_id_fkey})

return SuccessResponse()

Expand Down
48 changes: 30 additions & 18 deletions backend/bracket/utils/errors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections.abc import Iterator
from contextlib import contextmanager
from enum import auto
from typing import NoReturn

Expand All @@ -16,6 +18,9 @@ class UniqueIndex(EnumAutoStr):
class ForeignKey(EnumAutoStr):
stages_tournament_id_fkey = auto()
tournaments_club_id_fkey = auto()
stage_item_inputs_team_id_fkey = auto()
matches_team1_id_fkey = auto()
matches_team2_id_fkey = auto()


unique_index_violation_error_lookup = {
Expand All @@ -27,6 +32,9 @@ class ForeignKey(EnumAutoStr):
foreign_key_violation_error_lookup = {
ForeignKey.stages_tournament_id_fkey: "This tournament still has stages, delete those first",
ForeignKey.tournaments_club_id_fkey: "This club still has tournaments, delete those first",
ForeignKey.stage_item_inputs_team_id_fkey: "This team is still used in stage items",
ForeignKey.matches_team1_id_fkey: "This team is still part of matches",
ForeignKey.matches_team2_id_fkey: "This team is still part of matches",
}


Expand All @@ -50,21 +58,25 @@ def check_unique_constraint_violation(
)


def check_foreign_key_violation(
exc: asyncpg.exceptions.ForeignKeyViolationError, expected_violations: set[ForeignKey]
) -> NoReturn:
constraint_name = exc.as_dict()["constraint_name"]
assert constraint_name, "ForeignKeyViolationError occurred but no constraint_name defined"
assert constraint_name in ForeignKey.values(), "Unknown ForeignKeyViolationError occurred"
constraint = ForeignKey(constraint_name)

if (
constraint not in foreign_key_violation_error_lookup
or constraint not in expected_violations
):
raise exc

raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=foreign_key_violation_error_lookup[constraint],
)
@contextmanager
def check_foreign_key_violation(expected_violations: set[ForeignKey]) -> Iterator[None]:
try:
yield
except asyncpg.exceptions.ForeignKeyViolationError as exc:
constraint_name = exc.as_dict()["constraint_name"]
assert constraint_name, "ForeignKeyViolationError occurred but no constraint_name defined"
assert (
constraint_name in ForeignKey.values()
), f"Unknown ForeignKeyViolationError occurred: {constraint_name}"
constraint = ForeignKey(constraint_name)

if (
constraint not in foreign_key_violation_error_lookup
or constraint not in expected_violations
):
raise exc

raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=foreign_key_violation_error_lookup[constraint],
) from exc

0 comments on commit 1d763fe

Please sign in to comment.