mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 04:14:56 +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.
68 lines
1.9 KiB
Go
68 lines
1.9 KiB
Go
//go:build !sqliteonly
|
|
|
|
package backup
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"log/slog"
|
|
)
|
|
|
|
// DiscoverTenantTables queries information_schema for all tables with a tenant_id column.
|
|
// Returns table names as a set. Used to detect unregistered tables at backup time.
|
|
func DiscoverTenantTables(ctx context.Context, db *sql.DB) (map[string]bool, error) {
|
|
rows, err := db.QueryContext(ctx,
|
|
`SELECT table_name FROM information_schema.columns
|
|
WHERE column_name = 'tenant_id' AND table_schema = 'public'
|
|
ORDER BY table_name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
result := make(map[string]bool)
|
|
for rows.Next() {
|
|
var name string
|
|
if err := rows.Scan(&name); err != nil {
|
|
continue
|
|
}
|
|
result[name] = true
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// ValidateTableRegistry cross-checks the hardcoded table registry against
|
|
// the actual database schema. Returns warnings for unregistered tables.
|
|
func ValidateTableRegistry(ctx context.Context, db *sql.DB) []string {
|
|
discovered, err := DiscoverTenantTables(ctx, db)
|
|
if err != nil {
|
|
slog.Warn("backup.validate_registry", "error", err)
|
|
return nil
|
|
}
|
|
|
|
// Build set of registered table names
|
|
registered := make(map[string]bool)
|
|
for _, t := range TenantTables() {
|
|
registered[t.Name] = true
|
|
}
|
|
|
|
// Ephemeral tables intentionally excluded from backup
|
|
skipped := map[string]bool{
|
|
"traces": true, "spans": true, "usage_snapshots": true,
|
|
"activity_logs": true, "embedding_cache": true,
|
|
"pairing_requests": true, "paired_devices": true,
|
|
"channel_pending_messages": true, "cron_run_logs": true,
|
|
"team_user_grants": true,
|
|
// Ephemeral hook/webhook run history — not restored (regenerated at runtime).
|
|
"hook_executions": true, "webhook_calls": true,
|
|
}
|
|
|
|
var warnings []string
|
|
for table := range discovered {
|
|
if !registered[table] && !skipped[table] {
|
|
warnings = append(warnings, "table "+table+" has tenant_id but is not in backup registry — data will be skipped")
|
|
}
|
|
}
|
|
return warnings
|
|
}
|