aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/db
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-18 22:38:15 +0900
committernsfisis <nsfisis@gmail.com>2026-02-18 22:38:15 +0900
commit9f9efc2bc07810d2e06b37bad94e5922681eadef (patch)
tree79bcce2bf065a7ea282aa7855822c3bdee92ee7c /backend/db
parentc095200dc79f24c0cd17a2e3ba15c85a2971ea9a (diff)
downloadphperkaigi-2026-albatross-9f9efc2bc07810d2e06b37bad94e5922681eadef.tar.gz
phperkaigi-2026-albatross-9f9efc2bc07810d2e06b37bad94e5922681eadef.tar.zst
phperkaigi-2026-albatross-9f9efc2bc07810d2e06b37bad94e5922681eadef.zip
feat: refactor tournament to generic DB-backed N-person bracket
Replace hardcoded 6-person tournament with a generic single-elimination bracket system backed by new DB tables (tournaments, tournament_entries, tournament_matches). Includes admin CRUD, standard seeding algorithm, bye handling, and a CSS Grid bracket renderer on the frontend. Add comprehensive tests for backend API/admin handlers, seeding logic, and frontend bracket component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'backend/db')
-rw-r--r--backend/db/models.go23
-rw-r--r--backend/db/querier.go11
-rw-r--r--backend/db/query.sql.go246
3 files changed, 280 insertions, 0 deletions
diff --git a/backend/db/models.go b/backend/db/models.go
index c4a713d..3086c3b 100644
--- a/backend/db/models.go
+++ b/backend/db/models.go
@@ -74,6 +74,29 @@ type TestcaseResult struct {
CreatedAt pgtype.Timestamp
}
+type Tournament struct {
+ TournamentID int32
+ DisplayName string
+ BracketSize int32
+ NumRounds int32
+ CreatedAt pgtype.Timestamp
+}
+
+type TournamentEntry struct {
+ TournamentEntryID int32
+ TournamentID int32
+ UserID int32
+ Seed int32
+}
+
+type TournamentMatch struct {
+ TournamentMatchID int32
+ TournamentID int32
+ Round int32
+ Position int32
+ GameID *int32
+}
+
type User struct {
UserID int32
Username string
diff --git a/backend/db/querier.go b/backend/db/querier.go
index 89d4b55..3b9545a 100644
--- a/backend/db/querier.go
+++ b/backend/db/querier.go
@@ -17,11 +17,16 @@ type Querier interface {
CreateSubmission(ctx context.Context, arg CreateSubmissionParams) (int32, error)
CreateTestcase(ctx context.Context, arg CreateTestcaseParams) (int32, error)
CreateTestcaseResult(ctx context.Context, arg CreateTestcaseResultParams) error
+ CreateTournament(ctx context.Context, arg CreateTournamentParams) (int32, error)
+ CreateTournamentEntry(ctx context.Context, arg CreateTournamentEntryParams) error
+ CreateTournamentMatch(ctx context.Context, arg CreateTournamentMatchParams) error
CreateUser(ctx context.Context, username string) (int32, error)
CreateUserAuth(ctx context.Context, arg CreateUserAuthParams) error
DeleteExpiredSessions(ctx context.Context) error
DeleteSession(ctx context.Context, sessionID string) error
DeleteTestcase(ctx context.Context, testcaseID int32) error
+ DeleteTournamentEntries(ctx context.Context, tournamentID int32) error
+ DeleteTournamentMatches(ctx context.Context, tournamentID int32) error
GetGameByID(ctx context.Context, gameID int32) (GetGameByIDRow, error)
GetLatestState(ctx context.Context, arg GetLatestStateParams) (GetLatestStateRow, error)
GetLatestStatesOfMainPlayers(ctx context.Context, gameID int32) ([]GetLatestStatesOfMainPlayersRow, error)
@@ -32,6 +37,7 @@ type Querier interface {
GetSubmissionsByGameID(ctx context.Context, gameID int32) ([]Submission, error)
GetTestcaseByID(ctx context.Context, testcaseID int32) (Testcase, error)
GetTestcaseResultsBySubmissionID(ctx context.Context, submissionID int32) ([]TestcaseResult, error)
+ GetTournamentByID(ctx context.Context, tournamentID int32) (Tournament, error)
GetUserAuthByUsername(ctx context.Context, username string) (GetUserAuthByUsernameRow, error)
GetUserByID(ctx context.Context, userID int32) (User, error)
GetUserBySession(ctx context.Context, sessionID string) (User, error)
@@ -45,6 +51,9 @@ type Querier interface {
ListTestcases(ctx context.Context) ([]Testcase, error)
ListTestcasesByGameID(ctx context.Context, gameID int32) ([]Testcase, error)
ListTestcasesByProblemID(ctx context.Context, problemID int32) ([]Testcase, error)
+ ListTournamentEntries(ctx context.Context, tournamentID int32) ([]ListTournamentEntriesRow, error)
+ ListTournamentMatches(ctx context.Context, tournamentID int32) ([]TournamentMatch, error)
+ ListTournaments(ctx context.Context) ([]Tournament, error)
ListUsers(ctx context.Context) ([]User, error)
RemoveAllMainPlayers(ctx context.Context, gameID int32) error
SyncGameStateBestScoreSubmission(ctx context.Context, arg SyncGameStateBestScoreSubmissionParams) error
@@ -56,6 +65,8 @@ type Querier interface {
UpdateProblem(ctx context.Context, arg UpdateProblemParams) error
UpdateSubmissionStatus(ctx context.Context, arg UpdateSubmissionStatusParams) error
UpdateTestcase(ctx context.Context, arg UpdateTestcaseParams) error
+ UpdateTournament(ctx context.Context, arg UpdateTournamentParams) error
+ UpdateTournamentMatchGame(ctx context.Context, arg UpdateTournamentMatchGameParams) error
UpdateUser(ctx context.Context, arg UpdateUserParams) error
UpdateUserIconPath(ctx context.Context, arg UpdateUserIconPathParams) error
}
diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go
index 1d6d11c..50aa02e 100644
--- a/backend/db/query.sql.go
+++ b/backend/db/query.sql.go
@@ -186,6 +186,57 @@ func (q *Queries) CreateTestcaseResult(ctx context.Context, arg CreateTestcaseRe
return err
}
+const createTournament = `-- name: CreateTournament :one
+INSERT INTO tournaments (display_name, bracket_size, num_rounds)
+VALUES ($1, $2, $3)
+RETURNING tournament_id
+`
+
+type CreateTournamentParams struct {
+ DisplayName string
+ BracketSize int32
+ NumRounds int32
+}
+
+func (q *Queries) CreateTournament(ctx context.Context, arg CreateTournamentParams) (int32, error) {
+ row := q.db.QueryRow(ctx, createTournament, arg.DisplayName, arg.BracketSize, arg.NumRounds)
+ var tournament_id int32
+ err := row.Scan(&tournament_id)
+ return tournament_id, err
+}
+
+const createTournamentEntry = `-- name: CreateTournamentEntry :exec
+INSERT INTO tournament_entries (tournament_id, user_id, seed)
+VALUES ($1, $2, $3)
+`
+
+type CreateTournamentEntryParams struct {
+ TournamentID int32
+ UserID int32
+ Seed int32
+}
+
+func (q *Queries) CreateTournamentEntry(ctx context.Context, arg CreateTournamentEntryParams) error {
+ _, err := q.db.Exec(ctx, createTournamentEntry, arg.TournamentID, arg.UserID, arg.Seed)
+ return err
+}
+
+const createTournamentMatch = `-- name: CreateTournamentMatch :exec
+INSERT INTO tournament_matches (tournament_id, round, position)
+VALUES ($1, $2, $3)
+`
+
+type CreateTournamentMatchParams struct {
+ TournamentID int32
+ Round int32
+ Position int32
+}
+
+func (q *Queries) CreateTournamentMatch(ctx context.Context, arg CreateTournamentMatchParams) error {
+ _, err := q.db.Exec(ctx, createTournamentMatch, arg.TournamentID, arg.Round, arg.Position)
+ return err
+}
+
const createUser = `-- name: CreateUser :one
INSERT INTO users (username, display_name, is_admin)
VALUES ($1, $1, false)
@@ -242,6 +293,26 @@ func (q *Queries) DeleteTestcase(ctx context.Context, testcaseID int32) error {
return err
}
+const deleteTournamentEntries = `-- name: DeleteTournamentEntries :exec
+DELETE FROM tournament_entries
+WHERE tournament_id = $1
+`
+
+func (q *Queries) DeleteTournamentEntries(ctx context.Context, tournamentID int32) error {
+ _, err := q.db.Exec(ctx, deleteTournamentEntries, tournamentID)
+ return err
+}
+
+const deleteTournamentMatches = `-- name: DeleteTournamentMatches :exec
+DELETE FROM tournament_matches
+WHERE tournament_id = $1
+`
+
+func (q *Queries) DeleteTournamentMatches(ctx context.Context, tournamentID int32) error {
+ _, err := q.db.Exec(ctx, deleteTournamentMatches, tournamentID)
+ return err
+}
+
const getGameByID = `-- name: GetGameByID :one
SELECT game_id, game_type, is_public, display_name, duration_seconds, created_at, started_at, games.problem_id, problems.problem_id, title, description, language, sample_code FROM games
JOIN problems ON games.problem_id = problems.problem_id
@@ -643,6 +714,25 @@ func (q *Queries) GetTestcaseResultsBySubmissionID(ctx context.Context, submissi
return items, nil
}
+const getTournamentByID = `-- name: GetTournamentByID :one
+SELECT tournament_id, display_name, bracket_size, num_rounds, created_at FROM tournaments
+WHERE tournament_id = $1
+LIMIT 1
+`
+
+func (q *Queries) GetTournamentByID(ctx context.Context, tournamentID int32) (Tournament, error) {
+ row := q.db.QueryRow(ctx, getTournamentByID, tournamentID)
+ var i Tournament
+ err := row.Scan(
+ &i.TournamentID,
+ &i.DisplayName,
+ &i.BracketSize,
+ &i.NumRounds,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
const getUserAuthByUsername = `-- name: GetUserAuthByUsername :one
SELECT users.user_id, username, display_name, icon_path, is_admin, label, created_at, user_auth_id, user_auths.user_id, auth_type, password_hash FROM users
JOIN user_auths ON users.user_id = user_auths.user_id
@@ -1054,6 +1144,123 @@ func (q *Queries) ListTestcasesByProblemID(ctx context.Context, problemID int32)
return items, nil
}
+const listTournamentEntries = `-- name: ListTournamentEntries :many
+SELECT tournament_entries.tournament_entry_id, tournament_entries.tournament_id, tournament_entries.user_id, tournament_entries.seed, users.user_id, users.username, users.display_name, users.icon_path, users.is_admin, users.label, users.created_at
+FROM tournament_entries
+JOIN users ON tournament_entries.user_id = users.user_id
+WHERE tournament_entries.tournament_id = $1
+ORDER BY tournament_entries.seed
+`
+
+type ListTournamentEntriesRow struct {
+ TournamentEntryID int32
+ TournamentID int32
+ UserID int32
+ Seed int32
+ UserID_2 int32
+ Username string
+ DisplayName string
+ IconPath *string
+ IsAdmin bool
+ Label *string
+ CreatedAt pgtype.Timestamp
+}
+
+func (q *Queries) ListTournamentEntries(ctx context.Context, tournamentID int32) ([]ListTournamentEntriesRow, error) {
+ rows, err := q.db.Query(ctx, listTournamentEntries, tournamentID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []ListTournamentEntriesRow
+ for rows.Next() {
+ var i ListTournamentEntriesRow
+ if err := rows.Scan(
+ &i.TournamentEntryID,
+ &i.TournamentID,
+ &i.UserID,
+ &i.Seed,
+ &i.UserID_2,
+ &i.Username,
+ &i.DisplayName,
+ &i.IconPath,
+ &i.IsAdmin,
+ &i.Label,
+ &i.CreatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listTournamentMatches = `-- name: ListTournamentMatches :many
+SELECT tournament_match_id, tournament_id, round, position, game_id FROM tournament_matches
+WHERE tournament_id = $1
+ORDER BY round, position
+`
+
+func (q *Queries) ListTournamentMatches(ctx context.Context, tournamentID int32) ([]TournamentMatch, error) {
+ rows, err := q.db.Query(ctx, listTournamentMatches, tournamentID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []TournamentMatch
+ for rows.Next() {
+ var i TournamentMatch
+ if err := rows.Scan(
+ &i.TournamentMatchID,
+ &i.TournamentID,
+ &i.Round,
+ &i.Position,
+ &i.GameID,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listTournaments = `-- name: ListTournaments :many
+SELECT tournament_id, display_name, bracket_size, num_rounds, created_at FROM tournaments
+ORDER BY tournament_id
+`
+
+func (q *Queries) ListTournaments(ctx context.Context) ([]Tournament, error) {
+ rows, err := q.db.Query(ctx, listTournaments)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Tournament
+ for rows.Next() {
+ var i Tournament
+ if err := rows.Scan(
+ &i.TournamentID,
+ &i.DisplayName,
+ &i.BracketSize,
+ &i.NumRounds,
+ &i.CreatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const listUsers = `-- name: ListUsers :many
SELECT user_id, username, display_name, icon_path, is_admin, label, created_at FROM users
ORDER BY users.user_id
@@ -1305,6 +1512,45 @@ func (q *Queries) UpdateTestcase(ctx context.Context, arg UpdateTestcaseParams)
return err
}
+const updateTournament = `-- name: UpdateTournament :exec
+UPDATE tournaments
+SET display_name = $2, bracket_size = $3, num_rounds = $4
+WHERE tournament_id = $1
+`
+
+type UpdateTournamentParams struct {
+ TournamentID int32
+ DisplayName string
+ BracketSize int32
+ NumRounds int32
+}
+
+func (q *Queries) UpdateTournament(ctx context.Context, arg UpdateTournamentParams) error {
+ _, err := q.db.Exec(ctx, updateTournament,
+ arg.TournamentID,
+ arg.DisplayName,
+ arg.BracketSize,
+ arg.NumRounds,
+ )
+ return err
+}
+
+const updateTournamentMatchGame = `-- name: UpdateTournamentMatchGame :exec
+UPDATE tournament_matches
+SET game_id = $2
+WHERE tournament_match_id = $1
+`
+
+type UpdateTournamentMatchGameParams struct {
+ TournamentMatchID int32
+ GameID *int32
+}
+
+func (q *Queries) UpdateTournamentMatchGame(ctx context.Context, arg UpdateTournamentMatchGameParams) error {
+ _, err := q.db.Exec(ctx, updateTournamentMatchGame, arg.TournamentMatchID, arg.GameID)
+ return err
+}
+
const updateUser = `-- name: UpdateUser :exec
UPDATE users
SET