- Зачем:
- ярлык snapshot_date выжившей строки доставался жребием, а объём
пересборки DDS держался на константе окна с двумя несверяемыми копиями
(ADR 0014, развилка #163).
- Что:
- ods.order_rep переведена на ReplacingMergeTree(snapshot_date): выживает
строка позднейшего слепка, ярлык стал правдивым.
- order_scope.sql выводит окно из дней последнего слепка; ORDER_WINDOW_DAYS
и параметр window_days сняты со стенда, мёртвая защита краем источника
убрана.
- документы и комментарии SQL приведены к ADR 0014: роль ODS, условие
«одна дата — один слепок», объём прогона и слепые пятна конвейера.
- Проверка:
- make lint; make rebuild-storage, затем make smoke, make check-clickhouse,
make check-services — зелёные.
- после двух слепков snapshot_date всех 1917 строк ods.order_v равен
последнему слепку заказа, расхождений ноль; деловые поля построчно
совпали с эталоном argMax по updated_at из сырья.
- dds_transform без K: первый прогон 06-01…06-07, после нового дня мира
06-02…06-08, повтор прогона содержимого не изменил.
Closes#165
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Зачем:
- Дать слою расчётную сущность, которой в потоке нет: стенд режет визиты сам,
а VisitID генератора остаётся эталоном самопроверки.
- Отделить прожитый день от живого — иначе визит, начатый в 23:50, замёрз бы
обрезанным навсегда: пересобирать замёрзший день слой не станет.
Что:
- Добавлены dds.session_rep/_dist, двойник-донор dds.session_stage_rep/_dist и
публичный договор dds.session_v.
- Нарезка стоит на двух окнах: первое ставит шов там, где пауза длиннее
тридцати минут, второе считает нарастающий итог швов — номер визита. Границу
суток держит разбиение окна, отдельной проверки ей не нужно.
- Порядок в окнах полный, по метке времени и event_id: сортировок две, и при
равных метках они разошлись бы, разорвав сессию надвое.
- Объём прогона считается от первой дыры в слое по последний прожитый день;
прожитый день выводится из позиции мира и первого дня событий. Собирать
нечего — группа задач пропускается, а не зеленеет впустую.
- В dds_transform появилась группа session рядом с заказом; зависимости по
данным между ними нет.
- Потолок max_active_tasks: размноженная замена без него выносит планировщика
за отведённые ему 640 МиБ.
- parallel_distributed_insert_select у сессий не назначается вслух — ключ
шардирования у источника и цели один по контракту ко-локации.
- Записано, что позиция отвечает за «прожит», а не за «доехал»: сторож хвоста
потока приезжает отдельным тикетом.
Проверка:
- make lint
- make smoke (21 проверка), make check-clickhouse (10), make check-services
- Стартовый мир, пересобранный с нуля: 79367 сессий за восемь дней;
расхождение с uniqExact(VisitID) по паре «кука × день» — ноль; все 401185
событий ровно в одной сессии; session_id уникальны.
- Ни одной паузы длиннее тридцати минут внутри сессии и ни одной пары соседних
визитов куки, разделённой меньшим промежутком.
- Повтор после DROP PARTITION ALL: те же session_id и то же содержимое строк.
- Снесённая руками партиция 2026-06-04: следующий прогон взял хвост от неё,
дни до дыры не тронуты.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Зачем:
- Дать слою первую физическую сущность и показать идемпотентность, устроенную
иначе, чем в приёме: заменой партиции, а не колонкой версии.
- Родить механику дневной пересборки на кластере — образец для сессий и витрин.
Что:
- Добавлены 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>
- Зачем:
- Закрепить реалистичную границу доступа BI и убрать двусмысленность для неизвестных кодов.
- Что:
- Пояснены доступ к ODS, пояс счётчика и порядок DDL.
- Проверка равенства строк отделена от размера конкретной загрузки.
- Проверка:
- `git diff --check`.
- `make config-test` не стартовал: Snap требует работающий `snapd.apparmor`.
Зачем:
- Показать слой DDS как договор данных, которому не нужна физическая копия.
- Сохранить простой учебный стенд без отдельного маршрута ради узких прав.
Что:
- Добавлено кластерное представление dds.event_v с явными 44 колонками.
- Роли bi_reader разрешено чтение ODS, DDS и DM при договоре чтения через _v.
- Согласованы мастер-спека, ADR, справочники архитектуры и живые тикеты этапа.
- Зафиксировано исследование поведения DEFINER поверх Distributed.
Проверка:
- make config-test
- make smoke
- make check-clickhouse
- Приёмочные запросы: равенство ODS/DDS, состав колонок, коды устройств и права bi.
- Зачем:
- согласованная модель нужна для пакетной реализации DDS и понятного публичного интерфейса слоя.
- Что:
- описаны физическая карта идентичностей, полный пересчёт и отказ от first_matched_at.
- унифицирован публичный контракт _v для событий, заказов, сессий и карты.
- синхронизированы мастер-спека, имена объектов и граница доступа.
- Проверка:
- git diff --cached --check.
- Зачем:
- развилка #130 карты #127: заказу нужен объект в DDS — зерно, состав,
движок и судьба служебных колонок; резолюции этапа 4 нужен дом в git.
- Что:
- создан набор docs/architecture/dds/ по ADR 0011: README с картой
модели и правилами слоя, order.md с моделью заказа — зерно «заказ в
текущем состоянии», позиции массивами, MergeTree с заменой партиций,
свои _load_id/_load_ts вместо ярлыков ODS (закрывает наследство #85);
- указатели на модель добавлены в orders/README.md, orders/ingestion.md
и storage.md.
- Проверка:
- только документы; относительные ссылки сверены с деревом docs/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Зачем:
- правки #137 дважды рассказывали одно и то же: механика капкана жила и в
storage.md, и у вставок, а цифры замеров — и в прозе, и в леджере;
у читателя это спрашивает больше внимания, чем даёт.
- Что:
- комментарии у вставок order_parse.sql сжаты до довода с указателем на
storage.md; из капкана в storage.md цифры ушли в «Что проверено»;
в ingestion.md замеры перенесены из прозы в свой раздел «Что проверено».
- Проверка:
- рендер order_parse.sql по-прежнему делится ровно на два запроса
(из комментария убрана точка с запятой, по которой оператор делит файл).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- заводское parallel_distributed_insert_select = 2 исполняло вставку из
Distributed в Distributed локально на каждом шарде, минуя ключ цели:
1189 из 1662 заказов задвоились на обоих шардах, а ReplacingMergeTree
через границу шарда дубли не схлопывает.
- Что:
- в обе вставки sql/ods/order_parse.sql добавлено явное
parallel_distributed_insert_select = 0 с доводом комментарием.
- в docs/architecture/storage.md описан капкан «INSERT из Distributed в
Distributed при несовпадающих ключах» и записана проверка (#137).
- Проверка:
- межшардовые дубли по ods.order_rep дают 0 (было 1189), каждый шард
держит только свои ключи; uniqExact(order_id) не изменился.
- после прожитого дага world_next_day новый слепок принят, дублей
по-прежнему 0.
- make smoke, make check-clickhouse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- границы владения Compose и Airflow должны быть видны менти из одного источника истины.
- Что:
- добавлены спека и ADR жизненного цикла мира;
- обновлены README, архитектурные справочники и прежние решения;
- правило полной очистки записано как явное и проверяемое требование к агенту.
- Проверка:
- документация вычитана через `ai-text-lint` и `writing-for-agents`;
- выполнена литературная вычитка русского текста;
- `git diff --check`.
- Зачем:
- ODS должен показывать сущность заказа, а не способ её доставки слепком.
- Что:
- физические таблицы и представление переведены на имена `order_*`.
- SQL загрузки, DAG и документация приведены к тому же словарю.
- Проверка:
- `make config-test`, `make lint`.
- `make clean && make up`, `make smoke`, `make check-clickhouse`, `make check-services`.
Зачем: ревью в три линии нашло настоящий дефект. Девятая проверка кластера
звала dictGet('dds.products', …) — объекта после переноса не существует, и
make check-clickhouse падал. Промах вырос из недосчёта: упоминания dds.products
я искал с фильтром по расширениям, .sh туда не попал, и в ADR уехало «четыре
упоминания» вместо семи.
Что: проверка переведена на dic.products и сверяет цену, умножив её обратно на
сто, — так утверждается ещё и точность приведения к Decimal. Раздел 3
мастер-спеки и список пользователей README догнали перенос: оба описывали
отменённое устройство. В ADR 0012 добавлено условие пересмотра для дома — для
него его не было, хотя ради дома тикет и заводился; недосчёт записан в
«Что проверено» как урок. Разнобой обозначений сведён: карта таблиц в доке
хранилища перешла на имена баз строчными, таблица мастер-спеки — на заголовок
«Где», зону всюду зовут зоной, а не слоем. Термин «Справочник» переписан без
метафоры и уложен в формат глоссария. Вычтено лишнее: комментарии DDL
сократились вдвое, из ADR 0006 и 0007 убраны самооправдание и дублирующие
абзацы, отраслевой образец получил честную оговорку о непроверяемости.
Проверка: make config-test, make lint, make smoke и make check-clickhouse —
все зелёные, десять проверок кластера из десяти.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: каждое утверждение о словаре было записано трижды — в доке хранилища, в
комментариях DDL и в ADR 0012. Правка поведения стоила бы трёх согласованных
правок, а расхождение между копиями обнаружилось бы не сразу. Дока хранилища —
карта, а не пересказ реализации.
Что: раздел «Справочники» сведён к своему уровню — правило принадлежности зоны,
топология на кластере, единицы денег и указатели. Механика (движок, форма пути,
пользователь, окно обновления) осталась только в sql/ddl/05-dic-catalog.sql,
рядом с кодом, который она объясняет. Блок «Что проверено» отправлен к замерам
ADR 0012 вместо их пересказа — так же, как он уже поступает с ADR 0005.
Проверка: минус 30 строк; ссылок на убранный подзаголовок в репозитории нет,
три относительные ссылки нового текста разрешаются в существующие файлы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: место словаря в dds было унаследовано от первого объекта, а не решено.
Справочник читают несколько слоёв, а производит его в хранилище ни один —
прописка внутри одного из потребителей приписывала DDS владение, которого у
него нет.
Что: заведена база dic вне цепочки STG → ODS → DDS → DM, dds.products переехал
в dic.products. Под словарём появилась подложка dic.products_file на движке
File — намеренное усложнение ради урока: в бою источник словаря приезжает
процессом, а не лежит файлом у сервера. Источник объявлен формой query и
приводит цену из целых копеек каталога в Decimal(18, 2), как у денег бэкенда.
Обновление — окном LIFETIME(MIN 60 MAX 90) вместо ручной перезагрузки, ценой
заявленной неатомарности между нодами. У словаря свой беспарольный
пользователь dict с единственным правом на чтение dic. Суффикс _file добавлен
в конвенцию имён ADR 0006. Решение, отвергнутые варианты и условия пересмотра
— ADR 0012.
Проверка: make clean && make up на собранном заново стенде — зелено, make smoke
20 проверок и 0 ошибок, make lint чисто. Словарь LOADED со 180 строками, цена
Decimal(18, 2) и точна на копейках: 188990 → 1889.90. Пользователь bi читает
словарь dictGet-ом со второй ноды, analyst — подложку соединением, база dds
пуста. Минимальное право на движок замерено тремя пользователями: достаточно
GRANT FILE ON *.*, хотя отказ называет TABLE ENGINE ON File.
Closes#105
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- витринам нужен общий с генератором каталог, а менти — пример штатного обновления словаря.
- Что:
- каталог смонтирован read-only в обе ноды и создан как dds.products ON CLUSTER.
- роли получили dictGet, а check-clickhouse сверяет ответ под etl с исходным CSV.
- источник, раскладка и явная политика обновления записаны в документации.
- Проверка:
- make up с чистыми томами; make lint; make config-test; make smoke; make check-clickhouse.
- в generator: make lint typecheck test — 427 тестов прошли.
- Зачем:
- преобразования хранилища должны читаться рядом с целевым слоем, а DAG должен показывать оркестрацию.
- Что:
- запросы забора и разбора заказов разложены по каталогам STG и ODS.
- общий контракт провода подключён в обе ветви штатным шаблонизатором Airflow.
- SQL смонтирован во все службы Airflow, а правило раскладки записано в архитектуре.
- Проверка:
- make lint config-test smoke check-services.
- airflow tasks render для pull_batch и parse_batch; airflow tasks test для parse_batch.
- Зачем:
- менти должен различать версию заказа, наблюдение источника и запуск загрузки.
- Что:
- добавлены таблицы версий и брака заказов, а также представление ods.order_v.
- даг orders_ingest дополнен строгим переходом одного среза STG в две цели ODS.
- результаты опытов записаны в документации, дефект генератора вынесен в #102.
- Проверка:
- make lint config-test smoke check-clickhouse check-services.
Зачем:
- связать проигрывание модельных дней с пакетным приёмом заказов
- показать на одном стенде различие потокового push и пакетного pull
Что:
- добавлен топик, Kafka-чтец и реплицированное сырьё заказов
- добавлен даг orders_ingest с одним прямым чтением и идентификатором загрузки
- работники мира отправляют слепок, ждут приём и только затем двигают позицию
- решения, границы отказа и проверки отражены в ADR и архитектурных документах
Проверка:
- make lint
- make config-test
- make smoke
- make check-clickhouse
- make check-services
- Зачем:
- потребителю ODS нужна точная актуальная версия события без знания о FINAL.
- Что:
- создано ods.event_v над распределённой таблицей событий с FINAL.
- сверка стартового мира переведена на новое представление.
- документация различает физические версии ODS и бизнес-представление DDS.
- Проверка:
- make config-test.
- make check-clickhouse.
Зачем: датированная спека — событие истории, а устройство компонента — живой
документ; большое полотно плохо грузится и агентом, и человеком (ADR 0011).
Что: вычитание #84 слито с переустройством формы: набор
docs/architecture/orders/ — индекс README и семь файлов по частям устройства
(нарезка по правилу «семь плюс-минус два»); датированные файлы удалены,
ссылки перенацелены, AGENTS.md дополнен правилом подпапки. Приёмка
владельцем #88 пройдена, черновой статус снят из README.
Проверка: холодная сверка миграции свежим тредом — потерь решений нет;
обход относительных ссылок набора — битых нет.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Зачем:
- фраза «Date не участвует вовсе» выводила тип из-под общего правила:
про объявление это правда, про употребление — нет. Считая время по
EventDate без имени пояса, легко получить часы вне диапазона.
- Что:
- фраза заменена на две: Date хранит только номер дня, пояс при счёте
времени называют руками, промах виден по часам за границами 0–23.
- Проверка:
- замерено на стенде: без имени пояса часы от начала суток идут -4…19,
отрицательных 15 843 события; с названным поясом — 0…23
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- линия дефектов нашла три неверных утверждения и мёртвый замер, линия
уместности — три пересказа уже сказанного.
- Что:
- «тип колонки не решает, какое число ляжет» сужено до правды: разбор
отдаёт готовое число, а пояс приёмника решал бы судьбу строки.
- замер до правки типов помечен как неповторяемый на нынешнем стенде.
- правило о поясе сервера привязано к местам, где линза что-то решает:
матвью приёма пояс не называет, и это не нарушение.
- убраны: пересказ механики в ADR 0005, четыре строки учебного
комментария, утверждение о порядке файлов и «секунды от начала эпохи»
у миллисекундной метки.
- Проверка:
- make lint, make typecheck, make test (408 тестов)
- make clean && make up && make check-clickhouse — 9 из 9
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- конвенция #63 записана, а код её не достиг: колонки времени стояли без
пояса, и сходилось всё лишь потому, что пояс сервера — UTC.
- Что:
- UTCEventTime объявлен DateTime('UTC'), служебные метки _load_ts и
kafka_timestamp — DateTime64(3, 'UTC') в STG и ODS.
- parseDateTimeOrNull получил третьим аргументом 'UTC': маска сверяет
суффикс Z как букву, зоны из строки не берёт вовсе.
- контракт схемы и описание выгрузки несут тип с поясом; имя пояса
Europe/Samara встало рядом со смещением в world.py, сходимость сверяет
тест.
- учебный комментарий о линзе — у первой колонки с явным поясом.
- Проверка:
- make lint, make typecheck, make test (408 тестов)
- make clean && make up && make check-clickhouse — 9 из 9
- замер тикета повторён: под session_timezone='Europe/Samara' колонка
показана 2026-05-31 23:37:00, как и без настроек
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- раздел «Часовые пояса» прошёл два холодных ревью — по дефектам и по
уместности. Первое поймало ложный замер и три расхождения с живым
стендом, второе — материал не своей зоны и дубли (#63).
- Что:
- замер «расхождение живёт по HTTP» отозван: мерил toString(UTCEventTime)
в родном клиенте против голой колонки по HTTP, а это разные вещи.
Перемерено — клиенты ведут себя одинаково; записан верный факт: вывод
колонки идёт по поясу сессии, функция — по поясу типа.
- «по поясу сервера» заменено на «по поясу сессии, а тот по умолчанию
серверный» — в разделе и в ADR 0005; утверждение в ледгере переписано
под измеренный раскол вывода и типа.
- PARTITION BY toDate(_load_ts) больше не выдаётся за уже соблюдённое
правило: _load_ts сегодня DateTime64(3) без пояса.
- абзац ADR 0005 больше не спорит с цитатой вызова строкой выше.
- вырезано: веер отклонённых вариантов под заголовком (живые отказы
разложены прозой по своим абзацам, как принято в этом документе),
ссылка на несуществующую связку в world.py, осиротевшая строка про
Grafana, абзац про пояс показа — он уехал комментарием в #63.
- Проверка:
- make lint
- замеры повторены на живом стенде 8 августа 2026 года
- DDL к конвенции по-прежнему не приведён: документы описывают цель
- Зачем:
- пояс в стенде нигде не назван: числа верны только потому, что сервер
ClickHouse стоит в UTC, а правило понадобится в dds и витринах —
воронки, удержание, «покупки по дням» (#63).
- Что:
- раздел «Часовые пояса»: пояс — линза, называется в типе колонки либо в
вызове; какая именно — решает слой (ODS на языке выгрузки, DDS и витрины
на языке бизнеса); день берётся из EventDate.
- названы оба перехода, где пояс выбирается, включая разбор строки в
матвью — он берёт пояс у сервера и тип колонки этого не чинит.
- отвергнутые варианты прозой: умолчание сервера, TZ серверу, ODS в поясе
счётчика, хранение местного времени.
- три замера ушли в «Что проверено», сверка с документацией — от 8 августа.
- Проверка:
- make lint
- DDL к конвенции ещё не приведён: документ описывает цель, код идёт
следом тем же тикетом.
Зачем
Стенд поднимался пустым, и всякая приёмка следующих этапов начиналась с
ручной заливки данных. Теперь `make up` сам приводит стенд к одному и тому
же состоянию, а в git лежит то, чем это состояние проверяется.
Что
- Опись мира `data/world-inventory.json`: паспорт (зерно, версия
генератора, хеш каталога) и по строке на каждый из восьми дней — дата,
число событий, хеш байтов. Собирается `make inventory`, свежесть сторожит
`test_inventory.py` — тем же способом, что свежесть описания выгрузки.
- Разовая служба `world-init` вышла из-под профиля и играет в топик восемь
дней при каждом подъёме; зависимый у неё — `airflow-init`, иначе `--wait`
считает успешно отработавшую службу упавшей.
- `scripts/wait-for-world.sh` — вторая половина `make up`: приём
асинхронный, поэтому ждать надо доезда до `ods.event`, а не завершения
заливки. Ограниченный цикл опроса, не пауза наугад.
- Девятая проверка `make check-clickhouse`: подневный счёт событий против
описи, рамка по датам стартового мира, счёт через `FINAL`. При
расхождении называет, где искать, — в событиях или в браке.
- Порог «день ≤ 30 с» снят из спеки генератора в обоих местах: замер дал
1,7 с, порог был выше факта в восемнадцать раз. На его месте — замеры с
датой. Раздел 9 спеки закрыт: открытых вопросов не осталось.
- Слова: «манифест» стал описью мира, «зерновой мир» — стартовым миром
(решение владельца). Оба заведены в словарь CONTEXT.md.
Проверка
`make clean && make up` с нуля — 2 м 50 с, доехало ровно 401 185 событий.
`make check-clickhouse` зелёный (8 с), `make smoke` зелёный (9 с),
`make test` — 407 тестов за 71 с, `make lint`, `make typecheck`,
`make config-test` зелёные.
Что проверка умеет краснеть, снято двумя поломками: снос партиции
2026-06-03 дал диагноз «не доехали до ODS», негодная строка в сырье —
«сломан разбор». Строки опыта убраны, день переигран, счёт вернулся.
Тест свежести проверен молчаливой правкой цены в каталоге: покраснел.
Ссылка: #42
Зачем: parseDateTimeBestEffort на непонятной строке не краснеет, а достраивает
недостающее — обрезанное «20:00:21» становится первым января текущего года.
Такое сообщение проходило строгий приём с тихо неверным временем, то есть с
той самой порчей, ради которой класс key_field_unparsed и заведён.
Что:
- В обеих матвью разбор метки идёт parseDateTimeOrNull по формату
'%Y-%m-%dT%H:%i:%SZ'. Форма на проводе одна и каноническая, поэтому широта
best-effort не нужна вовсе, а платится за неё отключённой проверкой.
- Замеры в ADR 0005: три записи, которые best-effort достраивает; проверка,
что настройка cast_string_to_date_time_mode не спасает JSONExtract; сверка
на настоящих данных — по всем 101 252 строкам сырья модельного дня точный
формат разобрал метку у каждой и ни на одной не разошёлся с best-effort.
- Записано наблюдение стенда: пересозданная на живом чтеце матвью пропускает
ближайшее сообщение мимо ODS, через минуту то же сообщение разбирается.
Воспроизведено дважды; на нём я сам споткнулся при проверке этой правки.
Проверка: опыт строгого приёма прогнан заново — три сообщения дали событие и
два key_field_unparsed, включая обрезанную метку, которая раньше проходила
годной. DDL применяется на живом кластере.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: холодное ревью по двум линиям нашло дыру в следе опытов и три места,
где текст утверждает не то, что построено.
Что:
- Опыт «_load_ts переносится из сырья» прогнан и записан: у двух тысяч
событий метка совпала с меткой одной из доставок, случаев «метки нет среди
доставок» ноль. Туда же — ответ про форму ключа ODS: вопрос раздела 11
спеки закрывался молча.
- Дока хранилища говорила, что предикат собран из функций, не возвращающих
NULL; построено иначе — обнуляемый разбор есть, но кончается IS NOT NULL.
- Записана гарантия на JSONType: на не-JSON и пустой строке она отдаёт Null и
не бросает, то есть годится в предикат. Раньше первый класс брака стоял на
замере соседней функции.
- ttl_only_drop_parts у таблицы ошибок назван в доке хранилища.
- Комментарий матвью ужат: три вопроса строгого приёма пересказывали ADR 0005
целиком. Осталось то, чего по коду не видно, — запрет трогать arraySort и
замер про ISO-8601. Убрано неверное «в полусотне строк» и упоминание имени
таблицы хранилища в докстринге контракта генератора.
Проверка: DDL применяется на живом кластере; make lint, typecheck, docs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: цепочка Kafka → STG → ODS достраивается последним этажом. Сырьё уже
доезжает (#37), настоящие события в топике есть (#41), а типизированного слоя
не было — событие негде было прочитать колонками, а брак негде увидеть.
Что:
- sql/ddl/20-ods-tables.sql — ods.event_rep/_dist на ReplacingMergeTree с
версией _load_ts, партиция по EventDate, ключ по разделу 1.3 спеки,
шардирование cityHash64(ClientID); ods.event_errors_rep/_dist с классом
брака, своими ключами и сроком жизни в месяц.
- sql/ddl/30-ods-views.sql — две матвью над stg.hits_raw_dist. Годность
считает предикат из трёх частей, вторая матвью берёт его дословное
отрицание, класс брака пишется первым совпавшим из трёх.
- Метку времени разбирает parseDateTimeBestEffortOrNull, а не JSONExtract:
ISO-8601 с суффиксом Z JSONExtract не берёт вовсе. Спека генератора
обещала обратное — обещание поправлено, форма на проводе не менялась.
- Сверка объявлений (contract-тест) снята из документов и из докстрингов
schema.py: сверх строгого приёма она ловила только смену типа.
- Документация приведена в соответствие: ADR 0005, дока хранилища и обе
спеки; группа «сказано по памяти» в доке хранилища опустела.
Проверка: make up && make check-clickhouse (8 проверок, 7,5 с); make lint,
make typecheck, make test (406), make docs без диффа. Разовые опыты при
исполнении — в теле PR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: стенду нужен воспроизводимый холодный старт, при котором схема
хранилища и топик появляются сами, а сырьё из Kafka доезжает в STG обеими
нодами кластера — без ручных шагов между `make clean` и рабочим приёмом.
Что:
- `sql/ddl/` — три файла, применяются по порядку имён: базы `stg` и `ods`,
Kafka-чтец `hits_raw_kafka` формата RawBLOB, реплицируемая `hits_raw_rep`
с окном TTL в трое суток, распределённая `hits_raw_dist` и матвью
`hits_raw_mv`, переносящая сырьё вместе с метаданными доставки.
- `compose.yaml` — службы `kafka-init` (топик `hits` на две партиции, с
ремонтом уже созданного однопартиционного) и `clickhouse-init` (применяет
`/ddl/*.sql`); `hostname:` у обеих нод, чтобы `hostName()` отдавал имя узла,
а не идентификатор контейнера; `airflow-init` зависит от `clickhouse-init` —
без зависимого успешный одноразовый сервис считается упавшим для `--wait`.
- Доки: конвенции и раздел «Что проверено» в справочнике хранилища, указатели
и границы обещаний в ADR 0005, снятые пункты в разделе 11 спеки.
Проверка: `make lint`, `make typecheck`, `make config-test`, `make smoke`
(25 проверок), `make smoke-guards` — зелёные. Приёмочный прогон с чистого
тома подтвердил все пять критериев #37: холодный старт и идемпотентный
повтор, две партиции у `hits`, метаданные доставки у доехавшего сообщения,
обе партиции на обеих потребляющих нодах в одном прогоне, некорректный JSON
лежит сырым и приём не встаёт.
Известная граница: RawBLOB молча теряет запись с пустым значением и
запись-надгробие; принято как свойство, замер и довод — в справочнике
хранилища.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- холодное ревью связности нашло девять мест, где вставленный текст спорит с
соседним; отдельно вскрылось, что представление дат в JSON не зафиксировано
нигде, а #43 обязан его знать раньше, чем #41 напишет сериализатор.
- Что:
- гарантия приёма переписана: после снятия синхронной вставки «хотя бы один
раз» стало неправдой — есть и окно потери, и окно дубля.
- критерий выбора пяти опорных колонок приведён к списку, который он
порождает; `CounterID` оговорён отдельно.
- «переобработки у ODS нет вовсе» смягчено до пакетной: ручная вставка из
сырья в пределах окна возможна.
- в спеку генератора добавлена форма дат на проводе — ISO-8601, с доводом от
читаемости слоя сырья.
- убраны осиротевшая фраза про порядок сервисов, дубль порядка классов брака,
устаревшая датировка сверки и ещё три следа вставок.
- Проверка:
- make config-test
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- три холодных ревью и сверка с документацией ClickHouse нашли противоречия
между докой, ADR и спекой: исполнитель #37 получал два разных ответа на
один вопрос, а два утверждения о движке оказались неверными.
- Что:
- раскладка файлов DDL перестроена — сначала таблицы, матвью приёма
последней: иначе часть событий тихо минует ODS.
- синхронная вставка снята с пути приёма: настройка недостижима для потока
Kafka-движка и связывает шарды; на ETL-вставках осталась.
- у таблицы ошибок появился класс брака с порядком проверки, у сырья и
ошибок названы движки и ключи сортировки.
- в доку добавлен раздел «Что проверено»: сверенное с документацией,
проверяемое на стенде и сказанное по памяти разведены.
- в спеке выправлены источник матвью разбора, пять опорных колонок, имена
четырёх витрин и ссылка на несуществующую цель make.
- Проверка:
- make config-test
- grep по устаревшим именам файлов DDL и витрин — пусто
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- тикет #37 молча опирался на конвенции хранилища, которых в проекте не
было; без них #43 и следующие этапы разъехались бы в именах, служебных
колонках и механике приёма.
- Что:
- ADR 0005: топик читается байтами в STG, разбор идёт функциями в матвью
ODS; строгий приём — сверка набора ключей плюс Nullable на пяти опорных
колонках.
- ADR 0006: суффикс вида в именах объектов (_rep, _dist, _kafka, _mv, _v).
- docs/architecture/storage.md: конвенции имён и служебных колонок, путь в
keeper, раскладка по шардам, срок жизни сырья, свойства приёма, раскладка
файлов DDL и карта таблиц.
- спеки приведены в соответствие: механизм строгого приёма, имена объектов,
контракт транспорта «одно событие — одно сообщение Kafka», три проверки
при исполнении.
- Проверка:
- make config-test