fix: Redis查询核心bug修复 — 命令拆分+HGETALL结构化+DB号+插入换行
1. executeRedisQuery: client.Do()没传命令名导致ERR unknown command 2. HGETALL结果用reflect处理map类型,结构化输出field/value表格 3. 支持指定Redis DB号(之前固定DB=0) 4. 前端Redis命令插入用换行分隔,MySQL保持空格 5. cost_ms到execution_time_ms字段映射修复
This commit is contained in:
1 parent
6b5d8bede4
commit
828a0597c5
2 files changed
+97
-17
No files matched your search
@@ -3,6 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"sql-platform/internal/model"
|
"sql-platform/internal/model"
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"sql-platform/internal/middleware"
|
"sql-platform/internal/middleware"
|
||||||
"strings"
|
"strings"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /api/v1/instances/:id/detail/databases
|
// GET /api/v1/instances/:id/detail/databases
|
||||||
@@ -533,13 +535,14 @@ func ExecuteQuery(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
|
|
||||||
case "redis":
|
case "redis":
|
||||||
result, err := executeRedisQuery(&instance, input.SQL)
|
result, err := executeRedisQuery(&instance, input.Database, input.SQL)
|
||||||
costMs := time.Since(start).Milliseconds()
|
costMs := time.Since(start).Milliseconds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{"columns": []string{}, "rows": []interface{}{}, "total": 0, "cost_ms": costMs, "error": err.Error()})
|
c.JSON(http.StatusOK, gin.H{"columns": []string{}, "rows": []interface{}{}, "total": 0, "cost_ms": costMs, "error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"result": result, "cost_ms": costMs, "columns": []string{}, "rows": []interface{}{}, "total": 0})
|
saveQueryHistory(userID, input.InstanceID, input.Database, input.SQL, costMs, 1)
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "Unsupported database type: " + instance.DBType})
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "Unsupported database type: " + instance.DBType})
|
||||||
@@ -593,7 +596,7 @@ func executeSQLQuery(instance *model.Instance, dbName string, sqlStr string) (gi
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeRedisQuery(instance *model.Instance, command string) (string, error) {
|
func executeRedisQuery(instance *model.Instance, dbSelect string, command string) (gin.H, error) {
|
||||||
password := instance.Password
|
password := instance.Password
|
||||||
if strings.HasPrefix(password, "enc:") {
|
if strings.HasPrefix(password, "enc:") {
|
||||||
decrypted, err := decryptPassword(strings.TrimPrefix(password, "enc:"))
|
decrypted, err := decryptPassword(strings.TrimPrefix(password, "enc:"))
|
||||||
@@ -601,21 +604,30 @@ func executeRedisQuery(instance *model.Instance, command string) (string, error)
|
|||||||
password = decrypted
|
password = decrypted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse Redis DB number from dbSelect (e.g. "0", "1")
|
||||||
|
redisDB := 0
|
||||||
|
if dbSelect != "" {
|
||||||
|
if n, err := strconv.Atoi(dbSelect); err == nil && n >= 0 {
|
||||||
|
redisDB = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client := redis.NewClient(&redis.Options{
|
client := redis.NewClient(&redis.Options{
|
||||||
Addr: fmt.Sprintf("%s:%d", instance.Host, instance.Port),
|
Addr: fmt.Sprintf("%s:%d", instance.Host, instance.Port),
|
||||||
Password: password,
|
Password: password,
|
||||||
DB: 0,
|
DB: redisDB,
|
||||||
})
|
})
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := client.Ping(ctx).Err(); err != nil {
|
if err := client.Ping(ctx).Err(); err != nil {
|
||||||
return "", fmt.Errorf("Redis connection failed: %v", err)
|
return nil, fmt.Errorf("Redis connection failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Fields(command)
|
parts := strings.Fields(command)
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return "", fmt.Errorf("empty command")
|
return nil, fmt.Errorf("empty command")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := strings.ToUpper(parts[0])
|
cmd := strings.ToUpper(parts[0])
|
||||||
@@ -625,20 +637,84 @@ func executeRedisQuery(instance *model.Instance, command string) (string, error)
|
|||||||
"TYPE": true, "TTL": true, "EXISTS": true, "KEYS": true,
|
"TYPE": true, "TTL": true, "EXISTS": true, "KEYS": true,
|
||||||
"INFO": true, "DBSIZE": true, "SCAN": true, "STRLEN": true,
|
"INFO": true, "DBSIZE": true, "SCAN": true, "STRLEN": true,
|
||||||
"LLEN": true, "SCARD": true, "ZCARD": true, "HLEN": true,
|
"LLEN": true, "SCARD": true, "ZCARD": true, "HLEN": true,
|
||||||
|
"HKEYS": true, "HVALS": true, "HSCAN": true, "SSCAN": true,
|
||||||
|
"ZSCAN": true, "ZRANGEBYSCORE": true,
|
||||||
|
"GETSET": true, "GETRANGE": true, "SUBSTR": true,
|
||||||
}
|
}
|
||||||
if !readOnly[cmd] {
|
if !readOnly[cmd] {
|
||||||
return "", fmt.Errorf("only read-only commands allowed, got: %s", cmd)
|
return nil, fmt.Errorf("only read-only commands allowed, got: %s", cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
args := make([]interface{}, len(parts)-1)
|
// Build args: command name + all arguments
|
||||||
for i, p := range parts[1:] {
|
args := make([]interface{}, 0, len(parts))
|
||||||
args[i] = p
|
args = append(args, cmd)
|
||||||
|
for _, p := range parts[1:] {
|
||||||
|
args = append(args, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := client.Do(ctx, args...).Result()
|
result, err := client.Do(ctx, args...).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("command failed: %v", err)
|
return nil, fmt.Errorf("command failed: %v", err)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%v", result), nil
|
|
||||||
|
// Format result into rows/columns for frontend table display
|
||||||
|
columns := []string{"key", "value"}
|
||||||
|
var rows []interface{}
|
||||||
|
|
||||||
|
switch v := result.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
// Array result (MGET, LRANGE, etc.)
|
||||||
|
if cmd == "MGET" {
|
||||||
|
for i, val := range v {
|
||||||
|
rows = append(rows, map[string]interface{}{
|
||||||
|
"index": i,
|
||||||
|
"value": fmt.Sprintf("%v", val),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
columns = []string{"index", "value"}
|
||||||
|
} else {
|
||||||
|
for i, val := range v {
|
||||||
|
rows = append(rows, map[string]interface{}{
|
||||||
|
"index": i,
|
||||||
|
"value": fmt.Sprintf("%v", val),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case map[string]string:
|
||||||
|
// HGETALL returns map[string]string via some redis clients
|
||||||
|
for k, val := range v {
|
||||||
|
rows = append(rows, map[string]interface{}{
|
||||||
|
"field": k,
|
||||||
|
"value": val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
columns = []string{"field", "value"}
|
||||||
|
default:
|
||||||
|
// Use reflect to handle any map type (map[interface{}]interface{}, etc.)
|
||||||
|
rv := reflect.ValueOf(result)
|
||||||
|
if rv.Kind() == reflect.Map {
|
||||||
|
iter := rv.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
rows = append(rows, map[string]interface{}{
|
||||||
|
"field": fmt.Sprintf("%v", iter.Key().Interface()),
|
||||||
|
"value": fmt.Sprintf("%v", iter.Value().Interface()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
columns = []string{"field", "value"}
|
||||||
|
} else {
|
||||||
|
rows = append(rows, map[string]interface{}{
|
||||||
|
"command": command,
|
||||||
|
"result": fmt.Sprintf("%v", v),
|
||||||
|
})
|
||||||
|
columns = []string{"command", "result"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gin.H{
|
||||||
|
"columns": columns,
|
||||||
|
"rows": rows,
|
||||||
|
"total": len(rows),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveQueryHistory(userID int, instanceID int, database string, sqlStr string, costMs int64, rowCount int) {
|
func saveQueryHistory(userID int, instanceID int, database string, sqlStr string, costMs int64, rowCount int) {
|
||||||
|
|||||||
@@ -210,13 +210,14 @@ export default function QueriesPage() {
|
|||||||
setExecuting(true); setError(''); setResult(null);
|
setExecuting(true); setError(''); setResult(null);
|
||||||
try {
|
try {
|
||||||
const res = await queryApi.execute({ instance_id: Number(instanceId), database: database || undefined, sql }) as unknown as QueryResult;
|
const res = await queryApi.execute({ instance_id: Number(instanceId), database: database || undefined, sql }) as unknown as QueryResult;
|
||||||
if (res.error) {
|
const normalized = { ...res, execution_time_ms: res.execution_time_ms || res.cost_ms || 0 };
|
||||||
setError(res.error);
|
if (normalized.error) {
|
||||||
|
setError(normalized.error);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
} else {
|
} else {
|
||||||
setResult(res);
|
setResult(normalized);
|
||||||
}
|
}
|
||||||
setHistory(prev => [{ id: Date.now(), sql, instance_id: Number(instanceId), database, db_type: dbType, status: 'success', execution_time_ms: res.execution_time_ms || 0, result_preview: JSON.stringify(res.rows?.[0] || {}).substring(0, 100), created_at: new Date().toISOString(), favorite: false }, ...prev]);
|
setHistory(prev => [{ id: Date.now(), sql, instance_id: Number(instanceId), database, db_type: dbType, status: 'success', execution_time_ms: normalized.execution_time_ms, result_preview: JSON.stringify(normalized.rows?.[0] || {}).substring(0, 100), created_at: new Date().toISOString(), favorite: false }, ...prev]);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const raw = (e as { response?: { data?: { detail?: string | Array<{ msg: string }> } } })?.response?.data?.detail;
|
const raw = (e as { response?: { data?: { detail?: string | Array<{ msg: string }> } } })?.response?.data?.detail;
|
||||||
const msg = Array.isArray(raw) ? raw.map(v => v.msg).join('; ') : (raw || '查询失败');
|
const msg = Array.isArray(raw) ? raw.map(v => v.msg).join('; ') : (raw || '查询失败');
|
||||||
@@ -394,7 +395,10 @@ export default function QueriesPage() {
|
|||||||
{showSidebar && instanceId && (
|
{showSidebar && instanceId && (
|
||||||
<div className="w-[280px] shrink-0">
|
<div className="w-[280px] shrink-0">
|
||||||
<SchemaSidebar instanceId={instanceId} database={database} dbType={dbType}
|
<SchemaSidebar instanceId={instanceId} database={database} dbType={dbType}
|
||||||
onInsert={(text) => setSql(prev => prev + (prev.trim() ? ' ' : '') + text + ' ')} />
|
onInsert={(text) => {
|
||||||
|
const sep = dbType === 'redis' ? (sql.trim() ? '\n' : '') : (sql.trim() ? ' ' : '');
|
||||||
|
setSql(prev => prev + sep + text + (dbType === 'redis' ? '' : ' '));
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user