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:
2026-08-18 23:10:01 +03:00
parent 11a494f550
commit bdcbe48b59
6 changed files with 342 additions and 26 deletions
-3
View File
@@ -57,9 +57,6 @@
доли классов, стоимость доставки — калибровка при реализации; финальная
фиксация чисел — пересборка эталонного мира, этап 7. При пересборке правки
потребуют только числа, не устройство.
- **Проверки приёма** — опыты из [«Рисков и проверки»](ingestion.md) про брак
и версии в ODS — тикет перехода STG → ODS (#94). Допущение «один запуск —
одно чтение» принято живым прогоном при исполнении #93.
- **`_load_id` выше ODS** — вместе с устройством `dds.order` (#85).
- **Контур проверок качества для расхождений** (даг DQ, `dm.dq_summary`) —
остаётся в тумане карты #69; естественное место разговора — этап 4.
+21 -4
View File
@@ -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
+55 -6
View File
@@ -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`