result_store.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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.route_code,
  96. gs.status,
  97. gs.session_token_hash,
  98. gs.session_token_expires_at,
  99. gs.launched_at,
  100. gs.started_at,
  101. gs.ended_at,
  102. e.event_public_id,
  103. e.display_name,
  104. sr.id,
  105. sr.session_id,
  106. sr.result_status,
  107. sr.summary_jsonb::text,
  108. sr.final_duration_sec,
  109. sr.final_score,
  110. sr.completed_controls,
  111. sr.total_controls,
  112. sr.distance_meters::float8,
  113. sr.average_speed_kmh::float8,
  114. sr.max_heart_rate_bpm
  115. FROM game_sessions gs
  116. JOIN events e ON e.id = gs.event_id
  117. JOIN event_releases er ON er.id = gs.event_release_id
  118. LEFT JOIN session_results sr ON sr.session_id = gs.id
  119. WHERE gs.session_public_id = $1
  120. LIMIT 1
  121. `, sessionPublicID)
  122. return scanSessionResultRecord(row)
  123. }
  124. func (s *Store) ListSessionResultsByUserID(ctx context.Context, userID string, limit int) ([]SessionResultRecord, error) {
  125. if limit <= 0 || limit > 100 {
  126. limit = 20
  127. }
  128. rows, err := s.pool.Query(ctx, `
  129. SELECT
  130. gs.id,
  131. gs.session_public_id,
  132. gs.user_id,
  133. gs.event_id,
  134. gs.event_release_id,
  135. er.release_public_id,
  136. er.config_label,
  137. er.manifest_url,
  138. er.manifest_checksum_sha256,
  139. gs.device_key,
  140. gs.client_type,
  141. gs.route_code,
  142. gs.status,
  143. gs.session_token_hash,
  144. gs.session_token_expires_at,
  145. gs.launched_at,
  146. gs.started_at,
  147. gs.ended_at,
  148. e.event_public_id,
  149. e.display_name,
  150. sr.id,
  151. sr.session_id,
  152. sr.result_status,
  153. sr.summary_jsonb::text,
  154. sr.final_duration_sec,
  155. sr.final_score,
  156. sr.completed_controls,
  157. sr.total_controls,
  158. sr.distance_meters::float8,
  159. sr.average_speed_kmh::float8,
  160. sr.max_heart_rate_bpm
  161. FROM game_sessions gs
  162. JOIN events e ON e.id = gs.event_id
  163. JOIN event_releases er ON er.id = gs.event_release_id
  164. LEFT JOIN session_results sr ON sr.session_id = gs.id
  165. WHERE gs.user_id = $1
  166. AND gs.status IN ('finished', 'failed', 'cancelled')
  167. ORDER BY COALESCE(gs.ended_at, gs.updated_at, gs.created_at) DESC
  168. LIMIT $2
  169. `, userID, limit)
  170. if err != nil {
  171. return nil, fmt.Errorf("list session results by user id: %w", err)
  172. }
  173. defer rows.Close()
  174. var items []SessionResultRecord
  175. for rows.Next() {
  176. item, err := scanSessionResultRecordFromRows(rows)
  177. if err != nil {
  178. return nil, err
  179. }
  180. items = append(items, *item)
  181. }
  182. if err := rows.Err(); err != nil {
  183. return nil, fmt.Errorf("iterate session results by user id: %w", err)
  184. }
  185. return items, nil
  186. }
  187. func scanSessionResult(row pgx.Row) (*SessionResult, error) {
  188. var result SessionResult
  189. err := row.Scan(
  190. &result.ID,
  191. &result.SessionID,
  192. &result.ResultStatus,
  193. &result.SummaryJSON,
  194. &result.FinalDurationSec,
  195. &result.FinalScore,
  196. &result.CompletedControls,
  197. &result.TotalControls,
  198. &result.DistanceMeters,
  199. &result.AverageSpeedKmh,
  200. &result.MaxHeartRateBpm,
  201. )
  202. if err != nil {
  203. return nil, fmt.Errorf("scan session result: %w", err)
  204. }
  205. return &result, nil
  206. }
  207. func scanSessionResultRecord(row pgx.Row) (*SessionResultRecord, error) {
  208. var record SessionResultRecord
  209. var resultID *string
  210. var resultSessionID *string
  211. var resultStatus *string
  212. var resultSummaryJSON *string
  213. var finalDurationSec *int
  214. var finalScore *int
  215. var completedControls *int
  216. var totalControls *int
  217. var distanceMeters *float64
  218. var averageSpeedKmh *float64
  219. var maxHeartRateBpm *int
  220. err := row.Scan(
  221. &record.ID,
  222. &record.SessionPublicID,
  223. &record.UserID,
  224. &record.EventID,
  225. &record.EventReleaseID,
  226. &record.ReleasePublicID,
  227. &record.ConfigLabel,
  228. &record.ManifestURL,
  229. &record.ManifestChecksum,
  230. &record.DeviceKey,
  231. &record.ClientType,
  232. &record.RouteCode,
  233. &record.Status,
  234. &record.SessionTokenHash,
  235. &record.SessionTokenExpiresAt,
  236. &record.LaunchedAt,
  237. &record.StartedAt,
  238. &record.EndedAt,
  239. &record.EventPublicID,
  240. &record.EventDisplayName,
  241. &resultID,
  242. &resultSessionID,
  243. &resultStatus,
  244. &resultSummaryJSON,
  245. &finalDurationSec,
  246. &finalScore,
  247. &completedControls,
  248. &totalControls,
  249. &distanceMeters,
  250. &averageSpeedKmh,
  251. &maxHeartRateBpm,
  252. )
  253. if err != nil {
  254. if err == pgx.ErrNoRows {
  255. return nil, nil
  256. }
  257. return nil, fmt.Errorf("scan session result record: %w", err)
  258. }
  259. if resultID != nil {
  260. record.Result = &SessionResult{
  261. ID: *resultID,
  262. SessionID: derefString(resultSessionID),
  263. ResultStatus: derefString(resultStatus),
  264. SummaryJSON: derefString(resultSummaryJSON),
  265. FinalDurationSec: finalDurationSec,
  266. FinalScore: finalScore,
  267. CompletedControls: completedControls,
  268. TotalControls: totalControls,
  269. DistanceMeters: distanceMeters,
  270. AverageSpeedKmh: averageSpeedKmh,
  271. MaxHeartRateBpm: maxHeartRateBpm,
  272. }
  273. }
  274. return &record, nil
  275. }
  276. func scanSessionResultRecordFromRows(rows pgx.Rows) (*SessionResultRecord, error) {
  277. var record SessionResultRecord
  278. var resultID *string
  279. var resultSessionID *string
  280. var resultStatus *string
  281. var resultSummaryJSON *string
  282. var finalDurationSec *int
  283. var finalScore *int
  284. var completedControls *int
  285. var totalControls *int
  286. var distanceMeters *float64
  287. var averageSpeedKmh *float64
  288. var maxHeartRateBpm *int
  289. err := rows.Scan(
  290. &record.ID,
  291. &record.SessionPublicID,
  292. &record.UserID,
  293. &record.EventID,
  294. &record.EventReleaseID,
  295. &record.ReleasePublicID,
  296. &record.ConfigLabel,
  297. &record.ManifestURL,
  298. &record.ManifestChecksum,
  299. &record.DeviceKey,
  300. &record.ClientType,
  301. &record.RouteCode,
  302. &record.Status,
  303. &record.SessionTokenHash,
  304. &record.SessionTokenExpiresAt,
  305. &record.LaunchedAt,
  306. &record.StartedAt,
  307. &record.EndedAt,
  308. &record.EventPublicID,
  309. &record.EventDisplayName,
  310. &resultID,
  311. &resultSessionID,
  312. &resultStatus,
  313. &resultSummaryJSON,
  314. &finalDurationSec,
  315. &finalScore,
  316. &completedControls,
  317. &totalControls,
  318. &distanceMeters,
  319. &averageSpeedKmh,
  320. &maxHeartRateBpm,
  321. )
  322. if err != nil {
  323. return nil, fmt.Errorf("scan session result row: %w", err)
  324. }
  325. if resultID != nil {
  326. record.Result = &SessionResult{
  327. ID: *resultID,
  328. SessionID: derefString(resultSessionID),
  329. ResultStatus: derefString(resultStatus),
  330. SummaryJSON: derefString(resultSummaryJSON),
  331. FinalDurationSec: finalDurationSec,
  332. FinalScore: finalScore,
  333. CompletedControls: completedControls,
  334. TotalControls: totalControls,
  335. DistanceMeters: distanceMeters,
  336. AverageSpeedKmh: averageSpeedKmh,
  337. MaxHeartRateBpm: maxHeartRateBpm,
  338. }
  339. }
  340. return &record, nil
  341. }
  342. func derefString(value *string) string {
  343. if value == nil {
  344. return ""
  345. }
  346. return *value
  347. }