Перейти к основному содержимому

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

ПолеЗначение
Databasepin_dev_stats
Tabledim_developer
EngineReplacingMergeTree(UpsertedAt) — по ORDER BY схлопывается до строки с максимальным UpsertedAt
Owner servicedev-stats (DevStats.Background пишет через консюмер, DevStats.Api читает; DDL — DevStats.Migrator)
Source eventspin.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

ColumnTypeNullableSource (поле DeveloperEntity)Partition/OrderPII
DeveloperIdUUIDнетId (= CompanyEntity.Id)order (единственный ключ)нет
NameStringнетNamenoneнет
LegalNameNullable(String)даLegalNamenoneнет
StatusLowCardinality(String)нетStatusDraft / Published / Hiddennoneнет
LogoFileIdNullable(UUID)даLogoFileId (внешний storage/CDN)noneнет
SourceVersionInt64нетVersion источника — монотонный guardnoneнет
UpsertedAtDateTime64(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).

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы