package handler import ( "fmt" "net/http" "sql-platform/internal/model" "github.com/gin-gonic/gin" "time" "context" "github.com/redis/go-redis/v9" "sql-platform/internal/middleware" "strings" ) // GET /api/v1/instances/:id/detail/databases func DetailListDatabases(c *gin.Context) { id := c.Param("id") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } if instance.DBType == "redis" { c.JSON(http.StatusOK, []string{}) return } db, err := getDBConnection(&instance, "") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)}) return } defer db.Close() rows, err := db.Query("SHOW DATABASES") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } defer rows.Close() var databases []string for rows.Next() { var name string rows.Scan(&name) // Filter out system databases if name != "information_schema" && name != "performance_schema" && name != "mysql" && name != "sys" { databases = append(databases, name) } } if databases == nil { databases = []string{} } c.JSON(http.StatusOK, databases) } // GET /api/v1/instances/:id/detail/tables func DetailListTables(c *gin.Context) { id := c.Param("id") dbName := c.Query("database") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } if instance.DBType == "redis" { c.JSON(http.StatusOK, []interface{}{}) return } if dbName == "" { if instance.Database != nil { dbName = *instance.Database } } if dbName == "" { c.JSON(http.StatusBadRequest, gin.H{"detail": "database parameter required"}) return } db, err := getDBConnection(&instance, dbName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)}) return } defer db.Close() rows, err := db.Query("SHOW TABLE STATUS") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } defer rows.Close() cols, _ := rows.Columns() var tables []map[string]interface{} for rows.Next() { values := make([]interface{}, len(cols)) ptrValues := make([]interface{}, len(cols)) for i := range values { ptrValues[i] = &values[i] } rows.Scan(ptrValues...) // Normalize to lowercase keys for frontend compatibility rowMap := make(map[string]interface{}) for i, col := range cols { val := values[i] b, ok := val.([]byte) if ok { rowMap[strings.ToLower(col)] = string(b) } else { rowMap[strings.ToLower(col)] = val } } tables = append(tables, rowMap) } if tables == nil { tables = []map[string]interface{}{} } c.JSON(http.StatusOK, tables) } // GET /api/v1/instances/:id/detail/db-schema func DetailDbSchema(c *gin.Context) { id := c.Param("id") dbName := c.Query("database") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } if instance.DBType == "redis" { c.JSON(http.StatusOK, map[string]interface{}{}) return } if dbName == "" { if instance.Database != nil { dbName = *instance.Database } } if dbName == "" { c.JSON(http.StatusBadRequest, gin.H{"detail": "database parameter required"}) return } db, err := getDBConnection(&instance, dbName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)}) return } defer db.Close() // Get all tables and their columns tableRows, err := db.Query("SHOW TABLES") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } defer tableRows.Close() schema := make(map[string][]string) for tableRows.Next() { var tableName string tableRows.Scan(&tableName) // Get columns for this table colRows, err := db.Query("SHOW COLUMNS FROM " + "`" + tableName + "`") if err != nil { schema[tableName] = []string{} continue } var cols []string for colRows.Next() { var field, typ, null, key string var extra interface{} var def interface{} colRows.Scan(&field, &typ, &null, &key, &def, &extra) cols = append(cols, field+" ("+typ+")") } colRows.Close() if cols == nil { cols = []string{} } schema[tableName] = cols } c.JSON(http.StatusOK, schema) } // GET /api/v1/instances/:id/detail/table-detail func DetailTableDetail(c *gin.Context) { id := c.Param("id") dbName := c.Query("database") tableName := c.Query("table") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } if dbName == "" { if instance.Database != nil { dbName = *instance.Database } } if dbName == "" || tableName == "" { c.JSON(http.StatusBadRequest, gin.H{"detail": "database and table parameters required"}) return } db, err := getDBConnection(&instance, dbName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)}) return } defer db.Close() // Get DDL var ddl string db.QueryRow("SHOW CREATE TABLE `"+tableName+"`").Scan(&tableName, &ddl) // Get columns colRows, err := db.Query("SHOW FULL COLUMNS FROM `" + tableName + "`") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } defer colRows.Close() colCols, _ := colRows.Columns() var columns []map[string]interface{} for colRows.Next() { values := make([]interface{}, len(colCols)) ptrValues := make([]interface{}, len(colCols)) for i := range values { ptrValues[i] = &values[i] } colRows.Scan(ptrValues...) rowMap := make(map[string]interface{}) for i, col := range colCols { val := values[i] b, ok := val.([]byte) if ok { rowMap[col] = string(b) } else { rowMap[col] = val } } columns = append(columns, rowMap) } // Get indexes idxRows, err := db.Query("SHOW INDEX FROM `" + tableName + "`") if err == nil { defer idxRows.Close() idxCols, _ := idxRows.Columns() var indexes []map[string]interface{} for idxRows.Next() { values := make([]interface{}, len(idxCols)) ptrValues := make([]interface{}, len(idxCols)) for i := range values { ptrValues[i] = &values[i] } idxRows.Scan(ptrValues...) rowMap := make(map[string]interface{}) for i, col := range idxCols { val := values[i] b, ok := val.([]byte) if ok { rowMap[col] = string(b) } else { rowMap[col] = val } } indexes = append(indexes, rowMap) } c.JSON(http.StatusOK, gin.H{ "ddl": ddl, "columns": columns, "indexes": indexes, }) } else { c.JSON(http.StatusOK, gin.H{ "ddl": ddl, "columns": columns, "indexes": []interface{}{}, }) } } // GET /api/v1/instances/:id/detail/processlist func DetailProcesslist(c *gin.Context) { id := c.Param("id") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } if instance.DBType == "redis" { c.JSON(http.StatusOK, []interface{}{}) return } dbName := "" if instance.Database != nil { dbName = *instance.Database } db, err := getDBConnection(&instance, dbName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)}) return } defer db.Close() rows, err := db.Query("SHOW PROCESSLIST") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } defer rows.Close() cols, _ := rows.Columns() var processes []map[string]interface{} for rows.Next() { values := make([]interface{}, len(cols)) ptrValues := make([]interface{}, len(cols)) for i := range values { ptrValues[i] = &values[i] } rows.Scan(ptrValues...) rowMap := make(map[string]interface{}) for i, col := range cols { val := values[i] b, ok := val.([]byte) if ok { rowMap[col] = string(b) } else { rowMap[col] = val } } processes = append(processes, rowMap) } if processes == nil { processes = []map[string]interface{}{} } c.JSON(http.StatusOK, processes) } // GET /api/v1/instances/:id/detail/status func DetailStatus(c *gin.Context) { id := c.Param("id") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } if instance.DBType == "redis" { c.JSON(http.StatusOK, gin.H{ "key_metrics": map[string]string{}, "all_status": []interface{}{}, }) return } dbName := "" if instance.Database != nil { dbName = *instance.Database } db, err := getDBConnection(&instance, dbName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)}) return } defer db.Close() rows, err := db.Query("SHOW GLOBAL STATUS") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } defer rows.Close() keyMetrics := make(map[string]string) var allStatus []map[string]interface{} importantKeys := map[string]bool{ "Threads_connected": true, "Threads_running": true, "Threads_cached": true, "Queries": true, "Questions": true, "Slow_queries": true, "Connections": true, "Uptime": true, "Bytes_received": true, "Bytes_sent": true, "Table_locks_waited": true, "Table_locks_immediate": true, "Innodb_row_lock_waits": true, "Innodb_buffer_pool_reads": true, } for rows.Next() { var name, val string rows.Scan(&name, &val) allStatus = append(allStatus, map[string]interface{}{"name": name, "value": val}) if importantKeys[name] { keyMetrics[name] = val } } if allStatus == nil { allStatus = []map[string]interface{}{} } c.JSON(http.StatusOK, gin.H{ "key_metrics": keyMetrics, "all_status": allStatus, }) } // POST /api/v1/instances/:id/detail/processlist/:pid/kill func DetailKillProcess(c *gin.Context) { id := c.Param("id") pid := c.Param("pid") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } dbName := "" if instance.Database != nil { dbName = *instance.Database } db, err := getDBConnection(&instance, dbName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)}) return } defer db.Close() _, err = db.Exec("KILL " + pid) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Kill failed: %v", err)}) return } c.JSON(http.StatusOK, gin.H{"message": "Process " + pid + " killed"}) } // GET /api/v1/instances/:id/detail/users func DetailListUsers(c *gin.Context) { id := c.Param("id") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } if instance.DBType == "redis" { c.JSON(http.StatusOK, []interface{}{}) return } dbName := "" if instance.Database != nil { dbName = *instance.Database } db, err := getDBConnection(&instance, dbName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Connection failed: %v", err)}) return } defer db.Close() rows, err := db.Query("SELECT User, Host FROM mysql.user") if err != nil { // Might not have permission - return empty c.JSON(http.StatusOK, []interface{}{}) return } defer rows.Close() var users []map[string]interface{} for rows.Next() { var user, host string rows.Scan(&user, &host) users = append(users, map[string]interface{}{ "user": user, "host": host, }) } if users == nil { users = []map[string]interface{}{} } c.JSON(http.StatusOK, users) } // getDBConnection is defined in connection_tester.go or instance_detail.go // Using the one from connection_tester.go // POST /api/v1/query/execute func ExecuteQuery(c *gin.Context) { userID, _, _ := middleware.GetCurrentUser(c) var input struct { InstanceID int `json:"instance_id"` Database string `json:"database"` SQL string `json:"sql"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } if input.InstanceID == 0 || input.SQL == "" { c.JSON(http.StatusBadRequest, gin.H{"detail": "instance_id and sql are required"}) return } var instance model.Instance if err := model.DB.First(&instance, input.InstanceID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } start := time.Now() switch instance.DBType { case "mysql", "postgresql", "postgres", "pg": result, err := executeSQLQuery(&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 } result["cost_ms"] = costMs rowsInterface, _ := result["rows"].([]interface{}) saveQueryHistory(userID, input.InstanceID, input.Database, input.SQL, costMs, len(rowsInterface)) c.JSON(http.StatusOK, result) case "redis": result, err := executeRedisQuery(&instance, 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}) default: c.JSON(http.StatusBadRequest, gin.H{"detail": "Unsupported database type: " + instance.DBType}) } } func executeSQLQuery(instance *model.Instance, dbName string, sqlStr string) (gin.H, error) { db, err := getDBConnection(instance, dbName) if err != nil { return nil, fmt.Errorf("connection failed: %v", err) } defer db.Close() rows, err := db.Query(sqlStr) if err != nil { return nil, fmt.Errorf("query failed: %v", err) } defer rows.Close() cols, _ := rows.Columns() colNames := make([]string, len(cols)) for i, col := range cols { colNames[i] = col } var resultRows []interface{} for rows.Next() { values := make([]interface{}, len(cols)) ptrValues := make([]interface{}, len(cols)) for i := range values { ptrValues[i] = &values[i] } rows.Scan(ptrValues...) rowMap := make(map[string]interface{}) for i, col := range colNames { val := values[i] b, ok := val.([]byte) if ok { rowMap[col] = string(b) } else { rowMap[col] = val } } resultRows = append(resultRows, rowMap) } return gin.H{ "columns": colNames, "rows": resultRows, "total": len(resultRows), }, nil } func executeRedisQuery(instance *model.Instance, command string) (string, error) { password := instance.Password if strings.HasPrefix(password, "enc:") { decrypted, err := decryptPassword(strings.TrimPrefix(password, "enc:")) if err == nil { password = decrypted } } client := redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%d", instance.Host, instance.Port), Password: password, DB: 0, }) defer client.Close() ctx := context.Background() if err := client.Ping(ctx).Err(); err != nil { return "", fmt.Errorf("Redis connection failed: %v", err) } parts := strings.Fields(command) if len(parts) == 0 { return "", fmt.Errorf("empty command") } cmd := strings.ToUpper(parts[0]) readOnly := map[string]bool{ "GET": true, "MGET": true, "HGET": true, "HGETALL": true, "LRANGE": true, "SMEMBERS": true, "ZRANGE": true, "TYPE": true, "TTL": true, "EXISTS": true, "KEYS": true, "INFO": true, "DBSIZE": true, "SCAN": true, "STRLEN": true, "LLEN": true, "SCARD": true, "ZCARD": true, "HLEN": true, } if !readOnly[cmd] { return "", 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 } result, err := client.Do(ctx, args...).Result() if err != nil { return "", fmt.Errorf("command failed: %v", err) } return fmt.Sprintf("%v", result), nil } func saveQueryHistory(userID int, instanceID int, database string, sqlStr string, costMs int64, rowCount int) { var user model.User var inst model.Instance username := "" instName := "" dbType := "" if model.DB.First(&user, userID).Error == nil { username = user.Username } if model.DB.First(&inst, instanceID).Error == nil { instName = inst.Name dbType = inst.DBType } queryType := "SELECT" upper := strings.ToUpper(strings.TrimSpace(sqlStr)) if strings.HasPrefix(upper, "SHOW") || strings.HasPrefix(upper, "DESCRIBE") || strings.HasPrefix(upper, "EXPLAIN") { queryType = strings.SplitN(upper, " ", 2)[0] } history := model.QueryHistory{ UserID: userID, Username: username, InstanceID: instanceID, InstanceName: instName, DBType: dbType, DatabaseName: database, QueryContent: sqlStr, QueryType: queryType, ExecutionTimeMs: int(costMs), ResultRows: rowCount, IsSuccess: true, } model.DB.Create(&history) }