Files
Yevhen Odynets a2bfa8ea71 srrdb: persist per-day harvest ledger alongside releases
Add srrdb_log table with composite primary key (day, category) and counters
found, stored, ignored plus run_at.

Replace the release-only persistence path with UpsertAllSrrdbWithDay so releases
and their corresponding srrdb_log row are written in one SQLite transaction.
Preserve existing error handling: rollback reports the release/day that failed,
and commit errors return without writing a dump.

Handle fully ignored days as closed. When every fetched release is skipped by
ignore rules, still write the ledger row with zero stored releases. This prevents
the automatic day walk from endlessly retrying a day whose releases are filtered
out.

Derive and validate the ledger day from the resolved request URI. Missing or
invalid date: segments now fail before database writes. Keep run_at independent
from created_at semantics and continue populating created_at from the run time.

Preserve column and upsert semantics. category continues to use NOT NULL DEFAULT
empty string; status keeps COALESCE behavior on conflict; nfo, content, and
synced_at remain unchanged.
2026-09-30 12:44:40 +03:00

101 lines
3.0 KiB
Go

package repository
import (
"fmt"
"github.com/spf13/viper"
)
const (
// srrdbGroupsKey is the configuration section holding the group rules:
// groups.<column>.<value>: [group, ...].
srrdbGroupsKey = "groups"
// srrdbStatusColumn is the only column the rules may target. Rows are written
// through a fixed statement, so a second column would need a different way of
// building the insert.
srrdbStatusColumn = "status"
// srrdbIgnoreValue marks groups that must not reach the database at all. It is
// deliberately not a status of its own: the table rejects it with a CHECK, and
// an ignored release has no reason to be stored.
srrdbIgnoreValue = "ignore"
)
// srrdbGroupRules is the resolved rule set: the status every configured group
// receives, and the groups skipped before the insert.
type srrdbGroupRules struct {
status map[string]string
ignore map[string]bool
}
// srrdbLoadGroupRules reads groups.status.<value>: [group, ...].
//
// A group listed under two values is a configuration error rather than a question
// of precedence: the values arrive in a map, Go iterates it in an arbitrary order,
// and the status a release receives would then change from run to run.
func srrdbLoadGroupRules() (*srrdbGroupRules, error) {
rules := &srrdbGroupRules{
status: make(map[string]string),
ignore: make(map[string]bool),
}
for column, rawValues := range viper.GetStringMap(srrdbGroupsKey) {
if column != srrdbStatusColumn {
return nil, fmt.Errorf("%s.%s is not a column of srrdb_releases", srrdbGroupsKey, column)
}
values, ok := rawValues.(map[string]any)
if !ok {
return nil, fmt.Errorf("%s.%s must map a value to a list of groups", srrdbGroupsKey, column)
}
for value, rawList := range values {
groups, err := srrdbGroupList(value, rawList)
if err != nil {
return nil, err
}
for _, group := range groups {
if assigned, exists := rules.status[group]; exists {
return nil, fmt.Errorf("group %q is listed under both %q and %q", group, assigned, value)
}
rules.status[group] = value
if value == srrdbIgnoreValue {
rules.ignore[group] = true
}
}
}
}
return rules, nil
}
// srrdbGroupList converts one value's group list. An element that is not a string
// is rejected rather than dropped: YAML reads an unquoted 401 as a number, and
// losing that group silently would look like a rule that simply does not work.
func srrdbGroupList(value string, raw any) ([]string, error) {
switch list := raw.(type) {
case []string:
return list, nil
case []any:
groups := make([]string, 0, len(list))
for _, item := range list {
group, ok := item.(string)
if !ok {
return nil, fmt.Errorf("%s.%s.%s holds %v, which is not a group name: quote it in the configuration",
srrdbGroupsKey, srrdbStatusColumn, value, item)
}
groups = append(groups, group)
}
return groups, nil
default:
return nil, fmt.Errorf("%s.%s.%s must be a list of groups", srrdbGroupsKey, srrdbStatusColumn, value)
}
}