Add SQL-based KV store implementation
- Introduced a new `sqlkv.go` file implementing the KV interface using SQL storage. - Added methods for creating, retrieving, updating, and deleting key-value pairs in a SQL database. - Implemented support for multiple SQL dialects (MySQL, PostgreSQL, SQLite). - Included functionality for batch operations and key parsing. This change lays the foundation for a robust SQL-based key-value storage solution, enhancing data management capabilities.
This commit is contained in:
@@ -0,0 +1,776 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/db"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
)
|
||||
|
||||
const (
|
||||
sectionData = "unified/data"
|
||||
sectionEvents = "unified/events"
|
||||
)
|
||||
|
||||
// sqlKV implements the KV interface using SQL storage
|
||||
type sqlKV struct {
|
||||
db db.DB
|
||||
dialect sqltemplate.Dialect
|
||||
}
|
||||
|
||||
// NewSQLKV creates a new SQL-based KV store
|
||||
func NewSQLKV(dbProvider db.DBProvider) (KV, error) {
|
||||
if dbProvider == nil {
|
||||
return nil, errors.New("dbProvider is required")
|
||||
}
|
||||
|
||||
// Initialize the database connection
|
||||
ctx := context.Background()
|
||||
dbConn, err := dbProvider.Init(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initialize DB: %w", err)
|
||||
}
|
||||
|
||||
// Determine the SQL dialect
|
||||
var dialect sqltemplate.Dialect
|
||||
switch dbConn.DriverName() {
|
||||
case "mysql":
|
||||
dialect = sqltemplate.MySQL
|
||||
case "postgres":
|
||||
dialect = sqltemplate.PostgreSQL
|
||||
case "sqlite3", "sqlite":
|
||||
dialect = sqltemplate.SQLite
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database driver: %s", dbConn.DriverName())
|
||||
}
|
||||
|
||||
return &sqlKV{
|
||||
db: dbConn,
|
||||
dialect: dialect,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Verify that sqlKV implements KV interface
|
||||
var _ KV = &sqlKV{}
|
||||
|
||||
// Helper function to build identifiers safely
|
||||
func (k *sqlKV) ident(name string) (string, error) {
|
||||
return k.dialect.Ident(name)
|
||||
}
|
||||
|
||||
// Helper function to get table name for a section
|
||||
func (k *sqlKV) getTableName(section string) (string, error) {
|
||||
switch section {
|
||||
case sectionData:
|
||||
return k.ident("resource_history")
|
||||
case sectionEvents:
|
||||
return k.ident("resource_events")
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported section: %s", section)
|
||||
}
|
||||
}
|
||||
|
||||
// parsedKey represents the components of a key_path for the data section
|
||||
// Format: {Group}/{Resource}/{Namespace}/{Name}/{ResourceVersion}~{Action}~{Folder}
|
||||
type parsedKey struct {
|
||||
Group string
|
||||
Resource string
|
||||
Namespace string
|
||||
Name string
|
||||
ResourceVersion int64
|
||||
Action int // 1: create, 2: update, 3: delete
|
||||
Folder string
|
||||
}
|
||||
|
||||
// parseDataKey parses a data section key_path
|
||||
func parseDataKey(keyPath string) (*parsedKey, error) {
|
||||
// Split by ~ to separate main key from action and folder
|
||||
parts := strings.Split(keyPath, "~")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("invalid key format: expected 3 parts separated by '~', got %d", len(parts))
|
||||
}
|
||||
|
||||
// Split main key by /
|
||||
mainParts := strings.Split(parts[0], "/")
|
||||
if len(mainParts) != 5 {
|
||||
return nil, fmt.Errorf("invalid key format: expected 5 parts separated by '/', got %d", len(mainParts))
|
||||
}
|
||||
|
||||
// Parse resource version (stored as snowflake ID in key)
|
||||
rv, err := strconv.ParseInt(mainParts[4], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid resource_version: %w", err)
|
||||
}
|
||||
|
||||
// Convert action string to int
|
||||
var action int
|
||||
switch parts[1] {
|
||||
case "created":
|
||||
action = 1
|
||||
case "updated":
|
||||
action = 2
|
||||
case "deleted":
|
||||
action = 3
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid action: %s", parts[1])
|
||||
}
|
||||
|
||||
return &parsedKey{
|
||||
Group: mainParts[0],
|
||||
Resource: mainParts[1],
|
||||
Namespace: mainParts[2],
|
||||
Name: mainParts[3],
|
||||
ResourceVersion: rv,
|
||||
Action: action,
|
||||
Folder: parts[2], // May be empty string
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get retrieves the value for a key from the store
|
||||
func (k *sqlKV) Get(ctx context.Context, section string, key string) (io.ReadCloser, error) {
|
||||
if section == "" {
|
||||
return nil, fmt.Errorf("section is required")
|
||||
}
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("key is required")
|
||||
}
|
||||
|
||||
tableName, err := k.getTableName(section)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valueIdent, err := k.ident("value")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid column identifier: %w", err)
|
||||
}
|
||||
keyPathIdent, err := k.ident("key_path")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid column identifier: %w", err)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
"SELECT %s FROM %s WHERE %s = %s",
|
||||
valueIdent,
|
||||
tableName,
|
||||
keyPathIdent,
|
||||
k.dialect.ArgPlaceholder(1),
|
||||
)
|
||||
|
||||
// Execute the query
|
||||
var value []byte
|
||||
err = k.db.QueryRowContext(ctx, query, key).Scan(&value)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("query failed: %w", err)
|
||||
}
|
||||
|
||||
return io.NopCloser(bytes.NewReader(value)), nil
|
||||
}
|
||||
|
||||
// BatchGet retrieves multiple values for the given keys from the store
|
||||
// Uses a UNION ALL subquery with LEFT JOIN to preserve order and enable streaming
|
||||
func (k *sqlKV) BatchGet(ctx context.Context, section string, keys []string) iter.Seq2[KeyValue, error] {
|
||||
if section == "" {
|
||||
return func(yield func(KeyValue, error) bool) {
|
||||
yield(KeyValue{}, fmt.Errorf("section is required"))
|
||||
}
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
return func(yield func(KeyValue, error) bool) {
|
||||
// Empty result set - nothing to yield
|
||||
}
|
||||
}
|
||||
|
||||
return func(yield func(KeyValue, error) bool) {
|
||||
tableName, err := k.getTableName(section)
|
||||
if err != nil {
|
||||
yield(KeyValue{}, err)
|
||||
return
|
||||
}
|
||||
|
||||
keyPathIdent, err := k.ident("key_path")
|
||||
if err != nil {
|
||||
yield(KeyValue{}, fmt.Errorf("invalid column identifier: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
valueIdent, err := k.ident("value")
|
||||
if err != nil {
|
||||
yield(KeyValue{}, fmt.Errorf("invalid column identifier: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Build UNION ALL subquery to preserve key order
|
||||
// SELECT 0 AS idx, ? AS kp UNION ALL SELECT 1, ? UNION ALL ...
|
||||
var unionParts []string
|
||||
var args []interface{}
|
||||
argNum := 1
|
||||
|
||||
for i, key := range keys {
|
||||
if i == 0 {
|
||||
unionParts = append(unionParts, fmt.Sprintf(
|
||||
"SELECT %s AS idx, %s AS kp",
|
||||
k.dialect.ArgPlaceholder(argNum),
|
||||
k.dialect.ArgPlaceholder(argNum+1),
|
||||
))
|
||||
} else {
|
||||
unionParts = append(unionParts, fmt.Sprintf(
|
||||
"UNION ALL SELECT %s, %s",
|
||||
k.dialect.ArgPlaceholder(argNum),
|
||||
k.dialect.ArgPlaceholder(argNum+1),
|
||||
))
|
||||
}
|
||||
args = append(args, i, key)
|
||||
argNum += 2
|
||||
}
|
||||
|
||||
// Build the full query with LEFT JOIN to preserve order
|
||||
// This allows streaming results directly without buffering
|
||||
query := fmt.Sprintf(
|
||||
"SELECT v.idx, t.%s, t.%s FROM (%s) AS v LEFT JOIN %s t ON t.%s = v.kp ORDER BY v.idx",
|
||||
keyPathIdent,
|
||||
valueIdent,
|
||||
strings.Join(unionParts, " "),
|
||||
tableName,
|
||||
keyPathIdent,
|
||||
)
|
||||
|
||||
// Execute the query
|
||||
rows, err := k.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
yield(KeyValue{}, fmt.Errorf("query failed: %w", err))
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Stream results directly - no buffering needed!
|
||||
// Results come back in the order specified by idx
|
||||
for rows.Next() {
|
||||
var idx int
|
||||
var keyPath sql.NullString
|
||||
var value []byte
|
||||
|
||||
if err := rows.Scan(&idx, &keyPath, &value); err != nil {
|
||||
yield(KeyValue{}, fmt.Errorf("scan failed: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Skip keys that don't exist (LEFT JOIN returns NULL)
|
||||
if !keyPath.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
kv := KeyValue{
|
||||
Key: keyPath.String,
|
||||
Value: io.NopCloser(bytes.NewReader(value)),
|
||||
}
|
||||
|
||||
if !yield(kv, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
yield(KeyValue{}, fmt.Errorf("rows error: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keys returns all the keys in the store
|
||||
func (k *sqlKV) Keys(ctx context.Context, section string, opt ListOptions) iter.Seq2[string, error] {
|
||||
if section == "" {
|
||||
return func(yield func(string, error) bool) {
|
||||
yield("", fmt.Errorf("section is required"))
|
||||
}
|
||||
}
|
||||
|
||||
return func(yield func(string, error) bool) {
|
||||
tableName, err := k.getTableName(section)
|
||||
if err != nil {
|
||||
yield("", err)
|
||||
return
|
||||
}
|
||||
|
||||
keyPathIdent, err := k.ident("key_path")
|
||||
if err != nil {
|
||||
yield("", fmt.Errorf("invalid column identifier: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Build WHERE clauses
|
||||
var whereClauses []string
|
||||
var args []interface{}
|
||||
argNum := 1
|
||||
|
||||
// Start key (inclusive)
|
||||
if opt.StartKey != "" {
|
||||
whereClauses = append(whereClauses, fmt.Sprintf("%s >= %s", keyPathIdent, k.dialect.ArgPlaceholder(argNum)))
|
||||
args = append(args, opt.StartKey)
|
||||
argNum++
|
||||
}
|
||||
|
||||
// End key (exclusive)
|
||||
if opt.EndKey != "" {
|
||||
whereClauses = append(whereClauses, fmt.Sprintf("%s < %s", keyPathIdent, k.dialect.ArgPlaceholder(argNum)))
|
||||
args = append(args, opt.EndKey)
|
||||
argNum++
|
||||
}
|
||||
|
||||
// Build ORDER BY clause
|
||||
orderBy := "ASC"
|
||||
if opt.Sort == SortOrderDesc {
|
||||
orderBy = "DESC"
|
||||
}
|
||||
|
||||
// Build the query
|
||||
query := fmt.Sprintf(
|
||||
"SELECT %s FROM %s",
|
||||
keyPathIdent,
|
||||
tableName,
|
||||
)
|
||||
|
||||
if len(whereClauses) > 0 {
|
||||
query += " WHERE " + strings.Join(whereClauses, " AND ")
|
||||
}
|
||||
|
||||
query += fmt.Sprintf(" ORDER BY %s %s", keyPathIdent, orderBy)
|
||||
|
||||
if opt.Limit > 0 {
|
||||
query += fmt.Sprintf(" LIMIT %d", opt.Limit)
|
||||
}
|
||||
|
||||
// Execute the query
|
||||
rows, err := k.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
yield("", fmt.Errorf("query failed: %w", err))
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Yield each key
|
||||
for rows.Next() {
|
||||
var keyPath string
|
||||
if err := rows.Scan(&keyPath); err != nil {
|
||||
yield("", fmt.Errorf("scan failed: %w", err))
|
||||
return
|
||||
}
|
||||
if !yield(keyPath, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
yield("", fmt.Errorf("rows error: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save a new value - returns a WriteCloser to write the value to
|
||||
func (k *sqlKV) Save(ctx context.Context, section string, key string) (io.WriteCloser, error) {
|
||||
if section == "" {
|
||||
return nil, fmt.Errorf("section is required")
|
||||
}
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("key is required")
|
||||
}
|
||||
|
||||
return &sqlWriteCloser{
|
||||
kv: k,
|
||||
ctx: ctx,
|
||||
section: section,
|
||||
key: key,
|
||||
buf: &bytes.Buffer{},
|
||||
closed: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// sqlWriteCloser implements io.WriteCloser for SQL KV Save operations
|
||||
type sqlWriteCloser struct {
|
||||
kv *sqlKV
|
||||
ctx context.Context
|
||||
section string
|
||||
key string
|
||||
buf *bytes.Buffer
|
||||
closed bool
|
||||
}
|
||||
|
||||
// Write implements io.Writer
|
||||
func (w *sqlWriteCloser) Write(p []byte) (int, error) {
|
||||
if w.closed {
|
||||
return 0, fmt.Errorf("write to closed writer")
|
||||
}
|
||||
return w.buf.Write(p)
|
||||
}
|
||||
|
||||
// Close implements io.Closer - stores the buffered data in SQL
|
||||
func (w *sqlWriteCloser) Close() error {
|
||||
if w.closed {
|
||||
return nil
|
||||
}
|
||||
w.closed = true
|
||||
|
||||
value := w.buf.Bytes()
|
||||
|
||||
switch w.section {
|
||||
case sectionEvents:
|
||||
// Simple upsert for events section
|
||||
return w.closeEvents(value)
|
||||
case sectionData:
|
||||
// Complex multi-table transaction for data section
|
||||
return w.closeData(value)
|
||||
default:
|
||||
return fmt.Errorf("unsupported section: %s", w.section)
|
||||
}
|
||||
}
|
||||
|
||||
// closeEvents handles the simple upsert for the events section
|
||||
func (w *sqlWriteCloser) closeEvents(value []byte) error {
|
||||
tableName, err := w.kv.getTableName(w.section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyPathIdent, err := w.kv.ident("key_path")
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid column identifier: %w", err)
|
||||
}
|
||||
|
||||
valueIdent, err := w.kv.ident("value")
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid column identifier: %w", err)
|
||||
}
|
||||
|
||||
ph1 := w.kv.dialect.ArgPlaceholder(1)
|
||||
ph2 := w.kv.dialect.ArgPlaceholder(2)
|
||||
|
||||
var query string
|
||||
switch w.kv.dialect.DialectName() {
|
||||
case "postgres":
|
||||
query = fmt.Sprintf(
|
||||
"INSERT INTO %s (%s, %s) VALUES (%s, %s) ON CONFLICT (%s) DO UPDATE SET %s = EXCLUDED.%s",
|
||||
tableName, keyPathIdent, valueIdent, ph1, ph2, keyPathIdent, valueIdent, valueIdent,
|
||||
)
|
||||
case "mysql":
|
||||
query = fmt.Sprintf(
|
||||
"INSERT INTO %s (%s, %s) VALUES (%s, %s) ON DUPLICATE KEY UPDATE %s = VALUES(%s)",
|
||||
tableName, keyPathIdent, valueIdent, ph1, ph2, valueIdent, valueIdent,
|
||||
)
|
||||
case "sqlite":
|
||||
query = fmt.Sprintf(
|
||||
"INSERT INTO %s (%s, %s) VALUES (%s, %s) ON CONFLICT (%s) DO UPDATE SET %s = excluded.%s",
|
||||
tableName, keyPathIdent, valueIdent, ph1, ph2, keyPathIdent, valueIdent, valueIdent,
|
||||
)
|
||||
default:
|
||||
return fmt.Errorf("unsupported dialect: %s", w.kv.dialect.DialectName())
|
||||
}
|
||||
|
||||
_, err = w.kv.db.ExecContext(w.ctx, query, w.key, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert/update failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeData handles the complex multi-table transaction for the data section
|
||||
func (w *sqlWriteCloser) closeData(value []byte) error {
|
||||
// Parse the key to extract all fields
|
||||
parsed, err := parseDataKey(w.key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse key: %w", err)
|
||||
}
|
||||
|
||||
// Generate a GUID for this write
|
||||
guid := uuid.New().String()
|
||||
|
||||
// Execute all operations in a transaction
|
||||
return w.kv.db.WithTx(w.ctx, nil, func(ctx context.Context, tx db.Tx) error {
|
||||
// 1. Insert/update resource_history
|
||||
if err := w.upsertResourceHistory(ctx, tx, parsed, guid, value); err != nil {
|
||||
return fmt.Errorf("upsert resource_history: %w", err)
|
||||
}
|
||||
|
||||
// 2. Handle resource table based on action
|
||||
if parsed.Action == 3 { // deleted
|
||||
if err := w.deleteResource(ctx, tx, parsed); err != nil {
|
||||
return fmt.Errorf("delete resource: %w", err)
|
||||
}
|
||||
} else { // created or updated
|
||||
if err := w.upsertResource(ctx, tx, parsed, guid, value); err != nil {
|
||||
return fmt.Errorf("upsert resource: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Upsert resource_version table
|
||||
if err := w.upsertResourceVersion(ctx, tx, parsed); err != nil {
|
||||
return fmt.Errorf("upsert resource_version: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// upsertResourceHistory inserts/updates a row in the resource_history table
|
||||
func (w *sqlWriteCloser) upsertResourceHistory(ctx context.Context, tx db.Tx, parsed *parsedKey, guid string, value []byte) error {
|
||||
// Build identifiers
|
||||
tableIdent, _ := w.kv.ident("resource_history")
|
||||
guidIdent, _ := w.kv.ident("guid")
|
||||
groupIdent, _ := w.kv.ident("group")
|
||||
resourceIdent, _ := w.kv.ident("resource")
|
||||
namespaceIdent, _ := w.kv.ident("namespace")
|
||||
nameIdent, _ := w.kv.ident("name")
|
||||
rvIdent, _ := w.kv.ident("resource_version")
|
||||
prevRVIdent, _ := w.kv.ident("previous_resource_version")
|
||||
valueIdent, _ := w.kv.ident("value")
|
||||
actionIdent, _ := w.kv.ident("action")
|
||||
folderIdent, _ := w.kv.ident("folder")
|
||||
keyPathIdent, _ := w.kv.ident("key_path")
|
||||
|
||||
// Build placeholders
|
||||
var query string
|
||||
ph := func(n int) string { return w.kv.dialect.ArgPlaceholder(n) }
|
||||
|
||||
switch w.kv.dialect.DialectName() {
|
||||
case "postgres":
|
||||
query = fmt.Sprintf(`
|
||||
INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, 0, %s, %s, %s)
|
||||
ON CONFLICT (%s) DO UPDATE SET %s = EXCLUDED.%s, %s = EXCLUDED.%s`,
|
||||
tableIdent, guidIdent, keyPathIdent, groupIdent, resourceIdent, namespaceIdent, nameIdent,
|
||||
rvIdent, prevRVIdent, valueIdent, actionIdent, folderIdent,
|
||||
ph(1), ph(2), ph(3), ph(4), ph(5), ph(6), ph(7), ph(8), ph(9), ph(10),
|
||||
guidIdent, valueIdent, valueIdent, keyPathIdent, keyPathIdent,
|
||||
)
|
||||
case "mysql", "sqlite":
|
||||
// For MySQL and SQLite, use INSERT OR REPLACE (requires all columns)
|
||||
query = fmt.Sprintf(`
|
||||
REPLACE INTO %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, 0, %s, %s, %s)`,
|
||||
tableIdent, guidIdent, keyPathIdent, groupIdent, resourceIdent, namespaceIdent, nameIdent,
|
||||
rvIdent, prevRVIdent, valueIdent, actionIdent, folderIdent,
|
||||
ph(1), ph(2), ph(3), ph(4), ph(5), ph(6), ph(7), ph(8), ph(9), ph(10),
|
||||
)
|
||||
default:
|
||||
return fmt.Errorf("unsupported dialect: %s", w.kv.dialect.DialectName())
|
||||
}
|
||||
|
||||
_, err := tx.ExecContext(ctx, query,
|
||||
guid, w.key, parsed.Group, parsed.Resource, parsed.Namespace, parsed.Name,
|
||||
parsed.ResourceVersion, value, parsed.Action, parsed.Folder,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// upsertResource inserts/updates a row in the resource table
|
||||
func (w *sqlWriteCloser) upsertResource(ctx context.Context, tx db.Tx, parsed *parsedKey, guid string, value []byte) error {
|
||||
// Build identifiers
|
||||
tableIdent, _ := w.kv.ident("resource")
|
||||
guidIdent, _ := w.kv.ident("guid")
|
||||
groupIdent, _ := w.kv.ident("group")
|
||||
resourceIdent, _ := w.kv.ident("resource")
|
||||
namespaceIdent, _ := w.kv.ident("namespace")
|
||||
nameIdent, _ := w.kv.ident("name")
|
||||
rvIdent, _ := w.kv.ident("resource_version")
|
||||
valueIdent, _ := w.kv.ident("value")
|
||||
actionIdent, _ := w.kv.ident("action")
|
||||
|
||||
var query string
|
||||
ph := func(n int) string { return w.kv.dialect.ArgPlaceholder(n) }
|
||||
|
||||
switch w.kv.dialect.DialectName() {
|
||||
case "postgres":
|
||||
query = fmt.Sprintf(`
|
||||
INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (%s, %s, %s, %s) DO UPDATE SET
|
||||
%s = EXCLUDED.%s, %s = EXCLUDED.%s, %s = EXCLUDED.%s, %s = EXCLUDED.%s`,
|
||||
tableIdent, guidIdent, groupIdent, resourceIdent, namespaceIdent, nameIdent, rvIdent, valueIdent, actionIdent,
|
||||
ph(1), ph(2), ph(3), ph(4), ph(5), ph(6), ph(7), ph(8),
|
||||
namespaceIdent, groupIdent, resourceIdent, nameIdent,
|
||||
guidIdent, guidIdent, rvIdent, rvIdent, valueIdent, valueIdent, actionIdent, actionIdent,
|
||||
)
|
||||
case "mysql", "sqlite":
|
||||
query = fmt.Sprintf(`
|
||||
REPLACE INTO %s (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)`,
|
||||
tableIdent, guidIdent, groupIdent, resourceIdent, namespaceIdent, nameIdent, rvIdent, valueIdent, actionIdent,
|
||||
ph(1), ph(2), ph(3), ph(4), ph(5), ph(6), ph(7), ph(8),
|
||||
)
|
||||
default:
|
||||
return fmt.Errorf("unsupported dialect: %s", w.kv.dialect.DialectName())
|
||||
}
|
||||
|
||||
_, err := tx.ExecContext(ctx, query,
|
||||
guid, parsed.Group, parsed.Resource, parsed.Namespace, parsed.Name,
|
||||
parsed.ResourceVersion, value, parsed.Action,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// deleteResource deletes a row from the resource table
|
||||
func (w *sqlWriteCloser) deleteResource(ctx context.Context, tx db.Tx, parsed *parsedKey) error {
|
||||
tableIdent, _ := w.kv.ident("resource")
|
||||
groupIdent, _ := w.kv.ident("group")
|
||||
resourceIdent, _ := w.kv.ident("resource")
|
||||
namespaceIdent, _ := w.kv.ident("namespace")
|
||||
nameIdent, _ := w.kv.ident("name")
|
||||
|
||||
ph := func(n int) string { return w.kv.dialect.ArgPlaceholder(n) }
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
DELETE FROM %s WHERE %s = %s AND %s = %s AND %s = %s AND %s = %s`,
|
||||
tableIdent, groupIdent, ph(1), resourceIdent, ph(2), namespaceIdent, ph(3), nameIdent, ph(4),
|
||||
)
|
||||
|
||||
_, err := tx.ExecContext(ctx, query, parsed.Group, parsed.Resource, parsed.Namespace, parsed.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
// upsertResourceVersion inserts/updates the resource_version table
|
||||
func (w *sqlWriteCloser) upsertResourceVersion(ctx context.Context, tx db.Tx, parsed *parsedKey) error {
|
||||
tableIdent, _ := w.kv.ident("resource_version")
|
||||
groupIdent, _ := w.kv.ident("group")
|
||||
resourceIdent, _ := w.kv.ident("resource")
|
||||
rvIdent, _ := w.kv.ident("resource_version")
|
||||
|
||||
ph := func(n int) string { return w.kv.dialect.ArgPlaceholder(n) }
|
||||
|
||||
var query string
|
||||
switch w.kv.dialect.DialectName() {
|
||||
case "postgres":
|
||||
query = fmt.Sprintf(`
|
||||
INSERT INTO %s (%s, %s, %s) VALUES (%s, %s, %s)
|
||||
ON CONFLICT (%s, %s) DO UPDATE SET %s = EXCLUDED.%s`,
|
||||
tableIdent, groupIdent, resourceIdent, rvIdent, ph(1), ph(2), ph(3),
|
||||
groupIdent, resourceIdent, rvIdent, rvIdent,
|
||||
)
|
||||
case "mysql", "sqlite":
|
||||
query = fmt.Sprintf(`
|
||||
REPLACE INTO %s (%s, %s, %s) VALUES (%s, %s, %s)`,
|
||||
tableIdent, groupIdent, resourceIdent, rvIdent, ph(1), ph(2), ph(3),
|
||||
)
|
||||
default:
|
||||
return fmt.Errorf("unsupported dialect: %s", w.kv.dialect.DialectName())
|
||||
}
|
||||
|
||||
_, err := tx.ExecContext(ctx, query, parsed.Group, parsed.Resource, parsed.ResourceVersion)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete a value
|
||||
func (k *sqlKV) Delete(ctx context.Context, section string, key string) error {
|
||||
if section == "" {
|
||||
return fmt.Errorf("section is required")
|
||||
}
|
||||
if key == "" {
|
||||
return fmt.Errorf("key is required")
|
||||
}
|
||||
|
||||
tableName, err := k.getTableName(section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyPathIdent, err := k.ident("key_path")
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid column identifier: %w", err)
|
||||
}
|
||||
|
||||
// First check if key exists (to return ErrNotFound if missing)
|
||||
checkQuery := fmt.Sprintf(
|
||||
"SELECT 1 FROM %s WHERE %s = %s",
|
||||
tableName,
|
||||
keyPathIdent,
|
||||
k.dialect.ArgPlaceholder(1),
|
||||
)
|
||||
|
||||
var exists int
|
||||
err = k.db.QueryRowContext(ctx, checkQuery, key).Scan(&exists)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrNotFound
|
||||
}
|
||||
return fmt.Errorf("check existence failed: %w", err)
|
||||
}
|
||||
|
||||
// Delete the key
|
||||
deleteQuery := fmt.Sprintf(
|
||||
"DELETE FROM %s WHERE %s = %s",
|
||||
tableName,
|
||||
keyPathIdent,
|
||||
k.dialect.ArgPlaceholder(1),
|
||||
)
|
||||
|
||||
_, err = k.db.ExecContext(ctx, deleteQuery, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDelete removes multiple keys from the store
|
||||
func (k *sqlKV) BatchDelete(ctx context.Context, section string, keys []string) error {
|
||||
if section == "" {
|
||||
return fmt.Errorf("section is required")
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
return nil // Nothing to delete
|
||||
}
|
||||
|
||||
tableName, err := k.getTableName(section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyPathIdent, err := k.ident("key_path")
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid column identifier: %w", err)
|
||||
}
|
||||
|
||||
// Build IN clause placeholders
|
||||
placeholders := make([]string, len(keys))
|
||||
args := make([]interface{}, len(keys))
|
||||
for i, key := range keys {
|
||||
placeholders[i] = k.dialect.ArgPlaceholder(i + 1)
|
||||
args[i] = key
|
||||
}
|
||||
|
||||
// Build the query
|
||||
query := fmt.Sprintf(
|
||||
"DELETE FROM %s WHERE %s IN (%s)",
|
||||
tableName,
|
||||
keyPathIdent,
|
||||
strings.Join(placeholders, ", "),
|
||||
)
|
||||
|
||||
// Execute the query (idempotent - non-existent keys are silently ignored)
|
||||
_, err = k.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("batch delete failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnixTimestamp returns the current time in seconds since Epoch
|
||||
func (k *sqlKV) UnixTimestamp(ctx context.Context) (int64, error) {
|
||||
return time.Now().Unix(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user