aboutsummaryrefslogtreecommitdiffstats
path: root/syz-cluster/pkg/db/finding_repo.go
blob: afcc559b1a00792dc6183e80ccc5ea139620f2b2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// Copyright 2025 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package db

import (
	"context"
	"errors"

	"cloud.google.com/go/spanner"
	"github.com/google/uuid"
)

type FindingRepository struct {
	client *spanner.Client
	*genericEntityOps[Finding, string]
}

func NewFindingRepository(client *spanner.Client) *FindingRepository {
	return &FindingRepository{
		client: client,
		genericEntityOps: &genericEntityOps[Finding, string]{
			client:   client,
			keyField: "ID",
			table:    "Findings",
		},
	}
}

type FindingID struct {
	SessionID string
	TestName  string
	Title     string
}

// Store queries the information about the session and the existing finding and then
// requests a new Finding object to replace the old one.
// If the callback returns nil, nothing it updated.
func (repo *FindingRepository) Store(ctx context.Context, id *FindingID,
	cb func(session *Session, old *Finding) (*Finding, error)) error {
	_, err := repo.client.ReadWriteTransaction(ctx,
		func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
			// Query the existing finding, if it exists.
			oldFinding, err := readEntity[Finding](ctx, txn, spanner.Statement{
				SQL: "SELECT * from `Findings` WHERE `SessionID`=@sessionID " +
					"AND `TestName` = @testName AND `Title`=@title",
				Params: map[string]any{
					"sessionID": id.SessionID,
					"testName":  id.TestName,
					"title":     id.Title,
				},
			})
			if err != nil {
				return err
			}
			// Query the Session object.
			session, err := readEntity[Session](ctx, txn, spanner.Statement{
				SQL:    "SELECT * FROM `Sessions` WHERE `ID`=@id",
				Params: map[string]any{"id": id.SessionID},
			})
			if err != nil {
				return err
			}
			// Query the callback.
			finding, err := cb(session, oldFinding)
			if err != nil {
				return err
			} else if finding == nil {
				return nil // Just abort.
			} else if finding.ID == "" {
				finding.ID = uuid.NewString()
			}
			// Insert the finding.
			m, err := spanner.InsertStruct("Findings", finding)
			if err != nil {
				return err
			}
			var mutations []*spanner.Mutation
			if oldFinding != nil {
				mutations = append(mutations, spanner.Delete("Findings", spanner.Key{oldFinding.ID}))
			}
			mutations = append(mutations, m)
			return txn.BufferWrite(mutations)
		})
	return err
}

var errFindingExists = errors.New("the finding already exists")

// A helper for tests.
func (repo *FindingRepository) mustStore(ctx context.Context, finding *Finding) error {
	return repo.Store(ctx, &FindingID{
		SessionID: finding.SessionID,
		TestName:  finding.TestName,
		Title:     finding.Title,
	}, func(_ *Session, old *Finding) (*Finding, error) {
		if old != nil {
			return nil, errFindingExists
		}
		return finding, nil
	})
}

// nolint: dupl
func (repo *FindingRepository) ListForSession(ctx context.Context, sessionID string, limit int) ([]*Finding, error) {
	stmt := spanner.Statement{
		SQL:    "SELECT * FROM `Findings` WHERE `SessionID` = @session ORDER BY `TestName`, `Title`",
		Params: map[string]any{"session": sessionID},
	}
	addLimit(&stmt, limit)
	return repo.readEntities(ctx, stmt)
}