598 lines
17 KiB
Go
598 lines
17 KiB
Go
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(),
|
||
}
|
||
}
|