feat: разбор и хранение тренировок и состояния разума
- секции `workouts` и `stateOfMind` покрыты разбором: тренировка лежит одной строкой вместе с маршрутом и внутренними рядами, запись — по ключу `род + id`; миграция 00007 заводит обе таблицы и возвращает в очередь `partial`-доставки с этими ключами - сущность заменяется целиком, но условно: приехавшая побеждает, если не теряет содержания сохранённой (множество ключей и длины верхнеуровневых массивов), а при равном содержании выигрывает версия из более поздней доставки ЖУРНАЛА — «побеждает приехавшая» было бы функцией порядка свёртки, и живая витрина расходилась бы с пересборкой молча - отпечаток витрины покрывает тренировки и записи и снимается одним снимком базы; отчёт `reindex` считает «было и стало» по каждой единице хранения
This commit is contained in:
@@ -174,9 +174,14 @@ type report struct {
|
||||
dbPath string
|
||||
sourcePrint string
|
||||
sourceBuckets int64
|
||||
sourceBefore int64
|
||||
sourceAfter int64
|
||||
sourceMissing bool
|
||||
// sourceWorkouts и sourceRecords — то же «было» для остальных единиц
|
||||
// хранения витрины. Отпечаток отвечает «да/нет» за витрину целиком, поэтому
|
||||
// единица, которой нет в счётчиках, делает расхождение безадресным.
|
||||
sourceWorkouts int64
|
||||
sourceRecords int64
|
||||
sourceBefore int64
|
||||
sourceAfter int64
|
||||
sourceMissing bool
|
||||
}
|
||||
|
||||
// rebuild собирает витрину в промежуточный файл и переименовывает его в файл
|
||||
@@ -226,6 +231,12 @@ func rebuild(ctx context.Context, cfg *config.Config, t target, log *slog.Logger
|
||||
if rep.sourceBuckets, err = src.CountBuckets(ctx); err != nil {
|
||||
return canceledOr(rep, err, stopped)
|
||||
}
|
||||
if rep.sourceWorkouts, err = src.CountWorkouts(ctx); err != nil {
|
||||
return canceledOr(rep, err, stopped)
|
||||
}
|
||||
if rep.sourceRecords, err = src.CountRecords(ctx); err != nil {
|
||||
return canceledOr(rep, err, stopped)
|
||||
}
|
||||
}
|
||||
|
||||
removeDB(t.partial)
|
||||
|
||||
@@ -58,6 +58,8 @@ func writeReport(w io.Writer, r report) {
|
||||
|
||||
if r.sourceMissing {
|
||||
p(" объектов: %d", r.replay.Buckets)
|
||||
p(" тренировок: %d", r.replay.Workouts)
|
||||
p(" записей: %d", r.replay.Records)
|
||||
p("")
|
||||
p("рабочей базы %s нет — сверять не с чем, а заголовки доставок", r.dbPath)
|
||||
p("не восстанавливаются: в архиве их нет.")
|
||||
@@ -66,6 +68,8 @@ func writeReport(w io.Writer, r report) {
|
||||
// расхождения. Отпечатки отвечают «да/нет», а решение о подмене
|
||||
// необратимо; именно пара чисел 1737/1742 поймала прошлый дефект.
|
||||
p(" объектов: было %d, стало %d", r.sourceBuckets, r.replay.Buckets)
|
||||
p(" тренировок: было %d, стало %d", r.sourceWorkouts, r.replay.Workouts)
|
||||
p(" записей: было %d, стало %d", r.sourceRecords, r.replay.Records)
|
||||
p("")
|
||||
p(" отпечаток рабочей: %s", r.sourcePrint)
|
||||
p(" отпечаток пересобранной: %s", r.replay.Fingerprint)
|
||||
@@ -76,8 +80,9 @@ func writeReport(w io.Writer, r report) {
|
||||
p(" отпечатки совпали, но сверка НЕПОЛНА: часть журнала не прочитана")
|
||||
default:
|
||||
p(" отпечатки РАЗОШЛИСЬ")
|
||||
p(" ожидаемые причины: исправленный разбор; признак sealed не")
|
||||
p(" переносится (правила его выставления ещё нет)")
|
||||
p(" ожидаемые причины: исправленный разбор; покрытая разбором новая")
|
||||
p(" секция (её единиц хранения в рабочей базе нет по построению);")
|
||||
p(" признак sealed не переносится (правила его выставления ещё нет)")
|
||||
if partialJournal {
|
||||
p(" ВНИМАНИЕ: часть журнала не прочитана — расхождение может")
|
||||
p(" объясняться этим, а не разбором")
|
||||
|
||||
@@ -88,14 +88,16 @@ func TestОтчётНеРаскрываетДанныхОЗдоровье(t *tes
|
||||
replay: replay.Report{
|
||||
Bodies: 116,
|
||||
Outcome: replay.Outcome{Folded: 116, Partial: 53},
|
||||
Buckets: 2049, Fingerprint: "aaaa",
|
||||
Buckets: 2049, Workouts: 2, Records: 2, Fingerprint: "aaaa",
|
||||
},
|
||||
target: "/data/healthlog.db.rebuild",
|
||||
dbPath: "/data/healthlog.db",
|
||||
sourcePrint: "bbbb",
|
||||
sourceBuckets: 2040,
|
||||
sourceBefore: 116,
|
||||
sourceAfter: 116,
|
||||
target: "/data/healthlog.db.rebuild",
|
||||
dbPath: "/data/healthlog.db",
|
||||
sourcePrint: "bbbb",
|
||||
sourceBuckets: 2040,
|
||||
sourceWorkouts: 0,
|
||||
sourceRecords: 0,
|
||||
sourceBefore: 116,
|
||||
sourceAfter: 116,
|
||||
})
|
||||
out := buf.String()
|
||||
|
||||
@@ -113,7 +115,17 @@ func TestОтчётНеРаскрываетДанныхОЗдоровье(t *tes
|
||||
|
||||
// Расхождение отпечатков названо, и рядом — направление: «было/стало».
|
||||
// Отпечатки отвечают «да/нет», а решать по ним человеку необратимое.
|
||||
for _, want := range []string{"РАЗОШЛИСЬ", "было 2040, стало 2049", "task down", "mv "} {
|
||||
//
|
||||
// «Было/стало» обязано покрывать КАЖДУЮ единицу хранения: единица, которой
|
||||
// нет в счётчиках, делает расхождение безадресным — человек видит «не
|
||||
// совпало» при неизменившемся числе объектов. Класс «покрыта новая секция»
|
||||
// назван отдельно потому, что первый прогон после такого изменения
|
||||
// расходится гарантированно и штатно.
|
||||
for _, want := range []string{
|
||||
"РАЗОШЛИСЬ", "было 2040, стало 2049",
|
||||
"тренировок: было 0, стало 2", "записей: было 0, стало 2",
|
||||
"покрытая разбором новая", "task down", "mv ",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("отчёт не содержит %q", want)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user