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:
@@ -1,4 +1,4 @@
|
||||
-- Базы слоёв хранилища.
|
||||
-- Базы хранилища: слои цепочки и зона справочников.
|
||||
--
|
||||
-- Файлы этой папки применяются по порядку имён с ноды 1 и всегда ON CLUSTER:
|
||||
-- объекты обязаны появиться на обеих нодах, иначе распределённая таблица
|
||||
@@ -14,5 +14,11 @@ CREATE DATABASE IF NOT EXISTS stg ON CLUSTER clickstream_cluster;
|
||||
-- порядок файлов от этого не зависит.
|
||||
CREATE DATABASE IF NOT EXISTS ods ON CLUSTER clickstream_cluster;
|
||||
|
||||
-- DDS начинается со словаря товаров; таблицы слоя появятся на следующем этапе.
|
||||
-- Объекты DDS появятся на следующем этапе; база заводится заранее по той же
|
||||
-- причине, что и ODS.
|
||||
CREATE DATABASE IF NOT EXISTS dds ON CLUSTER clickstream_cluster;
|
||||
|
||||
-- Справочники стоят вне цепочки STG → ODS → DDS → DM: в хранилище их никто не
|
||||
-- производит, а читают их несколько слоёв ([ADR 0012]). Поэтому зона своя, и
|
||||
-- порядок слоёв к ней не применяется — её файл идёт сразу за этим.
|
||||
CREATE DATABASE IF NOT EXISTS dic ON CLUSTER clickstream_cluster;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
-- Каталог товаров: подложка на файловом движке и словарь поверх неё.
|
||||
--
|
||||
-- Словарь читает не файл, а таблицу хранилища — намеренное усложнение
|
||||
-- ([ADR 0012](../../docs/adr/0012-dictionary-home.md)). В бою справочник
|
||||
-- приезжает процессом, и предложение SOURCE с запросом и учётной записью —
|
||||
-- та форма, которую менти встретит; файловый источник работает, но редок.
|
||||
--
|
||||
-- Зона dic лежит вне цепочки STG → ODS → DDS → DM: справочник в хранилище
|
||||
-- никто не производит, а читают его несколько слоёв.
|
||||
|
||||
-- Подложка ничего не хранит: движок File перечитывает CSV на каждом запросе,
|
||||
-- поэтому правка каталога доезжает до словаря сама. Compose монтирует один и
|
||||
-- тот же файл в user_files обеих нод только для чтения.
|
||||
--
|
||||
-- Путь считается ОТ user_files, а не от корня данных: форма
|
||||
-- './user_files/catalog/products.csv' даёт FILE_DOESNT_EXIST. У файлового
|
||||
-- источника словаря база пути была другой — отсюда разница с прежним DDL.
|
||||
-- Типы здесь повторяют файл, а не модель: цена лежит целыми копейками, как её
|
||||
-- пишет генератор. Приведение к деньгам делает словарь.
|
||||
CREATE TABLE IF NOT EXISTS dic.products_file ON CLUSTER clickstream_cluster
|
||||
(
|
||||
sku String,
|
||||
name String,
|
||||
category String,
|
||||
brand String,
|
||||
price Int64,
|
||||
demand String
|
||||
)
|
||||
ENGINE = File(CSVWithNames, './catalog/products.csv');
|
||||
|
||||
-- Пользователь dict объявлен файлом настройки и умеет одно — читать dic.
|
||||
-- Назвать его обязательно: без user словарь идёт как default с пустым паролем
|
||||
-- и падает с AUTHENTICATION_FAILED. Хост локальный, поэтому запрос к подложке
|
||||
-- идёт без сети.
|
||||
--
|
||||
-- Окно обновления вместо LIFETIME(0): словарь перезагружается сам в случайный
|
||||
-- момент внутри окна. Случайность разводит обращения разных серверов к
|
||||
-- источнику, и цена у неё заявленная — ноды обновляются вразнобой, до 30
|
||||
-- секунд одна видит правку каталога, а вторая ещё нет.
|
||||
--
|
||||
-- Ключ строковый, поэтому COMPLEX_KEY_HASHED: числовой FLAT здесь неприменим.
|
||||
--
|
||||
-- Цена приводится к деньгам прямо в источнике — оттого форма query, а не
|
||||
-- table: словарь нормализует на входе, а не зеркалит подложку. В файле лежат
|
||||
-- целые копейки, наружу словарь отдаёт Decimal(18, 2), как заказы бэкенда.
|
||||
-- Единица в числе становится видна: 129000 против 1290.00.
|
||||
--
|
||||
-- Стык с событием на этом и стоит: в контракте события productPrice — целые
|
||||
-- РУБЛИ, округление формата. Разрыв между ценой каталога и ценой в событии
|
||||
-- намеренный, на нём держится урок про Float64.
|
||||
CREATE DICTIONARY IF NOT EXISTS dic.products ON CLUSTER clickstream_cluster
|
||||
(
|
||||
sku String,
|
||||
name String,
|
||||
category String,
|
||||
brand String,
|
||||
price Decimal(18, 2),
|
||||
demand String
|
||||
)
|
||||
PRIMARY KEY sku
|
||||
SOURCE(CLICKHOUSE(
|
||||
host 'localhost' port 9000 user 'dict'
|
||||
query 'SELECT sku, name, category, brand, toDecimal64(price, 2) / 100 AS price, demand FROM dic.products_file'
|
||||
))
|
||||
LAYOUT(COMPLEX_KEY_HASHED())
|
||||
LIFETIME(MIN 60 MAX 90);
|
||||
@@ -1,16 +0,0 @@
|
||||
-- Общий с генератором каталог товаров. ClickHouse разрешает файловому
|
||||
-- источнику читать только из user_files; Compose монтирует сюда один и тот же
|
||||
-- файл на обе ноды. Цена хранится в копейках, как и в контракте события.
|
||||
CREATE DICTIONARY IF NOT EXISTS dds.products ON CLUSTER clickstream_cluster
|
||||
(
|
||||
sku String,
|
||||
name String,
|
||||
category String,
|
||||
brand String,
|
||||
price Int64,
|
||||
demand String
|
||||
)
|
||||
PRIMARY KEY sku
|
||||
SOURCE(FILE(PATH './user_files/catalog/products.csv' FORMAT 'CSVWithNames'))
|
||||
LAYOUT(COMPLEX_KEY_HASHED())
|
||||
LIFETIME(0);
|
||||
Reference in New Issue
Block a user