feat(clickhouse): справочники вынесены в зону dic, словарь читает подложку
Зачем: место словаря в 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>
This commit is contained in:
@@ -12,8 +12,9 @@ keeper, Kafka, каркас сервисов. Этап 2 идёт: в `sql/ddl/`
|
||||
Этап 3 добавил вход второго источника и довёл его до ODS: топик `orders`, свой
|
||||
чтец, своё сырьё, версии заказов с таблицей ошибок и поверхность текущего
|
||||
состояния. Наполняет всю цепочку даг `orders_ingest` двумя шагами, а не матвью.
|
||||
Тот же этап принёс первый объект DDS — словарь товаров из общего с генератором
|
||||
CSV-файла.
|
||||
Тот же этап принёс первый справочник — словарь товаров из общего с генератором
|
||||
CSV-файла. Живёт он вне цепочки слоёв, в зоне `dic`, и читает не файл, а
|
||||
подложку над ним ([ADR 0012](../adr/0012-dictionary-home.md)).
|
||||
Дальше по тексту устройство описано так, как оно проектируется; построенное от
|
||||
заложенного отличает карта таблиц в конце.
|
||||
|
||||
@@ -33,6 +34,7 @@ CSV-файла.
|
||||
| `_rep` | локальная таблица шарда, движок семейства `Replicated*` |
|
||||
| `_dist` | `Distributed` поверх одноимённой локальной |
|
||||
| `_kafka` | таблица на движке `Kafka` |
|
||||
| `_file` | таблица на движке `File` — чтец файла |
|
||||
| `_mv` | материализованное представление |
|
||||
| `_v` | обычное представление |
|
||||
|
||||
@@ -40,7 +42,9 @@ CSV-файла.
|
||||
запрос к `stg.hits_raw` даёт громкую ошибку «нет такой таблицы» — а под голым
|
||||
именем в документах и разговоре понимается сущность, у которой этих объектов
|
||||
несколько. Единственное исключение — словари: у них воплощение одно, шардировать
|
||||
нечего, и суффикс ничего не различал бы.
|
||||
нечего, и суффикс ничего не различал бы. В зоне `dic` обе формы стоят рядом и
|
||||
правило видно целиком: `dic.products_file` — чтец файла, `dic.products` — сам
|
||||
словарь.
|
||||
|
||||
Распространённая конвенция, где голое имя означает локальную таблицу, а
|
||||
распределённая получает суффикс `_all`, ошибается иначе: забытый суффикс тихо
|
||||
@@ -416,22 +420,52 @@ kafka_offset)`: смотрят такую таблицу от класса, а
|
||||
разрастается до имени отдельного поля. Точная граница приёма — в
|
||||
[спецификации заказов](orders/ingestion.md).
|
||||
|
||||
## Словарь товаров
|
||||
## Справочники
|
||||
|
||||
`dds.products` читает `data/catalog/products.csv` напрямую. Compose монтирует
|
||||
каталог только для чтения в `user_files` обеих нод, а DDL создаёт словарь
|
||||
`ON CLUSTER`: имя и форма одни, но каждая нода держит свою копию в памяти.
|
||||
Справочные данные живут в базе `dic` — вне цепочки STG → ODS → DDS → DM. В
|
||||
хранилище их никто не производит: содержимое приходит из файла репозитория, а
|
||||
читают его несколько слоёв сразу. Поэтому зона своя, читать её вправе любой
|
||||
слой, писать — никто. Почему так, а не пропиской в слое модели, —
|
||||
[ADR 0012](../adr/0012-dictionary-home.md).
|
||||
|
||||
Источник `FILE` с форматом `CSVWithNames` читает заголовок файла. Строковый ключ
|
||||
`sku` требует `COMPLEX_KEY_HASHED`; числовой `FLAT` здесь неприменим. Цена
|
||||
остаётся целым числом копеек типа `Int64`, как в контракте события.
|
||||
### Словарь товаров
|
||||
|
||||
`LIFETIME(0)` отключает фоновое обновление. Каталог меняется только явной
|
||||
правкой репозитория, а независимый опрос двух нод позволил бы им временно
|
||||
отвечать разными версиями. Изменение применяют к обеим нодам штатной командой:
|
||||
`SYSTEM RELOAD DICTIONARY ON CLUSTER clickstream_cluster dds.products`.
|
||||
Это административная операция: её выполняют под `default`; роль `etl` права
|
||||
перезагрузки словарей не получает.
|
||||
Под словарём лежит подложка `dic.products_file` на движке `File`. Она ничего не
|
||||
хранит и перечитывает `data/catalog/products.csv` на каждом запросе. Compose
|
||||
монтирует каталог только для чтения в `user_files` обеих нод, DDL создаёт оба
|
||||
объекта `ON CLUSTER`: имя и форма одни, но каждая нода читает свой файл и держит
|
||||
свою копию словаря в памяти.
|
||||
|
||||
Путь у движка `File` считается **от `user_files`**, а не от корня данных.
|
||||
Форма `./user_files/catalog/products.csv`, которой требовал прежний файловый
|
||||
источник словаря, даёт `FILE_DOESNT_EXIST` — легко принять за пропавший монтаж.
|
||||
|
||||
Сам `dic.products` берёт подложку источником `CLICKHOUSE`, причём формой
|
||||
`query`, а не `table`: словарь нормализует данные на входе, а не зеркалит
|
||||
подложку. Пользователя надо называть явно — без него словарь идёт как `default`
|
||||
с пустым паролем и падает с `AUTHENTICATION_FAILED`. Ходит он беспарольным
|
||||
`dict`, у которого одно право: чтение `dic`. Хост локальный, поэтому запрос
|
||||
идёт без сети.
|
||||
|
||||
Строковый ключ `sku` требует `COMPLEX_KEY_HASHED`; числовой `FLAT` здесь
|
||||
неприменим.
|
||||
|
||||
**Цена: три единицы, и их не надо путать.** В файле каталога лежат целые
|
||||
копейки — так их пишет генератор, и часть цен несёт копейки намеренно. Словарь
|
||||
приводит их к `Decimal(18, 2)`, как у денег бэкенда: единица становится видна в
|
||||
самом числе, `129000` против `1290.00`. А в контракте события `productPrice` —
|
||||
целые **рубли**, округление формата. Разрыв между ценой каталога и ценой в
|
||||
событии заложен специально: на нём держится урок про `Float64` и расхождение
|
||||
представлений денег.
|
||||
|
||||
`LIFETIME(MIN 60 MAX 90)` включает фоновое обновление: правка каталога доезжает
|
||||
до словаря сама, без команды. Момент внутри окна случаен — так разводят
|
||||
обращения разных серверов к источнику, чтобы они не шли разом. Цена у этого
|
||||
заявленная: ноды обновляются вразнобой, и до тридцати секунд одна отвечает по
|
||||
новому каталогу, а вторая по старому. Разогнать словари вручную можно штатной
|
||||
командой — `SYSTEM RELOAD DICTIONARY ON CLUSTER clickstream_cluster
|
||||
dic.products`; это административная операция, её выполняют под `default`, роль
|
||||
`etl` права перезагрузки словарей не получает.
|
||||
|
||||
## Раскладка SQL
|
||||
|
||||
@@ -455,16 +489,19 @@ Airflow читает и собирает эти файлы штатным шаб
|
||||
|
||||
| Файл | Что в нём |
|
||||
|---|---|
|
||||
| `00-databases.sql` | базы слоёв |
|
||||
| `00-databases.sql` | базы слоёв и зона справочников |
|
||||
| `05-dic-catalog.sql` | подложка над CSV-каталогом и словарь товаров поверх неё |
|
||||
| `10-stg-tables.sql` | чтецы топиков `hits` и `orders`, локальные и распределённые таблицы сырья обоих источников |
|
||||
| `20-ods-tables.sql` | типизированное событие, версии заказа и обе таблицы ошибок |
|
||||
| `25-dds-dictionaries.sql` | словарь товаров из общего CSV-каталога |
|
||||
| `30-ods-views.sql` | актуальные события, текущие заказы и матвью разбора в ODS |
|
||||
| `40-stg-views.sql` | матвью приёма: чтец в сырьё |
|
||||
|
||||
Порядок задают два правила. Первое: матвью принадлежит слою своей цели, а не
|
||||
источника, — разбор из STG в ODS лежит среди файлов ODS, потому что наполняет
|
||||
ODS. Второе: матвью приёма создаётся последней из всех, и потому нарушает
|
||||
Справочники идут сразу за базами: зона `dic` не зависит ни от одного слоя, и
|
||||
правила порядка слоёв к ней не применяются.
|
||||
|
||||
Порядок остальных задают два правила. Первое: матвью принадлежит слою своей
|
||||
цели, а не источника, — разбор из STG в ODS лежит среди файлов ODS, потому что
|
||||
наполняет ODS. Второе: матвью приёма создаётся последней из всех, и потому нарушает
|
||||
нумерацию слоёв. Kafka-движок начинает читать топик ровно тогда, когда к нему
|
||||
привязывают первую матвью; создай её раньше разбора — и всё, что доедет в
|
||||
зазоре, ляжет в сырьё и не попадёт в ODS никуда, ни в событие, ни в ошибки. На
|
||||
@@ -507,7 +544,7 @@ ODS. Второе: матвью приёма создаётся последне
|
||||
|
||||
Ниже — то, что закладывают этапы 2 и 3; всё перечисленное лежит в `sql/ddl/`.
|
||||
|
||||
| Слой | Объект | Что это |
|
||||
| База | Объект | Что это |
|
||||
|---|---|---|
|
||||
| STG | `stg.hits_raw_kafka` | чтец топика `hits`, формат `RawBLOB` |
|
||||
| STG | `stg.hits_raw_rep` / `_dist` | сырая строка сообщения плюс метаданные доставки |
|
||||
@@ -521,13 +558,14 @@ ODS. Второе: матвью приёма создаётся последне
|
||||
| ODS | `ods.order_snapshot_rep` / `_dist` | типизированные версии заказа |
|
||||
| ODS | `ods.order_v` | текущая версия заказа на языке источника |
|
||||
| ODS | `ods.order_snapshot_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
|
||||
| DDS | `dds.products` | словарь товаров из общего с генератором CSV-каталога |
|
||||
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
|
||||
| `dic` | `dic.products` | словарь товаров поверх подложки |
|
||||
|
||||
Матвью разбора у заказов нет: срез сырья раскладывают по этим двум целям два
|
||||
`INSERT SELECT` шага `parse_batch` из файлов `sql/ods/order_snapshot_load.sql`
|
||||
и `sql/ods/order_snapshot_errors_load.sql`.
|
||||
|
||||
Таблицы DDS и слой DM появляются на следующих этапах; их состав задан разделом
|
||||
Слой DDS и слой DM появляются на следующих этапах; их состав задан разделом
|
||||
7 мастер-спеки и переносится сюда по мере постройки.
|
||||
|
||||
## Что проверено
|
||||
@@ -553,6 +591,23 @@ DDL-словарь с файловым источником внутри `user_f
|
||||
`SYSTEM RELOAD DICTIONARY ON CLUSTER`
|
||||
изменилась на обеих нодах; после отката и повторной команды вернулась обратно.
|
||||
|
||||
**Проверка зоны справочников 20 августа 2026 года (#105).** Подложка на движке
|
||||
`File` работает `ON CLUSTER` и перечитывает файл на каждом запросе: строка,
|
||||
дописанная снаружи, меняет счёт без команд. Словарь поверх неё обновляется сам
|
||||
внутри окна `LIFETIME` — замер поймал изменение через 116 секунд, без
|
||||
`SYSTEM RELOAD DICTIONARY`. Относительный путь у движка считается от
|
||||
`user_files`, а не от корня данных. Источник `CLICKHOUSE` без явного `user`
|
||||
идёт как `default` с пустым паролем и падает с `AUTHENTICATION_FAILED`;
|
||||
беспарольный пользователь из файла настройки принимается молча. Приведение
|
||||
`toDecimal64(price, 2) / 100` даёт `Decimal(18, 2)` и точно на ценах с
|
||||
копейками: `188990 → 1889.90`. Собранный с нуля стенд поднял оба объекта, `bi`
|
||||
читает словарь `dictGet`-ом со второй ноды, `analyst` — подложку соединением.
|
||||
|
||||
Отдельно про право на движок: отказ называет `TABLE ENGINE ON File`, но права
|
||||
с таким именем не хватает. Замер тремя пользователями показал, что достаточно
|
||||
`GRANT FILE ON *.*` — привилегии на источник, как у Kafka, — а `TABLE ENGINE ON
|
||||
File` не нужен вовсе. Текст ошибки здесь уводит в сторону.
|
||||
|
||||
**Проверка версий заказов 18 августа 2026 года (#94).** MCP Context7 подтвердил,
|
||||
что `JSONType` возвращает имя типа значения JSON, — на нём стоит проверка типов
|
||||
в предикате приёма заказов. Остальное снято на закреплённом ClickHouse 26.3.
|
||||
|
||||
Reference in New Issue
Block a user