fix: ejecutar sentencias PostgreSQL individualmente para evitar CREATE DATABASE en transacción
- Agrega splitPostgresStatements() que parsea SQL respetando dollar-quoting, strings y comentarios - En ExecuteSQL, si es PostgreSQL y hay múltiples sentencias, cada una se ejecuta con su propio db.Exec() - Evita el error: 'CREATE DATABASE cannot run inside a transaction block' - Error ocurría porque PostgreSQL envuelve multi-statement en transacción implícita al usar simple query protocol Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
c126c15ead
commit
fc647944b0
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user