feat: разбор и хранение тренировок и состояния разума
- секции `workouts` и `stateOfMind` покрыты разбором: тренировка лежит одной строкой вместе с маршрутом и внутренними рядами, запись — по ключу `род + id`; миграция 00007 заводит обе таблицы и возвращает в очередь `partial`-доставки с этими ключами - сущность заменяется целиком, но условно: приехавшая побеждает, если не теряет содержания сохранённой (множество ключей и длины верхнеуровневых массивов), а при равном содержании выигрывает версия из более поздней доставки ЖУРНАЛА — «побеждает приехавшая» было бы функцией порядка свёртки, и живая витрина расходилась бы с пересборкой молча - отпечаток витрины покрывает тренировки и записи и снимается одним снимком базы; отчёт `reindex` считает «было и стало» по каждой единице хранения
This commit is contained in:
@@ -17,7 +17,7 @@ import (
|
||||
// каждом глубоком проходе, и настоящий отказ правила слияния становился
|
||||
// неотличим от нормы — при том что счётчик перезаписей объявлен единственным
|
||||
// наблюдением за этим правилом.
|
||||
func TestMergePointsДребезгНеСчитаетсяСтолкновением(t *testing.T) {
|
||||
func TestMergeДребезгНеСчитаетсяСтолкновением(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string][2]string{
|
||||
@@ -41,10 +41,10 @@ func TestMergePointsДребезгНеСчитаетсяСтолкновение
|
||||
first := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", pair[0])
|
||||
second := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", pair[1])
|
||||
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{first}, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{first}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.MergePoints(ctx, []store.IncomingPoint{second}, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{second}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func TestMergePointsДребезгНеСчитаетсяСтолкновение
|
||||
// Настоящее столкновение обязано оставить след с координатами объекта: одно
|
||||
// число `overwrites` не говорит, какая метрика и какой час пострадали, и
|
||||
// расследовать перезапись по нему нечем.
|
||||
func TestMergePointsСтолкновениеОставляетКоординаты(t *testing.T) {
|
||||
func TestMergeСтолкновениеОставляетКоординаты(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -74,10 +74,10 @@ func TestMergePointsСтолкновениеОставляетКоординат
|
||||
poor := point(t, "heart_rate", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"Avg":61}`)
|
||||
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{rich}, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{rich}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.MergePoints(ctx, []store.IncomingPoint{poor}, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{poor}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func TestMergePointsСтолкновениеОставляетКоординат
|
||||
// экранирует `&`, `<` и `>` внутри содержимого — порчи значений это не даёт, но
|
||||
// обещание перестаёт быть правдой, а сравнение байтов при следующей доставке
|
||||
// той же точки начинает промахиваться навсегда.
|
||||
func TestMergePointsХранитУгловыеСкобкиИАмперсанд(t *testing.T) {
|
||||
func TestMergeХранитУгловыеСкобкиИАмперсанд(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -107,7 +107,7 @@ func TestMergePointsХранитУгловыеСкобкиИАмперсанд(t
|
||||
raw := `{"qty":1,"source":"Anton & Co <iPhone> \"x\""}`
|
||||
in := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", raw)
|
||||
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{in}, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{in}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func TestMergePointsХранитУгловыеСкобкиИАмперсанд(t
|
||||
// Смена единиц не имеет права молча переподписать уже сохранённые точки:
|
||||
// внутри точки единиц нет, и у ранних точек не остаётся ничего, по чему их
|
||||
// единицы восстановимы. Правило «первое непустое побеждает» плюс счётчик.
|
||||
func TestMergePointsСменаЕдиницНеПерезаписываетМолча(t *testing.T) {
|
||||
func TestMergeСменаЕдиницНеПерезаписываетМолча(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -148,10 +148,10 @@ func TestMergePointsСменаЕдиницНеПерезаписываетМол
|
||||
mi.End = mi.Start
|
||||
mi.Raw = []byte(`{"qty":2}`)
|
||||
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{km}, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{km}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.MergePoints(ctx, []store.IncomingPoint{mi}, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{mi}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
@@ -173,14 +173,14 @@ func TestMergePointsСменаЕдиницНеПерезаписываетМол
|
||||
// доставки схлопываются, и счётчик присланных систематически завышал бы
|
||||
// содержимое витрины — расхождение «прислали 1000, лежит 700» было бы невидимо
|
||||
// ровно тогда, когда точки начнут теряться по-настоящему.
|
||||
func TestMergePointsСчитаетСохранённые(t *testing.T) {
|
||||
func TestMergeСчитаетСохранённые(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
ctx := context.Background()
|
||||
|
||||
p := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":1}`)
|
||||
stats, err := st.MergePoints(ctx, []store.IncomingPoint{p, p, p}, "d1")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{p, p, p}}, store.DeliveryRef{ID: "d1"})
|
||||
if err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func TestMergePointsСчитаетСохранённые(t *testing.T) {
|
||||
t.Errorf("сохранено %d точек, ожидалась 1 (три точных повтора)", stats.Stored)
|
||||
}
|
||||
|
||||
stats, err = st.MergePoints(ctx, []store.IncomingPoint{p}, "d2")
|
||||
stats, err = st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{p}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("повтор: %v", err)
|
||||
}
|
||||
@@ -201,7 +201,7 @@ func TestMergePointsСчитаетСохранённые(t *testing.T) {
|
||||
// оставляет частичного состояния — а значит и не зависит от порядка обхода.
|
||||
// Раньше транзакция была на объект, и восемь прогонов одной доставки давали
|
||||
// семь разных наборов записанных объектов.
|
||||
func TestMergePointsОтказНеОставляетЧастиОбъектов(t *testing.T) {
|
||||
func TestMergeОтказНеОставляетЧастиОбъектов(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -218,7 +218,7 @@ func TestMergePointsОтказНеОставляетЧастиОбъектов(t
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
if _, err := st.MergePoints(ctx, in, "d1"); err == nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d1"}); err == nil {
|
||||
t.Fatal("слияние на отменённом контексте прошло успешно")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user