diff --git a/CONTEXT.md b/CONTEXT.md
index cfcd9cf..fd73d35 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -95,15 +95,14 @@ _Избегать_: «покупатель» про того, кто купил
товара, покупка — на страницу подтверждения заказа.
**Справочник**:
-Данные, входящие в хранилище сбоку, а не по цепочке слоёв: в хранилище их
-никто не производит, а читают их несколько слоёв. Дом — база `dic`, общая для
-всех справочников. Первый и пока единственный — каталог товаров.
-_Не путать_: слово «словарь» в проекте значит ещё две вещи — этот глоссарий
-понятий и объект `DICTIONARY` ClickHouse, одну из форм, в которой справочник
-доступен.
+Данные, которые хранилище не производит, а получает готовыми со стороны:
+содержимым владеет источник вне хранилища, слои только читают. Живут в базе
+`dic`. Первый и пока единственный — каталог товаров.
+_Избегать_: словарь — этим словом в проекте зовут и глоссарий понятий, и
+объект `DICTIONARY` ClickHouse, одну из форм, в которой справочник доступен.
**Каталог товаров**:
-`data/catalog/products.csv` — общий справочник генератора и словаря
+`data/catalog/products.csv` — общий файл генератора и словаря
ClickHouse. Форма файла решена, длина — нет: строки дописываются.
**Уровень спроса**:
diff --git a/README.md b/README.md
index 3a9a1b4..68f5f1d 100644
--- a/README.md
+++ b/README.md
@@ -278,12 +278,16 @@ uv run --project generator python -m clickstream_generator batch \
- Prometheus — `http://127.0.0.1:29090`;
- Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`.
-В ClickHouse четыре пользователя:
+В ClickHouse пять пользователей:
- `etl` применяет DDL и подключает Airflow; роль `etl_writer` читает и пишет
слои хранилища;
-- `bi` подключает Superset; роль `bi_reader` читает будущие слои DDS и DM;
-- `analyst` предназначен для подключения человека и читает все слои;
+- `bi` подключает Superset; роль `bi_reader` читает справочники `dic` и будущие
+ слои DDS и DM;
+- `analyst` предназначен для подключения человека и читает все слои и
+ справочники;
+- `dict` читает только базу `dic` и пароля не имеет: им словарь товаров ходит
+ за своей подложкой;
- `default` остаётся служебным: им ходят проверки здоровья и скрипты внутри
контейнеров, но не приложения.
diff --git a/docs/adr/0006-object-naming.md b/docs/adr/0006-object-naming.md
index 2c8d41d..24e1bea 100644
--- a/docs/adr/0006-object-naming.md
+++ b/docs/adr/0006-object-naming.md
@@ -9,10 +9,8 @@
топика, `_file` — чтец файла, `_mv` — материализованное представление, `_v` —
обычное представление.
-Суффикс `_file` добавлен 20 августа 2026 года вместе с подложкой под словарём
-товаров ([ADR 0012](0012-dictionary-home.md)). Он ровно параллелен `_kafka`:
-чтец внешнего источника, по копии на каждой ноде, без репликации и без
-распределённой пары.
+Суффикс `_file` пришёл с подложкой под словарём товаров
+([ADR 0012](0012-dictionary-home.md)).
Суффикс носит каждый физический объект, поэтому голого имени у таблицы не
существует: запрос к `stg.hits_raw` даёт ошибку «нет такой таблицы».
diff --git a/docs/adr/0007-clickhouse-access.md b/docs/adr/0007-clickhouse-access.md
index 549436e..f38c67f 100644
--- a/docs/adr/0007-clickhouse-access.md
+++ b/docs/adr/0007-clickhouse-access.md
@@ -20,7 +20,8 @@
Роль здесь шире прав на слои: образ ClickHouse требует отдельного разрешения
на запросы `ON CLUSTER`, на чтение системных таблиц и на каждый движок
-внешнего источника, а без второго клиент не открывает соединение вовсе. Состав прав каждой роли — работа тикета
+внешнего источника. Без права на системные таблицы клиент вовсе не открывает
+соединение. Состав прав каждой роли — работа тикета
реализации.
Пользователи, роли и права объявлены файлами настройки сервера, а не
@@ -54,12 +55,9 @@ DDS под ними, потому что так чаще всего и быва
видны всем читателям и никому на запись: зона входит в хранилище сбоку, и её
содержимым владеет файл репозитория, а не слой.
-**Беспарольный `dict` — намеренное исключение.** Пароля у него нет не по
-недосмотру: ходить этой учёткой некуда, кроме локального чтения одной базы, а
-пароль пришлось бы вписать в текст DDL словаря — то есть положить секрет в git.
-Право у него одно, `SELECT` на `dic`. Отдельная учётка под словарь взята из
-промышленной практики, где источник словаря читают минимальной служебной
-учётной записью, а не общим админом.
+**Беспарольный `dict`.** Пароль пришлось бы вписать в текст DDL словаря, то
+есть положить секрет в git. Ходить этой учёткой некуда: право у неё одно —
+`SELECT` на `dic`, и хост источника локальный.
**Секрет, а не учётные данные по репликам.** У каждой реплики в описании
кластера своя учётка — вписанная явно или подразумеваемая, и тогда это
diff --git a/docs/adr/0012-dictionary-home.md b/docs/adr/0012-dictionary-home.md
index d2b4de2..0a1ad17 100644
--- a/docs/adr/0012-dictionary-home.md
+++ b/docs/adr/0012-dictionary-home.md
@@ -33,18 +33,19 @@
владение, которого у него нет: DDS этот объект не производит, не обновляет и не
проверяет.
-**На направление данных ссылаться нельзя, и это важно не спутать.** Чтение из
-DM объекта, лежащего в DDS, — течение вниз, ровно то, которое разрешено.
-Промах был во владении, а не в направлении. Но зона вне цепочки закрывает и
-будущий случай: как только справочник понадобится выше по течению — например,
-проверка `sku` позиции при разборе заказа в ODS, — чтение `dds.*` из `ods.*`
-стало бы настоящей инверсией. У зоны вне цепочки направления относительно слоёв
-нет, и такой запрос законен по построению.
+**Промах был во владении, а не в направлении.** Чтение из DM объекта, лежащего
+в DDS, — течение вниз, ровно то, которое разрешено; на инверсию слоёв это
+решение не опирается. Зато зона закрывает будущий случай: как только справочник
+понадобится выше по течению — скажем, проверка `sku` при разборе заказа в ODS —
+чтение `dds.*` из `ods.*` стало бы настоящей инверсией. У зоны вне цепочки
+направления относительно слоёв нет, и такой запрос законен по построению.
-**Отраслевой образец говорит то же самое другим средством.** В хранилищах, где
-база одна, а слои выражены префиксами имён, справочники несут собственный
-префикс наравне с префиксами стадий — то есть образуют свой слой, а не
-приписаны к слою модели. Причём префикс накрывает всю цепочку справочника: и
+**Отраслевой образец говорит то же самое другим средством.** Владелец принёс
+устройство промышленного хранилища телекома — источник закрытый, проверить по
+ссылке нельзя, поэтому вес у довода такой же, как у полевого опыта, а не как у
+документации. Там база одна, слои выражены префиксами имён, и справочники несут
+собственный префикс наравне с префиксами стадий — то есть образуют свою зону, а
+не приписаны к слою модели. Причём префикс накрывает всю цепочку справочника: и
таблицу, и представление над ней, и сам объект словаря. Средство разное,
утверждение одно. У нас слои выражены базами — значит справочникам полагается
база.
@@ -82,8 +83,8 @@ DM объекта, лежащего в DDS, — течение вниз, ров
**Историзацию и представление «последняя загрузка» из образца не берём.** В
промышленном примере таблица копит загрузки с меткой времени, а представление
берёт последнюю: источник приезжал извне, и версий у него не было нигде. У нас
-версии есть — **история каталога это и есть git**. Дублировать её в ClickHouse значит
-поставить конструкцию, у которой на стенде нет причины, а расшифровывать её
+версии есть — **история каталога это и есть git**. Дублировать её в ClickHouse
+значит поставить конструкцию, у которой на стенде нет причины, а расшифровывать её
менти всё равно придётся.
**Материализованной таблицы с шагом наполнения тоже не берём.** Шаг наполнения
@@ -121,6 +122,12 @@ DM объекта, лежащего в DDS, — течение вниз, ров
## Условия пересмотра
+- **Дом.** Появляется справочные данные, которые хранилище производит само —
+ скажем, таблица соответствий, собранная из DDS. Посылка решения «в хранилище
+ их никто не производит» на них не распространяется, и зону придётся либо
+ сузить до пришедших извне, либо переопределить. Дешевле всего это до этапа 4:
+ как только витрины начнут звать словарь по имени, каждый разворот станет
+ правкой по всем ссылкам.
- Справочник перестаёт быть файлом репозитория и начинает приезжать извне —
возвращается вопрос устройства: историзованная таблица, «последняя загрузка»,
словарь поверх. Адрес это переживает.
@@ -142,8 +149,8 @@ DM объекта, лежащего в DDS, — течение вниз, ров
почти на все полторы минуты: одна перезагрузилась сразу после правки, вторая
ещё нет. Прежнее решение `LIFETIME(0)` окно не закрывало, а сжимало до
разброса исполнения `SYSTEM RELOAD DICTIONARY ON CLUSTER`; теперь оно
- раскрыто намеренно. Урок записан опорной точкой в мастер-спеке, тащить его в курс —
- отдельное решение.
+ раскрыто намеренно. Урок записан опорной точкой в мастер-спеке, тащить его
+ в курс — отдельное решение.
- **Номенклатура товаров — медленно изменяющийся справочник**, и в предметной
области это так. Стенд упрощает: файл в репозитории под версией git.
Историзации в хранилище нет, и обещать её этот документ не должен.
@@ -178,15 +185,25 @@ DM объекта, лежащего в DDS, — течение вниз, ров
- `SHOW DICTIONARIES` без указания базы возвращает пусто; словарь в базе стоит
среди таблиц слоя и отличается от них только колонкой движка в
`system.tables`.
-- До переноса `dds.products` упоминался в репозитории четырежды, в дагах — ноль.
+- До переноса `dds.products` стоял в семи строках трёх файлов: дока хранилища,
+ DDL словаря и проверка `scripts/check-clickhouse.sh`. Первый замер насчитал
+ четыре и проверку не увидел — искали с фильтром по расширениям, куда `.sh`
+ не попал. Из недосчёта вырос настоящий дефект: девятая проверка кластера
+ осталась звать несуществующий объект и упала. Поймало ревью, не автор.
+ Урок тот же, что в [ADR 0006](0006-object-naming.md): «прогнал по
+ репозиторию» — такое же утверждение, как утверждение о поведении системы, и
+ проверять его надо так же.
- Движок `File` требует у `etl` права на источник. Отказ называет
`TABLE ENGINE ON File`, но права с таким именем не хватает: замер тремя
пользователями показал, что работает `GRANT FILE ON *.*`, а `TABLE ENGINE ON
File` не нужен вовсе.
- Стенд, собранный с нуля этим решением, поднимается зелёным: оба объекта зоны
на месте, словарь `LOADED` со 180 строками, база `dds` пуста, `bi` читает
- словарь со второй ноды, `analyst` — подложку соединением. `make smoke` —
- 20 проверок, 0 ошибок.
+ словарь со второй ноды, `analyst` — подложку соединением. Словарь спрашивает
+ `make check-clickhouse` — 10 проверок из 10; девятая берёт известный `sku` на
+ обеих нодах и сверяет цену с колонкой файла, умножив её обратно на сто, так
+ что приведение к `Decimal` проверено вместе с ответом. `make smoke` словарь
+ не трогает вовсе.
Сверено по документации ClickHouse через MCP Context7 20 августа 2026 года.
diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md
index f932cbd..2dad3a2 100644
--- a/docs/architecture/storage.md
+++ b/docs/architecture/storage.md
@@ -431,15 +431,15 @@ kafka_offset)`: смотрят такую таблицу от класса, а
Первый и пока единственный справочник — каталог товаров: подложка
`dic.products_file` над CSV репозитория и словарь `dic.products` поверх неё.
-Реплик у зоны нет: обе ноды читают свой смонтированный файл и держат свою копию
-словаря в памяти. Словарь обновляется сам, окном `LIFETIME`, и потому после
-правки каталога ноды какое-то время отвечают по-разному. Разогнать его раньше
-срока — `SYSTEM RELOAD DICTIONARY ON CLUSTER`; это административная операция,
-роль `etl` права на неё не получает.
+Распределённых пар у зоны нет: обе ноды читают свой смонтированный файл и
+держат свою копию словаря в памяти. Словарь обновляется сам, окном `LIFETIME`,
+и потому после правки каталога ноды какое-то время отвечают по-разному.
+Разогнать его раньше срока — `SYSTEM RELOAD DICTIONARY ON CLUSTER`; это
+административная операция, роль `etl` права на неё не получает.
-Деньги каталога стоит держать в голове отдельно от остальных: в CSV лежат целые
-копейки, словарь отдаёт `Decimal(18, 2)`, а `productPrice` события — уже целые
-рубли. Разрыв намеренный, на нём стоит урок про `Float64` ([описание
+Деньги каталога живут в трёх единицах: в CSV — целые копейки, словарь отдаёт
+`Decimal(18, 2)`, `productPrice` события — целые рубли. Разрыв намеренный, на
+нём стоит урок про `Float64` ([описание
выгрузки](../formats/clickstream-event.md)).
Устройство обоих объектов — почему такой движок, такая форма пути, такой
@@ -525,18 +525,18 @@ Airflow читает и собирает эти файлы штатным шаб
| База | Объект | Что это |
|---|---|---|
-| STG | `stg.hits_raw_kafka` | чтец топика `hits`, формат `RawBLOB` |
-| STG | `stg.hits_raw_rep` / `_dist` | сырая строка сообщения плюс метаданные доставки |
-| STG | `stg.hits_raw_mv` | наполняет сырьё из чтеца |
-| STG | `stg.orders_raw_kafka` | чтец топика `orders`, формат `RawBLOB`, только на ноде 1 и без матвью |
-| STG | `stg.orders_raw_rep` / `_dist` | сырое сообщение слепка, метаданные доставки и `_load_id` |
-| ODS | `ods.event_rep` / `_dist` | типизированное широкое событие |
-| ODS | `ods.event_v` | актуальная версия события с полями источника |
-| ODS | `ods.event_errors_rep` / `_dist` | строки, не прошедшие строгий приём |
-| ODS | `ods.event_mv`, `ods.event_errors_mv` | разбор сырья в событие и в ошибки |
-| ODS | `ods.order_snapshot_rep` / `_dist` | типизированные версии заказа |
-| ODS | `ods.order_v` | текущая версия заказа на языке источника |
-| ODS | `ods.order_snapshot_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
+| `stg` | `stg.hits_raw_kafka` | чтец топика `hits`, формат `RawBLOB` |
+| `stg` | `stg.hits_raw_rep` / `_dist` | сырая строка сообщения плюс метаданные доставки |
+| `stg` | `stg.hits_raw_mv` | наполняет сырьё из чтеца |
+| `stg` | `stg.orders_raw_kafka` | чтец топика `orders`, формат `RawBLOB`, только на ноде 1 и без матвью |
+| `stg` | `stg.orders_raw_rep` / `_dist` | сырое сообщение слепка, метаданные доставки и `_load_id` |
+| `ods` | `ods.event_rep` / `_dist` | типизированное широкое событие |
+| `ods` | `ods.event_v` | актуальная версия события с полями источника |
+| `ods` | `ods.event_errors_rep` / `_dist` | строки, не прошедшие строгий приём |
+| `ods` | `ods.event_mv`, `ods.event_errors_mv` | разбор сырья в событие и в ошибки |
+| `ods` | `ods.order_snapshot_rep` / `_dist` | типизированные версии заказа |
+| `ods` | `ods.order_v` | текущая версия заказа на языке источника |
+| `ods` | `ods.order_snapshot_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
| `dic` | `dic.products` | словарь товаров поверх подложки |
diff --git a/docs/specs/2026-07-30-stand-v2-realism.md b/docs/specs/2026-07-30-stand-v2-realism.md
index 54270b2..77e715d 100644
--- a/docs/specs/2026-07-30-stand-v2-realism.md
+++ b/docs/specs/2026-07-30-stand-v2-realism.md
@@ -240,10 +240,12 @@ Ecommerce (заполнены только у торговых событий):
## 3. Каталог товаров
CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `category`,
-`brand`, `price`, `demand`) — **словарь ClickHouse** из файла. Тот же файл
-использует генератор — расхождений нет по построению. Даёт `dictGet` в
-витринах и разговор о политике обновления словаря. На кластере файл
-монтируется в обе ноды, словарь создаётся ON CLUSTER. Последняя колонка —
+`brand`, `price`, `demand`) — **словарь ClickHouse**. Тот же файл использует
+генератор — расхождений нет по построению. Даёт `dictGet` в витринах и
+разговор о политике обновления словаря. На кластере файл монтируется в обе
+ноды; над ним стоит подложка, а словарь читает её и живёт в зоне справочников
+`dic` вне цепочки слоёв ([ADR 0012](../adr/0012-dictionary-home.md)).
+Последняя колонка —
уровень спроса товара, заведена при исполнении #50 (спека генератора,
раздел 9): генератор решает по ней, что уходит из карточки в корзину.
@@ -402,7 +404,7 @@ README.
## 7. Слои: карта таблиц v2
-| Слой | Объект | Что это |
+| Где | Объект | Что это |
|---|---|---|
| Kafka | `hits`, `orders` | два топика: `hits` — 2 партиции, `orders` — одна |
| STG | `stg.hits_raw_kafka`, `stg.hits_raw` + MV | сырые строки событий, Kafka Engine на обеих нодах |
@@ -413,7 +415,7 @@ README.
| DDS | `dds.event_v` | представление над `ods.event`: snake_case-имена, расшифровка кодов `DeviceCategory`; витрины DM читают его, а не ODS напрямую |
| DDS | модель заказов | зерно, связи и материализация проектируются на этапе DDS |
| DDS | `dds.identity_map` | карта кука↔пользователь |
-| `dic` | подложка `products_file` и словарь `products` | каталог из CSV, вне цепочки слоёв ([ADR 0012](../adr/0012-dictionary-home.md)) |
+| DIC | подложка `products_file` и словарь `products` | каталог из CSV, вне цепочки слоёв ([ADR 0012](../adr/0012-dictionary-home.md)) |
| DM | витрины `dm.*_v`, `dm.dq_summary` | см. ниже |
У каждой таблицы слоя — пара из локальной и распределённой, имена по конвенции
@@ -697,9 +699,9 @@ v2, этап 0).
`RegionCityID`);
- неатомарное обновление словаря на кластере: ноды перезагружают его в
случайный момент внутри окна `LIFETIME`, фазы у них независимы, и после
- правки каталога две ноды отвечают на один `dictGet` по-разному — почти всё
- окно целиком. Обвязка готова ([ADR
- 0012](../adr/0012-dictionary-home.md)), урок остаётся на выбор;
+ правки каталога две ноды почти всё окно отвечают на один `dictGet`
+ по-разному. Обвязка готова ([ADR 0012](../adr/0012-dictionary-home.md)),
+ урок остаётся на выбор;
- лаба сессий: менти сначала собирает сессии сам, и только после — рассказ,
что с октября 2025 Метрика отдаёт `VisitID` прямо в хитах; частично
синтетическая постановка — осознанный приём;
diff --git a/infra/clickhouse/users.d/access.xml b/infra/clickhouse/users.d/access.xml
index 6beae61..785b76e 100644
--- a/infra/clickhouse/users.d/access.xml
+++ b/infra/clickhouse/users.d/access.xml
@@ -44,12 +44,7 @@
GRANT SELECT ON system.settings
-
+
GRANT SELECT ON dic.*
diff --git a/scripts/check-clickhouse.sh b/scripts/check-clickhouse.sh
index 71cca65..dcb4d6d 100755
--- a/scripts/check-clickhouse.sh
+++ b/scripts/check-clickhouse.sh
@@ -264,10 +264,13 @@ printf 'ЗЕЛЁНО: временные таблицы удалены; пров
# После снятия ловушек: своих объектов эти проверки не заводят и прибирать за
# собой им нечего — они только смотрят на то, что стенд произвёл сам.
printf 'Проверка 9/10: словарь товаров отвечает на обеих нодах...\n'
+# Цену словарь отдаёт в Decimal(18, 2), а каталог хранит целые копейки. Умножаем
+# обратно и сравниваем с колонкой файла: так проверка заодно утверждает, что
+# приведение в источнике словаря точное, а не только что словарь отвечает.
product_sql="SELECT
- dictGet('dds.products', 'name', tuple('HOME-0001')),
- dictGet('dds.products', 'category', tuple('HOME-0001')),
- dictGet('dds.products', 'price', tuple('HOME-0001'))
+ dictGet('dic.products', 'name', tuple('HOME-0001')),
+ dictGet('dic.products', 'category', tuple('HOME-0001')),
+ toInt64(dictGet('dic.products', 'price', tuple('HOME-0001')) * 100)
FORMAT TSV"
expected_product="$(
awk -F, \
diff --git a/sql/ddl/00-databases.sql b/sql/ddl/00-databases.sql
index bf9f0f3..a567a12 100644
--- a/sql/ddl/00-databases.sql
+++ b/sql/ddl/00-databases.sql
@@ -19,6 +19,6 @@ CREATE DATABASE IF NOT EXISTS ods ON CLUSTER clickstream_cluster;
CREATE DATABASE IF NOT EXISTS dds ON CLUSTER clickstream_cluster;
-- Справочники стоят вне цепочки STG → ODS → DDS → DM: в хранилище их никто не
--- производит, а читают их несколько слоёв ([ADR 0012]). Поэтому зона своя, и
--- порядок слоёв к ней не применяется — её файл идёт сразу за этим.
+-- производит, а читают их несколько слоёв — ADR 0012. Порядок слоёв к зоне не
+-- применяется, её файл идёт сразу за этим.
CREATE DATABASE IF NOT EXISTS dic ON CLUSTER clickstream_cluster;
diff --git a/sql/ddl/05-dic-catalog.sql b/sql/ddl/05-dic-catalog.sql
index 5f4fdfe..e525646 100644
--- a/sql/ddl/05-dic-catalog.sql
+++ b/sql/ddl/05-dic-catalog.sql
@@ -1,22 +1,14 @@
-- Каталог товаров: подложка на файловом движке и словарь поверх неё.
--
--- Словарь читает не файл, а таблицу хранилища — намеренное усложнение
--- ([ADR 0012](../../docs/adr/0012-dictionary-home.md)). В бою справочник
--- приезжает процессом, и предложение SOURCE с запросом и учётной записью —
--- та форма, которую менти встретит; файловый источник работает, но редок.
---
--- Зона dic лежит вне цепочки STG → ODS → DDS → DM: справочник в хранилище
--- никто не производит, а читают его несколько слоёв.
+-- Словарь читает не файл, а таблицу хранилища. Это намеренное усложнение ради
+-- урока: в бою справочник приезжает процессом, и предложение SOURCE с запросом
+-- и учётной записью — та форма, которую менти встретит. Доводы, отвергнутые
+-- варианты и условия пересмотра — ADR 0012.
-- Подложка ничего не хранит: движок File перечитывает CSV на каждом запросе,
--- поэтому правка каталога доезжает до словаря сама. Compose монтирует один и
--- тот же файл в user_files обеих нод только для чтения.
---
--- Путь считается ОТ user_files, а не от корня данных: форма
--- './user_files/catalog/products.csv' даёт FILE_DOESNT_EXIST. У файлового
--- источника словаря база пути была другой — отсюда разница с прежним DDL.
--- Типы здесь повторяют файл, а не модель: цена лежит целыми копейками, как её
--- пишет генератор. Приведение к деньгам делает словарь.
+-- поэтому правка каталога доезжает до словаря сама. Путь считается от
+-- user_files, а не от корня данных. Типы повторяют файл, а не модель: цена
+-- лежит целыми копейками, приведение делает словарь.
CREATE TABLE IF NOT EXISTS dic.products_file ON CLUSTER clickstream_cluster
(
sku String,
@@ -28,28 +20,16 @@ CREATE TABLE IF NOT EXISTS dic.products_file ON CLUSTER clickstream_cluster
)
ENGINE = File(CSVWithNames, './catalog/products.csv');
--- Пользователь dict объявлен файлом настройки и умеет одно — читать dic.
--- Назвать его обязательно: без user словарь идёт как default с пустым паролем
--- и падает с AUTHENTICATION_FAILED. Хост локальный, поэтому запрос к подложке
--- идёт без сети.
+-- Пользователь dict умеет одно — читать dic. Назвать его обязательно: без user
+-- словарь идёт как default с пустым паролем и падает. Хост локальный, поэтому
+-- запрос к подложке идёт без сети.
--
--- Окно обновления вместо LIFETIME(0): словарь перезагружается сам в случайный
--- момент внутри окна. Случайность разводит обращения разных серверов к
--- источнику, и цена у неё заявленная — ноды обновляются вразнобой. Ждать
--- правки каталога каждой из них приходится от нуля до верхней границы окна,
--- фазы у них независимы, поэтому расходиться они могут почти на все
--- полторы минуты: одна перезагрузилась сразу после правки, вторая ещё нет.
+-- Форма query, а не table: словарь приводит копейки каталога к Decimal(18, 2)
+-- прямо на входе, то есть нормализует, а не зеркалит подложку. Ключ строковый,
+-- поэтому COMPLEX_KEY_HASHED: числовой FLAT здесь неприменим.
--
--- Ключ строковый, поэтому COMPLEX_KEY_HASHED: числовой FLAT здесь неприменим.
---
--- Цена приводится к деньгам прямо в источнике — оттого форма query, а не
--- table: словарь нормализует на входе, а не зеркалит подложку. В файле лежат
--- целые копейки, наружу словарь отдаёт Decimal(18, 2), как заказы бэкенда.
--- Единица в числе становится видна: 129000 против 1290.00.
---
--- Стык с событием на этом и стоит: в контракте события productPrice — целые
--- РУБЛИ, округление формата. Разрыв между ценой каталога и ценой в событии
--- намеренный, на нём держится урок про Float64.
+-- Окно вместо LIFETIME(0): словарь обновляется сам, а цена этому — ноды
+-- расходятся почти на всё окно (ADR 0012).
CREATE DICTIONARY IF NOT EXISTS dic.products ON CLUSTER clickstream_cluster
(
sku String,