diff --git a/pkg/services/query_runner_service.go b/pkg/services/query_runner_service.go index 53e288d..c6a44d7 100644 --- a/pkg/services/query_runner_service.go +++ b/pkg/services/query_runner_service.go @@ -25,13 +25,13 @@ 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"` + 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. @@ -149,6 +149,37 @@ func ExecuteSQL(conx models.ConxDb, database, sqlText string) QueryResult { } result.RowCount = len(result.Rows) } else { + driver := strings.ToLower(conx.TipoDb.Nombre) + // PostgreSQL: cuando se envían múltiples sentencias en un solo Exec, el servidor + // las envuelve en una transacción implícita. CREATE DATABASE/DROP DATABASE no + // pueden correr dentro de una transacción, así que dividimos y ejecutamos c/u por separado. + if strings.Contains(driver, "postgres") { + stmts := splitPostgresStatements(trimmed) + if len(stmts) > 1 { + var totalAffected int64 + for _, stmt := range stmts { + res, execErr := db.Exec(stmt) + if execErr != nil { + elapsed := time.Since(start).Milliseconds() + preview := stmt + if len(preview) > 80 { + preview = preview[:80] + "..." + } + saveHistory(conx.ID, sqlText, "error", execErr.Error(), 0, elapsed) + return QueryResult{Error: fmt.Sprintf("[%s]: %s", preview, execErr.Error())} + } + if affected, err2 := res.RowsAffected(); err2 == nil { + totalAffected += affected + } + } + elapsed := time.Since(start).Milliseconds() + result.AffectedRows = totalAffected + result.DurationMs = elapsed + saveHistory(conx.ID, sqlText, "ok", "", totalAffected, elapsed) + return result + } + } + res, err := db.Exec(trimmed) elapsed := time.Since(start).Milliseconds() if err != nil { @@ -302,6 +333,107 @@ func openDynamicDBWithName(c models.ConxDb, dbName string) (*sql.DB, error) { } } +// splitPostgresStatements divide SQL multi-sentencia en sentencias individuales +// respetando dollar-quoting ($$ ... $$), strings con comillas simples y comentarios. +// Esto permite ejecutar cada sentencia por separado evitando que PostgreSQL +// envuelva múltiples statements en una transacción implícita (que bloquea CREATE DATABASE). +func splitPostgresStatements(sqlText string) []string { + var stmts []string + var cur strings.Builder + i, n := 0, len(sqlText) + + for i < n { + ch := sqlText[i] + + // Dollar-quoting: $tag$ ... $tag$ (tag puede ser vacío: $$) + if ch == '$' { + j := i + 1 + for j < n && sqlText[j] != '$' && sqlText[j] != '\n' { + j++ + } + if j < n && sqlText[j] == '$' { + tag := sqlText[i : j+1] // e.g. "$$" or "$func$" + cur.WriteString(tag) + i = j + 1 + closeIdx := strings.Index(sqlText[i:], tag) + if closeIdx >= 0 { + cur.WriteString(sqlText[i : i+closeIdx+len(tag)]) + i = i + closeIdx + len(tag) + } else { + cur.WriteString(sqlText[i:]) + i = n + } + continue + } + } + + // String con comillas simples + if ch == '\'' { + cur.WriteByte(ch) + i++ + for i < n { + c := sqlText[i] + cur.WriteByte(c) + i++ + if c == '\'' { + if i < n && sqlText[i] == '\'' { + cur.WriteByte(sqlText[i]) + i++ + } else { + break + } + } + } + continue + } + + // Comentario de línea (--) + if ch == '-' && i+1 < n && sqlText[i+1] == '-' { + cur.WriteByte(ch) + i++ + for i < n && sqlText[i] != '\n' { + cur.WriteByte(sqlText[i]) + i++ + } + continue + } + + // Comentario de bloque /* ... */ + if ch == '/' && i+1 < n && sqlText[i+1] == '*' { + cur.WriteString("/*") + i += 2 + for i < n { + if sqlText[i] == '*' && i+1 < n && sqlText[i+1] == '/' { + cur.WriteString("*/") + i += 2 + break + } + cur.WriteByte(sqlText[i]) + i++ + } + continue + } + + // Punto y coma: fin de sentencia + if ch == ';' { + if stmt := strings.TrimSpace(cur.String()); stmt != "" { + stmts = append(stmts, stmt) + } + cur.Reset() + i++ + continue + } + + cur.WriteByte(ch) + i++ + } + + if stmt := strings.TrimSpace(cur.String()); stmt != "" { + stmts = append(stmts, stmt) + } + return stmts +} + func isSelectStatement(sql string) bool { upper := strings.ToUpper(strings.TrimSpace(sql)) keywords := []string{"SELECT ", "SHOW ", "DESCRIBE ", "EXPLAIN ", "WITH ", "PRAGMA "} @@ -537,8 +669,8 @@ func mongoInsertOne(ctx context.Context, coll *mongo.Collection, args []string) } return &QueryResult{ IsSelect: true, AffectedRows: 1, - Columns: []string{"insertedId"}, - Rows: []map[string]any{{"insertedId": fmt.Sprintf("%v", res.InsertedID)}}, + Columns: []string{"insertedId"}, + Rows: []map[string]any{{"insertedId": fmt.Sprintf("%v", res.InsertedID)}}, RowCount: 1, }, nil }