docs(orders): зафиксирован версионный приём заказов

- Зачем:
  - отменённая подмена партиции snapshot_date противоречила порционному чтению Kafka и могла обучать потере ранее принятых версий.
- Что:
  - добавлены спецификация приёма заказов и ADR о версионном ODS с ods.order_v.
  - согласованы мастер-спека, дока хранилища, ADR 0008 и исследование формата.
  - зафиксированы граница приёма, координаты загрузки, диагностические повторы и отложенное проектирование DDS.
- Проверка:
  - git diff --cached --check.
  - горячее ревью по правилам репозитория и принятому решению.
  - два прохода холодного ревью.
This commit is contained in:
2026-08-16 21:42:11 +03:00
parent 8d54a3caba
commit ede1df765c
6 changed files with 311 additions and 65 deletions
+42 -16
View File
@@ -78,15 +78,16 @@ keeper, Kafka, каркас сервисов. Этап 2 идёт: в `sql/ddl/`
Ключи ко-локации названы заранее, потому что на них стоит политика соединений из
раздела 6 спеки: обычное соединение разрешено только по ключу ко-локации, всё
прочее — через `GLOBAL`. Значит `dds.session` и `dds.identity_map` шардируются по
`cityHash64(ClientID)`, а `dds.order` и производные от заказа — по
`cityHash64(order_id)`. Ключи витрин появятся вместе с самими витринами.
`cityHash64(ClientID)`. Объекты DDS с зерном заказа должны сохранять ко-локацию
по `cityHash64(order_id)`; ключи остальных частей будущей модели и витрин
появятся вместе с ними.
Открытый вопрос на будущее — не сама замена партиций: операции с ними по
локальным таблицам правило разрешает прямо. Вопрос в шаге до неё. Партиция-донор
должна быть уже разложена по шардам по тому же ключу, а разложить её можно
только вставкой через распределённую таблицу — значит у каждой пакетной сущности
появится вторая пара объектов, и имени для неё конвенция пока не даёт. Решать
это вместе со сборкой DDS, а не задним числом.
Замена партиций не используется для `ods.order_snapshot`: версии прошлых дней
доливаются, а прямое чтение Kafka не задаёт границы полного слепка ([ADR
0010](../adr/0010-order-versions-in-ods.md)). Если партиционная пересборка
понадобится будущим объектам DDS или DM, партиция-донор должна быть заранее
разложена по шардам по тому же ключу. Форму донора следует решать вместе с таким
объектом, а не переносить на ODS заранее.
## Служебные колонки
@@ -118,7 +119,8 @@ keeper, Kafka, каркас сервисов. Этап 2 идёт: в `sql/ddl/`
Само сообщение лежит в колонке `raw` тем, чем пришло: чтец читает байты и ничего
не проверяет, поэтому там оказываются и целые сообщения, и мусор. События ниже
разбирают матвью ODS ([ADR 0005](../adr/0005-event-ingestion.md)), заказы —
пакетный шаг ([ADR 0008](../adr/0008-order-ingestion.md)).
пакетный шаг ([ADR 0008](../adr/0008-order-ingestion.md), [ADR
0010](../adr/0010-order-versions-in-ods.md)).
Движок таблицы сырья — обычный `ReplicatedMergeTree`, `ORDER BY (kafka_partition,
kafka_offset)`: разбор полётов идёт от «какое сообщение», другого ключа у сырья и
@@ -130,8 +132,8 @@ kafka_offset)`: разбор полётов идёт от «какое сооб
заказов — пакетным шагом. Дальше метка переносится в ODS как есть и отвечает на
вопрос «когда строка приехала в хранилище», а не «когда её разобрали». В
`ods.event` она же служит колонкой версии `ReplacingMergeTree` и схлопывает
повтор доставки. `ods.order_snapshot` повтор не схлопывает: пакетный шаг
заменяет целиком партицию `snapshot_date`.
повтор доставки. У `ods.order_snapshot` версию задаёт `updated_at` источника;
`_load_ts` только показывает, когда конкретная строка приехала.
`created_at` и `updated_at` заказа к служебным колонкам хранилища не относятся.
Они приезжают в сообщении как аудит строки в БД источника и в ODS разбираются в
@@ -141,8 +143,8 @@ kafka_offset)`: разбор полётов идёт от «какое сооб
Пакетной переобработки у событий в ODS нет: слой наполняют матвью, а не задание
Airflow. Переделать разобранное руками можно вставкой из сырья с фильтром по
`_load_ts` в пределах трёхсуточного окна; ничья по версии разрешается в пользу
вставленного позже. Заказы, напротив, по построению перерабатываются дневными
партициями пакетного шага.
вставленного позже. Заказы разбирает пакетный шаг из неизменного среза STG по
`_load_id`; дневные партиции ODS он не заменяет.
Имя согласовано с каноном служебных полей соседнего учебного стенда на
Greenplum, чтобы словарь был общим у двух хранилищ; ведущее подчёркивание у
@@ -151,9 +153,27 @@ Greenplum, чтобы словарь был общим у двух хранил
запрещено совпадать с именами виртуальных колонок, а не носить подчёркивание.
Общего идентификатора пачки загрузки (`_load_id`) нет. У потока событий нет ни
пачки, ни `run_id`, и колонка была бы пустой формальностью. Пакетный шаг заказов
не делает её общей конвенцией: конкретный слой заведёт `run_id`, только когда у
него появится названный читатель этой координаты.
пачки, ни `run_id`, и колонка была бы пустой формальностью. У заказов читатель
назван: `_load_id` равен `run_id` Airflow и переносится из STG в годную строку
ODS и в таблицу ошибок. Нужен ли он выше ODS, решается вместе с моделью DDS.
## Версии заказов
`ods.order_snapshot_rep` хранит принятые версии в
`ReplacingMergeTree(updated_at)`: ключ сортировки — `order_id`, партиция —
`toDate(created_at)`. `ods.order_snapshot_dist` шардирует по
`cityHash64(order_id)`. Все версии заказа лежат в одной партиции, чтобы их могли
схлопывать фоновые слияния, и на одном шарде, чтобы распределённый `FINAL`
выбрал одного победителя.
Физическая пара нужна для загрузки и диагностики. Обычное чтение показывает
версии, которые ещё не убрали фоновые слияния, и не является архивом истории.
`ods.order_v` служит поверхностью точного текущего состояния для следующих
слоёв. Представление сохраняет язык источника и не решает, какой станет модель
DDS. `snapshot_date` в нём остаётся датой наблюдения строки, а не ключом
публикации. Полное решение — в
[ADR 0010](../adr/0010-order-versions-in-ods.md) и
[спецификации приёма заказов](../specs/2026-08-16-order-ingestion.md).
## Часовые пояса
@@ -367,6 +387,12 @@ D0 и к реальному календарю не привязана; паке
kafka_offset)`: смотрят такую таблицу от класса, а внутри класса — по координатам
доставки.
`ods.order_snapshot_errors` держит тот же диагностический минимум и `_load_id`
запуска. У заказов три класса по приоритету: `not_an_object`,
`keyset_mismatch`, `field_invalid`. Сырой текст остаётся рядом, поэтому класс не
разрастается до имени отдельного поля. Точная граница приёма — в
[спецификации заказов](../specs/2026-08-16-order-ingestion.md).
## Раскладка DDL
Файлы лежат в `sql/ddl/` и применяются по порядку имён. Сначала все статичные