mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-05 08:14:39 +00:00
Closes #1076, #1338. Fix A — tenant backup aborted with SQLSTATE 42703 "column id does not exist" because exportQuery() hardcoded ORDER BY id. Several tenant-scoped tables have composite PKs and no id column. Add a TableDef.OrderBy field, honor it in exportQuery(), and set it for every id-less registry table: config_secrets, agent_team_members, tenant_hook_budget, system_configs, builtin_tool_tenant_configs, skill_tenant_configs, user_agent_profiles. Fix B — hooks, tenant_hook_budget and webhook config were missing from the backup registry, silently dropping their data on backup/restore. Add hooks, tenant_hook_budget, webhooks (preserve) plus the hook_agents junction (via ParentJoin through hooks, composite PK), and mark hook_executions and webhook_calls as ephemeral in the skipped list. Fix C — restoring on a fresh server failed with "N active DB connection(s) detected" because the gateway's own pool connections were counted as active clients. Tag pool connections with application_name='goclaw' (pg.OpenDB) and exclude them in CheckActiveConnections; genuine external clients still block. Adds unit + integration regression tests, including an export-over-every- registered-table test that surfaced the additional id-less tables.
93 lines
2.4 KiB
Go
93 lines
2.4 KiB
Go
//go:build !sqliteonly
|
|
|
|
package backup
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
)
|
|
|
|
// RestoreDatabase restores a PostgreSQL database from a plain-SQL dump reader.
|
|
// Uses a temporary .pgpass file (0600) to pass credentials securely.
|
|
// The child psql process receives only PGPASSFILE, PATH, HOME, LC_ALL=C.
|
|
func RestoreDatabase(ctx context.Context, dsn string, dumpReader io.Reader) error {
|
|
creds, err := ParseDSN(dsn)
|
|
if err != nil {
|
|
return fmt.Errorf("parse DSN: %w", err)
|
|
}
|
|
|
|
psql, err := exec.LookPath("psql")
|
|
if err != nil {
|
|
return fmt.Errorf("psql not found on PATH: %w", err)
|
|
}
|
|
|
|
tempDir, pgpassPath, err := WritePgpass(creds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
args := []string{
|
|
"--host", creds.Host,
|
|
"--port", creds.Port,
|
|
"--username", creds.User,
|
|
"--dbname", creds.DBName,
|
|
"--no-password",
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, psql, args...)
|
|
cmd.Env = CleanEnv(pgpassPath)
|
|
cmd.Stdin = dumpReader
|
|
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
errMsg := strings.TrimSpace(stderr.String())
|
|
if errMsg == "" {
|
|
errMsg = err.Error()
|
|
}
|
|
// Truncate very long psql error output.
|
|
if len(errMsg) > 512 {
|
|
errMsg = errMsg[:512] + "..."
|
|
}
|
|
return fmt.Errorf("psql restore failed: %s", errMsg)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CheckActiveConnections returns the number of active backend connections to the
|
|
// database (excluding the current connection). Used as a pre-restore safety check.
|
|
func CheckActiveConnections(ctx context.Context, dsn string) (int, error) {
|
|
creds, err := ParseDSN(dsn)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("parse DSN: %w", err)
|
|
}
|
|
|
|
db, err := sql.Open("pgx", dsn)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("open db: %w", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
var count int
|
|
// Exclude the gateway's own pool connections, tagged application_name='goclaw'
|
|
// (see pg.PoolApplicationName). Otherwise a running gateway blocks its own
|
|
// restore on a fresh server (issue #1338). External clients are still counted.
|
|
query := `SELECT COUNT(*) FROM pg_stat_activity
|
|
WHERE datname = $1 AND pid <> pg_backend_pid()
|
|
AND application_name <> 'goclaw'`
|
|
if err := db.QueryRowContext(ctx, query, creds.DBName).Scan(&count); err != nil {
|
|
return 0, fmt.Errorf("query pg_stat_activity: %w", err)
|
|
}
|
|
return count, nil
|
|
}
|