github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/pkg/db/finding_repo.go (about) 1 // Copyright 2025 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package db 5 6 import ( 7 "context" 8 "errors" 9 10 "cloud.google.com/go/spanner" 11 "github.com/google/uuid" 12 ) 13 14 type FindingRepository struct { 15 client *spanner.Client 16 *genericEntityOps[Finding, string] 17 } 18 19 func NewFindingRepository(client *spanner.Client) *FindingRepository { 20 return &FindingRepository{ 21 client: client, 22 genericEntityOps: &genericEntityOps[Finding, string]{ 23 client: client, 24 keyField: "ID", 25 table: "Findings", 26 }, 27 } 28 } 29 30 type FindingID struct { 31 SessionID string 32 TestName string 33 Title string 34 } 35 36 // Store queries the information about the session and the existing finding and then 37 // requests a new Finding object to replace the old one. 38 // If the callback returns nil, nothing it updated. 39 func (repo *FindingRepository) Store(ctx context.Context, id *FindingID, 40 cb func(session *Session, old *Finding) (*Finding, error)) error { 41 _, err := repo.client.ReadWriteTransaction(ctx, 42 func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { 43 // Query the existing finding, if it exists. 44 oldFinding, err := readEntity[Finding](ctx, txn, spanner.Statement{ 45 SQL: "SELECT * from `Findings` WHERE `SessionID`=@sessionID " + 46 "AND `TestName` = @testName AND `Title`=@title", 47 Params: map[string]interface{}{ 48 "sessionID": id.SessionID, 49 "testName": id.TestName, 50 "title": id.Title, 51 }, 52 }) 53 if err != nil { 54 return err 55 } 56 // Query the Session object. 57 session, err := readEntity[Session](ctx, txn, spanner.Statement{ 58 SQL: "SELECT * FROM `Sessions` WHERE `ID`=@id", 59 Params: map[string]interface{}{"id": id.SessionID}, 60 }) 61 if err != nil { 62 return err 63 } 64 // Query the callback. 65 finding, err := cb(session, oldFinding) 66 if err != nil { 67 return err 68 } else if finding == nil { 69 return nil // Just abort. 70 } else if finding.ID == "" { 71 finding.ID = uuid.NewString() 72 } 73 // Insert the finding. 74 m, err := spanner.InsertStruct("Findings", finding) 75 if err != nil { 76 return err 77 } 78 var mutations []*spanner.Mutation 79 if oldFinding != nil { 80 mutations = append(mutations, spanner.Delete("Findings", spanner.Key{oldFinding.ID})) 81 } 82 mutations = append(mutations, m) 83 return txn.BufferWrite(mutations) 84 }) 85 return err 86 } 87 88 var errFindingExists = errors.New("the finding already exists") 89 90 // A helper for tests. 91 func (repo *FindingRepository) mustStore(ctx context.Context, finding *Finding) error { 92 return repo.Store(ctx, &FindingID{ 93 SessionID: finding.SessionID, 94 TestName: finding.TestName, 95 Title: finding.Title, 96 }, func(_ *Session, old *Finding) (*Finding, error) { 97 if old != nil { 98 return nil, errFindingExists 99 } 100 return finding, nil 101 }) 102 } 103 104 // nolint: dupl 105 func (repo *FindingRepository) ListForSession(ctx context.Context, sessionID string, limit int) ([]*Finding, error) { 106 stmt := spanner.Statement{ 107 SQL: "SELECT * FROM `Findings` WHERE `SessionID` = @session ORDER BY `TestName`, `Title`", 108 Params: map[string]interface{}{"session": sessionID}, 109 } 110 addLimit(&stmt, limit) 111 return repo.readEntities(ctx, stmt) 112 }