diff --git a/backend/internal/handler/instance_detail.go b/backend/internal/handler/instance_detail.go index 94c5c3c..dce6ac4 100644 --- a/backend/internal/handler/instance_detail.go +++ b/backend/internal/handler/instance_detail.go @@ -3,6 +3,7 @@ package handler import ( "fmt" "net/http" + "reflect" "sql-platform/internal/model" @@ -12,6 +13,7 @@ import ( "github.com/redis/go-redis/v9" "sql-platform/internal/middleware" "strings" + "strconv" ) // GET /api/v1/instances/:id/detail/databases @@ -533,13 +535,14 @@ func ExecuteQuery(c *gin.Context) { c.JSON(http.StatusOK, result) case "redis": - result, err := executeRedisQuery(&instance, input.SQL) + result, err := executeRedisQuery(&instance, input.Database, input.SQL) costMs := time.Since(start).Milliseconds() if err != nil { c.JSON(http.StatusOK, gin.H{"columns": []string{}, "rows": []interface{}{}, "total": 0, "cost_ms": costMs, "error": err.Error()}) 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: 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 } -func executeRedisQuery(instance *model.Instance, command string) (string, error) { +func executeRedisQuery(instance *model.Instance, dbSelect string, command string) (gin.H, error) { password := instance.Password if strings.HasPrefix(password, "enc:") { decrypted, err := decryptPassword(strings.TrimPrefix(password, "enc:")) @@ -601,21 +604,30 @@ func executeRedisQuery(instance *model.Instance, command string) (string, error) 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{ Addr: fmt.Sprintf("%s:%d", instance.Host, instance.Port), Password: password, - DB: 0, + DB: redisDB, }) defer client.Close() ctx := context.Background() 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) if len(parts) == 0 { - return "", fmt.Errorf("empty command") + return nil, fmt.Errorf("empty command") } 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, "INFO": true, "DBSIZE": true, "SCAN": true, "STRLEN": 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] { - 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) - for i, p := range parts[1:] { - args[i] = p + // Build args: command name + all arguments + args := make([]interface{}, 0, len(parts)) + args = append(args, cmd) + for _, p := range parts[1:] { + args = append(args, p) } + result, err := client.Do(ctx, args...).Result() 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) { diff --git a/frontend/src/pages/QueriesPage.tsx b/frontend/src/pages/QueriesPage.tsx index 06177dc..261d8ee 100644 --- a/frontend/src/pages/QueriesPage.tsx +++ b/frontend/src/pages/QueriesPage.tsx @@ -210,13 +210,14 @@ export default function QueriesPage() { setExecuting(true); setError(''); setResult(null); try { const res = await queryApi.execute({ instance_id: Number(instanceId), database: database || undefined, sql }) as unknown as QueryResult; - if (res.error) { - setError(res.error); + const normalized = { ...res, execution_time_ms: res.execution_time_ms || res.cost_ms || 0 }; + if (normalized.error) { + setError(normalized.error); setResult(null); } 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) { 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 || '查询失败'); @@ -394,7 +395,10 @@ export default function QueriesPage() { {showSidebar && instanceId && (
setSql(prev => prev + (prev.trim() ? ' ' : '') + text + ' ')} /> + onInsert={(text) => { + const sep = dbType === 'redis' ? (sql.trim() ? '\n' : '') : (sql.trim() ? ' ' : ''); + setSql(prev => prev + sep + text + (dbType === 'redis' ? '' : ' ')); + }} />
)}