docs(storage): конвенции и приём событий выправлены после ревью

- Зачем:
  - три холодных ревью и сверка с документацией ClickHouse нашли противоречия
    между докой, ADR и спекой: исполнитель #37 получал два разных ответа на
    один вопрос, а два утверждения о движке оказались неверными.
- Что:
  - раскладка файлов DDL перестроена — сначала таблицы, матвью приёма
    последней: иначе часть событий тихо минует ODS.
  - синхронная вставка снята с пути приёма: настройка недостижима для потока
    Kafka-движка и связывает шарды; на ETL-вставках осталась.
  - у таблицы ошибок появился класс брака с порядком проверки, у сырья и
    ошибок названы движки и ключи сортировки.
  - в доку добавлен раздел «Что проверено»: сверенное с документацией,
    проверяемое на стенде и сказанное по памяти разведены.
  - в спеке выправлены источник матвью разбора, пять опорных колонок, имена
    четырёх витрин и ссылка на несуществующую цель make.
- Проверка:
  - make config-test
  - grep по устаревшим именам файлов DDL и витрин — пусто

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 21:25:35 +03:00
co-authored by Claude Opus 5
parent dedb6c6739
commit 31b274175a
6 changed files with 284 additions and 65 deletions
+51 -12
View File
@@ -5,8 +5,12 @@
## Решение
Топик `hits` читает одна Kafka-таблица формата `RawBLOB`: сообщение ложится в
`stg.hits_raw_rep` строкой, как пришло, рядом с метаданными доставки. Ни
`stg.hits_raw_dist` строкой, как пришло, рядом с метаданными доставки. Ни
типизации, ни проверки на этом шаге нет — слой сырья ничего не интерпретирует.
У формата есть следствие для DDL: он читает вход в одно значение и рассчитан на
таблицу с единственной колонкой, поэтому у чтеца она ровно одна — `raw`, а
метаданные доставки берутся только из виртуальных колонок и добавить к чтецу
что-либо своё нельзя.
Типизированный слой наполняют две матвью, привязанные к `stg.hits_raw_dist`.
Поля достаются `JSONExtract`. В таблицу ошибок уходят три класса брака:
@@ -16,8 +20,17 @@
на валидность: `isValidJSON('123')` возвращает единицу, скаляр — тоже законный
JSON.
Классы пересекаются: скаляр проваливает заодно и сверку ключей, потому что
`JSONExtractKeys` от него даёт пустой массив. Поэтому они проверяются по порядку,
а в колонку `error_class` пишется первый совпавший — `not_an_object`,
`keyset_mismatch`, `key_field_unparsed`. Приём тот же, что у `mismatch_class` в
витрине сверки: пересекающиеся классы плюс объявленный приоритет.
Присутствие полей целиком держит сверка набора ключей — одно сравнение
отсортированного `JSONExtractKeys` с контрактным списком. Обязательны все сорок
`arraySort(JSONExtractKeys(raw))` с контрактным списком, завёрнутым в тот же
`arraySort`. Обёртка с обеих сторон стоит ноль и снимает ошибку, которая иначе
увела бы в брак вообще всё: сорок семь CamelCase-имён, выписанных руками ровно в
байтовом порядке. Обязательны все сорок
семь полей: генератор шлёт их все в каждом событии, а «пусто» по контракту —
пустое значение, а не отсутствие ключа. Этим же закрыт критерий #43 про опечатку
в имени.
@@ -40,6 +53,16 @@ JSON.
'stream'` не используется тоже: при чтении байтами на входе нечему ломаться, и
ошибке разбора взяться неоткуда.
Отсюда ограничение на форму выражений разбора: они собираются только из функций,
которые не бросают исключений. `JSONExtract` и родственные возвращают значение по
умолчанию или NULL, но не падают, — и правило репозитория «грязные записи не
валят пайплайн» держится теперь именно на этом. Исключение в матвью не ошибка
формата, его не перехватит никакой режим Kafka-движка: вставка упадёт, офсеты не
закоммитятся, блок пойдёт читаться снова. Ломается это громко и чинится без
потерь — поправил матвью, потребление продолжилось с некоммиченного офсета, — но
пока не починено, топик стоит. Поэтому приведение `Nullable` к необнуляемому типу
и любая арифметика в этих выражениях живут за предикатом, который NULL уже отсёк.
Этим решение снимает ограничение, записанное в постановке #37: «ошибки разбора
должны рождаться на шаге Kafka-движка». Оно ставилось как условие достижимости
критерия #43 про громкую ошибку на опечатку в имени поля — критерий достижим и
@@ -116,26 +139,42 @@ contract-тест из #43 сюда не дотягивается — он ср
сообщений, которые не разобрались, и оставляет их пустыми для разобранных.
Отсюда весь довод о том, что типизированный чтец не может наполнить слой сырья.
Составные типы `Nullable` не оборачивает: `Nullable(Array)`, `Nullable(Map)` и
`Nullable(Tuple)` не поддерживаются, а `Nullable` внутри них — да. Отсюда
слепота `Nullable`-разбора на двенадцати колонках и необходимость сверки ключей.
`Nullable` внутри кортежа разрешён, поэтому запасной вариант со свёрткой в
именованный кортеж жив.
Составные типы `Nullable` не оборачивает: `Nullable(Array)` и `Nullable(Map)` не
поддерживаются, а `Nullable` внутри них — да. Отсюда слепота `Nullable`-разбора
на двенадцати колонках и необходимость сверки ключей. Оговорка про кортеж:
`Nullable(Tuple)` в ClickHouse всё-таки есть, но за настройкой
`enable_nullable_tuple_type` и в статусе беты; на довод это не влияет — кортежей
в контракте нет. `Nullable` внутри кортежа разрешён без всяких флагов, поэтому
запасной вариант со свёрткой в именованный кортеж жив.
Оттуда же: у Kafka-движка есть третий режим обработки ошибок —
`dead_letter_queue` с записью в системную таблицу. Он отвергнут независимо от
остального: спека требует свои `*_errors`, системная таблица их не заменяет.
Форматы `RawBLOB` и `LineAsString` в ClickHouse есть, и Kafka-движок
поддерживает все форматы.
поддерживает все форматы. `RawBLOB` читает вход в одно значение и рассчитан на
таблицу с единственным полем `String` — отсюда ограничение на форму чтеца.
Матвью с источником-`Distributed` срабатывает на вставку именно в эту
распределённую таблицу — блок она видит до разрезания по шардам. Проверено
владельцем на рабочих проектах; на стенде подтверждается заодно с приёмкой #37.
На живом стенде проверяется при исполнении #37, и то же внесено в раздел 11
спеки:
На живом стенде проверяется при исполнении #37. Первые два пункта внесены в
раздел 11 спеки как несущие; остальные — однострочные `SELECT`, их довольно
прогнать заодно:
- `RawBLOB` в Kafka-движке даёт ровно одну строку на сообщение;
- `RawBLOB` в Kafka-движке даёт ровно одну строку на сообщение. Проверять это
нужно первым и до написания DDL: формулировка «читает вход в одно значение»
про файл понятна, а про пачку сообщений из топика — нет, и если сообщения
склеятся, переделывать придётся решение целиком, а не DDL. Опыт стоит трёх
сообщений и одного `count()`. Запасной вариант — `LineAsString`: он режет по
переводу строки, а события у нас однострочные; цена запасного — сообщение с
переводом строки внутри даст две строки вместо одной;
- форма именованного кортежа в `JSONExtract` с `Nullable`-членами — нужна для
свёртки сорока семи вызовов в один, если разбор окажется дорогим.
свёртки сорока семи вызовов в один, если разбор окажется дорогим;
- `isValidJSON('123')` возвращает единицу, а `JSONExtractKeys` от скаляра —
пустой массив. На обоих стоят классы брака и их приоритет, а документация
поведение на не-объекте не описывает: два `SELECT` закрывают вопрос;
- `JSONAsString` действительно падает на некорректном JSON, а не пропускает
строку. На этом стоит отказ от него в пользу `RawBLOB`; документация про
ошибочный ввод молчит.
+10 -3
View File
@@ -29,12 +29,14 @@
половине кластера, выглядит как честное число. При суффиксе вида голого имени
нет вовсе, и та же ошибка становится громкой.
Второй довод — что группируется в `SHOW TABLES`. Суффикс группирует объекты по
сущности: все четыре объекта топика `hits` стоят рядом, потому что различаются
Второй довод — что группируется в списке по алфавиту. Суффикс группирует объекты
по сущности: все четыре объекта топика `hits` стоят рядом, потому что различаются
хвостом. Префиксный стиль, которым пользовался стенд-предшественник (`kafka_*`,
`mv_*`, `v_*`), группирует по технологии, и объекты одной сущности
расползаются по алфавиту. В хранилище, где у одной сущности живёт по три-четыре
воплощения, полезнее первое.
воплощения, полезнее первое. Довод про дерево в клиенте и про `ORDER BY name`:
порядок выдачи `SHOW TABLES` документация не оговаривает, так что на него здесь
опираться нельзя.
Третий — преемственность: `_rep` и `_dist` уже используются владельцем в других
хранилищах на ClickHouse, и общий словарь между стендами стоит больше, чем
@@ -60,3 +62,8 @@
объектов слоёв STG, ODS, DDS и DM, включая пары локальная/распределённая,
представления и матвью. Единственным объектом без выводимого суффикса оказался
словарь `products` — отсюда исключение в решении.
Первый прогон был неполным: четыре витрины из восьми остались с префиксом, и
заметило это холодное ревью, а не автор. Имена приведены в порядок 5 августа
2026 года. Урок не про имена: «прогнал по документу» — такое же утверждение,
как утверждение о поведении системы, и проверять его надо так же.