yvbolt/db_redis.go

281 lines
7.0 KiB
Go

package main
import (
"context"
"fmt"
"strconv"
"unsafe"
"yvbolt/lexer"
"github.com/redis/go-redis/v9"
"github.com/ying32/govcl/vcl"
)
type redisLoadedDatabase struct {
displayName string
db *redis.Client
nav *vcl.TTreeNode
currentDb int
maxDb int
serverVersion string // populated at connection-time from INFO command
arena []*navData // keepalive
}
func (ld *redisLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *redisLoadedDatabase) DriverName() string {
return "Redis " + ld.serverVersion
}
func (ld *redisLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *redisLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *redisLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) error {
ctx := context.Background()
if len(ndata.bucketPath) == 0 {
// Top-level: Show info() on main Properties tab
infostr, err := ld.db.Info(ctx).Result()
if err != nil {
return fmt.Errorf("Retreiving database info: %w", err)
}
f.propertiesBox.SetText(infostr)
// Leave data tab disabled (default behaviour)
return nil
} else if len(ndata.bucketPath) == 1 {
// One selected database
// Figure out its content
err := ld.db.Do(ctx, "SELECT", ndata.bucketPath[0]).Err()
if err != nil {
return fmt.Errorf("Switching to database %q: %w", ndata.bucketPath[0], err)
}
allKeys, err := ld.db.Keys(ctx, "*").Result()
if err != nil {
return fmt.Errorf("Listing keys in database %q: %w", ndata.bucketPath[0], err)
}
f.propertiesBox.SetText(fmt.Sprintf("Database %s\nTotal keys: %d\n", ndata.bucketPath[0], len(allKeys)))
// Redis always uses Key + Value as the columns
colKey := f.contentBox.Columns().Add()
colKey.Title().SetCaption("Key")
colType := f.contentBox.Columns().Add()
colType.Title().SetCaption("Type")
colVal := f.contentBox.Columns().Add()
colVal.Title().SetCaption("Value")
for _, key := range allKeys {
typeName, err := ld.db.Type(ctx, key).Result()
if err != nil {
return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
}
rpos := f.contentBox.RowCount()
f.contentBox.SetRowCount(rpos + 1)
f.contentBox.SetCells(0, rpos, formatUtf8([]byte(key)))
f.contentBox.SetCells(1, rpos, typeName)
switch typeName {
case "string":
val, err := ld.db.Get(ctx, key).Result()
if err != nil {
return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
}
f.contentBox.SetCells(2, rpos, val)
case "hash":
val, err := ld.db.HGetAll(ctx, key).Result()
if err != nil {
return fmt.Errorf("Loading %q/%q: %w", ndata.bucketPath[0], key, err)
}
// It's a map[string]string
f.contentBox.SetCells(2, rpos, formatAny(val))
case "lists":
fallthrough
case "sets":
fallthrough
case "sorted sets":
fallthrough
case "stream":
fallthrough
default:
f.contentBox.SetCells(2, rpos, "<<<other object type>>>")
}
}
// Valid
vcl_stringgrid_columnwidths(f.contentBox)
f.contentBox.SetEnabled(true)
return nil
} else {
return fmt.Errorf("Unexpected nav position %q", ndata.bucketPath)
}
}
func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
// ctx := context.Background()
if len(ndata.bucketPath) == 0 {
// Top-level: list of all child databases (usually 16x)
ret := make([]string, 0, ld.maxDb)
for i := 0; i < ld.maxDb; i++ {
ret = append(ret, fmt.Sprintf("%d", i))
}
return ret, nil
} else if len(ndata.bucketPath) == 1 {
// One selected database
// No child keys underneath it
return []string{}, nil
} else {
return nil, fmt.Errorf("Unexpected nav position %q", ndata.bucketPath)
}
}
func (ld *redisLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
ctx := context.Background()
// Need to parse the query into separate string+args fields for the protocol
fields, err := lexer.Fields(query)
if err != nil {
return fmt.Errorf("Parsing the query: %w", err)
}
fields_boxed := box_interface(fields)
ret, err := ld.db.Do(ctx, fields_boxed...).Result()
if err != nil {
return fmt.Errorf("The redis query returned an error: %w", err)
}
vcl_stringgrid_clear(resultArea)
colVal := resultArea.Columns().Add()
colVal.Title().SetCaption("Result")
// The result is probably a single value or a string slice
switch ret := ret.(type) {
case []string:
// Multiple values
for _, single := range ret {
rpos := resultArea.RowCount()
resultArea.SetRowCount(rpos + 1)
resultArea.SetCells(0, rpos, formatUtf8([]byte(single)))
}
default:
// Single value
rpos := resultArea.RowCount()
resultArea.SetRowCount(rpos + 1)
resultArea.SetCells(0, rpos, formatAny(ret)) // formatUtf8
}
vcl_stringgrid_columnwidths(resultArea)
resultArea.SetEnabled(true)
return nil
}
func (ld *redisLoadedDatabase) Close() {
_ = ld.db.Close()
ld.arena = nil
}
var _ loadedDatabase = &redisLoadedDatabase{} // interface assertion
//
func (f *TMainForm) redisConnect(opts *redis.Options) {
// TODO load in background thread to stop blocking the UI
ctx := context.Background()
ld := &redisLoadedDatabase{
displayName: opts.Addr,
currentDb: 0,
maxDb: 1,
serverVersion: "<unknown>",
}
// Patch in a hook to remember current DB after keepalive reconnection
opts.DB = 0 // Default
opts.OnConnect = func(ctx context.Context, cn *redis.Conn) error {
return cn.Select(ctx, ld.currentDb).Err()
}
// NewClient doesn't necessarily connect, so it can't throw an err
ld.db = redis.NewClient(opts)
// Make an INFO request (mandatory)
info, err := ld.db.InfoMap(ctx).Result()
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to connect to Redis server: During INFO: %v", err))
return
}
if serverInfo, ok := info["Server"]; ok {
if v, ok := serverInfo["redis_version"]; ok {
ld.serverVersion = v
}
}
// List available databases (usually 1..16) with "0" as default
// If this fails, probably the target redis does not support multiple databases
// (e.g. Redis Cluster). Assume max=0.
if maxDatabases, err := ld.db.ConfigGet(ctx, "databases").Result(); err == nil {
// Got a result. Must parse it
m, err := strconv.ParseInt(maxDatabases["databases"], 10, 64)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to connect to Redis server: During CONFIG GET databases: %v", err))
return
}
ld.maxDb = int(m)
}
// Done with setup
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
f.Buckets.SetSelected(ld.nav) // Select new element
ld.Keepalive(navData)
}