feat(orders): добавлен версионный ODS заказов
- Зачем: - менти должен различать версию заказа, наблюдение источника и запуск загрузки. - Что: - добавлены таблицы версий и брака заказов, а также представление ods.order_v. - даг orders_ingest дополнен строгим переходом одного среза STG в две цели ODS. - результаты опытов записаны в документации, дефект генератора вынесен в #102. - Проверка: - make lint config-test smoke check-clickhouse check-services.
This commit is contained in:
@@ -57,9 +57,6 @@
|
||||
доли классов, стоимость доставки — калибровка при реализации; финальная
|
||||
фиксация чисел — пересборка эталонного мира, этап 7. При пересборке правки
|
||||
потребуют только числа, не устройство.
|
||||
- **Проверки приёма** — опыты из [«Рисков и проверки»](ingestion.md) про брак
|
||||
и версии в ODS — тикет перехода STG → ODS (#94). Допущение «один запуск —
|
||||
одно чтение» принято живым прогоном при исполнении #93.
|
||||
- **`_load_id` выше ODS** — вместе с устройством `dds.order` (#85).
|
||||
- **Контур проверок качества для расхождений** (даг DQ, `dm.dq_summary`) —
|
||||
остаётся в тумане карты #69; естественное место разговора — этап 4.
|
||||
|
||||
@@ -60,10 +60,10 @@ JSON-объектом с точным набором ключей: `order_id`, `
|
||||
`items`, `snapshot_date`.
|
||||
|
||||
Скалярные поля проверяются по типу JSON. Деньги дополнительно обязаны быть
|
||||
строками с ровно двумя знаками после точки, времена — строками RFC 3339 в UTC с
|
||||
обязательными миллисекундами, дата слепка — строкой `YYYY-MM-DD`. `items`
|
||||
проверяется только как JSON-массив. Каноническая форма и основания выбора
|
||||
зафиксированы в
|
||||
строками неотрицательной суммы с ровно двумя знаками после точки — минус в эту
|
||||
форму не входит; времена — строками RFC 3339 в UTC с обязательными
|
||||
миллисекундами; дата слепка — строкой `YYYY-MM-DD`. `items` проверяется только
|
||||
как JSON-массив. Каноническая форма и основания выбора зафиксированы в
|
||||
[исследовании формата](../../research/2026-08-16-order-snapshot-wire-format.md).
|
||||
|
||||
Проверять все верхнеуровневые поля здесь уместно: их одиннадцать, и десять
|
||||
@@ -156,6 +156,23 @@ JSON-объектом с точным набором ключей: `order_id`, `
|
||||
|
||||
## Что проверено
|
||||
|
||||
Переход STG → ODS снят на живом стенде 18 августа 2026 года при исполнении #94.
|
||||
Опыты этого раздела прогнаны и подтвердили обещанное: разбиение сырья на годные
|
||||
строки и брак полное и непересекающееся; две версии одного заказа легли в одну
|
||||
партицию и на один шард, а `ods.order_v` вернуло позднюю независимо от фонового
|
||||
слияния; повтор задачи с тем же `_load_id` строку в `ods.order_v` и её
|
||||
`_load_ts` не изменил, а таблица ошибок записала тот же брак второй раз; строки
|
||||
опытов убраны. Числа, механика опытов и поведение ClickHouse, на которое всё это
|
||||
опирается, — [документ хранилища](../storage.md), «Что проверено».
|
||||
|
||||
Одно расхождение с ожиданием осталось, и оно снаружи приёма: восемь настоящих
|
||||
строк слепка получили класс `field_invalid` — все версии одного заказа с
|
||||
отрицательным `total`. Класс заслужен, граница верна, дефект в генераторе и
|
||||
заведён отдельным issue
|
||||
[#102](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/102).
|
||||
Пока он не починен, критерий «честный прогон дня даёт пустой `_errors`» на
|
||||
стартовом мире не выполняется.
|
||||
|
||||
Забор из Kafka в STG снят на живом стенде 18 августа 2026 года при исполнении
|
||||
#93: одно прямое чтение приносит весь слепок дня, метаданные доставки доступны,
|
||||
офсеты коммитятся, а сбой забора не двигает позицию мира. Числа — [ADR
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
keeper, Kafka, каркас сервисов. Этап 2 идёт: в `sql/ddl/` лежит вся цепочка
|
||||
`Kafka → STG → ODS` — чтец топика `hits`, таблицы сырья, типизированное
|
||||
событие с таблицей ошибок, поверхность актуального состояния и три матвью.
|
||||
Этап 3 добавил вход второго источника: топик `orders`, свой чтец и своё сырьё,
|
||||
которое наполняет даг `orders_ingest`, а не матвью. Дальше по тексту устройство
|
||||
описано так, как оно проектируется; построенное от заложенного отличает карта
|
||||
таблиц в конце.
|
||||
Этап 3 добавил вход второго источника и довёл его до ODS: топик `orders`, свой
|
||||
чтец, своё сырьё, версии заказов с таблицей ошибок и поверхность текущего
|
||||
состояния. Наполняет всю цепочку даг `orders_ingest` двумя шагами, а не матвью.
|
||||
Дальше по тексту устройство описано так, как оно проектируется; построенное от
|
||||
заложенного отличает карта таблиц в конце.
|
||||
|
||||
Зона ответственности у документа одна — хранилище. Генератор описан отдельно:
|
||||
его замысел — в [спеке генератора](../specs/2026-08-01-generator.md), формат
|
||||
@@ -422,8 +423,8 @@ kafka_offset)`: смотрят такую таблицу от класса, а
|
||||
|---|---|
|
||||
| `00-databases.sql` | базы слоёв |
|
||||
| `10-stg-tables.sql` | чтецы топиков `hits` и `orders`, локальные и распределённые таблицы сырья обоих источников |
|
||||
| `20-ods-tables.sql` | типизированное событие и таблица ошибок |
|
||||
| `30-ods-views.sql` | актуальные события и матвью разбора в ODS |
|
||||
| `20-ods-tables.sql` | типизированное событие, версии заказа и обе таблицы ошибок |
|
||||
| `30-ods-views.sql` | актуальные события, текущие заказы и матвью разбора в ODS |
|
||||
| `40-stg-views.sql` | матвью приёма: чтец в сырьё |
|
||||
|
||||
Порядок задают два правила. Первое: матвью принадлежит слою своей цели, а не
|
||||
@@ -482,6 +483,12 @@ ODS. Второе: матвью приёма создаётся последне
|
||||
| ODS | `ods.event_v` | актуальная версия события с полями источника |
|
||||
| ODS | `ods.event_errors_rep` / `_dist` | строки, не прошедшие строгий приём |
|
||||
| ODS | `ods.event_mv`, `ods.event_errors_mv` | разбор сырья в событие и в ошибки |
|
||||
| ODS | `ods.order_snapshot_rep` / `_dist` | типизированные версии заказа |
|
||||
| ODS | `ods.order_v` | текущая версия заказа на языке источника |
|
||||
| ODS | `ods.order_snapshot_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
|
||||
|
||||
Матвью разбора у заказов нет: срез сырья раскладывают по этим двум целям два
|
||||
`INSERT SELECT` шага `parse_batch` в даге `orders_ingest`.
|
||||
|
||||
Слои DDS и DM появляются на следующих этапах; их состав задан разделом 7
|
||||
мастер-спеки и переносится сюда по мере постройки.
|
||||
@@ -502,6 +509,48 @@ MCP Context7 подтвердила обычное представление с
|
||||
`ods.event_v` остался равен `ods.event_dist FINAL`. Имена и типы всех колонок
|
||||
представления совпали с распределённой таблицей.
|
||||
|
||||
**Проверка версий заказов 18 августа 2026 года (#94).** MCP Context7 подтвердил,
|
||||
что `JSONType` возвращает имя типа значения JSON, — на нём стоит проверка типов
|
||||
в предикате приёма заказов. Остальное снято на закреплённом ClickHouse 26.3.
|
||||
|
||||
Три находки касаются не заказов, а самого ClickHouse, и знать их стоит любому,
|
||||
кто пишет здесь строгий разбор:
|
||||
|
||||
- **`toDateOrNull` календарь не проверяет.** `'2026-02-30'` он молча превращает
|
||||
в `2026-03-02`, `'2026-13-01'` — в `1970-01-01`. Для строгого приёма он
|
||||
поэтому не годится: нужен `parseDateTimeInJodaSyntaxOrNull(…, 'yyyy-MM-dd',
|
||||
'UTC')`, который на обеих строках даёт `NULL`.
|
||||
- **Маска разбора формы не держит.** `parseDateTime64InJodaSyntaxOrNull` по
|
||||
маске `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` берёт и `2026-6-3T14:21:07.123Z` — без
|
||||
ведущих нулей. Форму приходится сверять отдельно, регулярным выражением;
|
||||
зато календарь маска проверяет честно (30 февраля, 13-й месяц, 25-й час
|
||||
дают `NULL`), как и лишние или недостающие знаки долей секунды и смещение
|
||||
`+00:00` вместо `Z`.
|
||||
- **У временных типов есть потолок, и он ниже, чем кажется.** `DateTime64(3)`
|
||||
заканчивается на `2299-12-31`, `DateTime` — на `2106-02-07`; строки за
|
||||
потолком разбор отдаёт как `NULL`. Это ловит опыт, который берёт «заведомо
|
||||
далёкий» год: 2999-й не разбирается вовсе.
|
||||
|
||||
Про сам приём заказов снято следующее. Предикат формы провода разложил все
|
||||
13 661 строку сырья, приехавшую при исполнении #93, без исключений и без
|
||||
`NULL`: 13 653 годных и 8 брака, сумма сошлась с общим счётом по каждому
|
||||
`_load_id`, пересечение ветвей — ноль. Все восемь строк брака — версии одного
|
||||
заказа с отрицательным `total`; это дефект генератора, разобранный в
|
||||
[спецификации приёма](orders/ingestion.md), «Что проверено». На управляемой
|
||||
порции из пяти строк (две версии
|
||||
одного заказа плюс по одной строке каждого класса брака) годные ушли в
|
||||
`ods.order_snapshot`, брак — в `ods.order_snapshot_errors` с ожидаемыми
|
||||
классами. Обе версии легли в одну партицию и на один шард; `FINAL` через
|
||||
распределённую таблицу вернул одну строку — ту, у которой `updated_at` позже, —
|
||||
тогда как физический счёт показывал две. Повтор задачи с тем же `_load_id`
|
||||
строку в `ods.order_v` и её `_load_ts` не изменил, а таблица ошибок записала
|
||||
тот же брак второй раз, как и обещано спецификацией приёма.
|
||||
|
||||
Отдельно измерено, что **сводка вставки не годится в счётчики строк**: у
|
||||
запроса с `WHERE` `read_rows` считает прочитанное с диска, а не подошедшее, —
|
||||
на одном и том же срезе из пяти строк две вставки дали 4 и 5. Сколько чего
|
||||
легло, спрашивают у самих таблиц по `_load_id`.
|
||||
|
||||
**Сверено с документацией.** Собственная колонка с именем виртуальной делает
|
||||
виртуальную недоступной. При вставке в `Distributed` шард выбирается по ключу
|
||||
шардирования; фоновый режим — умолчание, а `distributed_foreground_insert = 1`
|
||||
|
||||
Reference in New Issue
Block a user