package mysql import ( "context" "database/sql" "errors" "time" ) // RetentionInventory 是指定截止时间前的只读保留清单。 // 它只提供核对数量,不代表记录已经满足归档条件,也不执行删除或搬迁。 type RetentionInventory struct { // Cutoff 是清单使用的 UTC 截止时间,严格早于该时间的记录才会计入。 Cutoff time.Time `json:"cutoff"` // ExpiredIdempotency 是已过幂等保留期的请求记录数量。 ExpiredIdempotency uint64 `json:"expired_idempotency"` // TerminalTasks 是已进入确定终态且已完成的任务数量。 TerminalTasks uint64 `json:"terminal_tasks"` // UncertainTasks 是仍带未知效果的任务数量,必须人工核对后才能处理。 UncertainTasks uint64 `json:"uncertain_tasks"` // ActiveTasks 是尚未终结的任务数量,不能进入普通历史归档。 ActiveTasks uint64 `json:"active_tasks"` // TerminalWorkflows 是已进入确定终态且已完成的流程数量。 TerminalWorkflows uint64 `json:"terminal_workflows"` // ActiveWorkflows 是尚未终结的流程数量,不能进入普通历史归档。 ActiveWorkflows uint64 `json:"active_workflows"` // AcknowledgedDeliveries 是已经确认的结果交付数量。 AcknowledgedDeliveries uint64 `json:"acknowledged_deliveries"` // UnconfirmedDeliveries 是仍未确认的结果交付数量,不能无声清理。 UnconfirmedDeliveries uint64 `json:"unconfirmed_deliveries"` // ProcessedInbox 是已经完成内部消费的流程事件数量。 ProcessedInbox uint64 `json:"processed_inbox"` // UnprocessedInbox 是尚未完成内部消费的流程事件数量,不能清理。 UnprocessedInbox uint64 `json:"unprocessed_inbox"` // Events 是截止时间前追加的不可变事件数量。 Events uint64 `json:"events"` // AuditEvents 是截止时间前追加的管理审计数量,默认应单独保留。 AuditEvents uint64 `json:"audit_events"` } // ReadRetentionInventory performs bounded-by-time, read-only count queries. // It deliberately does not decide policy or mutate any table; callers must // review retention windows, backups, outstanding effects and audit rules first. func ReadRetentionInventory(ctx context.Context, db *sql.DB, cutoff time.Time) (RetentionInventory, error) { if db == nil { return RetentionInventory{}, errors.New("database is required") } if cutoff.IsZero() { return RetentionInventory{}, errors.New("cutoff is required") } cutoff = cutoff.UTC() var now time.Time if err := db.QueryRowContext(ctx, "SELECT UTC_TIMESTAMP(6)").Scan(&now); err != nil { return RetentionInventory{}, errors.New("database time unavailable") } if cutoff.After(now.UTC()) { return RetentionInventory{}, errors.New("cutoff cannot be in the future") } result := RetentionInventory{Cutoff: cutoff} queries := []struct { destination *uint64 query string }{ {&result.ExpiredIdempotency, "SELECT COUNT(*) FROM orbit_idempotency_keys WHERE expires_at'ACKED' AND created_at'PROCESSED' AND created_at