Сверка purchase против заказов: dm.purchase_vs_orders (A+C) #155

Closed
opened 2026-08-25 15:01:05 +03:00 by ddmitry · 1 comment
Owner

Part of #6.

Цель

Витрина сверки A+C: FULL OUTER GLOBAL JOIN клиентских purchase и заказов
бэкенда, классы расхождений приоритетом, служебный класс awaiting_order
внутри живого окна. Учебный результат — сюжет этапа: «поведение по
трекеру, деньги по бэкенду» на живых расхождениях A (отмена) и C (дельта
суммы).

Что войдёт

  • DDL dm.purchase_vs_orders_rep / _dist / _v: зерно — заказ; колонки
    спеки §7 (order_day, order_id, declared_revenue, items_total,
    status, mismatch_class); шардирование cityHash64(order_id) —
    ко-локация заказов сохранена.
  • SQL сверки: соединение по purchase_id[1] = order_id — имя контракта, в
    ODS это purchaseID; правило одного элемента массива — комментарием
    (спека §4); обе стороны — подзапросы (FULL OUTER с голой левой
    Distributed двоит непарные строки — исследование #129); сравнимая
    база — items_total, не total. Вставка в _dist — с осознанным
    parallel_distributed_insert_select: источники разложены по client_id
    и order_id, цель — по order_id (капкан storage.md).
  • Классификатор — полный приоритет спеки §4: cancelled > lost_event >
    duplicate_event > amount_delta > match; awaiting_order — вне
    приоритетов, для purchase без заказа внутри живого окна. До этапа 6
    наблюдаемы только match, cancelled, amount_delta,
    awaiting_order — классификатор данных не выдумывает.
  • Ритм — от заказов: замена партиций окна K; задача purchase_vs_orders
    в dm_transform.

Границы

  • Механика расхождений B и D, опоздания-как-урок — этап 6 (#8): генератор
    не трогать.
  • DQ-сверка счётчиков против описи — тикет контура DQ.

Кому что

SQL сверки — Opus: главный учебный SQL этапа. Codex — счётные опыты по
классам по брифу.

Сначала прочитать

  • мастер-спека, разделы 4 и 7 — механика сверки и состав витрины, это
    спека тикета;
  • docs/architecture/dm.md — форма и ритм;
  • docs/research/2026-08-22-distributed-join.md — обе стороны подзапросами;
  • docs/architecture/dds/order.md («Читатели») и event.md (purchase_* в
    контракте).

Проверка

  • make lint; на стенде make smoke, make check-clickhouse и
    make check-services (задача в даге) зелёные.
  • На стартовом мире mismatch_class принимает только match,
    cancelled, amount_delta, awaiting_order; match — большинство;
    cancelled и amount_delta наблюдаемы (счёт > 0) — точные счётчики
    сверит звено DQ против описи.
  • Выборочный amount_delta объясняется позициями: дельта равна сумме
    вычеркнутой позиции.
  • awaiting_order живёт только в днях живого окна; в днях с закрытым
    окном таких строк нет.
  • Повтор прогона: деловое содержимое без изменений; строки заказа —
    на шарде его order_id.
Part of #6. ## Цель Витрина сверки A+C: FULL OUTER GLOBAL JOIN клиентских `purchase` и заказов бэкенда, классы расхождений приоритетом, служебный класс `awaiting_order` внутри живого окна. Учебный результат — сюжет этапа: «поведение по трекеру, деньги по бэкенду» на живых расхождениях A (отмена) и C (дельта суммы). ## Что войдёт - DDL `dm.purchase_vs_orders_rep` / `_dist` / `_v`: зерно — заказ; колонки спеки §7 (`order_day`, `order_id`, `declared_revenue`, `items_total`, `status`, `mismatch_class`); шардирование `cityHash64(order_id)` — ко-локация заказов сохранена. - SQL сверки: соединение по `purchase_id[1] = order_id` — имя контракта, в ODS это `purchaseID`; правило одного элемента массива — комментарием (спека §4); обе стороны — подзапросы (FULL OUTER с голой левой Distributed двоит непарные строки — исследование #129); сравнимая база — `items_total`, не `total`. Вставка в `_dist` — с осознанным `parallel_distributed_insert_select`: источники разложены по `client_id` и `order_id`, цель — по `order_id` (капкан storage.md). - Классификатор — полный приоритет спеки §4: `cancelled` > `lost_event` > `duplicate_event` > `amount_delta` > `match`; `awaiting_order` — вне приоритетов, для `purchase` без заказа внутри живого окна. До этапа 6 наблюдаемы только `match`, `cancelled`, `amount_delta`, `awaiting_order` — классификатор данных не выдумывает. - Ритм — от заказов: замена партиций окна K; задача `purchase_vs_orders` в `dm_transform`. ## Границы - Механика расхождений B и D, опоздания-как-урок — этап 6 (#8): генератор не трогать. - DQ-сверка счётчиков против описи — тикет контура DQ. ## Кому что SQL сверки — Opus: главный учебный SQL этапа. Codex — счётные опыты по классам по брифу. ## Сначала прочитать - мастер-спека, разделы 4 и 7 — механика сверки и состав витрины, это спека тикета; - docs/architecture/dm.md — форма и ритм; - docs/research/2026-08-22-distributed-join.md — обе стороны подзапросами; - docs/architecture/dds/order.md («Читатели») и event.md (`purchase_*` в контракте). ## Проверка - [x] `make lint`; на стенде `make smoke`, `make check-clickhouse` и `make check-services` (задача в даге) зелёные. - [x] На стартовом мире `mismatch_class` принимает только `match`, `cancelled`, `amount_delta`, `awaiting_order`; `match` — большинство; `cancelled` и `amount_delta` наблюдаемы (счёт > 0) — точные счётчики сверит звено DQ против описи. - [x] Выборочный `amount_delta` объясняется позициями: дельта равна сумме вычеркнутой позиции. - [x] `awaiting_order` живёт только в днях живого окна; в днях с закрытым окном таких строк нет. - [x] Повтор прогона: деловое содержимое без изменений; строки заказа — на шарде его `order_id`.
ddmitry added a new dependency 2026-08-25 15:01:27 +03:00
Author
Owner

Резолюция вопросов готовности

Перед реализацией сверены мастер-спека, dm.md, etl.md, модель стартового мира и фактический мир генератора. Ниже — договор, который закрывает оставленные в постановке развилки.

Объём пересборки

У сверки два источника с разными правыми границами. Заказная сторона берёт изменяемые дни из последнего принятого слепка ODS; событийная — дни, которые мир уже прожил. world_position определяет только завершённость событий и не подменяет границы слепка заказов.

Прогон заменяет один непрерывный отрезок:

  • первая сборка охватывает всю доступную историю до последнего прожитого дня;
  • затем левая граница — самая ранняя из начала окна последнего слепка и первого дня источников, отсутствующего в витрине;
  • правая граница — самая поздняя из конца окна последнего слепка и последнего прожитого дня с purchase;
  • готовность хвоста событий перед конвейером остаётся ответственностью #156.

На стартовом мире это дни 0…7: последний принятый слепок несёт дни 0…6, а в прожитом дне 7 есть 255 покупок без заказа. Поэтому обычный прогон сверки затрагивает K + 1 день, но это следствие состояния двух источников, не арифметика от K.

Отклонено: вычислять K + 1 только от world_position — позиция не доказывает, какой слепок принят; полностью пересчитывать историю — закрытые дни теряют обещание замерзания; подмешивать живой хвост отдельным представлением — в главном учебном SQL появляется вторая механика без нового урока.

Деньги события хранятся как Decimal

Внешний формат остаётся честным: трекер присылает purchaseRevenue как Array(Float64), и генератор с описанием выгрузки не меняются. Граница хранения один раз нормализует денежную величину:

  • ods.event_rep.purchaseRevenue — Array(Decimal(18, 2));
  • матвью приёма разбирает JSON-числа сразу в Decimal, без промежуточного Float64;
  • dds.event_v.purchase_revenue наследует этот тип без нового вычисления;
  • в DM declared_revenue становится скаляром Nullable(Decimal(18, 2)) после свёртки покупки до зерна заказа.

Так ODS сохраняет смысл поля источника — деньги с точностью до копейки, — но не переносит вверх неподходящий для денег машинный тип. Сырой JSON остаётся в STG на срок расследования. Создавать ради одной колонки физическую копию широкого события в DDS или повторять округление в каждой витрине не нужно.

Проверка репозитория на текущем main не нашла исполняемых потребителей purchaseRevenue / purchase_revenue: колонка пока только сохраняется и переименовывается. Первые читатели — эта сверка и будущая utm_effectiveness, поэтому изменение входит в скоуп #155 с ограниченным радиусом. При реализации спорный DDL прямого разбора Array(Decimal(18, 2)) сверить через Context7 и на поднятом ClickHouse 26.3; проверку ожидаемой схемы ODS поправить вместе с DDL.

Непарные строки и публичный договор

Отсутствие стороны соединения — данные, а не значение по умолчанию. SQL выполняет FULL OUTER JOIN с обнуляемыми непарными колонками (join_use_nulls = 1); нули и пустые строки стендового умолчания join_use_nulls = 0 здесь запрещены, потому что стирают различие «нет события / нет заказа».

  • order_id и order_day обязательны: берутся с имеющейся стороны; у пары дни совпадают по модели мира;
  • declared_revenue — Nullable(Decimal(18, 2)); слово declared сохраняет её клиентское происхождение;
  • items_total — Nullable(Decimal(18, 2));
  • status — обнуляемая низкокардинальная строка;
  • mismatch_class обязателен;
  • _load_id / _load_ts входят в физическую строку и явный договор _v по правилу dm.md.

purchase без заказа получает awaiting_order. Затем для строк с заказом действует приоритет спеки: cancelled > lost_event > duplicate_event > amount_delta > match. Благодаря этому отменённый заказ без события остаётся cancelled, а не lost_event.

Событийная сторона

События purchase сворачиваются до зерна заказа до соединения: номер из purchase_id[1], число событий для duplicate_event, одна дата и одна объявленная сумма. Генератор гарантирует, что у сюжетного дубля номер, сумма и день одинаковы, поэтому отдельной защитной политики выбора между разными значениями не нужно. amount_delta сравнивает два Decimal; способ представления исходного Float64 не создаёт класс расхождения и не заставляет потребителя витрины заново решать округление.

Проверка

К перечисленным в тикете проверкам добавить make config-test и явный запуск dm_transform. make check-services запускает только пробники test_clickhouse и test_kafka, поэтому сам по себе не доказывает наличие и успех задачи purchase_vs_orders.

На чистом хранении дополнительно проверить типы purchaseRevenue в ODS и purchase_revenue в DDS, точное сохранение копеек и равенство суммы событию источника. На стартовом мире сверить, что число awaiting_order равно числу покупок последнего прожитого дня без заказа, а после следующего слепка прежний свежий день пересобран и ожидавшие строки перешли в обычные классы. Точные счётчики закрытых дней по-прежнему принадлежат #158 и описи мира.

## Резолюция вопросов готовности Перед реализацией сверены мастер-спека, `dm.md`, `etl.md`, модель стартового мира и фактический мир генератора. Ниже — договор, который закрывает оставленные в постановке развилки. ### Объём пересборки У сверки два источника с разными правыми границами. Заказная сторона берёт изменяемые дни из последнего принятого слепка ODS; событийная — дни, которые мир уже прожил. `world_position` определяет только завершённость событий и не подменяет границы слепка заказов. Прогон заменяет один непрерывный отрезок: - первая сборка охватывает всю доступную историю до последнего прожитого дня; - затем левая граница — самая ранняя из начала окна последнего слепка и первого дня источников, отсутствующего в витрине; - правая граница — самая поздняя из конца окна последнего слепка и последнего прожитого дня с `purchase`; - готовность хвоста событий перед конвейером остаётся ответственностью #156. На стартовом мире это дни 0…7: последний принятый слепок несёт дни 0…6, а в прожитом дне 7 есть 255 покупок без заказа. Поэтому обычный прогон сверки затрагивает K + 1 день, но это следствие состояния двух источников, не арифметика от K. Отклонено: вычислять K + 1 только от `world_position` — позиция не доказывает, какой слепок принят; полностью пересчитывать историю — закрытые дни теряют обещание замерзания; подмешивать живой хвост отдельным представлением — в главном учебном SQL появляется вторая механика без нового урока. ### Деньги события хранятся как Decimal Внешний формат остаётся честным: трекер присылает `purchaseRevenue` как `Array(Float64)`, и генератор с описанием выгрузки не меняются. Граница хранения один раз нормализует денежную величину: - `ods.event_rep.purchaseRevenue` — `Array(Decimal(18, 2))`; - матвью приёма разбирает JSON-числа сразу в Decimal, без промежуточного Float64; - `dds.event_v.purchase_revenue` наследует этот тип без нового вычисления; - в DM `declared_revenue` становится скаляром `Nullable(Decimal(18, 2))` после свёртки покупки до зерна заказа. Так ODS сохраняет смысл поля источника — деньги с точностью до копейки, — но не переносит вверх неподходящий для денег машинный тип. Сырой JSON остаётся в STG на срок расследования. Создавать ради одной колонки физическую копию широкого события в DDS или повторять округление в каждой витрине не нужно. Проверка репозитория на текущем `main` не нашла исполняемых потребителей `purchaseRevenue` / `purchase_revenue`: колонка пока только сохраняется и переименовывается. Первые читатели — эта сверка и будущая `utm_effectiveness`, поэтому изменение входит в скоуп #155 с ограниченным радиусом. При реализации спорный DDL прямого разбора `Array(Decimal(18, 2))` сверить через Context7 и на поднятом ClickHouse 26.3; проверку ожидаемой схемы ODS поправить вместе с DDL. ### Непарные строки и публичный договор Отсутствие стороны соединения — данные, а не значение по умолчанию. SQL выполняет FULL OUTER JOIN с обнуляемыми непарными колонками (`join_use_nulls = 1`); нули и пустые строки стендового умолчания `join_use_nulls = 0` здесь запрещены, потому что стирают различие «нет события / нет заказа». - `order_id` и `order_day` обязательны: берутся с имеющейся стороны; у пары дни совпадают по модели мира; - `declared_revenue` — `Nullable(Decimal(18, 2))`; слово `declared` сохраняет её клиентское происхождение; - `items_total` — `Nullable(Decimal(18, 2))`; - `status` — обнуляемая низкокардинальная строка; - `mismatch_class` обязателен; - `_load_id` / `_load_ts` входят в физическую строку и явный договор `_v` по правилу `dm.md`. `purchase` без заказа получает `awaiting_order`. Затем для строк с заказом действует приоритет спеки: `cancelled` > `lost_event` > `duplicate_event` > `amount_delta` > `match`. Благодаря этому отменённый заказ без события остаётся `cancelled`, а не `lost_event`. ### Событийная сторона События `purchase` сворачиваются до зерна заказа до соединения: номер из `purchase_id[1]`, число событий для `duplicate_event`, одна дата и одна объявленная сумма. Генератор гарантирует, что у сюжетного дубля номер, сумма и день одинаковы, поэтому отдельной защитной политики выбора между разными значениями не нужно. `amount_delta` сравнивает два Decimal; способ представления исходного Float64 не создаёт класс расхождения и не заставляет потребителя витрины заново решать округление. ### Проверка К перечисленным в тикете проверкам добавить `make config-test` и явный запуск `dm_transform`. `make check-services` запускает только пробники `test_clickhouse` и `test_kafka`, поэтому сам по себе не доказывает наличие и успех задачи `purchase_vs_orders`. На чистом хранении дополнительно проверить типы `purchaseRevenue` в ODS и `purchase_revenue` в DDS, точное сохранение копеек и равенство суммы событию источника. На стартовом мире сверить, что число `awaiting_order` равно числу покупок последнего прожитого дня без заказа, а после следующего слепка прежний свежий день пересобран и ожидавшие строки перешли в обычные классы. Точные счётчики закрытых дней по-прежнему принадлежат #158 и описи мира.
ddmitry self-assigned this 2026-08-26 23:14:36 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: ddmitry/clickstream-data-platform#155