diff --git a/docs/architecture/dds/order.md b/docs/architecture/dds/order.md index 1baa249..e1b4007 100644 --- a/docs/architecture/dds/order.md +++ b/docs/architecture/dds/order.md @@ -65,15 +65,15 @@ ODS: это единственный устойчивый ключ сущнос одинаковы у всех строк дня и отвечают на вопрос «каким запуском и когда собран этот день». -Ярлыки ODS — `snapshot_date`, `_load_id` приёма, `_load_ts` прибытия — в -модель не переезжают. Лгать они перестали: колонкой версии в ODS стала дата -слепка, и у выжившей строки она равна последнему слепку, в котором заказ был -([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Довод стал проще: -читателя у этих колонок здесь нет — модель отвечает на вопрос «что сейчас -известно о заказе», а не «когда его показывал источник». Отсюда правило слоя: +Ярлыки ODS (`snapshot_date`, `_load_id` приёма, `_load_ts` прибытия) в модель +не переезжают. Они описывают наблюдение и доставку строки в ODS. Модель DDS +хранит текущее деловое состояние заказа. В ODS `snapshot_date` задаёт версию и +называет последний слепок, в котором источник показал заказ +([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Дату +наблюдения и координаты доставки читатель спрашивает у ODS по +`order_id`. Правило слоя: **служебные колонки — координаты собственной загрузки слоя; чужие координаты -спрашиваются у их слоя по ключу сущности**. Этим закрыто наследство #85: -читатель у чужих ярлыков выше ODS не нашёлся и при названных читателях модели. +спрашиваются у их слоя по ключу сущности**. Происшествие качества данных расследуется цепочкой, где каждый шаг спрашивает свой слой на его языке: по `_load_id` строки DDS видно, какой diff --git a/docs/architecture/etl.md b/docs/architecture/etl.md index fa492ea..cd1f54b 100644 --- a/docs/architecture/etl.md +++ b/docs/architecture/etl.md @@ -5,10 +5,9 @@ движки, что именно пересобирается — живут в доках слоя ([карта модели DDS](dds/README.md)); здесь — только конвейер. -Спека обещала `etl_pipeline` этапом 5 — он понадобился раньше. Конвейер -рождается этапом 4, и временного в нём ничего нет: дневная сборка вперёд -останется и после. Этапу 5 достаётся его законный предмет — партиционная -переобработка, умение переиграть произвольный прошлый день по команде. +Дневная сборка вперёд появляется на этапе 4 и остаётся основным режимом +конвейера. На этапе 5 к ней добавляется партиционная переобработка: запуск +произвольного прошлого дня по команде. ## Форма @@ -47,25 +46,28 @@ Поэтому один и тот же запуск одинаково верен из триггера, с кнопки менти и после падения, а разгону на семь дней хватает одного прогона. -Окно заказов — **дни, которые нёс последний слепок**. Называет их сам -источник: `snapshot_date` выжившей строки равен последнему слепку, в котором -заказ был, поэтому оба края окна трансформация читает прямо из ODS -([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Своего числа окна у -стенда нет, и расходиться с миром нечему. +Окно заказов задаёт последний слепок, наблюдаемый в ODS. У каждого заказа +`snapshot_date` равен последнему слепку, в котором источник его показал. +Трансформация выбирает строки с максимальной `snapshot_date` и берёт среди них +минимальный и максимальный `order_date`. Это границы дней, состояние которых +источник ещё может изменить +([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Кроме живого окна, +трансформация закрывает пропуски: находит самый ранний день, представленный в +ODS и ещё не собранный в DDS, затем при необходимости расширяет до него левую +границу. Позиция мира заказам не отвечает: она говорит, какой день прожит, а не какой доехал. Прогон дня D везёт слепок дня D − 1, да и ставится позиция раньше трансформации, так что после неудачного приёма обгоняет хранилище. Слой обязан считать объём по тому, что у него есть, а не по тому, что мир о себе заявляет. -У сессий всё наоборот: им отвечает только позиция, потому что вопрос у них -другой. Им нужен не день, о котором рассказал источник, а день, который мир -**прожил**: события живой день от прожитого не отличают — и посреди дня D, -и сразу после его конца позднейший день событий равен D. Соберись сессии посреди -дня, визит, начатый в 23:50, остался бы обрезанным навсегда: замёрзший день слой -не пересобирает. -Отсюда правило — последний прожитый день равен первому дню событий плюс позиция -минус один ([`sql/dds/session_scope.sql`](../../sql/dds/session_scope.sql)). +Для сессий границу задаёт `world_position`: она отличает полностью прожитый +день от ещё идущего. Сами события этой границы не показывают: и посреди дня D, +и сразу после его конца их позднейший день равен D. Если собрать сессии посреди +дня, визит, начатый в 23:50, останется обрезанным: завершённый день слой +повторно не собирает. Поэтому последний прожитый день равен первому дню +событий плюс позиция минус один +([`sql/dds/session_scope.sql`](../../sql/dds/session_scope.sql)). Оговорка про «доехал» остаётся в силе и здесь: за хвост потока отвечает не позиция, а «Готовность источника». diff --git a/docs/architecture/orders/ingestion.md b/docs/architecture/orders/ingestion.md index 5dd389a..ed4a0d0 100644 --- a/docs/architecture/orders/ingestion.md +++ b/docs/architecture/orders/ingestion.md @@ -7,10 +7,10 @@ ## Проблема Заказы приезжают полным слепком окна изменяемости, но Kafka передаёт его -отдельными сообщениями и не сообщает потребителю, где слепок закончился. Прямое -чтение Kafka Engine возвращает одну порцию. Поэтому прежняя публикация через -`REPLACE PARTITION snapshot_date` приравнивала дату наблюдения к отсутствующей -транспортной границе и могла заменить день неполным набором строк. +отдельными сообщениями и не сообщает потребителю, где слепок закончился. +Прямое чтение Kafka Engine возвращает одну порцию без признака полноты. +Поэтому `snapshot_date` не может служить границей публикации: `REPLACE +PARTITION` могла бы заменить партицию дня неполным набором строк. Одновременно типизированный ODS не должен принимать правдоподобные значения по умолчанию из грязного JSON или останавливать весь пакет из-за одной строки. @@ -81,11 +81,11 @@ JSON-объектом с точным набором ключей: `order_id`, ` малый контракт заказа нет причины. Граница строгости заканчивается на форме провода: содержимое позиций и бизнес-инварианты намеренно остаются ниже. -Одно условие контракта предикатом не проверить, и названо оно всё равно -здесь: **одна дата — один слепок**. Вторую выгрузку того же дня приём не -отличит от первой, а колонка версии — дата слепка — на ней даёт ничью. -Модельный источник выгружает день один раз; настоящему такое условие ставят -договором, а не предикатом +Контракт источника задаёт частоту: **одной дате соответствует один слепок**. Это +свойство всего слепка, поэтому предикат одной строки его не проверяет. Вторая +выгрузка того же дня даст ничью по `snapshot_date`, и выбор строки потеряет +однозначность. Модельный источник выполняет условие; в настоящей интеграции +его фиксируют в договоре с источником ([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Класс брака выбирается первым совпадением: @@ -113,11 +113,11 @@ JSON-объектом с точным набором ключей: `order_id`, ` - `_load_id` — какой запуск Airflow принял строку; - `_load_ts` — когда строка приехала в хранилище. -Версию выбирает `snapshot_date`: выживает строка позднейшего слепка -([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Поэтому ярлык -выжившей строки — правдивый ответ на вопрос «когда заказ видели в последний -раз», и по нему трансформация DDS читает объём пересборки. `_load_id` и -`_load_ts` выжившей строки говорят про ту же доставку. +Версию выбирает `snapshot_date`: выживает строка с максимальной датой слепка +([ADR 0014](../../adr/0014-order-version-snapshot-date.md)). Поэтому +`snapshot_date` показывает последнее наблюдение заказа. По строкам последней +даты слепка DDS определяет окно пересборки. `_load_id` и `_load_ts` принадлежат +той физической строке, которую сохранило слияние, и описывают её доставку. Схема версий стоит на дисциплине источника: слепок с поздней датой не показывает состояния старше. Модельный мир её держит — состояние читается на @@ -211,19 +211,15 @@ dbt snapshots — стратегия `check` вместо `timestamp`. `system.query_log` на первой ноде осталось ровно по одному завершённому запросу пользователя `etl` для забора в STG, записи годных строк и записи брака. -Дисциплина источника замерена 22 августа 2026 года при исполнении #137: во -всех 3034 парах строк с равным `updated_at` деловые поля совпали побайтово, по -слепкам метка ни разу не убывала, а `ods.order_v` сошёлся построчно с эталоном -`argMax` по `updated_at`, посчитанным прямо из сырья. Тем же замером виден -произвол ярлыка при версии по деловой метке — довод, на котором стоит -[ADR 0014](../../adr/0014-order-version-snapshot-date.md). +Дисциплина источника замерена 22 августа 2026 года при исполнении #137. +Во всех 3034 парах строк с равным `updated_at` деловые поля совпали +побайтово, а по слепкам метка ни разу не убывала. -Правдивость ярлыка снята на пересобранном мире 26 августа 2026 года при -исполнении #165. После двух слепков `snapshot_date` каждой из 1917 строк -`ods.order_v` равен последнему слепку, который нёс её заказ: расхождений ноль -при 3356 физически лежащих строках — выбор был не единственным. Деловое -содержимое сменой колонки версии не тронулось: те же строки построчно совпали -с эталоном `argMax` по `updated_at` из сырья по всем полям. +Текущее состояние проверено на пересобранном мире 26 августа 2026 года при +исполнении #165. После двух слепков физическая таблица содержала 3356 версий +1917 заказов. У всех строк `ods.order_v` дата слепка совпала с последним +слепком заказа; деловые поля совпали с эталоном `argMax` по `updated_at` из +сырья. В обеих сверках расхождений нет. Забор из Kafka в STG снят на живом стенде 18 августа 2026 года при исполнении #93: одно прямое чтение приносит весь слепок дня, метаданные доставки доступны, diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md index 9b6fea8..1933f86 100644 --- a/docs/architecture/storage.md +++ b/docs/architecture/storage.md @@ -234,12 +234,13 @@ ODS и в таблицу ошибок. Нужен ли он выше ODS, реш версии, которые ещё не убрали фоновые слияния, и не является архивом истории. `ods.order_v` служит поверхностью точного текущего состояния для следующих слоёв. Представление сохраняет язык источника и не решает, какой станет модель -DDS. `snapshot_date` в нём — дата наблюдения и колонка версии, но не ключ -публикации. Полное решение — в -[ADR 0010](../adr/0010-order-versions-in-ods.md), колонка версии — -в [ADR 0014](../adr/0014-order-version-snapshot-date.md); механика приёма — -в [спецификации приёма заказов](orders/ingestion.md). Какой моделью заказ -живёт дальше — [модель заказа в DDS](dds/order.md). +DDS. `snapshot_date` в нём одновременно дата наблюдения и колонка версии. Она +не доказывает полноту принятой порции, поэтому ODS не публикует партиции по +дате слепка. Модель версионного ODS записана в +[ADR 0010](../adr/0010-order-versions-in-ods.md), выбор колонки версии описан в +[ADR 0014](../adr/0014-order-version-snapshot-date.md), пакетный приём описан в +[спецификации приёма заказов](orders/ingestion.md). Контракт следующего слоя +задан в [модели заказа в DDS](dds/order.md). ## Часовые пояса diff --git a/docs/research/2026-08-16-order-snapshot-wire-format.md b/docs/research/2026-08-16-order-snapshot-wire-format.md index 4e60cf5..9c7df0e 100644 --- a/docs/research/2026-08-16-order-snapshot-wire-format.md +++ b/docs/research/2026-08-16-order-snapshot-wire-format.md @@ -126,6 +126,10 @@ parseDateTime64InJodaSyntaxOrNull( использовать и для типизированной строки, и для маршрутизации ошибки; нулевая дата не нужна. +> Актуальный стенд: версию `ods.order` задаёт `snapshot_date` +> ([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Следующий +> абзац фиксирует исходный вывод исследования и не описывает текущий контракт. + `updated_at` допустим как колонка версии `ReplacingMergeTree`: ClickHouse явно разрешает для `ver` тип `DateTime64` и оставляет строку с максимальной версией @@ -135,12 +139,6 @@ parseDateTime64InJodaSyntaxOrNull( монотонные миллисекундные `updated_at` на один заказ; отдельный счётчик версий без такого сценария был бы лишним. -> Вывод о колонке версии пересмотрен: версию `ods.order` задаёт `snapshot_date` -> ([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Там же разобрано, -> почему обещанию документации про ничью — «побеждает вставленная позже» — -> верить нельзя: замер на стенде его опроверг. Абзац выше оставлен как был, -> документ историчен. - ## Дата слепка и константы мира Периодический слепок имеет зерно заранее заданного периода — например, дня, — diff --git a/docs/specs/2026-07-30-stand-v2-realism.md b/docs/specs/2026-07-30-stand-v2-realism.md index 868b20c..79f5f3a 100644 --- a/docs/specs/2026-07-30-stand-v2-realism.md +++ b/docs/specs/2026-07-30-stand-v2-realism.md @@ -222,10 +222,11 @@ Ecommerce (заполнены только у торговых событий): - `ods.order` принимает версии заказа в `ReplacingMergeTree(snapshot_date)`: `ORDER BY order_id`, партиция по дню неизменного `created_at`, шардирование по `cityHash64(order_id)`. Дата - слепка служит колонкой версии — выживает строка позднейшего слепка - ([ADR 0014](../adr/0014-order-version-snapshot-date.md)), — но публикацию - и идемпотентность по-прежнему не задаёт. Физическое чтение может видеть - несколько версий; `ods.order_v` возвращает точное текущее состояние. В DDS одна строка хранит + слепка служит колонкой версии: выживает строка с максимальной + `snapshot_date` ([ADR 0014](../adr/0014-order-version-snapshot-date.md)). + Дата не доказывает полноту принятой порции, поэтому ODS не заменяет партицию + слепка целиком. Физическое чтение может видеть несколько версий; + `ods.order_v` возвращает точное текущее состояние. В DDS одна строка хранит один заказ с позициями в типизированных массивах; физическая модель и пакетное наполнение описаны в [доке заказа](../architecture/dds/order.md). @@ -453,13 +454,9 @@ README. матвью к нему не привязана, и сырьё забирает шаг, которым управляет Airflow — одно прямое чтение вставляет порцию в `stg.orders_raw` с `_load_id` запуска. Один следующий `task_id` двумя последовательными запросами пишет годные строки -и ошибки из того же среза. `ods.order` хранит версии по дате слепка -([ADR 0014](../adr/0014-order-version-snapshot-date.md)), а не публикует -партицию `snapshot_date`. Слой сырья остаётся точкой повтора и -разбора одной принятой порции. - -Стенд получает от этого два режима приёма рядом, поток и слепок, и сравнение -из опорных точек раздела 12 переформулировано под них. +и ошибки из того же среза. `ods.order` выбирает текущую версию по максимальной +`snapshot_date` ([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Слой +сырья остаётся точкой повтора и разбора одной принятой порции. Состав служебных колонок задаёт дока хранилища. Спеке важны два следствия: `ods.event` и `ods.order` получают метку загрузки `_load_ts`, но версию