blob: 3acceb52cd41b8f79c0c4915faf5b4caa550f2cc [file] [log] [blame]
package sqlcoveragestore
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgconn"
"go.skia.org/infra/go/coverage/config"
pb "go.skia.org/infra/go/coverage/proto/v1"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/sql/pool"
)
// statement is an SQL statement identifier.
type statement int
const (
// The identifiers for all the SQL statements used.
addFile statement = iota
addBuilder
addTestSuite
deleteFile
listTestSuite
listAll
listBuilder
)
const (
CockroachDB string = "cockroachdb"
Spanner string = "spanner"
)
// statementsByDialect holds all the raw SQL statemens used per Dialect of SQL.
var statements = map[statement]string{
addFile: `
INSERT INTO testsuitemapping (file_name, builder_name, test_suite_name)
SELECT * FROM (
VALUES ($1::STRING, $2::STRING, $3::STRING[])
) AS vals (file_name, builder_name, test_suite_name)
WHERE NOT EXISTS (
SELECT 1 FROM testsuitemapping
WHERE testsuitemapping.file_name = vals.file_name)
`,
addBuilder: `
INSERT INTO testsuitemapping (file_name, builder_name, test_suite_name)
SELECT * FROM (
VALUES ($1::STRING, $2::STRING, $3::STRING[])
) AS vals (file_name, builder_name, test_suite_name)
WHERE NOT EXISTS (
SELECT 1 FROM testsuitemapping
WHERE testsuitemapping.file_name = vals.file_name
AND testsuitemapping.builder_name = vals.builder_name)
`,
addTestSuite: `
UPDATE testsuitemapping
SET test_suite_name = array_append(test_suite_name,$3::STRING)
WHERE testsuitemapping.file_name = $1::STRING
AND testsuitemapping.builder_name = $2::STRING
AND NOT $3::STRING=ANY(testsuitemapping.test_suite_name)
`,
deleteFile: `
DELETE FROM
testsuitemapping WHERE file_name=$1 AND builder_name=$2`,
listTestSuite: `
SELECT * FROM testsuitemapping
WHERE file_name=$1 AND builder_name=$2`,
listAll: `
SELECT * FROM testsuitemapping`,
}
// coverageStore implements the coverage.Store interface.
type CoverageStore struct {
// db is the database interface.
db pool.Pool
statements map[statement]string
dbType string
}
func New(db pool.Pool, config *config.CoverageConfig) (*CoverageStore, error) {
sqlStatements := statements
if config.DatabaseType == Spanner {
sqlStatements = statements_spanner
}
return &CoverageStore{
db: db,
statements: sqlStatements,
dbType: config.DatabaseType,
}, nil
}
// Add implements the coverage.CoverageStore interface.
func (s *CoverageStore) Add(ctx context.Context, req *pb.CoverageChangeRequest) error {
if s.dbType == Spanner {
id, _, builderName, suiteNames, _, err := s.readData(ctx, req)
if err != nil {
return err
}
if id == "" || builderName != req.GetBuilderName() {
// This is either a case where no row exists or there is a row but
// it's for a different builder. In both situations, we insert a new entry.
rows, err := s.sqlExecInsert(ctx, s.statements[addFile], req)
if err != nil || rows > 0 {
sklog.Errorf("Add Failed: %s", s.statements[addFile])
return err
}
} else {
// Here we only need to add a new test suite.
allSuiteNames := map[string]bool{}
for _, suite := range suiteNames {
allSuiteNames[suite] = true
}
for _, suite := range req.GetTestSuiteName() {
allSuiteNames[suite] = true
}
allSuites := []string{}
for suite := range allSuiteNames {
allSuites = append(allSuites, suite)
}
_, err = s.db.Exec(ctx, s.statements[addTestSuite], req.GetFileName(), req.GetBuilderName(), allSuites)
return err
}
} else {
// This is the CockroachDB implementation.
rows, err := s.sqlExecInsert(ctx, s.statements[addFile], req)
if err != nil || rows > 0 {
sklog.Errorf("Add Failed: %s", s.statements[addFile])
return err
}
rows, err = s.sqlExecInsert(ctx, s.statements[addBuilder], req)
if err != nil || rows > 0 {
return err
}
rows, err = s.sqlExecUpdate(ctx, s.statements[addTestSuite], req)
if err == nil && rows == 0 {
err = errors.New("No Rows Added")
}
return err
}
return nil
}
// Delete removes the Filename with the given filename.
func (s *CoverageStore) Delete(ctx context.Context, req *pb.CoverageChangeRequest) error {
rows, err := s.sqlExecDelete(ctx, s.statements[deleteFile], req)
if err == nil && rows == 0 {
err = errors.New("No Rows Deleted")
}
return err
}
// List retrieves all the Coverage mapppings.
func (s *CoverageStore) List(ctx context.Context, req *pb.CoverageListRequest) ([]string, error) {
sklog.Debugf("List: %s", req)
var response struct {
id string
file_name string
builder_name string
test_suite_name []string
last_modified time.Time
}
rows, err := s.db.Query(ctx, s.statements[listTestSuite], req.GetFileName(), req.GetBuilderName())
if err != nil {
sklog.Errorf("SQL: %s", s.statements[listTestSuite])
return nil, err
}
defer rows.Close()
counter := 0
for rows.Next() {
counter++
if err := rows.Scan(&response.id, &response.file_name, &response.builder_name,
&response.test_suite_name, &response.last_modified); err != nil {
sklog.Debugf("Row Error: %s", err)
} else {
sklog.Debugf("Response: %s", response.test_suite_name)
}
}
if counter == 0 {
err = errors.New(fmt.Sprintf("No Rows Found for: %v", req.GetFileName()))
}
return response.test_suite_name, err
}
// List retrieves all the Coverage mapppings.
func (s *CoverageStore) ListAll(ctx context.Context, req *pb.CoverageRequest) ([]*pb.CoverageResponse, error) {
sklog.Debugf("List: %s", req)
rows, err := s.db.Query(ctx, s.statements[listAll])
if err != nil {
sklog.Errorf("SQL: %s", s.statements[listAll])
return nil, err
}
defer rows.Close()
counter := 0
var responses []*pb.CoverageResponse
for rows.Next() {
var coverageResponse pb.CoverageResponse
counter++
var response struct {
id string
last_modified time.Time
}
if err := rows.Scan(&response.id, &coverageResponse.FileName, &coverageResponse.BuilderName,
&coverageResponse.TestSuiteName, &response.last_modified); err != nil {
sklog.Debugf("Row Error: %s", err)
} else {
responses = append(responses, &coverageResponse)
}
}
if counter == 0 {
err = errors.New(fmt.Sprintf("No Rows Found for: %v", req))
}
sklog.Debugf("Responses: %s", responses)
return responses, err
}
func (s *CoverageStore) sqlExecUpdate(ctx context.Context, sqlStatement string, req *pb.CoverageChangeRequest) (int64, error) {
var result pgconn.CommandTag
var err error
var rows int64
for _, suite := range req.GetTestSuiteName() {
result, err = s.db.Exec(ctx, sqlStatement, req.GetFileName(),
req.GetBuilderName(), suite)
if err != nil {
sklog.Errorf("Update Failed")
return 0, err
}
rows += result.RowsAffected()
}
return rows, nil
}
func (s *CoverageStore) readData(ctx context.Context, req *pb.CoverageChangeRequest) (string, string, string, []string, time.Time, error) {
results, err := s.db.Query(ctx, s.statements[listBuilder], req.GetFileName(), req.GetBuilderName())
if err != nil {
return "", "", "", nil, time.Time{}, err
}
var id, fileName, builderName string
var suiteNames []string
var lastModified time.Time
for results.Next() {
if err = results.Scan(&id, &fileName, &builderName, &suiteNames, &lastModified); err != nil {
sklog.Debugf("Row Error: %s", err)
}
break
}
return id, fileName, builderName, suiteNames, lastModified, err
}
func (s *CoverageStore) sqlExecInsert(ctx context.Context, sqlStatement string, req *pb.CoverageChangeRequest) (int64, error) {
result, err := s.db.Exec(ctx, sqlStatement, req.GetFileName(),
req.GetBuilderName(), req.GetTestSuiteName())
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
func (s *CoverageStore) sqlExecDelete(ctx context.Context, sqlStatement string, req *pb.CoverageChangeRequest) (int64, error) {
result, err := s.db.Exec(ctx, sqlStatement, req.GetFileName(), req.GetBuilderName())
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}