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) { 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 { vcl.ShowMessage(fmt.Sprintf("Retreiving database info: %v", err)) return } f.propertiesBox.SetText(infostr) // Leave data tab disabled (default behaviour) } 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 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 { vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err)) return } 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 { vcl.ShowMessage(fmt.Sprintf("Loading %q/%q: %v", ndata.bucketPath[0], key, err)) return } f.contentBox.SetCells(2, rpos, 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 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, "<<>>") } } // Valid vcl_stringgrid_columnwidths(f.contentBox) f.contentBox.SetEnabled(true) } else { vcl.ShowMessage(fmt.Sprintf("Unexpected nav position %q", ndata.bucketPath)) return } } func (n *redisLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error { return ErrNotSupported } 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: "", } // 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) }