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.
|
// QueryResult contiene el resultado de una consulta SQL.
|
||||||
type QueryResult struct {
|
type QueryResult struct {
|
||||||
Columns []string `json:"columns"`
|
Columns []string `json:"columns"`
|
||||||
Rows []map[string]any `json:"rows"`
|
Rows []map[string]any `json:"rows"`
|
||||||
RowCount int `json:"row_count"`
|
RowCount int `json:"row_count"`
|
||||||
AffectedRows int64 `json:"affected_rows"`
|
AffectedRows int64 `json:"affected_rows"`
|
||||||
DurationMs int64 `json:"duration_ms"`
|
DurationMs int64 `json:"duration_ms"`
|
||||||
IsSelect bool `json:"is_select"`
|
IsSelect bool `json:"is_select"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// openDynamicDB abre una conexión a la base de datos indicada por ConxDb.
|
// 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)
|
result.RowCount = len(result.Rows)
|
||||||
} else {
|
} 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)
|
res, err := db.Exec(trimmed)
|
||||||
elapsed := time.Since(start).Milliseconds()
|
elapsed := time.Since(start).Milliseconds()
|
||||||
if err != nil {
|
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 {
|
func isSelectStatement(sql string) bool {
|
||||||
upper := strings.ToUpper(strings.TrimSpace(sql))
|
upper := strings.ToUpper(strings.TrimSpace(sql))
|
||||||
keywords := []string{"SELECT ", "SHOW ", "DESCRIBE ", "EXPLAIN ", "WITH ", "PRAGMA "}
|
keywords := []string{"SELECT ", "SHOW ", "DESCRIBE ", "EXPLAIN ", "WITH ", "PRAGMA "}
|
||||||
@@ -537,8 +669,8 @@ func mongoInsertOne(ctx context.Context, coll *mongo.Collection, args []string)
|
|||||||
}
|
}
|
||||||
return &QueryResult{
|
return &QueryResult{
|
||||||
IsSelect: true, AffectedRows: 1,
|
IsSelect: true, AffectedRows: 1,
|
||||||
Columns: []string{"insertedId"},
|
Columns: []string{"insertedId"},
|
||||||
Rows: []map[string]any{{"insertedId": fmt.Sprintf("%v", res.InsertedID)}},
|
Rows: []map[string]any{{"insertedId": fmt.Sprintf("%v", res.InsertedID)}},
|
||||||
RowCount: 1,
|
RowCount: 1,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user