Merge branch 'copilot/vcard-api-integraciones-anlisis': fix PostgreSQL CREATE DATABASE in transaction

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Lizandro Guarnizo
2026-05-20 20:39:09 -05:00
co-authored by Copilot
+141 -9
View File
@@ -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
}