добавлен словарь категориальных значений HAE → коды HealthKit
- фазы сна, контекст пульса и имена тренировок попадают в реестр `category_value` (миграция 00010): строка хранится дословно, выведенный код лежит рядом отдельной записью, а не полем внутри точки - словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в сыром архиве нет - наблюдение входит в отпечаток витрины, выведенный код — нет: он производная от словаря, а не от журнала
This commit is contained in:
@@ -246,7 +246,8 @@ func (s *Store) Merge(ctx context.Context, in Incoming, from DeliveryRef) (Merge
|
||||
stats.RecordsWritten = written
|
||||
stats.EntitiesHeld += held
|
||||
stats.HeldAt = clipRefs(append(stats.HeldAt, heldAt...))
|
||||
return nil
|
||||
|
||||
return mergeCategories(ctx, tx, in.Categories, from)
|
||||
})
|
||||
if err != nil {
|
||||
return MergeStats{}, err
|
||||
@@ -602,6 +603,18 @@ func readBucket(ctx context.Context, tx *sql.Tx, key bucketKey) (Bucket, bool, e
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
// writeBucket пишет часовой объект.
|
||||
//
|
||||
// `first_delivery_id` в `DO UPDATE` НЕ входит намеренно: провенанс объекта —
|
||||
// «кто создал строку», то есть функция ПОРЯДКА СВЁРТКИ, а не журнала. Это
|
||||
// допустимо ровно потому, что в отпечаток витрины он не идёт
|
||||
// (см. fingerprintBuckets): расхождение по нему ненаблюдаемо и решений по нему
|
||||
// не принимают.
|
||||
//
|
||||
// Единице хранения, чей провенанс входит в отпечаток, такого правила МАЛО — там
|
||||
// нужен явный минимум по журналу, иначе живой приём и пересборка разойдутся при
|
||||
// одинаковом журнале. Образец — mergeCategories в category.go. Сказано здесь,
|
||||
// потому что копировать будут отсюда: этот upsert старше и проще.
|
||||
func writeBucket(ctx context.Context, tx *sql.Tx, b Bucket, now time.Time) error {
|
||||
payload, err := encodePayload(b.Points)
|
||||
if err != nil {
|
||||
@@ -806,10 +819,12 @@ func (s *Store) CountBuckets(ctx context.Context) (int64, error) {
|
||||
// Значит «объектов столько же» совпадёт и при заведомо сломанном правиле, а
|
||||
// отпечаток — нет.
|
||||
//
|
||||
// Покрывает ВСЕ единицы хранения — часовые объекты, тренировки и записи.
|
||||
// Отпечаток одних объектов давал бы «состояние сошлось» при разъехавшихся
|
||||
// тренировках, то есть ломался бы молча тем самым изменением, которое добавило
|
||||
// данные.
|
||||
// Покрывает ВСЕ единицы хранения — часовые объекты, тренировки, записи и реестр
|
||||
// категориальных значений. Отпечаток одних объектов давал бы «состояние
|
||||
// сошлось» при разъехавшихся тренировках, то есть ломался бы молча тем самым
|
||||
// изменением, которое добавило данные.
|
||||
//
|
||||
// От реестра берётся НАБЛЮДЕНИЕ, но не выведенный код: см. fingerprintCategories.
|
||||
//
|
||||
// Все разделы читаются ОДНИМ снимком базы: отпечаток рабочей витрины снимается
|
||||
// под живым приёмом, и запросы вне общей транзакции дали бы смесь «объекты до»
|
||||
@@ -830,17 +845,65 @@ func (s *Store) Fingerprint(ctx context.Context) (string, error) {
|
||||
if err := fingerprintEntities(ctx, tx, h); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := fingerprintCategories(ctx, tx, h); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// Признак раздела впереди строки: без него строка одного раздела может совпасть
|
||||
// со строкой другого, и два разных состояния витрины дали бы один отпечаток.
|
||||
const (
|
||||
fpBucket = "b"
|
||||
fpWorkout = "w"
|
||||
fpRecord = "r"
|
||||
fpBucket = "b"
|
||||
fpWorkout = "w"
|
||||
fpRecord = "r"
|
||||
fpCategory = "c"
|
||||
)
|
||||
|
||||
// fingerprintCategories добавляет в отпечаток реестр категориальных значений —
|
||||
// его НАБЛЮДЕНИЕ, но не выведенный код.
|
||||
//
|
||||
// Ключ и провенанс — функция журнала: те же тела в том же порядке дают их
|
||||
// побайтно. Код — функция журнала И версии словаря в бинаре. Включи его сюда, и
|
||||
// отпечаток перестал бы отвечать на свой единственный вопрос («дал ли повтор
|
||||
// журнала то же состояние») ровно тогда, когда его задают: всякое пополнение
|
||||
// словаря давало бы расхождение при побайтно совпавшем журнале, а человек,
|
||||
// принимающий по отпечатку необратимое решение о подмене базы, читал бы это как
|
||||
// дефект. Правильность вывода кода проверяют тесты словаря — это другой вопрос,
|
||||
// и смешение обесценило бы оракул.
|
||||
//
|
||||
// Значений наружу отпечаток не раскрывает: он хеш.
|
||||
func fingerprintCategories(ctx context.Context, tx *sql.Tx, h io.Writer) error {
|
||||
const q = `
|
||||
SELECT metric, field, value, first_seen_utc, first_delivery_id
|
||||
FROM category_value ORDER BY metric, field, value`
|
||||
|
||||
rows, err := tx.QueryContext(ctx, q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select category values: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
for rows.Next() {
|
||||
var metric, field, value, firstSeen, deliveryID string
|
||||
if err := rows.Scan(&metric, &field, &value, &firstSeen, &deliveryID); err != nil {
|
||||
return fmt.Errorf("scan category value: %w", err)
|
||||
}
|
||||
// Длина впереди каждого поля переменной длины: значение приходит из тела
|
||||
// дословно и может содержать что угодно, включая признак раздела и
|
||||
// разделители. Без длины пара (`a`, `b|c`) дала бы ту же строку, что
|
||||
// (`a|b`, `c`), — то есть два разных состояния витрины сошлись бы
|
||||
// отпечатком. Тот же приём в fingerprintBuckets и по той же причине.
|
||||
fmt.Fprintf(h, "%s|%d:%s|%d:%s|%d:%s|%s|%d:%s\n",
|
||||
fpCategory, len(metric), metric, len(field), field,
|
||||
len(value), value, firstSeen, len(deliveryID), deliveryID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("select category values: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fingerprintBuckets(ctx context.Context, tx *sql.Tx, h io.Writer) error {
|
||||
const q = `
|
||||
SELECT metric, layer, hour_utc, content_hash, points, units, sealed FROM bucket
|
||||
|
||||
Reference in New Issue
Block a user