Files
cmr-mini/backend/internal/store/postgres/result_store.go

380 lines
9.2 KiB
Go

package postgres
import (
"context"
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5"
)
type SessionResult struct {
ID string
SessionID string
ResultStatus string
SummaryJSON string
FinalDurationSec *int
FinalScore *int
CompletedControls *int
TotalControls *int
DistanceMeters *float64
AverageSpeedKmh *float64
MaxHeartRateBpm *int
}
type UpsertSessionResultParams struct {
SessionID string
ResultStatus string
Summary map[string]any
FinalDurationSec *int
FinalScore *int
CompletedControls *int
TotalControls *int
DistanceMeters *float64
AverageSpeedKmh *float64
MaxHeartRateBpm *int
}
type SessionResultRecord struct {
Session
Result *SessionResult
}
func (s *Store) UpsertSessionResult(ctx context.Context, tx Tx, params UpsertSessionResultParams) (*SessionResult, error) {
summaryJSON, err := json.Marshal(params.Summary)
if err != nil {
return nil, fmt.Errorf("marshal session summary: %w", err)
}
row := tx.QueryRow(ctx, `
INSERT INTO session_results (
session_id,
result_status,
summary_jsonb,
final_duration_sec,
final_score,
completed_controls,
total_controls,
distance_meters,
average_speed_kmh,
max_heart_rate_bpm
)
VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (session_id) DO UPDATE SET
result_status = EXCLUDED.result_status,
summary_jsonb = EXCLUDED.summary_jsonb,
final_duration_sec = EXCLUDED.final_duration_sec,
final_score = EXCLUDED.final_score,
completed_controls = EXCLUDED.completed_controls,
total_controls = EXCLUDED.total_controls,
distance_meters = EXCLUDED.distance_meters,
average_speed_kmh = EXCLUDED.average_speed_kmh,
max_heart_rate_bpm = EXCLUDED.max_heart_rate_bpm
RETURNING
id,
session_id,
result_status,
summary_jsonb::text,
final_duration_sec,
final_score,
completed_controls,
total_controls,
distance_meters::float8,
average_speed_kmh::float8,
max_heart_rate_bpm
`, params.SessionID, params.ResultStatus, string(summaryJSON), params.FinalDurationSec, params.FinalScore, params.CompletedControls, params.TotalControls, params.DistanceMeters, params.AverageSpeedKmh, params.MaxHeartRateBpm)
return scanSessionResult(row)
}
func (s *Store) GetSessionResultByPublicID(ctx context.Context, sessionPublicID string) (*SessionResultRecord, error) {
row := s.pool.QueryRow(ctx, `
SELECT
gs.id,
gs.session_public_id,
gs.user_id,
gs.event_id,
gs.event_release_id,
er.release_public_id,
er.config_label,
er.manifest_url,
er.manifest_checksum_sha256,
gs.device_key,
gs.client_type,
gs.assignment_mode,
gs.variant_id,
gs.variant_name,
gs.route_code,
gs.status,
gs.session_token_hash,
gs.session_token_expires_at,
gs.launched_at,
gs.started_at,
gs.ended_at,
e.event_public_id,
e.display_name,
sr.id,
sr.session_id,
sr.result_status,
sr.summary_jsonb::text,
sr.final_duration_sec,
sr.final_score,
sr.completed_controls,
sr.total_controls,
sr.distance_meters::float8,
sr.average_speed_kmh::float8,
sr.max_heart_rate_bpm
FROM game_sessions gs
JOIN events e ON e.id = gs.event_id
JOIN event_releases er ON er.id = gs.event_release_id
LEFT JOIN session_results sr ON sr.session_id = gs.id
WHERE gs.session_public_id = $1
LIMIT 1
`, sessionPublicID)
return scanSessionResultRecord(row)
}
func (s *Store) ListSessionResultsByUserID(ctx context.Context, userID string, limit int) ([]SessionResultRecord, error) {
if limit <= 0 || limit > 100 {
limit = 20
}
rows, err := s.pool.Query(ctx, `
SELECT
gs.id,
gs.session_public_id,
gs.user_id,
gs.event_id,
gs.event_release_id,
er.release_public_id,
er.config_label,
er.manifest_url,
er.manifest_checksum_sha256,
gs.device_key,
gs.client_type,
gs.assignment_mode,
gs.variant_id,
gs.variant_name,
gs.route_code,
gs.status,
gs.session_token_hash,
gs.session_token_expires_at,
gs.launched_at,
gs.started_at,
gs.ended_at,
e.event_public_id,
e.display_name,
sr.id,
sr.session_id,
sr.result_status,
sr.summary_jsonb::text,
sr.final_duration_sec,
sr.final_score,
sr.completed_controls,
sr.total_controls,
sr.distance_meters::float8,
sr.average_speed_kmh::float8,
sr.max_heart_rate_bpm
FROM game_sessions gs
JOIN events e ON e.id = gs.event_id
JOIN event_releases er ON er.id = gs.event_release_id
LEFT JOIN session_results sr ON sr.session_id = gs.id
WHERE gs.user_id = $1
AND gs.status IN ('finished', 'failed', 'cancelled')
ORDER BY COALESCE(gs.ended_at, gs.updated_at, gs.created_at) DESC
LIMIT $2
`, userID, limit)
if err != nil {
return nil, fmt.Errorf("list session results by user id: %w", err)
}
defer rows.Close()
var items []SessionResultRecord
for rows.Next() {
item, err := scanSessionResultRecordFromRows(rows)
if err != nil {
return nil, err
}
items = append(items, *item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate session results by user id: %w", err)
}
return items, nil
}
func scanSessionResult(row pgx.Row) (*SessionResult, error) {
var result SessionResult
err := row.Scan(
&result.ID,
&result.SessionID,
&result.ResultStatus,
&result.SummaryJSON,
&result.FinalDurationSec,
&result.FinalScore,
&result.CompletedControls,
&result.TotalControls,
&result.DistanceMeters,
&result.AverageSpeedKmh,
&result.MaxHeartRateBpm,
)
if err != nil {
return nil, fmt.Errorf("scan session result: %w", err)
}
return &result, nil
}
func scanSessionResultRecord(row pgx.Row) (*SessionResultRecord, error) {
var record SessionResultRecord
var resultID *string
var resultSessionID *string
var resultStatus *string
var resultSummaryJSON *string
var finalDurationSec *int
var finalScore *int
var completedControls *int
var totalControls *int
var distanceMeters *float64
var averageSpeedKmh *float64
var maxHeartRateBpm *int
err := row.Scan(
&record.ID,
&record.SessionPublicID,
&record.UserID,
&record.EventID,
&record.EventReleaseID,
&record.ReleasePublicID,
&record.ConfigLabel,
&record.ManifestURL,
&record.ManifestChecksum,
&record.DeviceKey,
&record.ClientType,
&record.AssignmentMode,
&record.VariantID,
&record.VariantName,
&record.RouteCode,
&record.Status,
&record.SessionTokenHash,
&record.SessionTokenExpiresAt,
&record.LaunchedAt,
&record.StartedAt,
&record.EndedAt,
&record.EventPublicID,
&record.EventDisplayName,
&resultID,
&resultSessionID,
&resultStatus,
&resultSummaryJSON,
&finalDurationSec,
&finalScore,
&completedControls,
&totalControls,
&distanceMeters,
&averageSpeedKmh,
&maxHeartRateBpm,
)
if err != nil {
if err == pgx.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("scan session result record: %w", err)
}
if resultID != nil {
record.Result = &SessionResult{
ID: *resultID,
SessionID: derefString(resultSessionID),
ResultStatus: derefString(resultStatus),
SummaryJSON: derefString(resultSummaryJSON),
FinalDurationSec: finalDurationSec,
FinalScore: finalScore,
CompletedControls: completedControls,
TotalControls: totalControls,
DistanceMeters: distanceMeters,
AverageSpeedKmh: averageSpeedKmh,
MaxHeartRateBpm: maxHeartRateBpm,
}
}
return &record, nil
}
func scanSessionResultRecordFromRows(rows pgx.Rows) (*SessionResultRecord, error) {
var record SessionResultRecord
var resultID *string
var resultSessionID *string
var resultStatus *string
var resultSummaryJSON *string
var finalDurationSec *int
var finalScore *int
var completedControls *int
var totalControls *int
var distanceMeters *float64
var averageSpeedKmh *float64
var maxHeartRateBpm *int
err := rows.Scan(
&record.ID,
&record.SessionPublicID,
&record.UserID,
&record.EventID,
&record.EventReleaseID,
&record.ReleasePublicID,
&record.ConfigLabel,
&record.ManifestURL,
&record.ManifestChecksum,
&record.DeviceKey,
&record.ClientType,
&record.AssignmentMode,
&record.VariantID,
&record.VariantName,
&record.RouteCode,
&record.Status,
&record.SessionTokenHash,
&record.SessionTokenExpiresAt,
&record.LaunchedAt,
&record.StartedAt,
&record.EndedAt,
&record.EventPublicID,
&record.EventDisplayName,
&resultID,
&resultSessionID,
&resultStatus,
&resultSummaryJSON,
&finalDurationSec,
&finalScore,
&completedControls,
&totalControls,
&distanceMeters,
&averageSpeedKmh,
&maxHeartRateBpm,
)
if err != nil {
return nil, fmt.Errorf("scan session result row: %w", err)
}
if resultID != nil {
record.Result = &SessionResult{
ID: *resultID,
SessionID: derefString(resultSessionID),
ResultStatus: derefString(resultStatus),
SummaryJSON: derefString(resultSummaryJSON),
FinalDurationSec: finalDurationSec,
FinalScore: finalScore,
CompletedControls: completedControls,
TotalControls: totalControls,
DistanceMeters: distanceMeters,
AverageSpeedKmh: averageSpeedKmh,
MaxHeartRateBpm: maxHeartRateBpm,
}
}
return &record, nil
}
func derefString(value *string) string {
if value == nil {
return ""
}
return *value
}