feat(clickhouse): справочники вынесены в зону dic, словарь читает подложку #107

Merged
ddmitry merged 4 commits from feat/105-dictionary-home into main 2026-08-20 12:26:41 +03:00
Showing only changes of commit b3e66034fc - Show all commits
+24 -54
View File
@@ -424,48 +424,27 @@ kafka_offset)`: смотрят такую таблицу от класса, а
Справочные данные живут в базе `dic` — вне цепочки STG → ODS → DDS → DM. В Справочные данные живут в базе `dic` — вне цепочки STG → ODS → DDS → DM. В
хранилище их никто не производит: содержимое приходит из файла репозитория, а хранилище их никто не производит: содержимое приходит из файла репозитория, а
читают его несколько слоёв сразу. Поэтому зона своя, читать её вправе любой читают его несколько слоёв сразу. Отсюда правило принадлежности: читать зону
слой, писать — никто. Почему так, а не пропиской в слое модели, вправе любой слой, писать — никто. Почему зона, а не прописка в слое модели, и
[ADR 0012](../adr/0012-dictionary-home.md). почему словарь читает подложку, а не файл, — [ADR
0012](../adr/0012-dictionary-home.md).
### Словарь товаров Первый и пока единственный справочник — каталог товаров: подложка
`dic.products_file` над CSV репозитория и словарь `dic.products` поверх неё.
Реплик у зоны нет: обе ноды читают свой смонтированный файл и держат свою копию
словаря в памяти. Словарь обновляется сам, окном `LIFETIME`, и потому после
правки каталога ноды какое-то время отвечают по-разному. Разогнать его раньше
срока — `SYSTEM RELOAD DICTIONARY ON CLUSTER`; это административная операция,
роль `etl` права на неё не получает.
Под словарём лежит подложка `dic.products_file` на движке `File`. Она ничего не Деньги каталога стоит держать в голове отдельно от остальных: в CSV лежат целые
хранит и перечитывает `data/catalog/products.csv` на каждом запросе. Compose копейки, словарь отдаёт `Decimal(18, 2)`, а `productPrice` события — уже целые
монтирует каталог только для чтения в `user_files` обеих нод, DDL создаёт оба рубли. Разрыв намеренный, на нём стоит урок про `Float64` ([описание
объекта `ON CLUSTER`: имя и форма одни, но каждая нода читает свой файл и держит выгрузки](../formats/clickstream-event.md)).
свою копию словаря в памяти.
Путь у движка `File` считается **от `user_files`**, а не от корня данных. Устройство обоих объектов — почему такой движок, такая форма пути, такой
Форма `./user_files/catalog/products.csv`, которой требовал прежний файловый пользователь и такое окно — расписано в
источник словаря, даёт `FILE_DOESNT_EXIST` — легко принять за пропавший монтаж. [`sql/ddl/05-dic-catalog.sql`](../../sql/ddl/05-dic-catalog.sql).
Сам `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 ## Раскладка SQL
@@ -592,21 +571,12 @@ DDL-словарь с файловым источником внутри `user_f
изменилась на обеих нодах; после отката и повторной команды вернулась обратно. изменилась на обеих нодах; после отката и повторной команды вернулась обратно.
**Проверка зоны справочников 20 августа 2026 года (#105).** Подложка на движке **Проверка зоны справочников 20 августа 2026 года (#105).** Подложка на движке
`File` работает `ON CLUSTER` и перечитывает файл на каждом запросе: строка, `File` перечитывает файл на каждом запросе, словарь поверх неё обновляется сам
дописанная снаружи, меняет счёт без команд. Словарь поверх неё обновляется сам внутри окна `LIFETIME`, а собранный с нуля стенд поднимает оба объекта и отдаёт
внутри окна `LIFETIME` — замер поймал изменение через 116 секунд, без словарь читателям. Замеры целиком — в
`SYSTEM RELOAD DICTIONARY`. Относительный путь у движка считается от [ADR 0012](../adr/0012-dictionary-home.md), раздел «Что проверено»; там же
`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 подтвердил, **Проверка версий заказов 18 августа 2026 года (#94).** MCP Context7 подтвердил,
что `JSONType` возвращает имя типа значения JSON, — на нём стоит проверка типов что `JSONType` возвращает имя типа значения JSON, — на нём стоит проверка типов