ms-tester/internal/tester/repository/pg_contests_repository.go
2025-04-01 18:01:20 +05:00

628 lines
16 KiB
Go

package repository
import (
"context"
"fmt"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"github.com/jmoiron/sqlx"
"strings"
)
type ContestRepository struct {
db *sqlx.DB
}
func NewContestRepository(db *sqlx.DB) *ContestRepository {
return &ContestRepository{
db: db,
}
}
const createContestQuery = "INSERT INTO contests (title) VALUES (?) RETURNING id"
func (r *ContestRepository) CreateContest(ctx context.Context, title string) (int32, error) {
const op = "ContestRepository.CreateContest"
query := r.db.Rebind(createContestQuery)
rows, err := r.db.QueryxContext(ctx, query, title)
if err != nil {
return 0, handlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, handlePgErr(err, op)
}
return id, nil
}
const readContestByIdQuery = "SELECT * from contests WHERE id=? LIMIT 1"
func (r *ContestRepository) ReadContestById(ctx context.Context, id int32) (*models.Contest, error) {
const op = "ContestRepository.ReadContestById"
var contest models.Contest
query := r.db.Rebind(readContestByIdQuery)
err := r.db.GetContext(ctx, &contest, query, id)
if err != nil {
return nil, handlePgErr(err, op)
}
return &contest, nil
}
const deleteContestQuery = "DELETE FROM contests WHERE id=?"
func (r *ContestRepository) DeleteContest(ctx context.Context, id int32) error {
const op = "ContestRepository.DeleteContest"
query := r.db.Rebind(deleteContestQuery)
_, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return handlePgErr(err, op)
}
return nil
}
const addTaskQuery = `INSERT INTO tasks (problem_id, contest_id, position)
VALUES (?, ?, COALESCE((SELECT MAX(position) FROM tasks WHERE contest_id = ?), 0) + 1)
RETURNING id
`
func (r *ContestRepository) AddTask(ctx context.Context, contestId int32, problemId int32) (int32, error) {
const op = "ContestRepository.AddTask"
query := r.db.Rebind(addTaskQuery)
rows, err := r.db.QueryxContext(ctx, query, problemId, contestId, contestId)
if err != nil {
return 0, handlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, handlePgErr(err, op)
}
return id, nil
}
const deleteTaskQuery = "DELETE FROM tasks WHERE id=?"
func (r *ContestRepository) DeleteTask(ctx context.Context, taskId int32) error {
const op = "ContestRepository.DeleteTask"
query := r.db.Rebind(deleteTaskQuery)
_, err := r.db.ExecContext(ctx, query, taskId)
if err != nil {
return handlePgErr(err, op)
}
return nil
}
const addParticipantQuery = "INSERT INTO participants (user_id ,contest_id, name) VALUES (?, ?, ?) RETURNING id"
func (r *ContestRepository) AddParticipant(ctx context.Context, contestId int32, userId int32) (int32, error) {
const op = "ContestRepository.AddParticipant"
query := r.db.Rebind(addParticipantQuery)
name := ""
rows, err := r.db.QueryxContext(ctx, query, contestId, userId, name)
if err != nil {
return 0, handlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, err
}
return id, nil
}
const deleteParticipantQuery = "DELETE FROM participants WHERE id=?"
func (r *ContestRepository) DeleteParticipant(ctx context.Context, participantId int32) error {
const op = "ContestRepository.DeleteParticipant"
query := r.db.Rebind(deleteParticipantQuery)
_, err := r.db.ExecContext(ctx, query, participantId)
if err != nil {
return handlePgErr(err, op)
}
return nil
}
const readTasksQuery = `SELECT tasks.id,
problem_id,
contest_id,
position,
title,
memory_limit,
time_limit,
tasks.created_at,
tasks.updated_at
FROM tasks
INNER JOIN problems ON tasks.problem_id = problems.id
WHERE contest_id = ? ORDER BY position`
func (r *ContestRepository) ReadTasks(ctx context.Context, contestId int32) ([]*models.TasksListItem, error) {
const op = "ContestRepository.ReadTasks"
var tasks []*models.TasksListItem
query := r.db.Rebind(readTasksQuery)
err := r.db.SelectContext(ctx, &tasks, query, contestId)
if err != nil {
return nil, handlePgErr(err, op)
}
return tasks, nil
}
const (
readContestsListQuery = `SELECT id, title, created_at, updated_at FROM contests LIMIT ? OFFSET ?`
countContestsQuery = "SELECT COUNT(*) FROM contests"
)
func (r *ContestRepository) ListContests(ctx context.Context, filter models.ContestsFilter) (*models.ContestsList, error) {
const op = "ContestRepository.ReadTasks"
var contests []*models.ContestsListItem
query := r.db.Rebind(readContestsListQuery)
err := r.db.SelectContext(ctx, &contests, query, filter.PageSize, filter.Offset())
if err != nil {
return nil, handlePgErr(err, op)
}
query = r.db.Rebind(countContestsQuery)
var count int32
err = r.db.GetContext(ctx, &count, query)
if err != nil {
return nil, handlePgErr(err, op)
}
return &models.ContestsList{
Contests: contests,
Pagination: models.Pagination{
Total: models.Total(count, filter.PageSize),
Page: filter.Page,
},
}, nil
}
const (
readParticipantsListQuery = `SELECT id, user_id, name, created_at, updated_at FROM participants WHERE contest_id = ? LIMIT ? OFFSET ?`
countParticipantsQuery = "SELECT COUNT(*) FROM participants WHERE contest_id = ?"
)
func (r *ContestRepository) ListParticipants(ctx context.Context, filter models.ParticipantsFilter) (*models.ParticipantsList, error) {
const op = "ContestRepository.ReadParticipants"
if filter.PageSize > 20 {
filter.PageSize = 1
}
var participants []*models.ParticipantsListItem
query := r.db.Rebind(readParticipantsListQuery)
err := r.db.SelectContext(ctx, &participants, query, filter.ContestId, filter.PageSize, filter.Offset())
if err != nil {
return nil, handlePgErr(err, op)
}
query = r.db.Rebind(countParticipantsQuery)
var count int32
err = r.db.GetContext(ctx, &count, query, filter.ContestId)
if err != nil {
return nil, handlePgErr(err, op)
}
return &models.ParticipantsList{
Participants: participants,
Pagination: models.Pagination{
Total: models.Total(count, filter.PageSize),
Page: filter.Page,
},
}, nil
}
const (
updateContestQuery = "UPDATE contests SET title = COALESCE(?, title) WHERE id = ?"
)
func (r *ContestRepository) UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error {
const op = "ContestRepository.UpdateContest"
query := r.db.Rebind(updateContestQuery)
_, err := r.db.ExecContext(ctx, query, contestUpdate.Title, id)
if err != nil {
return handlePgErr(err, op)
}
return nil
}
const (
updateParticipantQuery = "UPDATE participants SET name = COALESCE(?, name) WHERE id = ?"
)
func (r *ContestRepository) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
const op = "ContestRepository.UpdateParticipant"
query := r.db.Rebind(updateParticipantQuery)
_, err := r.db.ExecContext(ctx, query, participantUpdate.Name, id)
if err != nil {
return handlePgErr(err, op)
}
return nil
}
const (
readSolutionQuery = "SELECT * FROM solutions WHERE id = ?"
)
func (r *ContestRepository) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) {
const op = "ContestRepository.ReadSolution"
query := r.db.Rebind(readSolutionQuery)
var solution models.Solution
err := r.db.GetContext(ctx, &solution, query, id)
if err != nil {
return nil, handlePgErr(err, op)
}
return &solution, nil
}
const (
createSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution)
VALUES (?, ?, ?, ?, ?)
RETURNING id`
)
func (r *ContestRepository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
const op = "ContestRepository.CreateSolution"
query := r.db.Rebind(createSolutionQuery)
rows, err := r.db.QueryxContext(ctx,
query,
creation.TaskId,
creation.ParticipantId,
creation.Language,
creation.Penalty,
creation.Solution,
)
if err != nil {
return 0, handlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, handlePgErr(err, op)
}
return id, nil
}
func (r *ContestRepository) ListSolutions(ctx context.Context, filter models.SolutionsFilter) (*models.SolutionsList, error) {
const op = "ContestRepository.ListSolutions"
baseQuery := `
SELECT s.id,
s.participant_id,
p2.name as participant_name,
s.state,
s.score,
s.penalty,
s.time_stat,
s.memory_stat,
s.language,
s.task_id,
t.position as task_position,
p.title as task_title,
t.contest_id,
c.title,
s.updated_at,
s.created_at
FROM solutions s
LEFT JOIN tasks t ON s.task_id = t.id
LEFT JOIN problems p ON t.problem_id = p.id
LEFT JOIN contests c ON t.contest_id = c.id
LEFT JOIN participants p2 on s.participant_id = p2.id
WHERE 1=1
`
var conditions []string
var args []interface{}
if filter.ContestId != nil {
conditions = append(conditions, "s.contest_id = ?")
args = append(args, *filter.ContestId)
}
if filter.ParticipantId != nil {
conditions = append(conditions, "s.participant_id = ?")
args = append(args, *filter.ParticipantId)
}
if filter.TaskId != nil {
conditions = append(conditions, "s.task_id = ?")
args = append(args, *filter.TaskId)
}
if filter.Language != nil {
conditions = append(conditions, "s.language = ?")
args = append(args, *filter.Language)
}
if filter.State != nil {
conditions = append(conditions, "s.state = ?")
args = append(args, *filter.State)
}
if len(conditions) > 0 {
baseQuery += " AND " + strings.Join(conditions, " AND ")
}
if filter.Order != nil {
orderDirection := "ASC"
if *filter.Order < 0 {
orderDirection = "DESC"
}
baseQuery += fmt.Sprintf(" ORDER BY s.id %s", orderDirection)
}
countQuery := "SELECT COUNT(*) FROM (" + baseQuery + ") as count_table"
var totalCount int32
err := r.db.QueryRowxContext(ctx, r.db.Rebind(countQuery), args...).Scan(&totalCount)
if err != nil {
return nil, handlePgErr(err, op)
}
offset := (filter.Page - 1) * filter.PageSize
baseQuery += " LIMIT ? OFFSET ?"
args = append(args, filter.PageSize, offset)
rows, err := r.db.QueryxContext(ctx, r.db.Rebind(baseQuery), args...)
if err != nil {
return nil, handlePgErr(err, op)
}
defer rows.Close()
var solutions []*models.SolutionsListItem
for rows.Next() {
var solution models.SolutionsListItem
err = rows.StructScan(&solution)
if err != nil {
return nil, handlePgErr(err, op)
}
solutions = append(solutions, &solution)
}
if err = rows.Err(); err != nil {
return nil, handlePgErr(err, op)
}
return &models.SolutionsList{
Solutions: solutions,
Pagination: models.Pagination{
Total: models.Total(totalCount, filter.PageSize),
Page: filter.Page,
},
}, nil
}
const (
readTaskQuery = `
SELECT
t.id,
t.position,
p.title,
p.time_limit,
p.memory_limit,
t.problem_id,
t.contest_id,
p.legend_html,
p.input_format_html,
p.output_format_html,
p.notes_html,
p.scoring_html,
t.created_at,
t.updated_at
FROM tasks t
LEFT JOIN problems p ON t.problem_id = p.id
WHERE t.id = ?
`
)
func (r *ContestRepository) ReadTask(ctx context.Context, id int32) (*models.Task, error) {
const op = "ContestRepository.ReadTask"
query := r.db.Rebind(readTaskQuery)
var task models.Task
err := r.db.GetContext(ctx, &task, query, id)
if err != nil {
return nil, handlePgErr(err, op)
}
return &task, nil
}
const (
// state=5 - AC
readStatisticsQuery = `
SELECT t.id as task_id,
t.position,
COUNT(*) as total,
COUNT(CASE WHEN s.state = 5 THEN 1 END) as success
FROM tasks t
LEFT JOIN solutions s ON t.id = s.task_id
WHERE t.contest_id = ?
GROUP BY t.id, t.position
ORDER BY t.position;
`
solutionsQuery = `
WITH RankedSolutions AS (
SELECT
s.id,
s.participant_id,
p2.name as participant_name,
s.state,
s.score,
s.penalty,
s.time_stat,
s.memory_stat,
s.language,
s.task_id,
t.position as task_position,
p.title as task_title,
t.contest_id,
c.title as contest_title,
s.updated_at,
s.created_at,
ROW_NUMBER() OVER (
PARTITION BY s.participant_id, s.task_id
ORDER BY
CASE WHEN s.state = 5 THEN 0 ELSE 1 END,
s.created_at
) as rn
FROM solutions s
LEFT JOIN tasks t ON s.task_id = t.id
LEFT JOIN problems p ON t.problem_id = p.id
LEFT JOIN contests c ON t.contest_id = c.id
LEFT JOIN participants p2 on s.participant_id = p2.id
WHERE t.contest_id = ?
)
SELECT
rs.id,
rs.participant_id,
rs.participant_name,
rs.state,
rs.score,
rs.penalty,
rs.time_stat,
rs.memory_stat,
rs.language,
rs.task_id,
rs.task_position,
rs.task_title,
rs.contest_id,
rs.contest_title,
rs.updated_at,
rs.created_at
FROM RankedSolutions rs
WHERE rs.rn = 1;
`
participantsQuery = `
WITH Attempts AS (
SELECT
s.participant_id,
s.task_id,
COUNT(*) FILTER (WHERE s.state != 5 AND s.created_at < (
SELECT MIN(s2.created_at)
FROM solutions s2
WHERE s2.participant_id = s.participant_id
AND s2.task_id = s.task_id
AND s2.state = 5
)) as failed_attempts,
MIN(CASE WHEN s.state = 5 THEN s.penalty END) as success_penalty
FROM solutions s
JOIN tasks t ON t.id = s.task_id
WHERE t.contest_id = :contest_id
GROUP BY s.participant_id, s.task_id
)
SELECT
p.id,
p.name,
COUNT(DISTINCT CASE WHEN a.success_penalty IS NOT NULL THEN a.task_id END) as solved_in_total,
COALESCE(SUM(CASE WHEN a.success_penalty IS NOT NULL
THEN a.failed_attempts * :penalty + a.success_penalty
ELSE 0 END), 0) as penalty_in_total
FROM participants p
LEFT JOIN Attempts a ON a.participant_id = p.id
WHERE p.contest_id = :contest_id
GROUP BY p.id, p.name
`
)
func (r *ContestRepository) ReadMonitor(ctx context.Context, contestId int32) (*models.Monitor, error) {
const op = "ContestRepository.ReadMonitor"
query := r.db.Rebind(readStatisticsQuery)
rows, err := r.db.QueryxContext(ctx, query, contestId)
if err != nil {
return nil, handlePgErr(err, op)
}
defer rows.Close()
var monitor models.Monitor
for rows.Next() {
var stat models.ProblemStatSummary
err = rows.StructScan(&stat)
if err != nil {
return nil, handlePgErr(err, op)
}
monitor.Summary = append(monitor.Summary, &stat)
}
var solutions []*models.SolutionsListItem
err = r.db.SelectContext(ctx, &solutions, r.db.Rebind(solutionsQuery), contestId)
if err != nil {
return nil, handlePgErr(err, op)
}
penalty := int32(20) // FIXME
namedQuery := r.db.Rebind(participantsQuery)
rows3, err := r.db.NamedQueryContext(ctx, namedQuery, map[string]interface{}{
"contest_id": contestId,
"penalty": penalty,
})
if err != nil {
return nil, handlePgErr(err, op)
}
defer rows3.Close()
solutionsMap := make(map[int32][]*models.SolutionsListItem)
for _, solution := range solutions {
solutionsMap[solution.ParticipantId] = append(solutionsMap[solution.ParticipantId], solution)
}
for rows3.Next() {
var stat models.ParticipantsStat
err = rows3.StructScan(&stat)
if err != nil {
return nil, handlePgErr(err, op)
}
if sols, ok := solutionsMap[stat.Id]; ok {
stat.Solutions = sols
}
monitor.Participants = append(monitor.Participants, &stat)
}
return &monitor, nil
}