result_store.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. package postgres
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "github.com/jackc/pgx/v5"
  7. )
  8. type SessionResult struct {
  9. ID string
  10. SessionID string
  11. ResultStatus string
  12. SummaryJSON string
  13. FinalDurationSec *int
  14. FinalScore *int
  15. CompletedControls *int
  16. TotalControls *int
  17. DistanceMeters *float64
  18. AverageSpeedKmh *float64
  19. MaxHeartRateBpm *int
  20. }
  21. type UpsertSessionResultParams struct {
  22. SessionID string
  23. ResultStatus string
  24. Summary map[string]any
  25. FinalDurationSec *int
  26. FinalScore *int
  27. CompletedControls *int
  28. TotalControls *int
  29. DistanceMeters *float64
  30. AverageSpeedKmh *float64
  31. MaxHeartRateBpm *int
  32. }
  33. type SessionResultRecord struct {
  34. Session
  35. Result *SessionResult
  36. }
  37. func (s *Store) UpsertSessionResult(ctx context.Context, tx Tx, params UpsertSessionResultParams) (*SessionResult, error) {
  38. summaryJSON, err := json.Marshal(params.Summary)
  39. if err != nil {
  40. return nil, fmt.Errorf("marshal session summary: %w", err)
  41. }
  42. row := tx.QueryRow(ctx, `
  43. INSERT INTO session_results (
  44. session_id,
  45. result_status,
  46. summary_jsonb,
  47. final_duration_sec,
  48. final_score,
  49. completed_controls,
  50. total_controls,
  51. distance_meters,
  52. average_speed_kmh,
  53. max_heart_rate_bpm
  54. )
  55. VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7, $8, $9, $10)
  56. ON CONFLICT (session_id) DO UPDATE SET
  57. result_status = EXCLUDED.result_status,
  58. summary_jsonb = EXCLUDED.summary_jsonb,
  59. final_duration_sec = EXCLUDED.final_duration_sec,
  60. final_score = EXCLUDED.final_score,
  61. completed_controls = EXCLUDED.completed_controls,
  62. total_controls = EXCLUDED.total_controls,
  63. distance_meters = EXCLUDED.distance_meters,
  64. average_speed_kmh = EXCLUDED.average_speed_kmh,
  65. max_heart_rate_bpm = EXCLUDED.max_heart_rate_bpm
  66. RETURNING
  67. id,
  68. session_id,
  69. result_status,
  70. summary_jsonb::text,
  71. final_duration_sec,
  72. final_score,
  73. completed_controls,
  74. total_controls,
  75. distance_meters::float8,
  76. average_speed_kmh::float8,
  77. max_heart_rate_bpm
  78. `, params.SessionID, params.ResultStatus, string(summaryJSON), params.FinalDurationSec, params.FinalScore, params.CompletedControls, params.TotalControls, params.DistanceMeters, params.AverageSpeedKmh, params.MaxHeartRateBpm)
  79. return scanSessionResult(row)
  80. }
  81. func (s *Store) GetSessionResultByPublicID(ctx context.Context, sessionPublicID string) (*SessionResultRecord, error) {
  82. row := s.pool.QueryRow(ctx, `
  83. SELECT
  84. gs.id,
  85. gs.session_public_id,
  86. gs.user_id,
  87. gs.event_id,
  88. gs.event_release_id,
  89. er.release_public_id,
  90. er.config_label,
  91. er.manifest_url,
  92. er.manifest_checksum_sha256,
  93. gs.device_key,
  94. gs.client_type,
  95. gs.assignment_mode,
  96. gs.variant_id,
  97. gs.variant_name,
  98. gs.route_code,
  99. gs.status,
  100. gs.session_token_hash,
  101. gs.session_token_expires_at,
  102. gs.launched_at,
  103. gs.started_at,
  104. gs.ended_at,
  105. e.event_public_id,
  106. e.display_name,
  107. sr.id,
  108. sr.session_id,
  109. sr.result_status,
  110. sr.summary_jsonb::text,
  111. sr.final_duration_sec,
  112. sr.final_score,
  113. sr.completed_controls,
  114. sr.total_controls,
  115. sr.distance_meters::float8,
  116. sr.average_speed_kmh::float8,
  117. sr.max_heart_rate_bpm
  118. FROM game_sessions gs
  119. JOIN events e ON e.id = gs.event_id
  120. JOIN event_releases er ON er.id = gs.event_release_id
  121. LEFT JOIN session_results sr ON sr.session_id = gs.id
  122. WHERE gs.session_public_id = $1
  123. LIMIT 1
  124. `, sessionPublicID)
  125. return scanSessionResultRecord(row)
  126. }
  127. func (s *Store) ListSessionResultsByUserID(ctx context.Context, userID string, limit int) ([]SessionResultRecord, error) {
  128. if limit <= 0 || limit > 100 {
  129. limit = 20
  130. }
  131. rows, err := s.pool.Query(ctx, `
  132. SELECT
  133. gs.id,
  134. gs.session_public_id,
  135. gs.user_id,
  136. gs.event_id,
  137. gs.event_release_id,
  138. er.release_public_id,
  139. er.config_label,
  140. er.manifest_url,
  141. er.manifest_checksum_sha256,
  142. gs.device_key,
  143. gs.client_type,
  144. gs.assignment_mode,
  145. gs.variant_id,
  146. gs.variant_name,
  147. gs.route_code,
  148. gs.status,
  149. gs.session_token_hash,
  150. gs.session_token_expires_at,
  151. gs.launched_at,
  152. gs.started_at,
  153. gs.ended_at,
  154. e.event_public_id,
  155. e.display_name,
  156. sr.id,
  157. sr.session_id,
  158. sr.result_status,
  159. sr.summary_jsonb::text,
  160. sr.final_duration_sec,
  161. sr.final_score,
  162. sr.completed_controls,
  163. sr.total_controls,
  164. sr.distance_meters::float8,
  165. sr.average_speed_kmh::float8,
  166. sr.max_heart_rate_bpm
  167. FROM game_sessions gs
  168. JOIN events e ON e.id = gs.event_id
  169. JOIN event_releases er ON er.id = gs.event_release_id
  170. LEFT JOIN session_results sr ON sr.session_id = gs.id
  171. WHERE gs.user_id = $1
  172. AND gs.status IN ('finished', 'failed', 'cancelled')
  173. ORDER BY COALESCE(gs.ended_at, gs.updated_at, gs.created_at) DESC
  174. LIMIT $2
  175. `, userID, limit)
  176. if err != nil {
  177. return nil, fmt.Errorf("list session results by user id: %w", err)
  178. }
  179. defer rows.Close()
  180. var items []SessionResultRecord
  181. for rows.Next() {
  182. item, err := scanSessionResultRecordFromRows(rows)
  183. if err != nil {
  184. return nil, err
  185. }
  186. items = append(items, *item)
  187. }
  188. if err := rows.Err(); err != nil {
  189. return nil, fmt.Errorf("iterate session results by user id: %w", err)
  190. }
  191. return items, nil
  192. }
  193. func scanSessionResult(row pgx.Row) (*SessionResult, error) {
  194. var result SessionResult
  195. err := row.Scan(
  196. &result.ID,
  197. &result.SessionID,
  198. &result.ResultStatus,
  199. &result.SummaryJSON,
  200. &result.FinalDurationSec,
  201. &result.FinalScore,
  202. &result.CompletedControls,
  203. &result.TotalControls,
  204. &result.DistanceMeters,
  205. &result.AverageSpeedKmh,
  206. &result.MaxHeartRateBpm,
  207. )
  208. if err != nil {
  209. return nil, fmt.Errorf("scan session result: %w", err)
  210. }
  211. return &result, nil
  212. }
  213. func scanSessionResultRecord(row pgx.Row) (*SessionResultRecord, error) {
  214. var record SessionResultRecord
  215. var resultID *string
  216. var resultSessionID *string
  217. var resultStatus *string
  218. var resultSummaryJSON *string
  219. var finalDurationSec *int
  220. var finalScore *int
  221. var completedControls *int
  222. var totalControls *int
  223. var distanceMeters *float64
  224. var averageSpeedKmh *float64
  225. var maxHeartRateBpm *int
  226. err := row.Scan(
  227. &record.ID,
  228. &record.SessionPublicID,
  229. &record.UserID,
  230. &record.EventID,
  231. &record.EventReleaseID,
  232. &record.ReleasePublicID,
  233. &record.ConfigLabel,
  234. &record.ManifestURL,
  235. &record.ManifestChecksum,
  236. &record.DeviceKey,
  237. &record.ClientType,
  238. &record.AssignmentMode,
  239. &record.VariantID,
  240. &record.VariantName,
  241. &record.RouteCode,
  242. &record.Status,
  243. &record.SessionTokenHash,
  244. &record.SessionTokenExpiresAt,
  245. &record.LaunchedAt,
  246. &record.StartedAt,
  247. &record.EndedAt,
  248. &record.EventPublicID,
  249. &record.EventDisplayName,
  250. &resultID,
  251. &resultSessionID,
  252. &resultStatus,
  253. &resultSummaryJSON,
  254. &finalDurationSec,
  255. &finalScore,
  256. &completedControls,
  257. &totalControls,
  258. &distanceMeters,
  259. &averageSpeedKmh,
  260. &maxHeartRateBpm,
  261. )
  262. if err != nil {
  263. if err == pgx.ErrNoRows {
  264. return nil, nil
  265. }
  266. return nil, fmt.Errorf("scan session result record: %w", err)
  267. }
  268. if resultID != nil {
  269. record.Result = &SessionResult{
  270. ID: *resultID,
  271. SessionID: derefString(resultSessionID),
  272. ResultStatus: derefString(resultStatus),
  273. SummaryJSON: derefString(resultSummaryJSON),
  274. FinalDurationSec: finalDurationSec,
  275. FinalScore: finalScore,
  276. CompletedControls: completedControls,
  277. TotalControls: totalControls,
  278. DistanceMeters: distanceMeters,
  279. AverageSpeedKmh: averageSpeedKmh,
  280. MaxHeartRateBpm: maxHeartRateBpm,
  281. }
  282. }
  283. return &record, nil
  284. }
  285. func scanSessionResultRecordFromRows(rows pgx.Rows) (*SessionResultRecord, error) {
  286. var record SessionResultRecord
  287. var resultID *string
  288. var resultSessionID *string
  289. var resultStatus *string
  290. var resultSummaryJSON *string
  291. var finalDurationSec *int
  292. var finalScore *int
  293. var completedControls *int
  294. var totalControls *int
  295. var distanceMeters *float64
  296. var averageSpeedKmh *float64
  297. var maxHeartRateBpm *int
  298. err := rows.Scan(
  299. &record.ID,
  300. &record.SessionPublicID,
  301. &record.UserID,
  302. &record.EventID,
  303. &record.EventReleaseID,
  304. &record.ReleasePublicID,
  305. &record.ConfigLabel,
  306. &record.ManifestURL,
  307. &record.ManifestChecksum,
  308. &record.DeviceKey,
  309. &record.ClientType,
  310. &record.AssignmentMode,
  311. &record.VariantID,
  312. &record.VariantName,
  313. &record.RouteCode,
  314. &record.Status,
  315. &record.SessionTokenHash,
  316. &record.SessionTokenExpiresAt,
  317. &record.LaunchedAt,
  318. &record.StartedAt,
  319. &record.EndedAt,
  320. &record.EventPublicID,
  321. &record.EventDisplayName,
  322. &resultID,
  323. &resultSessionID,
  324. &resultStatus,
  325. &resultSummaryJSON,
  326. &finalDurationSec,
  327. &finalScore,
  328. &completedControls,
  329. &totalControls,
  330. &distanceMeters,
  331. &averageSpeedKmh,
  332. &maxHeartRateBpm,
  333. )
  334. if err != nil {
  335. return nil, fmt.Errorf("scan session result row: %w", err)
  336. }
  337. if resultID != nil {
  338. record.Result = &SessionResult{
  339. ID: *resultID,
  340. SessionID: derefString(resultSessionID),
  341. ResultStatus: derefString(resultStatus),
  342. SummaryJSON: derefString(resultSummaryJSON),
  343. FinalDurationSec: finalDurationSec,
  344. FinalScore: finalScore,
  345. CompletedControls: completedControls,
  346. TotalControls: totalControls,
  347. DistanceMeters: distanceMeters,
  348. AverageSpeedKmh: averageSpeedKmh,
  349. MaxHeartRateBpm: maxHeartRateBpm,
  350. }
  351. }
  352. return &record, nil
  353. }
  354. func derefString(value *string) string {
  355. if value == nil {
  356. return ""
  357. }
  358. return *value
  359. }