docs(storage): устройство справочников оставлено в одном месте
Зачем: каждое утверждение о словаре было записано трижды — в доке хранилища, в комментариях DDL и в ADR 0012. Правка поведения стоила бы трёх согласованных правок, а расхождение между копиями обнаружилось бы не сразу. Дока хранилища — карта, а не пересказ реализации. Что: раздел «Справочники» сведён к своему уровню — правило принадлежности зоны, топология на кластере, единицы денег и указатели. Механика (движок, форма пути, пользователь, окно обновления) осталась только в sql/ddl/05-dic-catalog.sql, рядом с кодом, который она объясняет. Блок «Что проверено» отправлен к замерам ADR 0012 вместо их пересказа — так же, как он уже поступает с ADR 0005. Проверка: минус 30 строк; ссылок на убранный подзаголовок в репозитории нет, три относительные ссылки нового текста разрешаются в существующие файлы. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -424,48 +424,27 @@ kafka_offset)`: смотрят такую таблицу от класса, а
|
||||
|
||||
Справочные данные живут в базе `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`. Она ничего не
|
||||
хранит и перечитывает `data/catalog/products.csv` на каждом запросе. Compose
|
||||
монтирует каталог только для чтения в `user_files` обеих нод, DDL создаёт оба
|
||||
объекта `ON CLUSTER`: имя и форма одни, но каждая нода читает свой файл и держит
|
||||
свою копию словаря в памяти.
|
||||
Деньги каталога стоит держать в голове отдельно от остальных: в CSV лежат целые
|
||||
копейки, словарь отдаёт `Decimal(18, 2)`, а `productPrice` события — уже целые
|
||||
рубли. Разрыв намеренный, на нём стоит урок про `Float64` ([описание
|
||||
выгрузки](../formats/clickstream-event.md)).
|
||||
|
||||
Путь у движка `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/ddl/05-dic-catalog.sql`](../../sql/ddl/05-dic-catalog.sql).
|
||||
|
||||
## Раскладка SQL
|
||||
|
||||
@@ -592,21 +571,12 @@ DDL-словарь с файловым источником внутри `user_f
|
||||
изменилась на обеих нодах; после отката и повторной команды вернулась обратно.
|
||||
|
||||
**Проверка зоны справочников 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` не нужен вовсе. Текст ошибки здесь уводит в сторону.
|
||||
`File` перечитывает файл на каждом запросе, словарь поверх неё обновляется сам
|
||||
внутри окна `LIFETIME`, а собранный с нуля стенд поднимает оба объекта и отдаёт
|
||||
словарь читателям. Замеры целиком — в
|
||||
[ADR 0012](../adr/0012-dictionary-home.md), раздел «Что проверено»; там же
|
||||
разобрано, почему отказ в праве на движок называет не то право, которого не
|
||||
хватает.
|
||||
|
||||
**Проверка версий заказов 18 августа 2026 года (#94).** MCP Context7 подтвердил,
|
||||
что `JSONType` возвращает имя типа значения JSON, — на нём стоит проверка типов
|
||||
|
||||
Reference in New Issue
Block a user