1
0
mirror of https://github.com/rancher/steve.git synced 2025-06-30 08:42:06 +00:00
steve/pkg/sqlcache/informer/indexer.go
Silvio Moioli 3350323f91
sql: propagate and use contexts (#465)
Previous SQLite-related code used context.Background() and context.TODO() because it was not developed with context awareness.

This commit propagates the main Steve context so that it can be used when interacting with SQL context-aware functions.

This PR removes all production-code use of context.Background() and context.TODO() and replaces test-code use of TODO with Background.

Contributes to rancher/rancher#47825
2025-02-12 09:46:10 +01:00

264 lines
7.8 KiB
Go

package informer
import (
"context"
"database/sql"
"errors"
"fmt"
"reflect"
"strings"
"sync"
"github.com/rancher/steve/pkg/sqlcache/db"
"github.com/rancher/steve/pkg/sqlcache/db/transaction"
"k8s.io/client-go/tools/cache"
)
const (
selectQueryFmt = `
SELECT object, objectnonce, dekid FROM "%[1]s"
WHERE key IN (
SELECT key FROM "%[1]s_indices"
WHERE name = ? AND value IN (?%s)
)
`
createTableFmt = `CREATE TABLE IF NOT EXISTS "%[1]s_indices" (
name TEXT NOT NULL,
value TEXT NOT NULL,
key TEXT NOT NULL REFERENCES "%[1]s"(key) ON DELETE CASCADE,
PRIMARY KEY (name, value, key)
)`
createIndexFmt = `CREATE INDEX IF NOT EXISTS "%[1]s_indices_index" ON "%[1]s_indices"(name, value)`
deleteIndicesFmt = `DELETE FROM "%s_indices" WHERE key = ?`
addIndexFmt = `INSERT INTO "%s_indices" (name, value, key) VALUES (?, ?, ?) ON CONFLICT DO NOTHING`
listByIndexFmt = `SELECT object, objectnonce, dekid FROM "%[1]s"
WHERE key IN (
SELECT key FROM "%[1]s_indices"
WHERE name = ? AND value = ?
)`
listKeyByIndexFmt = `SELECT DISTINCT key FROM "%s_indices" WHERE name = ? AND value = ?`
listIndexValuesFmt = `SELECT DISTINCT value FROM "%s_indices" WHERE name = ?`
)
// Indexer is a SQLite-backed cache.Indexer which builds upon Store adding an index table
type Indexer struct {
ctx context.Context
Store
indexers cache.Indexers
indexersLock sync.RWMutex
deleteIndicesQuery string
addIndexQuery string
listByIndexQuery string
listKeysByIndexQuery string
listIndexValuesQuery string
deleteIndicesStmt *sql.Stmt
addIndexStmt *sql.Stmt
listByIndexStmt *sql.Stmt
listKeysByIndexStmt *sql.Stmt
listIndexValuesStmt *sql.Stmt
}
var _ cache.Indexer = (*Indexer)(nil)
type Store interface {
db.Client
cache.Store
GetByKey(key string) (item any, exists bool, err error)
GetName() string
RegisterAfterUpsert(f func(key string, obj any, tx transaction.Client) error)
RegisterAfterDelete(f func(key string, tx transaction.Client) error)
GetShouldEncrypt() bool
GetType() reflect.Type
}
// NewIndexer returns a cache.Indexer backed by SQLite for objects of the given example type
func NewIndexer(ctx context.Context, indexers cache.Indexers, s Store) (*Indexer, error) {
dbName := db.Sanitize(s.GetName())
err := s.WithTransaction(ctx, true, func(tx transaction.Client) error {
createTableQuery := fmt.Sprintf(createTableFmt, dbName)
_, err := tx.Exec(createTableQuery)
if err != nil {
return &db.QueryError{QueryString: createTableQuery, Err: err}
}
createIndexQuery := fmt.Sprintf(createIndexFmt, dbName)
_, err = tx.Exec(createIndexQuery)
if err != nil {
return &db.QueryError{QueryString: createIndexQuery, Err: err}
}
return nil
})
if err != nil {
return nil, err
}
i := &Indexer{
ctx: ctx,
Store: s,
indexers: indexers,
}
i.RegisterAfterUpsert(i.AfterUpsert)
i.deleteIndicesQuery = fmt.Sprintf(deleteIndicesFmt, db.Sanitize(s.GetName()))
i.addIndexQuery = fmt.Sprintf(addIndexFmt, db.Sanitize(s.GetName()))
i.listByIndexQuery = fmt.Sprintf(listByIndexFmt, db.Sanitize(s.GetName()))
i.listKeysByIndexQuery = fmt.Sprintf(listKeyByIndexFmt, db.Sanitize(s.GetName()))
i.listIndexValuesQuery = fmt.Sprintf(listIndexValuesFmt, db.Sanitize(s.GetName()))
i.deleteIndicesStmt = s.Prepare(i.deleteIndicesQuery)
i.addIndexStmt = s.Prepare(i.addIndexQuery)
i.listByIndexStmt = s.Prepare(i.listByIndexQuery)
i.listKeysByIndexStmt = s.Prepare(i.listKeysByIndexQuery)
i.listIndexValuesStmt = s.Prepare(i.listIndexValuesQuery)
return i, nil
}
/* Core methods */
// AfterUpsert updates indices of an object
func (i *Indexer) AfterUpsert(key string, obj any, tx transaction.Client) error {
// delete all
_, err := tx.Stmt(i.deleteIndicesStmt).Exec(key)
if err != nil {
return &db.QueryError{QueryString: i.deleteIndicesQuery, Err: err}
}
// re-insert all values
i.indexersLock.RLock()
defer i.indexersLock.RUnlock()
for indexName, indexFunc := range i.indexers {
values, err := indexFunc(obj)
if err != nil {
return err
}
for _, value := range values {
_, err = tx.Stmt(i.addIndexStmt).Exec(indexName, value, key)
if err != nil {
return &db.QueryError{QueryString: i.addIndexQuery, Err: err}
}
}
}
return nil
}
/* Satisfy cache.Indexer */
// Index returns a list of items that match the given object on the index function
func (i *Indexer) Index(indexName string, obj any) (result []any, err error) {
i.indexersLock.RLock()
defer i.indexersLock.RUnlock()
indexFunc := i.indexers[indexName]
if indexFunc == nil {
return nil, fmt.Errorf("index with name %s does not exist", indexName)
}
values, err := indexFunc(obj)
if err != nil {
return nil, err
}
if len(values) == 0 {
return nil, nil
}
// typical case
if len(values) == 1 {
return i.ByIndex(indexName, values[0])
}
// atypical case - more than one value to lookup
// HACK: sql.Statement.Query does not allow to pass slices in as of go 1.19 - create an ad-hoc statement
query := fmt.Sprintf(selectQueryFmt, db.Sanitize(i.GetName()), strings.Repeat(", ?", len(values)-1))
stmt := i.Prepare(query)
defer func() {
cerr := i.CloseStmt(stmt)
if cerr != nil {
err = errors.Join(err, &db.QueryError{QueryString: query, Err: cerr})
}
}()
// HACK: Query will accept []any but not []string
params := []any{indexName}
for _, value := range values {
params = append(params, value)
}
rows, err := i.QueryForRows(i.ctx, stmt, params...)
if err != nil {
return nil, &db.QueryError{QueryString: query, Err: err}
}
return i.ReadObjects(rows, i.GetType(), i.GetShouldEncrypt())
}
// ByIndex returns the stored objects whose set of indexed values
// for the named index includes the given indexed value
func (i *Indexer) ByIndex(indexName, indexedValue string) ([]any, error) {
rows, err := i.QueryForRows(i.ctx, i.listByIndexStmt, indexName, indexedValue)
if err != nil {
return nil, &db.QueryError{QueryString: i.listByIndexQuery, Err: err}
}
return i.ReadObjects(rows, i.GetType(), i.GetShouldEncrypt())
}
// IndexKeys returns a list of the Store keys of the objects whose indexed values in the given index include the given indexed value
func (i *Indexer) IndexKeys(indexName, indexedValue string) ([]string, error) {
i.indexersLock.RLock()
defer i.indexersLock.RUnlock()
indexFunc := i.indexers[indexName]
if indexFunc == nil {
return nil, fmt.Errorf("Index with name %s does not exist", indexName)
}
rows, err := i.QueryForRows(i.ctx, i.listKeysByIndexStmt, indexName, indexedValue)
if err != nil {
return nil, &db.QueryError{QueryString: i.listKeysByIndexQuery, Err: err}
}
return i.ReadStrings(rows)
}
// ListIndexFuncValues wraps safeListIndexFuncValues and panics in case of I/O errors
func (i *Indexer) ListIndexFuncValues(name string) []string {
result, err := i.safeListIndexFuncValues(name)
if err != nil {
panic(fmt.Errorf("unexpected error in safeListIndexFuncValues: %w", err))
}
return result
}
// safeListIndexFuncValues returns all the indexed values of the given index
func (i *Indexer) safeListIndexFuncValues(indexName string) ([]string, error) {
rows, err := i.QueryForRows(i.ctx, i.listIndexValuesStmt, indexName)
if err != nil {
return nil, &db.QueryError{QueryString: i.listIndexValuesQuery, Err: err}
}
return i.ReadStrings(rows)
}
// GetIndexers returns the indexers
func (i *Indexer) GetIndexers() cache.Indexers {
i.indexersLock.RLock()
defer i.indexersLock.RUnlock()
return i.indexers
}
// AddIndexers adds more indexers to this Store. If you call this after you already have data
// in the Store, the results are undefined.
func (i *Indexer) AddIndexers(newIndexers cache.Indexers) error {
i.indexersLock.Lock()
defer i.indexersLock.Unlock()
if i.indexers == nil {
i.indexers = make(map[string]cache.IndexFunc)
}
for k, v := range newIndexers {
i.indexers[k] = v
}
return nil
}