dim_developer (ClickHouse dimension)
Реплика-измерение «Застройщик» в собственной БД dev-stats. JOIN-free справочник для резолва имени/статуса
застройщика при отображении метрик и для фильтрации по верхнему уровню иерархии
Developer → Project → Building → LayoutGroup → Layout → PropertyObject (ADR-0057). Дом полей источника —
chessboard: DeveloperEntity; здесь — только колонки, нужные
dev-stats для фильтрации и подписи.
Назначение
dim_developer хранит текущий снапшот застройщика (по одной логической строке на DeveloperId),
реплицируемый из события pin.chessboard.developer-upserted.v1.
Назначение — JOIN-free-фильтрация и отображение: факты (property_object_view_facts, роллапы) уже несут
денормализованный DeveloperId, а это измерение даёт человекочитаемое Name/LogoFileId для подписей
графиков ЛК застройщика и позволяет ограничить/проверить scope (DeveloperId актора = свой застройщик,
см. Security). В отличие от ClickHouseFact это НЕ append-only факт, а стабильное измерение:
последняя версия перезаписывает предыдущую (ReplacingMergeTree).
TenantId контекста dev-stats = DeveloperId = CompanyEntity.Id (тип Developer, ADR-0052).
Table
| Поле | Значение |
|---|---|
| Database | pin_dev_stats |
| Table | dim_developer |
| Engine | ReplacingMergeTree(UpsertedAt) — по ORDER BY схлопывается до строки с максимальным UpsertedAt |
| Owner service | dev-stats (DevStats.Background пишет через консюмер, DevStats.Api читает; DDL — DevStats.Migrator) |
| Source events | pin.chessboard.developer-upserted.v1 |
| Partition / Order | без партиционирования (сотни строк); ORDER BY (DeveloperId) |
| Freshness SLA | ≤ 5 минут от события до видимости (batch-upsert консюмера) |
Стабильное измерение, не append-only факт: дедуп — на стороне движка. Онлайн-чтение — без FINAL:
последняя версия берётся argMax(col, UpsertedAt) c GROUP BY DeveloperId.
Columns
| Column | Type | Nullable | Source (поле DeveloperEntity) | Partition/Order | PII |
|---|---|---|---|---|---|
DeveloperId | UUID | нет | Id (= CompanyEntity.Id) | order (единственный ключ) | нет |
Name | String | нет | Name | none | нет |
LegalName | Nullable(String) | да | LegalName | none | нет |
Status | LowCardinality(String) | нет | Status — Draft / Published / Hidden | none | нет |
LogoFileId | Nullable(UUID) | да | LogoFileId (внешний storage/CDN) | none | нет |
SourceVersion | Int64 | нет | Version источника — монотонный guard | none | нет |
UpsertedAt | DateTime64(3) | нет | время события (CreatedAt) | none (версия движка) | нет |
Минимальный набор для фильтрации/подписи: идентификатор, имя, публичный статус, лого. Показатели карточки
(Highlights), контакты, FoundedYear в dev-stats не нужны — не реплицируются.
Инварианты
- INV-DIM-DEV-1 (единственность): одна логическая строка на
DeveloperId(единственный компонентORDER BY); при merge остаётся строка с максимальнымUpsertedAt. - INV-DIM-DEV-2 (tenant):
DeveloperId = CompanyEntity.Id(типDeveloper)= TenantIdконтекстаdev-stats(ADR-0052); значение иммутабельно. - INV-DIM-DEV-3 (не удаляется): строка живёт, пока существует застройщик;
Status = Hiddenпомечает публичную скрытость, но строку не удаляет — иначе 3-летние факты не резолвили бы подпись. - INV-DIM-DEV-4 (precedence): актуальность — по
UpsertedAt(max побеждает);SourceVersionмонотонна per-key, служит анти-out-of-order-диагностикой; устаревшая доставка не понижает строку. - INV-DIM-DEV-5 (минимальность/без PII): реплицируется только набор для подписи/фильтра; контакты и операционные поля не хранятся (застройщик — организация, raw-PII нет).
Кросс-сервисный контракт
- Ожидает от
chessboard(developer-upserted):Id,Name,LegalName?,Status,LogoFileId?,Version;key = DeveloperId(порядок per-developer), at-least-once, атомарная публикация снапшота, additive-only для minor. - Предоставляет downstream (в пределах
dev-stats): JOIN-free подпись (Name/LogoFileId) верхнего уровня иерархии и проверку scope. Измерение не на критическом пути обогащения факта — факт несёт денормализованныйDeveloperId;dim_developerнужен лишь для человекочитаемой подписи/фильтра при чтении. - При отсутствии/устаревании: строки ещё нет → метрика резолвится по
DeveloperId(= актор), подпись деградирует до id без ошибки; устаревшая доставка → берётся max(UpsertedAt).
Deduplication
- Dedup key:
DeveloperId(единственный компонентORDER BY). Повторная доставка события даёт строку с тем же ключом;ReplacingMergeTree(UpsertedAt)при merge оставляет строку с максимальнымUpsertedAt. - Replay behavior: идемпотентный upsert. Переигрывание топика не плодит логических дублей после merge;
до merge — дубль-устойчивое чтение
argMax(Name, UpsertedAt) … GROUP BY DeveloperId,FINALв online-запросах запрещён (правило SDK). - Version guard: консюмер применяет событие только при
event.Version > SourceVersionуже известной строки (anti-out-of-order, как вchessboard); при равной/меньшей версии — идемпотентный пропуск.
Partition И TTL
- Partition key: нет — измерение крошечное (застройщиков сотни), партиционирование избыточно.
- Order key:
(DeveloperId)— измерение адресуется точечно по id (резолв подписи, проверка scope). - TTL/retention: нет — измерение живёт, пока существует застройщик.
Status = Hiddenне удаляет строку (архивные факты 3-летней глубины должны резолвиться), а помечает публичную скрытость.
Consumers И Backfill
| Process | Назначение | Checkpoint |
|---|---|---|
Приём pin.chessboard.developer-upserted.v1 (фоновый ingest) | Проверка версии, маппинг snapshot → строка измерения, batch-upsert (≤ 1000 строк / 5 с) | Kafka consumer offset (commit после подтверждённой вставки батча) |
| Backfill измерений | Разовая перезаливка / повтор топика developer-upserted при вводе сервиса (идемпотентно) | Kafka offset / диапазон DeveloperId |
Security
- Raw PII не хранится: застройщик — организация, контакты в измерение не реплицируются.
- Доступ — роль застройщика, permission
dev-stats.view, scope строго = свойDeveloperId; каждый запрос API жёстко фильтруетDeveloperId = актор, кросс-застройщик-чтение измерения невозможно. - ClickHouse не является operational source of truth: источник профиля застройщика —
chessboard.
Acceptance criteria
- Happy upsert. Given
developer-upsertedс непустымId, When консюмер измерений обработал батч, Then вdim_developerесть строка сName/Status/LogoFileIdиUpsertedAt= время события; чтениеargMax(…, UpsertedAt) GROUP BY DeveloperId(безFINAL) отдаёт эти значения. - Идемпотентный повтор / устаревшая доставка. Given то же/старое событие доставлено повторно, When
обработано, Then после merge логически одна строка; побеждает max(
UpsertedAt) (старый снапшот не понижает). - Hidden не удаляет. Given застройщик переведён в
Status = Hidden, When событие обработано, Then строка остаётся, факты 3-летней глубины продолжают резолвить подпись. - Деградация подписи. Given строки ещё нет при чтении метрики, Then метрика резолвится по
DeveloperId, подпись показывает id вместоNameбез ошибки. - Scope. Given запрос застройщика, When читается измерение, Then возвращается только строка
DeveloperId = актор; чужой id →403(fail-closed).
Обратные ссылки
Автоссылки: где используется этот документ.
- Консюмеры (1): Consumer: DevStatsDeveloperUpsertedConsumer
- dimensions (5): dim_building (ClickHouse dimension), dim_layout (ClickHouse dimension), dim_layout_group (ClickHouse dimension), dim_project (ClickHouse dimension), dim_property_object (ClickHouse dimension)
- registries (1): Реестр сущностей PIN
Связанные документы
- Источник: chessboard:
DeveloperEntity, событие developer-upserted. - Соседние измерения: project.md, building.md, layout-group.md, layout.md, property-object.md.
- Шаблон: clickhouse-fact-template.md; ADR-0057, ADR-0056.