package services import ( "context" "database/sql" "fmt" "strings" "time" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" _ "github.com/microsoft/go-mssqldb" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // connTimeout es el tiempo máximo para establecer una conexión a la DB externa. const connTimeout = 8 * time.Second // QueryResult contiene el resultado de una consulta SQL. type QueryResult struct { Columns []string `json:"columns"` Rows []map[string]any `json:"rows"` RowCount int `json:"row_count"` AffectedRows int64 `json:"affected_rows"` DurationMs int64 `json:"duration_ms"` IsSelect bool `json:"is_select"` Error string `json:"error,omitempty"` } // openDynamicDB abre una conexión a la base de datos indicada por ConxDb. func openDynamicDB(c models.ConxDb) (*sql.DB, error) { driver := strings.ToLower(c.TipoDb.Nombre) host := c.Servidor.IpServidor port := c.Puerto user := c.Usuario pass := c.Password var dsn string var driverName string switch { case strings.Contains(driver, "postgres"): driverName = "postgres" dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s sslmode=disable connect_timeout=8", host, port, user, pass) case strings.Contains(driver, "mysql") || strings.Contains(driver, "mariadb"): driverName = "mysql" dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/?timeout=8s&readTimeout=20s&writeTimeout=20s", user, pass, host, port) case strings.Contains(driver, "sqlite"): driverName = "sqlite3" dsn = host // para sqlite el host es la ruta del archivo case strings.Contains(driver, "sqlserver") || strings.Contains(driver, "mssql"): driverName = "sqlserver" dsn = fmt.Sprintf("sqlserver://%s:%s@%s:%s?dial+timeout=8", user, pass, host, port) default: return nil, fmt.Errorf("driver no soportado: %s", driver) } db, err := sql.Open(driverName, dsn) if err != nil { return nil, err } db.SetConnMaxLifetime(30 * time.Second) db.SetMaxOpenConns(2) // Verificar conectividad inmediatamente para fallar rápido en lugar de bloquear al hacer la primera query ctx, cancel := context.WithTimeout(context.Background(), connTimeout) defer cancel() if err := db.PingContext(ctx); err != nil { db.Close() return nil, fmt.Errorf("no se pudo conectar al servidor de base de datos: %w", err) } return db, nil } // ExecuteSQL ejecuta SQL arbitrario contra la conexión y devuelve QueryResult. // También guarda en query_history. func ExecuteSQL(conx models.ConxDb, database, sqlText string) QueryResult { start := time.Now() db, err := openDynamicDB(conx) if err != nil { saveHistory(conx.ID, sqlText, "error", err.Error(), 0, time.Since(start).Milliseconds()) return QueryResult{Error: err.Error()} } defer db.Close() // Si se especifica una base de datos para seleccionar if database != "" { driver := strings.ToLower(conx.TipoDb.Nombre) if strings.Contains(driver, "postgres") { // En postgres se cambia con SET search_path o reconectando con dbname en DSN db2, err2 := openDynamicDBWithName(conx, database) if err2 == nil { db.Close() db = db2 } } else { if _, err2 := db.Exec("USE " + quoteIdentifier(database, conx.TipoDb.Nombre)); err2 != nil { saveHistory(conx.ID, sqlText, "error", err2.Error(), 0, time.Since(start).Milliseconds()) return QueryResult{Error: err2.Error()} } } } trimmed := strings.TrimSpace(sqlText) isSelect := isSelectStatement(trimmed) var result QueryResult result.IsSelect = isSelect if isSelect { rows, err := db.Query(trimmed) if err != nil { elapsed := time.Since(start).Milliseconds() saveHistory(conx.ID, sqlText, "error", err.Error(), 0, elapsed) return QueryResult{Error: err.Error(), IsSelect: true} } defer rows.Close() cols, _ := rows.Columns() result.Columns = cols for rows.Next() { vals := make([]any, len(cols)) ptrs := make([]any, len(cols)) for i := range vals { ptrs[i] = &vals[i] } rows.Scan(ptrs...) row := make(map[string]any, len(cols)) for i, col := range cols { v := vals[i] if b, ok := v.([]byte); ok { row[col] = string(b) } else { row[col] = v } } result.Rows = append(result.Rows, row) } result.RowCount = len(result.Rows) } else { res, err := db.Exec(trimmed) elapsed := time.Since(start).Milliseconds() if err != nil { saveHistory(conx.ID, sqlText, "error", err.Error(), 0, elapsed) return QueryResult{Error: err.Error()} } affected, _ := res.RowsAffected() result.AffectedRows = affected result.DurationMs = elapsed saveHistory(conx.ID, sqlText, "ok", "", affected, elapsed) return result } result.DurationMs = time.Since(start).Milliseconds() saveHistory(conx.ID, sqlText, "ok", "", int64(result.RowCount), result.DurationMs) return result } // ListDatabases devuelve la lista de bases de datos del servidor. func ListDatabases(conx models.ConxDb) ([]string, error) { db, err := openDynamicDB(conx) if err != nil { return nil, err } defer db.Close() driver := strings.ToLower(conx.TipoDb.Nombre) var query string switch { case strings.Contains(driver, "postgres"): query = "SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname" case strings.Contains(driver, "mysql") || strings.Contains(driver, "mariadb"): query = "SHOW DATABASES" case strings.Contains(driver, "sqlserver") || strings.Contains(driver, "mssql"): query = "SELECT name FROM sys.databases ORDER BY name" default: return []string{"main"}, nil } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() rows, err := db.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() var dbs []string for rows.Next() { var name string rows.Scan(&name) dbs = append(dbs, name) } return dbs, nil } // ListTables devuelve las tablas de una base de datos. func ListTables(conx models.ConxDb, database string) ([]string, error) { driver := strings.ToLower(conx.TipoDb.Nombre) var db *sql.DB var err error if strings.Contains(driver, "postgres") { db, err = openDynamicDBWithName(conx, database) } else { db, err = openDynamicDB(conx) } if err != nil { return nil, err } defer db.Close() var query string switch { case strings.Contains(driver, "postgres"): query = "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename" case strings.Contains(driver, "mysql") || strings.Contains(driver, "mariadb"): if _, err := db.Exec("USE " + quoteIdentifier(database, conx.TipoDb.Nombre)); err != nil { return nil, err } query = "SHOW TABLES" case strings.Contains(driver, "sqlserver") || strings.Contains(driver, "mssql"): query = fmt.Sprintf("USE [%s]; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' ORDER BY TABLE_NAME", database) default: query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" } rows, err := db.Query(query) if err != nil { return nil, err } defer rows.Close() var tables []string for rows.Next() { var name string rows.Scan(&name) tables = append(tables, name) } return tables, nil } // TestConnection verifica si la conexión es válida. func TestDBConnection(conx models.ConxDb) error { db, err := openDynamicDB(conx) if err != nil { return err } defer db.Close() return db.Ping() } // ── helpers ────────────────────────────────────────────────────────────────── func openDynamicDBWithName(c models.ConxDb, dbName string) (*sql.DB, error) { host := c.Servidor.IpServidor port := c.Puerto user := c.Usuario pass := c.Password driver := strings.ToLower(c.TipoDb.Nombre) var dsn string switch { case strings.Contains(driver, "postgres"): dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable connect_timeout=8", host, port, user, pass, dbName) db, err := sql.Open("postgres", dsn) if err != nil { return nil, err } db.SetConnMaxLifetime(30 * time.Second) db.SetMaxOpenConns(2) ctx, cancel := context.WithTimeout(context.Background(), connTimeout) defer cancel() if err := db.PingContext(ctx); err != nil { db.Close() return nil, fmt.Errorf("no se pudo conectar a la base de datos '%s': %w", dbName, err) } return db, nil default: return openDynamicDB(c) } } func isSelectStatement(sql string) bool { upper := strings.ToUpper(strings.TrimSpace(sql)) keywords := []string{"SELECT ", "SHOW ", "DESCRIBE ", "EXPLAIN ", "WITH ", "PRAGMA "} for _, kw := range keywords { if strings.HasPrefix(upper, kw) { return true } } return false } func quoteIdentifier(name, driver string) string { d := strings.ToLower(driver) if strings.Contains(d, "postgres") { return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` } return "`" + strings.ReplaceAll(name, "`", "``") + "`" } func saveHistory(conxID uint, sqlText, status, errMsg string, rows, durationMs int64) { models.SaveQueryHistory(models.QueryHistory{ ConxDbID: conxID, SQL: sqlText, Status: status, ErrorMsg: errMsg, RowsAffect: rows, DurationMs: durationMs, ExecutedAt: time.Now(), }) }