package db import ( "context" "fmt" "strconv" "strings" "time" "github.com/redis/go-redis/v9" ) func redisOpts(c ConnReq) (*redis.Options, error) { host := c.Host if host == "" { host = "127.0.0.1" } port := c.Port if port == 0 { port = 6379 } db := 0 if c.Database != "" { if n, err := strconv.Atoi(c.Database); err == nil { db = n } } opts := &redis.Options{ Addr: fmt.Sprintf("%s:%d", host, port), Password: c.Password, Username: c.User, DB: db, DialTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } if SSLEnabled(c) { tlsCfg, err := tlsConfigFrom(c) if err != nil { return nil, err } opts.TLSConfig = tlsCfg } return opts, nil } func openRedis(c ConnReq) (*redis.Client, error) { opts, err := redisOpts(c) if err != nil { return nil, err } return redis.NewClient(opts), nil } func testRedis(c ConnReq) error { cli, err := openRedis(c) if err != nil { return err } defer cli.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() return cli.Ping(ctx).Err() } func listDatabasesRedis(c ConnReq) ([]string, error) { cli, err := openRedis(c) if err != nil { return nil, err } defer cli.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // INFO keyspace 可拿已用 DB;统一暴露 0-15 便于切换 _ = ctx out := make([]string, 16) for i := 0; i < 16; i++ { out[i] = strconv.Itoa(i) } return out, nil } func listTablesRedis(c ConnReq) ([]string, error) { cli, err := openRedis(c) if err != nil { return nil, err } defer cli.Close() ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) defer cancel() var keys []string var cursor uint64 for { batch, next, err := cli.Scan(ctx, cursor, "*", 200).Result() if err != nil { return nil, err } keys = append(keys, batch...) cursor = next if cursor == 0 || len(keys) >= 500 { break } } if keys == nil { keys = []string{} } return keys, nil } func execRedis(parent context.Context, c ConnReq) ExecResult { start := time.Now() sql := strings.TrimSpace(c.SQL) if sql == "" { return ExecResult{Error: "empty sql", TimeMs: 0} } cli, err := openRedis(c) if err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } defer cli.Close() if parent == nil { parent = context.Background() } ctx, cancel := context.WithTimeout(parent, 10*time.Second) defer cancel() fields := splitRedisCmd(sql) if len(fields) == 0 { return ExecResult{Error: "empty command", TimeMs: time.Since(start).Milliseconds()} } cmd := strings.ToUpper(fields[0]) args := fields[1:] switch cmd { case "PING": val, err := cli.Ping(ctx).Result() if err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } return ExecResult{Columns: []string{"result"}, Rows: []map[string]interface{}{{"result": val}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()} case "GET": if len(args) < 1 { return ExecResult{Error: "GET key", TimeMs: time.Since(start).Milliseconds()} } val, err := cli.Get(ctx, args[0]).Result() if err == redis.Nil { return ExecResult{Columns: []string{"key", "value"}, Rows: []map[string]interface{}{}, RowCount: 0, TimeMs: time.Since(start).Milliseconds()} } if err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } return ExecResult{Columns: []string{"key", "value"}, Rows: []map[string]interface{}{{"key": args[0], "value": val}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()} case "SET": if len(args) < 2 { return ExecResult{Error: "SET key value", TimeMs: time.Since(start).Milliseconds()} } if err := cli.Set(ctx, args[0], strings.Join(args[1:], " "), 0).Err(); err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } return ExecResult{Columns: []string{"affected_rows"}, Rows: []map[string]interface{}{{"affected_rows": 1}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()} case "DEL": if len(args) < 1 { return ExecResult{Error: "DEL key [key...]", TimeMs: time.Since(start).Milliseconds()} } n, err := cli.Del(ctx, args...).Result() if err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } return ExecResult{Columns: []string{"affected_rows"}, Rows: []map[string]interface{}{{"affected_rows": n}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()} case "KEYS": pat := "*" if len(args) > 0 { pat = args[0] } keys, err := cli.Keys(ctx, pat).Result() if err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } rows := make([]map[string]interface{}, 0, len(keys)) for _, k := range keys { rows = append(rows, map[string]interface{}{"key": k}) } return ExecResult{Columns: []string{"key"}, Rows: rows, RowCount: len(rows), TimeMs: time.Since(start).Milliseconds()} case "SCAN": keys, err := listTablesRedis(c) if err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } rows := make([]map[string]interface{}, 0, len(keys)) for _, k := range keys { rows = append(rows, map[string]interface{}{"key": k}) } return ExecResult{Columns: []string{"key"}, Rows: rows, RowCount: len(rows), TimeMs: time.Since(start).Milliseconds()} case "HGETALL": if len(args) < 1 { return ExecResult{Error: "HGETALL key", TimeMs: time.Since(start).Milliseconds()} } c2 := c c2.Key = args[0] c2.Limit = redisValueRowCap v := GetRedisKey(c2) if v.Error != "" && v.Type == "" { return ExecResult{Error: v.Error, TimeMs: time.Since(start).Milliseconds()} } return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, Error: v.Error} case "LRANGE": if len(args) < 1 { return ExecResult{Error: "LRANGE key [start stop]", TimeMs: time.Since(start).Milliseconds()} } c2 := c c2.Key = args[0] c2.Limit = redisValueRowCap v := GetRedisKey(c2) return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, Error: v.Error} case "SMEMBERS": if len(args) < 1 { return ExecResult{Error: "SMEMBERS key", TimeMs: time.Since(start).Milliseconds()} } c2 := c c2.Key = args[0] v := GetRedisKey(c2) return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, Error: v.Error} case "ZRANGE": if len(args) < 1 { return ExecResult{Error: "ZRANGE key [start stop]", TimeMs: time.Since(start).Milliseconds()} } c2 := c c2.Key = args[0] v := GetRedisKey(c2) return ExecResult{Columns: v.Columns, Rows: v.Rows, RowCount: v.RowCount, TimeMs: time.Since(start).Milliseconds(), Truncated: v.Truncated, Limit: v.Limit, Error: v.Error} case "TYPE": if len(args) < 1 { return ExecResult{Error: "TYPE key", TimeMs: time.Since(start).Milliseconds()} } t, err := cli.Type(ctx, args[0]).Result() if err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } return ExecResult{Columns: []string{"key", "type"}, Rows: []map[string]interface{}{{"key": args[0], "type": t}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()} case "TTL": if len(args) < 1 { return ExecResult{Error: "TTL key", TimeMs: time.Since(start).Milliseconds()} } n, err := cli.TTL(ctx, args[0]).Result() if err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } return ExecResult{Columns: []string{"key", "ttl_sec"}, Rows: []map[string]interface{}{{"key": args[0], "ttl_sec": int64(n.Seconds())}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds()} default: return ExecResult{Error: fmt.Sprintf("暂不支持命令 %s(可用: PING/GET/SET/DEL/KEYS/SCAN/TYPE/TTL/HGETALL/LRANGE/SMEMBERS/ZRANGE)", cmd), TimeMs: time.Since(start).Milliseconds()} } } func splitRedisCmd(s string) []string { var out []string var cur strings.Builder inQ := false esc := false for _, r := range s { switch { case esc: cur.WriteRune(r) esc = false case r == '\\': esc = true case r == '"': inQ = !inQ case !inQ && (r == ' ' || r == '\t' || r == '\n'): if cur.Len() > 0 { out = append(out, cur.String()) cur.Reset() } default: cur.WriteRune(r) } } if cur.Len() > 0 { out = append(out, cur.String()) } return out } func isRedis(d string) bool { return driverKey(d) == "redis" } // IsRedisDriver exported for router guards. func IsRedisDriver(d string) bool { return isRedis(d) } func listColumnsRedis(c ConnReq, key string) ([]ColumnInfo, error) { cli, err := openRedis(c) if err != nil { return nil, err } defer cli.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() typ, err := cli.Type(ctx, key).Result() if err != nil { return nil, err } ttl, _ := cli.TTL(ctx, key).Result() return []ColumnInfo{ {Name: key, Type: typ, Comment: fmt.Sprintf("ttl=%ds", int64(ttl.Seconds()))}, }, nil } const redisKeyListCap = 500 const redisValueRowCap = 500 func redisKeyName(c ConnReq) string { if c.Key != "" { return c.Key } return c.Table } func redisScanPattern(c ConnReq) string { p := strings.TrimSpace(c.Pattern) if p == "" { return "*" } return p } func redisValueLimit(c ConnReq) int { n := c.Limit if n <= 0 { n = redisValueRowCap } if n > MaxRowLimit { n = MaxRowLimit } return n } func ttlSeconds(d time.Duration) int64 { // go-redis maps Redis TTL -1 (no expire) / -2 (missing) to -1ns/-2ns // (some versions use -1s/-2s). Truncating with d/time.Second turns those into 0. switch d { case time.Duration(-1), -1 * time.Second: return -1 case time.Duration(-2), -2 * time.Second: return -2 } return int64(d / time.Second) } // ListRedisKeys SCAN + pipeline TYPE/TTL. Cap ~500. func ListRedisKeys(c ConnReq) RedisKeysResult { start := time.Now() cli, err := openRedis(c) if err != nil { return RedisKeysResult{Keys: []RedisKeyInfo{}, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } defer cli.Close() ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) defer cancel() pat := redisScanPattern(c) var names []string var cursor uint64 truncated := false for { batch, next, err := cli.Scan(ctx, cursor, pat, 200).Result() if err != nil { return RedisKeysResult{Keys: []RedisKeyInfo{}, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } names = append(names, batch...) cursor = next if len(names) >= redisKeyListCap { truncated = true names = names[:redisKeyListCap] break } if cursor == 0 { break } } if names == nil { names = []string{} } out := make([]RedisKeyInfo, len(names)) if len(names) == 0 { return RedisKeysResult{Keys: out, Truncated: truncated, TimeMs: time.Since(start).Milliseconds()} } // Pipeline TYPE + TTL (avoid N+1) pipe := cli.Pipeline() typeCmds := make([]*redis.StatusCmd, len(names)) ttlCmds := make([]*redis.DurationCmd, len(names)) for i, k := range names { typeCmds[i] = pipe.Type(ctx, k) ttlCmds[i] = pipe.TTL(ctx, k) } _, _ = pipe.Exec(ctx) for i, k := range names { typ := "none" if t, err := typeCmds[i].Result(); err == nil { typ = t } ttl := int64(-2) if d, err := ttlCmds[i].Result(); err == nil { ttl = ttlSeconds(d) } out[i] = RedisKeyInfo{Name: k, Type: typ, TTL: ttl} } return RedisKeysResult{Keys: out, Truncated: truncated, TimeMs: time.Since(start).Milliseconds()} } // GetRedisKey typed value → ResultPane columns/rows. func GetRedisKey(c ConnReq) RedisKeyValue { start := time.Now() key := redisKeyName(c) if key == "" { return RedisKeyValue{Error: "缺少 key", TimeMs: time.Since(start).Milliseconds()} } cli, err := openRedis(c) if err != nil { return RedisKeyValue{Name: key, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } defer cli.Close() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() typ, err := cli.Type(ctx, key).Result() if err != nil { return RedisKeyValue{Name: key, Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } ttl := int64(-2) if d, err := cli.TTL(ctx, key).Result(); err == nil { ttl = ttlSeconds(d) } limit := redisValueLimit(c) base := RedisKeyValue{Name: key, Type: typ, TTL: ttl, Limit: limit} switch typ { case "none": base.Columns = []string{"value"} base.Rows = []map[string]interface{}{} base.RowCount = 0 base.Error = "key 不存在" base.TimeMs = time.Since(start).Milliseconds() return base case "string": val, err := cli.Get(ctx, key).Result() if err == redis.Nil { base.Columns = []string{"value"} base.Rows = []map[string]interface{}{} base.RowCount = 0 base.TimeMs = time.Since(start).Milliseconds() return base } if err != nil { base.Error = errMsg(err) base.TimeMs = time.Since(start).Milliseconds() return base } base.Columns = []string{"value"} base.Rows = []map[string]interface{}{{"value": val}} base.RowCount = 1 base.TimeMs = time.Since(start).Milliseconds() return base case "hash": fields, err := cli.HGetAll(ctx, key).Result() if err != nil { base.Error = errMsg(err) base.TimeMs = time.Since(start).Milliseconds() return base } base.Columns = []string{"field", "value"} rows := make([]map[string]interface{}, 0, len(fields)) for f, v := range fields { rows = append(rows, map[string]interface{}{"field": f, "value": v}) if len(rows) >= limit { base.Truncated = true break } } base.Rows = rows base.RowCount = len(rows) base.TimeMs = time.Since(start).Milliseconds() return base case "list": vals, err := cli.LRange(ctx, key, 0, int64(limit)-1).Result() if err != nil { base.Error = errMsg(err) base.TimeMs = time.Since(start).Milliseconds() return base } truncated := false n, _ := cli.LLen(ctx, key).Result() if n > int64(len(vals)) { truncated = true } base.Columns = []string{"index", "value"} rows := make([]map[string]interface{}, 0, len(vals)) for i, v := range vals { rows = append(rows, map[string]interface{}{"index": i, "value": v}) } base.Rows = rows base.RowCount = len(rows) base.Truncated = truncated base.TimeMs = time.Since(start).Milliseconds() return base case "set": base.Columns = []string{"member"} var rows []map[string]interface{} var scursor uint64 truncated := false for { batch, next, err := cli.SScan(ctx, key, scursor, "*", 200).Result() if err != nil { base.Error = errMsg(err) base.TimeMs = time.Since(start).Milliseconds() return base } for _, m := range batch { rows = append(rows, map[string]interface{}{"member": m}) if len(rows) >= limit { truncated = true break } } scursor = next if truncated || scursor == 0 { break } } if rows == nil { rows = []map[string]interface{}{} } base.Rows = rows base.RowCount = len(rows) base.Truncated = truncated base.TimeMs = time.Since(start).Milliseconds() return base case "zset": vals, err := cli.ZRangeWithScores(ctx, key, 0, int64(limit-1)).Result() if err != nil { base.Error = errMsg(err) base.TimeMs = time.Since(start).Milliseconds() return base } truncated := false n, _ := cli.ZCard(ctx, key).Result() if n > int64(len(vals)) { truncated = true } base.Columns = []string{"score", "member"} rows := make([]map[string]interface{}, 0, len(vals)) for _, z := range vals { rows = append(rows, map[string]interface{}{"score": z.Score, "member": z.Member}) } base.Rows = rows base.RowCount = len(rows) base.Truncated = truncated base.TimeMs = time.Since(start).Milliseconds() return base default: // stream / recon / module types: meta only base.Columns = []string{"info"} base.Rows = []map[string]interface{}{{"info": fmt.Sprintf("类型 %s 暂不展开,可用命令查看", typ)}} base.RowCount = 1 base.TimeMs = time.Since(start).Milliseconds() return base } } // DeleteRedisKey DEL one key. func DeleteRedisKey(c ConnReq) ExecResult { start := time.Now() key := redisKeyName(c) if key == "" { return ExecResult{Error: "缺少 key", TimeMs: time.Since(start).Milliseconds()} } cli, err := openRedis(c) if err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } defer cli.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() n, err := cli.Del(ctx, key).Result() if err != nil { return ExecResult{Error: errMsg(err), TimeMs: time.Since(start).Milliseconds()} } return ExecResult{ Columns: []string{"affected_rows"}, Rows: []map[string]interface{}{{"affected_rows": n}}, RowCount: 1, TimeMs: time.Since(start).Milliseconds(), } }