blob: 333e428f2b50d187462ca6859644f8acd3d482d3 [file] [log] [blame]
// Package sqlshortcutstore implements shortcut.Store using an SQL database.
//
// Please see perf/sql/migrations for the database schema used.
package sqlshortcutstore
import (
"context"
"database/sql"
"encoding/json"
"io"
"go.skia.org/infra/go/query"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/perf/go/shortcut"
perfsql "go.skia.org/infra/perf/go/sql"
)
// statement is an SQL statement identifier.
type statement int
const (
// The identifiers for all the SQL statements used.
insertShortcut statement = iota
getShortcut
getAllShortcuts
)
// statements allows looking up raw SQL statements by their statement id.
type statements map[statement]string
// statementsByDialect holds all the raw SQL statemens used per Dialect of SQL.
var statementsByDialect = map[perfsql.Dialect]statements{
perfsql.CockroachDBDialect: {
insertShortcut: `
INSERT INTO
Shortcuts (id, trace_ids)
VALUES
($1, $2)
ON CONFLICT
DO NOTHING`,
getShortcut: `
SELECT
(trace_ids)
FROM
Shortcuts
WHERE
id=$1
`,
getAllShortcuts: `
SELECT
(trace_ids)
FROM
Shortcuts
`,
},
}
// SQLShortcutStore implements the shortcut.Store interface using an SQL
// database.
type SQLShortcutStore struct {
// preparedStatements are all the prepared SQL statements.
preparedStatements map[statement]*sql.Stmt
}
// New returns a new *SQLShortcutStore.
//
// We presume all migrations have been run against db before this function is
// called.
func New(db *sql.DB, dialect perfsql.Dialect) (*SQLShortcutStore, error) {
preparedStatements := map[statement]*sql.Stmt{}
for key, statement := range statementsByDialect[dialect] {
prepared, err := db.Prepare(statement)
if err != nil {
return nil, skerr.Wrapf(err, "Failed to prepare statment %v %q", key, statement)
}
preparedStatements[key] = prepared
}
return &SQLShortcutStore{
preparedStatements: preparedStatements,
}, nil
}
// Insert implements the shortcut.Store interface.
func (s *SQLShortcutStore) Insert(ctx context.Context, r io.Reader) (string, error) {
shortcut := &shortcut.Shortcut{}
if err := json.NewDecoder(r).Decode(shortcut); err != nil {
return "", skerr.Wrapf(err, "Unable to read shortcut body")
}
return s.InsertShortcut(ctx, shortcut)
}
// InsertShortcut implements the shortcut.Store interface.
func (s *SQLShortcutStore) InsertShortcut(ctx context.Context, sc *shortcut.Shortcut) (string, error) {
for _, key := range sc.Keys {
if !query.ValidateKey(key) {
return "", skerr.Fmt("Tried to store an invalid trace key: %q", key)
}
}
id := shortcut.IDFromKeys(sc)
b, err := json.Marshal(sc)
if err != nil {
return "", err
}
if _, err := s.preparedStatements[insertShortcut].ExecContext(ctx, id, string(b)); err != nil {
return "", skerr.Wrap(err)
}
return id, nil
}
// Get implements the shortcut.Store interface.
func (s *SQLShortcutStore) Get(ctx context.Context, id string) (*shortcut.Shortcut, error) {
var encoded string
if err := s.preparedStatements[getShortcut].QueryRowContext(ctx, id).Scan(&encoded); err != nil {
return nil, skerr.Wrapf(err, "Failed to load shortcuts.")
}
var sc shortcut.Shortcut
if err := json.Unmarshal([]byte(encoded), &sc); err != nil {
return nil, skerr.Wrapf(err, "Failed to decode keys.")
}
return &sc, nil
}
// GetAll implements the shortcut.Store interface.
func (s *SQLShortcutStore) GetAll(ctx context.Context) (<-chan *shortcut.Shortcut, error) {
ret := make(chan *shortcut.Shortcut)
rows, err := s.preparedStatements[getAllShortcuts].QueryContext(ctx)
if err != nil {
return ret, skerr.Wrapf(err, "Failed to query for all shortcuts.")
}
go func() {
defer close(ret)
var encoded string
for rows.Next() {
if err := rows.Scan(&encoded); err != nil {
sklog.Warningf("Failed to load all shortcuts: %s", err)
continue
}
var sc shortcut.Shortcut
if err := json.Unmarshal([]byte(encoded), &sc); err != nil {
sklog.Warningf("Failed to decode all shortcuts: %s", err)
continue
}
ret <- &sc
}
}()
return ret, nil
}