feat(ods): типизированное событие, строгий приём и таблица ошибок
Зачем: цепочка Kafka → STG → ODS достраивается последним этажом. Сырьё уже доезжает (#37), настоящие события в топике есть (#41), а типизированного слоя не было — событие негде было прочитать колонками, а брак негде увидеть. Что: - sql/ddl/20-ods-tables.sql — ods.event_rep/_dist на ReplacingMergeTree с версией _load_ts, партиция по EventDate, ключ по разделу 1.3 спеки, шардирование cityHash64(ClientID); ods.event_errors_rep/_dist с классом брака, своими ключами и сроком жизни в месяц. - sql/ddl/30-ods-views.sql — две матвью над stg.hits_raw_dist. Годность считает предикат из трёх частей, вторая матвью берёт его дословное отрицание, класс брака пишется первым совпавшим из трёх. - Метку времени разбирает parseDateTimeBestEffortOrNull, а не JSONExtract: ISO-8601 с суффиксом Z JSONExtract не берёт вовсе. Спека генератора обещала обратное — обещание поправлено, форма на проводе не менялась. - Сверка объявлений (contract-тест) снята из документов и из докстрингов schema.py: сверх строгого приёма она ловила только смену типа. - Документация приведена в соответствие: ADR 0005, дока хранилища и обе спеки; группа «сказано по памяти» в доке хранилища опустела. Проверка: make up && make check-clickhouse (8 проверок, 7,5 с); make lint, make typecheck, make test (406), make docs без диффа. Разовые опыты при исполнении — в теле PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,8 +33,8 @@
|
||||
- **Детерминизм до байта.** Одно зерно — побайтово тот же снимок; сверка —
|
||||
хешами манифеста. Транспорт (офсеты Kafka, темп) — вне обещания.
|
||||
- **Схема — контракт генератора.** Python-модуль с чистыми данными;
|
||||
хранилище строится по рендеренной документации, границу сторожит
|
||||
contract-тест.
|
||||
хранилище строится по рендеренной документации, границу сторожит строгий
|
||||
приём на стороне хранилища.
|
||||
- **Один сериализатор, глупые приёмники.** День-функция выдаёт канонические
|
||||
байты; приёмники — файл, Kafka пачкой, Kafka с темпом.
|
||||
- **Числа.** Средний день ~50 тыс. событий; эталонный снимок — 14 дней;
|
||||
@@ -229,10 +229,13 @@
|
||||
- **Сторона хранилища пишется по документации, не генерируется.** DDL
|
||||
`ods.event`, SELECT матвью, `dds.event_v`, трансформации — работа
|
||||
следующих этапов по «описанию выгрузки», как в бою хранилище адаптируется
|
||||
к источнику. Границу сторожат два боевых механизма: строгий приём
|
||||
к источнику. Границу сторожит боевой механизм — строгий приём
|
||||
(`Nullable`-разбор со сверкой набора ключей, таблицы `*_errors` — раздел 6
|
||||
мастер-спеки) и contract-тест в smoke — сравнение `system.columns`
|
||||
поднятого стенда со схемой генератора.
|
||||
мастер-спеки). Сверка объявлений (`system.columns` поднятого стенда против
|
||||
контракта) здесь стояла вторым механизмом и снята при исполнении #43:
|
||||
сверх строгого приёма она ловила ровно одно — смену типа колонки, — а её
|
||||
ловит и сверка разобранного события, причём на живых данных, а не на
|
||||
объявлениях.
|
||||
|
||||
Отклонено с доводами:
|
||||
|
||||
@@ -263,10 +266,18 @@
|
||||
- **Даты и время на проводе — ISO-8601.** `EventDate` уезжает как `2026-06-01`,
|
||||
`UTCEventTime` — как `2026-06-01T12:34:56Z`. Довод — читаемость сырья: весь
|
||||
смысл слоя STG в том, что менти открывает колонку `raw` в обычном клиенте и
|
||||
разбирает событие глазами, а число эпохи этот урок убивает. Разбору это
|
||||
ничего не стоит: `JSONExtract(raw, 'UTCEventTime', 'Nullable(DateTime)')`
|
||||
принимает ISO без плясок. Колонка `ecommerce` — строка, внутри которой лежит
|
||||
экранированный JSON, как отдаёт Метрика.
|
||||
разбирает событие глазами, а число эпохи этот урок убивает. Колонка
|
||||
`ecommerce` — строка, внутри которой лежит экранированный JSON, как отдаёт
|
||||
Метрика.
|
||||
|
||||
Оговорка про цену разбора, вписанная сюда 6 августа и оказавшаяся неверной:
|
||||
здесь стояло, что `JSONExtract(raw, 'UTCEventTime', 'Nullable(DateTime)')`
|
||||
«принимает ISO без плясок». Не принимает — на строке с суффиксом `Z` он
|
||||
отдаёт NULL, и при исполнении #43 это увело бы в брак все события до
|
||||
единого. Измерено на стенде 7 августа 2026 года; форма на проводе от этого
|
||||
не меняется, меняется выражение разбора на стороне хранилища —
|
||||
`parseDateTimeBestEffortOrNull` вместо `JSONExtract`
|
||||
([ADR 0005](../adr/0005-event-ingestion.md)).
|
||||
|
||||
Форму реализует сериализатор (#41), хранилище (#43) читает то, что он
|
||||
положил: порядок тикетов развёрнут 6 августа 2026 года, и отправитель идёт
|
||||
@@ -283,7 +294,8 @@
|
||||
ReplacingMergeTree.
|
||||
- **Эталонный снимок при старте стенда — через Kafka, пакетным режимом
|
||||
проигрывателя.** Отдельный механизм заливки не строится: каждый `make up`
|
||||
бесплатно прогоняет весь конвейер и contract-тест на настоящих данных.
|
||||
бесплатно прогоняет весь конвейер на настоящих данных, и строгий приём
|
||||
хранилища проверяет контракт тем же прогоном.
|
||||
Оговорка «если заливка уйдёт в десятки минут — вернуться к прямой
|
||||
загрузке» проверена при фиксации чисел: 14 × 50 тыс. ≈ 700 тыс. событий —
|
||||
расчётно минута-две, запас есть.
|
||||
@@ -395,8 +407,8 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
||||
Внесены в мастер-спеку тем же коммитом, что и эта спека:
|
||||
|
||||
- **Раздел 1.4**: «из контракта выводятся DDL и валидация» заменено на data
|
||||
contract — хранилище пишется по документации, границу сторожит
|
||||
contract-тест (раздел 3 здесь).
|
||||
contract — хранилище пишется по документации, границу сторожит строгий
|
||||
приём (раздел 3 здесь).
|
||||
- **Раздел 8**: артефакт `data/startup_history/` в git заменён манифестом;
|
||||
снимок генерируется на месте (раздел 5 здесь). Туман «политика
|
||||
версионирования артефакта» закрыт этим же ходом: версионируется манифест.
|
||||
|
||||
Reference in New Issue
Block a user