2024-06-22 05:36:19 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-06-23 01:07:33 +00:00
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"unsafe"
|
|
|
|
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
"github.com/ying32/govcl/vcl"
|
2024-06-23 02:54:33 +00:00
|
|
|
"github.com/ying32/govcl/vcl/types"
|
2024-06-22 05:36:19 +00:00
|
|
|
)
|
2024-06-23 01:07:33 +00:00
|
|
|
|
|
|
|
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) {
|
2024-06-23 02:54:33 +00:00
|
|
|
ctx := context.Background()
|
2024-06-23 01:07:33 +00:00
|
|
|
|
2024-06-23 02:54:33 +00:00
|
|
|
if len(ndata.bucketPath) == 0 {
|
|
|
|
// Top-level: Show info() on main Properties tab
|
|
|
|
infostr, err := ld.db.Info(ctx).Result()
|
|
|
|
if err != nil {
|
|
|
|
vcl.ShowMessage(fmt.Sprintf("Retreiving database info: %v", err))
|
|
|
|
return
|
|
|
|
}
|
2024-06-23 01:07:33 +00:00
|
|
|
|
2024-06-23 02:54:33 +00:00
|
|
|
f.propertiesBox.SetText(infostr)
|
2024-06-23 01:07:33 +00:00
|
|
|
|
2024-06-23 02:54:33 +00:00
|
|
|
// Disable data tab
|
2024-06-23 01:07:33 +00:00
|
|
|
f.contentBox.SetEnabled(false)
|
|
|
|
f.contentBox.Clear()
|
|
|
|
|
2024-06-23 02:54:33 +00:00
|
|
|
} 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 {
|
|
|
|
vcl.ShowMessage(fmt.Sprintf("Switching to database %q: %v", ndata.bucketPath[0], err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
allKeys, err := ld.db.Keys(ctx, "*").Result()
|
|
|
|
if err != nil {
|
|
|
|
vcl.ShowMessage(fmt.Sprintf("Listing keys in database %q: %v", ndata.bucketPath[0], err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
f.propertiesBox.SetText(fmt.Sprintf("Database %s\nTotal keys: %d\n", ndata.bucketPath[0], len(allKeys)))
|
|
|
|
|
|
|
|
// Redis always uses Key + Value as the columns
|
|
|
|
|
|
|
|
f.contentBox.SetEnabled(false)
|
|
|
|
f.contentBox.Clear()
|
2024-06-23 01:07:33 +00:00
|
|
|
|
|
|
|
f.contentBox.Columns().Clear()
|
|
|
|
colKey := f.contentBox.Columns().Add()
|
|
|
|
colKey.SetCaption("Key")
|
|
|
|
colKey.SetWidth(MY_WIDTH)
|
|
|
|
colKey.SetAlignment(types.TaLeftJustify)
|
2024-06-23 03:57:20 +00:00
|
|
|
colType := f.contentBox.Columns().Add()
|
|
|
|
colType.SetCaption("Type")
|
2024-06-23 01:07:33 +00:00
|
|
|
colVal := f.contentBox.Columns().Add()
|
|
|
|
colVal.SetCaption("Value")
|
|
|
|
|
2024-06-23 02:54:33 +00:00
|
|
|
for _, key := range allKeys {
|
2024-06-23 03:57:20 +00:00
|
|
|
typeName, err := ld.db.Type(ctx, key).Result()
|
2024-06-23 02:54:33 +00:00
|
|
|
if err != nil {
|
2024-06-23 03:57:20 +00:00
|
|
|
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
|
2024-06-23 02:54:33 +00:00
|
|
|
return
|
2024-06-23 01:07:33 +00:00
|
|
|
}
|
|
|
|
|
2024-06-23 02:54:33 +00:00
|
|
|
dataEntry := f.contentBox.Items().Add()
|
2024-06-23 03:57:20 +00:00
|
|
|
dataEntry.SetCaption(key) // formatUtf8
|
|
|
|
dataEntry.SubItems().Add(typeName)
|
|
|
|
|
|
|
|
switch typeName {
|
|
|
|
case "string":
|
|
|
|
val, err := ld.db.Get(ctx, key).Result()
|
|
|
|
if err != nil {
|
|
|
|
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
dataEntry.SubItems().Add(val)
|
|
|
|
|
|
|
|
case "hash":
|
|
|
|
val, err := ld.db.HGetAll(ctx, key).Result()
|
|
|
|
if err != nil {
|
|
|
|
vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// It's a map[string]string
|
|
|
|
dataEntry.SubItems().Add(formatAny(val))
|
|
|
|
|
|
|
|
case "lists":
|
|
|
|
fallthrough
|
|
|
|
case "sets":
|
|
|
|
fallthrough
|
|
|
|
case "sorted sets":
|
|
|
|
fallthrough
|
|
|
|
case "stream":
|
|
|
|
fallthrough
|
|
|
|
|
|
|
|
default:
|
|
|
|
dataEntry.SubItems().Add("<<<other object type>>>")
|
|
|
|
}
|
|
|
|
|
2024-06-23 01:07:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Valid
|
2024-06-23 02:54:33 +00:00
|
|
|
f.contentBox.SetEnabled(true)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
vcl.ShowMessage(fmt.Sprintf("Unexpected nav position %q", ndata.bucketPath))
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
2024-06-23 01:07:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
2024-06-23 02:54:33 +00:00
|
|
|
// 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)
|
|
|
|
|
|
|
|
}
|
2024-06-23 01:07:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (ld *redisLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
// Need to parse the query into separate string+args fields for the protocol
|
|
|
|
// TODO This needs to better handle quotes, escaping, ...
|
|
|
|
fields := strings.Fields(query)
|
|
|
|
|
|
|
|
fields_boxed := box_interface(fields)
|
|
|
|
|
|
|
|
ret, err := ld.db.Do(ctx, fields_boxed...).Result()
|
|
|
|
if err != nil {
|
|
|
|
vcl.ShowMessage(fmt.Sprintf("The redis query returned an error: %v", err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-06-23 03:57:33 +00:00
|
|
|
resultArea.SetEnabled(false)
|
|
|
|
resultArea.Clear()
|
|
|
|
|
|
|
|
resultArea.Columns().Clear()
|
|
|
|
colVal := resultArea.Columns().Add()
|
|
|
|
colVal.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 {
|
|
|
|
cell := resultArea.Items().Add()
|
|
|
|
cell.SetCaption(single) // formatUtf8
|
|
|
|
}
|
|
|
|
|
|
|
|
default:
|
|
|
|
// Single value
|
|
|
|
dataEntry := resultArea.Items().Add()
|
|
|
|
dataEntry.SetCaption(formatAny(ret)) // formatUtf8
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
resultArea.SetEnabled(true)
|
2024-06-23 01:07:33 +00:00
|
|
|
}
|
|
|
|
|
2024-06-23 03:28:15 +00:00
|
|
|
func (ld *redisLoadedDatabase) Close() {
|
|
|
|
_ = ld.db.Close()
|
|
|
|
ld.arena = nil
|
|
|
|
}
|
|
|
|
|
2024-06-23 01:07:33 +00:00
|
|
|
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)
|
2024-06-23 02:54:49 +00:00
|
|
|
f.Buckets.SetSelected(ld.nav) // Select new element
|
2024-06-23 01:07:33 +00:00
|
|
|
|
|
|
|
ld.Keepalive(navData)
|
|
|
|
}
|