feat(dds): добавлена модель заказа и пересборка окна заменой партиций

Зачем:
- Дать слою первую физическую сущность и показать идемпотентность, устроенную
  иначе, чем в приёме: заменой партиции, а не колонкой версии.
- Родить механику дневной пересборки на кластере — образец для сессий и витрин.

Что:
- Добавлены dds.order_rep/_dist, двойник-донор dds.order_stage_rep/_dist и
  публичный договор dds.order_v с разбором позиций в три массива.
- Добавлены запросы объёма прогона, сборки в двойник и замены партиции по дню.
- Добавлен даг dds_transform: группа задач order, замена — размноженной задачей.
- Окно изменяемости K пришло на сторону стенда якорем compose; владелец числа
  остаётся у генератора.
- Объём считается от позднейшего дня заказа и первого недостающего дня: ярлыки
  доставки ODS для этого непригодны, а максимум собранного дня прячет дыры.
- Записано, чего конвейер о себе не знает, и адресовано контуру DQ.

Проверка:
- make lint
- make smoke, make check-clickhouse, make check-services
- Прогон по стартовому миру: 1917 заказов, счёт равен ods.order_v, каждый заказ
  на шарде своего cityHash64(order_id), сумма позиций равна items_total.
- Повтор без нового слепка: содержимое то же, _load_id новый; после слепка
  пересобрано окно, день за окном не тронут.
- Снесённая руками партиция 2026-06-01 восстановлена побайтово.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ddadminandClaude Opus 5 committed 2026-08-26 05:20:39 -04:00
1 parent 96829718f9
commit 1e8b83ffbb
9 files changed
+555 -6

No files matched your search

+47 -5
View File
@@ -15,7 +15,9 @@ keeper, Kafka, каркас сервисов. Этап 2 собран: в `sql/d
Тот же этап принёс первый справочник — словарь товаров из общего с генератором
CSV-файла. Живёт он вне цепочки слоёв, в зоне `dic`, и читает не файл, а
подложку над ним ([ADR 0012](../adr/0012-dictionary-home.md)). Этап 4 начат
публичным договором события `dds.event_v` над точным состоянием ODS.
публичным договором события `dds.event_v` над точным состоянием ODS, а следом
получил первую физическую модель слоя — заказ: его пересобирает заменой
партиций даг `dds_transform`.
Дальше по тексту устройство описано так, как оно проектируется; построенное от
заложенного отличает карта таблиц в конце.
@@ -129,6 +131,14 @@ FROM источник_dist`. Заводское значение `parallel_distr
разложена по шардам по тому же ключу. Форму донора следует решать вместе с таким
объектом, а не переносить на ODS заранее.
Первым такой донор понадобился заказу DDS, и правило на нём видно целиком:
`dds.order_stage_rep` объявлен через `AS dds.order_rep` и шардируется тем же
`cityHash64(order_id)`. Совпадение структуры требует сама операция, а совпадение
ключа — устройство замены: она идёт по локальным таблицам, шард за шардом, и
строка, попавшая в доноре не на свой шард, при подмене пропала бы с одного шарда
и задвоилась на другом. Доводы целиком — в
[`sql/ddl/45-dds-tables.sql`](../../sql/ddl/45-dds-tables.sql).
## Служебные колонки
Собственные колонки не повторяют имён виртуальных. Виртуальные даёт движок:
@@ -508,7 +518,8 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
| `20-ods-tables.sql` | типизированное событие, версии заказа и обе таблицы ошибок |
| `30-ods-views.sql` | актуальные события, текущие заказы и матвью разбора в ODS |
| `40-stg-views.sql` | матвью приёма: чтец в сырьё |
| `50-dds-views.sql` | публичный договор события на языке модели DDS |
| `45-dds-tables.sql` | физическая модель заказа и таблица-двойник, донор её партиций |
| `50-dds-views.sql` | публичные договоры события и заказа на языке модели DDS |
Справочники идут сразу за базами: зона `dic` не зависит ни от одного слоя, и
правила порядка слоёв к ней не применяются.
@@ -567,6 +578,9 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
| `ods` | `ods.order_v` | текущая версия заказа на языке источника |
| `ods` | `ods.order_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
| `dds` | `dds.event_v` | событие на языке бизнес-модели без физической копии |
| `dds` | `dds.order_rep` / `_dist` | заказ в текущем состоянии, партиция — день заказа |
| `dds` | `dds.order_stage_rep` / `_dist` | двойник заказа: в нём день собирается перед заменой партиции |
| `dds` | `dds.order_v` | публичный договор заказа |
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
| `dic` | `dic.products` | словарь товаров поверх подложки |
@@ -574,9 +588,10 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
`INSERT SELECT` одной задачи `parse_batch` из файла
`sql/ods/order_parse.sql`.
Слой DDS начат представлением `dds.event_v`; остальные его сущности и слой DM
появятся в следующих тикетах этапа. Их состав задан разделом 7 мастер-спеки и
переносится сюда по мере постройки.
В слое DDS построены событие и заказ, и устроены они намеренно по-разному:
событие остаётся представлением над ODS, заказ получил физическую модель.
Сессии, карту идентичностей и слой DM принесут следующие тикеты этапа. Их
состав задан разделом 7 мастер-спеки и переносится сюда по мере постройки.
## Что проверено
@@ -788,5 +803,32 @@ Kafka с пустым значением (ноль байт) и запись-н
таблице. Это вычитано, а не измерено. На устройство приёма оговорка не влияет:
служебные колонки мы заполняем выражением при любом ответе.
**Проверка замены партиций 26 августа 2026 года (#151).** Context7 подтвердил
договор `REPLACE PARTITION`: `ON CLUSTER` в синтаксисе есть, а источник и цель
обязаны совпадать структурой, ключом партиции, ключом сортировки, первичным
ключом и политикой хранения. На закреплённом ClickHouse 26.3 замена по локальным
таблицам с `ON CLUSTER` прошла на обеих нодах, а сборка стартового мира в
двойник разложила 1662 заказа строго по ключу: шард 1 получил только чётные
`cityHash64(order_id)`, шард 2 — только нечётные.
Тем же днём измерены ещё три поведения ClickHouse.
- **Параметр в `ALTER … PARTITION` не подставляется.** Запрос с `{day:Date}` в
выражении партиции отвечает кодом 62, синтаксической ошибкой; литерал
проходит. Отсюда единственное место в репозитории, где значение попадает в
запрос подстановкой Jinja, а не параметром ClickHouse
([`sql/dds/order_replace.sql`](../../sql/dds/order_replace.sql)).
- **`TRUNCATE` — отдельное право, в `ALTER` оно не входит.** Роль `etl`, у
которой есть `ALTER ON dds.*`, получает на `TRUNCATE` код 497, а
`ALTER TABLE … DROP PARTITION ALL` на той же таблице проходит. Двойник
поэтому чистится снятием партиций: у операции нашлась форма, на которую право
уже выдано, и расширять доступ не понадобилось.
- **`JSONExtract` читает `Decimal` прямо из строки JSON.** Цена на проводе —
строка с двумя знаками (`"1289.99"`), и разбор в
`Array(Tuple(sku String, qty UInt32, price Decimal(18, 2)))` даёт число без
промежуточного `toDecimal64`. Проверено на живом слепке; честность разбора
сторожит инвариант бэкенда — сумма «цена × количество» совпала с `items_total`
у всех 1662 заказов.
**Сказано по памяти, проверки нет.** Группа пуста: оба утверждения, ждавшие
матвью разбора, закрыты опытами при исполнении #43 и переехали выше.