dim_layout_group (ClickHouse dimension)
Реплика-измерение «Группа планировок» в БД dev-stats. JOIN-free справочник для резолва имени группы и для
фильтрации метрик по уровню LayoutGroup (уровень иерархии между проектом и планировкой, ADR-0057). Дом
полей источника — chessboard: LayoutGroupEntity; здесь — только
колонки, нужные dev-stats.
Назначение
dim_layout_group хранит текущий снапшот группы планировок (одна логическая строка на LayoutGroupId),
реплицируемый из pin.chessboard.layout-group-upserted.v1.
Даёт подпись графика (Name — «Студии», «Люкс с видом на море»), порядок отображения (SortOrder) и
родительские id для восхождения по иерархии/фильтрации. Не факт, а стабильное измерение (ReplacingMergeTree).
Table
| Поле | Значение |
|---|---|
| Database | pin_dev_stats |
| Table | dim_layout_group |
| Engine | ReplacingMergeTree(UpsertedAt) |
| Owner service | dev-stats (DevStats.Background пишет, DevStats.Api читает; DDL — DevStats.Migrator) |
| Source events | pin.chessboard.layout-group-upserted.v1 |
| Partition / Order | без партиционирования (сотни строк); ORDER BY (LayoutGroupId) |
| Freshness SLA | ≤ 5 минут от события до видимости |
Стабильное измерение, не append-only факт: последняя версия перезаписывает предыдущую. Онлайн-чтение —
дубль-устойчивое, без FINAL: актуальная строка берётся argMax(col, UpsertedAt) c GROUP BY LayoutGroupId.
Columns
| Column | Type | Nullable | Source (поле LayoutGroupEntity) | Partition/Order | PII |
|---|---|---|---|---|---|
LayoutGroupId | UUID | нет | Id | order (единственный ключ) | нет |
ProjectId | UUID | нет | ProjectId — родитель | none | нет |
DeveloperId | UUID | нет | TenantId (= DeveloperId проекта) — вверх по иерархии | none | нет |
Name | String | нет | Name («Студии», «Люкс с видом на море») | none | нет |
SortOrder | Int32 | нет | SortOrder — порядок отображения групп | none | нет |
IsArchived | UInt8 | нет | IsArchived (soft-delete группы) — статус-эквивалент | none | нет |
SourceVersion | Int64 | нет | Version источника — монотонный guard | none | нет |
UpsertedAt | DateTime64(3) | нет | время события (CreatedAt) | none (версия движка) | нет |
Минимальный набор для фильтрации/подписи. Счётчики (LayoutsCount) и диапазоны площадей (AreaMin/AreaMax)
в dev-stats не нужны — не реплицируются. У группы нет статусного цикла (только активна/архив через
soft-delete), поэтому вместо Status реплицируется флаг IsArchived из snapshot-а события.
Инварианты
- INV-DIM-LG-1 (единственность): одна логическая строка на
LayoutGroupId(единственный компонентORDER BY); merge оставляет строку с максимальнымUpsertedAt. - INV-DIM-LG-2 (родители денормализованы):
ProjectIdиDeveloperIdреплицируются в строку группы для восхождения по иерархии/проверки scope без обращения кdim_project. - INV-DIM-LG-3 (не удаляется): строка живёт, пока существует группа;
IsArchived = 1помечает архив, но не удаляет (3-летние факты должны резолвить подпись/фильтр по уровнюLayoutGroup). - INV-DIM-LG-4 (precedence): актуальность — по
UpsertedAt(max побеждает);SourceVersionмонотонна per-key, служит анти-out-of-order-диагностикой. - INV-DIM-LG-5 (статус-эквивалент): у группы нет статусного цикла — вместо
Statusреплицируется флагIsArchived(активна/архив);SortOrderопределяет порядок отображения групп.
Кросс-сервисный контракт
- Ожидает от
chessboard(layout-group-upserted):Id,ProjectId,TenantId(=DeveloperId),Name,SortOrder,IsArchived,Version;key = LayoutGroupId, at-least-once, атомарная публикация снапшота, additive-only для minor. - Предоставляет downstream (в пределах
dev-stats): JOIN-free подпись (Name— «Студии», «Люкс с видом на море»),SortOrderи атрибуты фильтра метрик по уровнюLayoutGroup. Измерение не на критическом пути обогащения факта: факт несёт денормализованныйLayoutGroupId(изdim_property_object);dim_layout_groupнужен для подписи/фильтра при чтении. - При отсутствии/устаревании: строки ещё нет → метрика уровня
LayoutGroupрезолвится поLayoutGroupId, подпись деградирует до id без ошибки; устаревшая доставка → берётся max(UpsertedAt).
Deduplication
- Dedup key:
LayoutGroupId;ReplacingMergeTree(UpsertedAt)оставляет строку с максимальнымUpsertedAt. - Replay behavior: идемпотентный upsert; до merge — дубль-устойчивое чтение
argMax(…, UpsertedAt) … GROUP BY LayoutGroupId(FINALв online запрещён). - Version guard: событие применяется только при
event.Version > SourceVersion; иначе пропуск.
Partition И TTL
- Partition key: нет — групп сотни.
- Order key:
(LayoutGroupId)— точечный резолв и фильтрWHERE DeveloperId = актор AND ProjectId = … AND LayoutGroupId = …. - TTL/retention: нет — измерение живёт, пока существует группа;
IsArchived = 1помечает архив, строку не удаляет (3-летние факты должны резолвиться).
Consumers И Backfill
| Process | Назначение | Checkpoint |
|---|---|---|
Приём pin.chessboard.layout-group-upserted.v1 (фоновый ingest) | Проверка версии, маппинг snapshot → строка измерения, batch-upsert (≤ 1000 / 5 с) | Kafka consumer offset (commit после вставки батча) |
| Backfill измерений | Повтор топика layout-group-upserted при вводе сервиса (идемпотентно) | Kafka offset / диапазон LayoutGroupId |
Security
- Raw PII не хранится (группа планировок — справочный объект).
- Доступ — роль застройщика, permission
dev-stats.view, scope строго = свойDeveloperId; каждый запрос фильтруетDeveloperId = актор. - ClickHouse не operational source of truth: источник группы —
chessboard.
Acceptance criteria
- Happy upsert. Given
layout-group-upsertedс непустымId, When обработан батч, Then вdim_layout_groupесть строка сName/SortOrder/IsArchivedи родителямиProjectId/DeveloperId; чтениеargMax(…, UpsertedAt) GROUP BY LayoutGroupId(безFINAL) отдаёт значения. - Идемпотентный повтор / устаревшая доставка. Given то же/старое событие доставлено повторно, When обработано,
Then после merge логически одна строка; побеждает max(
UpsertedAt). - Архив не удаляет. Given группа переведена в
IsArchived = 1, When событие обработано, Then строка остаётся, факты 3-летней глубины резолвят подпись. - Деградация подписи. Given строки ещё нет при чтении метрики уровня
LayoutGroup, Then метрика резолвится поLayoutGroupId, подпись показывает id без ошибки. - Scope. Given запрос застройщика, When фильтруется
dim_layout_group, Then только строкиDeveloperId = актор.
Обратные ссылки
Автоссылки: где используется этот документ.
- Консюмеры (1): Consumer: DevStatsLayoutGroupUpsertedConsumer
- dimensions (5): dim_building (ClickHouse dimension), dim_developer (ClickHouse dimension), dim_layout (ClickHouse dimension), dim_project (ClickHouse dimension), dim_property_object (ClickHouse dimension)
- registries (1): Реестр сущностей PIN
Связанные документы
- Источник: chessboard:
LayoutGroupEntity, событие layout-group-upserted. - Соседние измерения: developer.md, project.md, building.md, layout.md, property-object.md.
- Шаблон: clickhouse-fact-template.md; ADR-0057, ADR-0056.