package main import ( "context" "errors" "fmt" "strconv" "strings" "unsafe" "github.com/redis/go-redis/v9" "github.com/ying32/govcl/vcl" // "github.com/ying32/govcl/vcl/types" ) 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) { /* // Load properties bucketDisplayName := strings.Join(ndata.bucketPath, `/`) content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ld.db.Stats(), bucketDisplayName) f.propertiesBox.SetText(content) // Load data f.contentBox.SetEnabled(false) f.contentBox.Clear() // Bolt always uses Key + Value as the columns f.contentBox.Columns().Clear() colKey := f.contentBox.Columns().Add() colKey.SetCaption("Key") colKey.SetWidth(MY_WIDTH) colKey.SetAlignment(types.TaLeftJustify) colVal := f.contentBox.Columns().Add() colVal.SetCaption("Value") err := ld.db.View(func(tx *bbolt.Tx) error { b := boltTargetBucket(tx, ndata.bucketPath) if b == nil { // no such bucket return nil } // Valid f.contentBox.Clear() c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { dataEntry := f.contentBox.Items().Add() dataEntry.SetCaption(formatUtf8(k)) dataEntry.SubItems().Add(formatUtf8(v)) } return nil }) if err != nil { vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error())) return } // Valid f.contentBox.SetEnabled(true)*/ } func (ld *redisLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { // In the bolt implementation, the nav is a recursive tree of child buckets return nil, errors.New("TODO") //return boltChildBucketNames(ld.db, ndata.bucketPath) } 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 } // Put ret into the data field somehow fmt.Printf("result\n%#v\n", ret) } 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: "", } // 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) ld.Keepalive(navData) }