@@ -35,6 +35,8 @@ type Store interface {
3535
3636 ListAllRuns (ctx context.Context ) ([]Run , error )
3737
38+ UpsertSuite (ctx context.Context , suite * Suite ) error
39+
3840 BulkInsertTestStatsBlockLogs (
3941 ctx context.Context , logs []* TestStatsBlockLog ,
4042 ) error
@@ -47,6 +49,9 @@ type Store interface {
4749 QueryTestStatsBlockLogs (
4850 ctx context.Context , params * QueryParams ,
4951 ) (* QueryResult , error )
52+ QuerySuites (
53+ ctx context.Context , params * QueryParams ,
54+ ) (* QueryResult , error )
5055}
5156
5257// Compile-time interface check.
@@ -122,6 +127,7 @@ func (s *store) Start(ctx context.Context) error {
122127 & Run {},
123128 & TestStat {},
124129 & TestStatsBlockLog {},
130+ & Suite {},
125131 ); err != nil {
126132 return fmt .Errorf ("running index migrations: %w" , err )
127133 }
@@ -338,6 +344,19 @@ func (s *store) DeleteTestStatsBlockLogsForRun(
338344 return nil
339345}
340346
347+ // UpsertSuite inserts or updates a suite record keyed by suite_hash.
348+ func (s * store ) UpsertSuite (ctx context.Context , suite * Suite ) error {
349+ result := s .db .WithContext (ctx ).
350+ Where ("suite_hash = ?" , suite .SuiteHash ).
351+ Assign (suite ).
352+ FirstOrCreate (suite )
353+ if result .Error != nil {
354+ return fmt .Errorf ("upserting suite: %w" , result .Error )
355+ }
356+
357+ return nil
358+ }
359+
341360// QueryRuns executes a flexible query against the runs table using the
342361// validated QueryParams. It returns paginated results with a total count.
343362func (s * store ) QueryRuns (
@@ -462,6 +481,45 @@ func (s *store) QueryTestStatsBlockLogs(
462481 }, nil
463482}
464483
484+ // QuerySuites executes a flexible query against the suites table using
485+ // the validated QueryParams. It returns paginated results with a total
486+ // count.
487+ func (s * store ) QuerySuites (
488+ ctx context.Context , params * QueryParams ,
489+ ) (* QueryResult , error ) {
490+ q := applyQuery (s .db .WithContext (ctx ), & Suite {}, params )
491+
492+ // When select is specified, scan into maps so the JSON response
493+ // only contains the requested columns (no zero-valued extras).
494+ if len (params .Select ) > 0 {
495+ return scanMaps (q , params )
496+ }
497+
498+ var total int64
499+ if err := q .Count (& total ).Error ; err != nil {
500+ return nil , fmt .Errorf ("counting suites: %w" , err )
501+ }
502+
503+ var suites []Suite
504+ if err := q .Offset (params .Offset ).
505+ Limit (params .Limit ).
506+ Find (& suites ).Error ; err != nil {
507+ return nil , fmt .Errorf ("querying suites: %w" , err )
508+ }
509+
510+ data := make ([]SuiteResponse , 0 , len (suites ))
511+ for i := range suites {
512+ data = append (data , toSuiteResponse (& suites [i ]))
513+ }
514+
515+ return & QueryResult {
516+ Data : data ,
517+ Total : total ,
518+ Limit : params .Limit ,
519+ Offset : params .Offset ,
520+ }, nil
521+ }
522+
465523// scanMaps scans query results into []map[string]any so only the selected
466524// columns appear in the JSON response.
467525func scanMaps (
0 commit comments