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  }