chore(схема): canon-sync schema.sql v8.58 — 3 таблицы автоподбора + RLS

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-28 16:09:34 +03:00
parent 23263d18a0
commit 9b4622da85
+100 -2
View File
@@ -1,6 +1,7 @@
-- =============================================================================
-- schema.sql — единая схема БД для SaaS-аналога crm.bp-gr.ru («Лидерра»)
-- Версия: v8.57 (26.06.2026 — RLS GUC hardening: ВСЕ 44 политики tenant_isolation приведены к NULLIF(current_setting('app.current_tenant_id', true), '')::bigint — устранён класс отказов на Managed PG/PgBouncer, когда GUC app.current_tenant_id пуст ('' → 22P02) либо не задан (→ 42704). Инцидент 26.06: вход в портал падал на резолве users (60 ошибок, все на users). 5 bootstrap-таблиц (users, auth_log, email_verifications, user_recovery_codes, user_sessions) дополнительно получили разрешающую ветку «NULLIF(...) IS NULL OR ...» — читаются/пишутся ДО tenant-контекста на auth-роутах без 'tenant' middleware; при ЗАДАННОМ tenant изоляция НЕ меняется (rls-reviewer APPROVE). Структурно БД НЕ меняется — переписаны только USING/WITH CHECK (счётчики таблиц/индексов/RLS=44/функций/триггеров без изменений). Миграция 2026_06_26_153000_rls_nullif_guc_hardening (идемпотентна). Применена на боевой кластер; lead_charges FORCE RLS сохранён.)
-- Версия: v8.58 (28.06.2026 — Автоподбор конкурентов: +3 regular-таблицы autopodbor_runs/competitors/sources, RLS tenant_isolation в харднинг-форме (ENABLE+FORCE+NULLIF), +6 индексов (вкл. 2 UNIQUE dedup: autopodbor_competitor_dedup, autopodbor_source_dedup), +3 RLS-политики. Счётчики: таблиц 79→82 (regular 69→72) / индексов 124→130 / RLS 44→47. Миграции 2026_06_28_100000/100100/100200. Функций/триггеров/партиций без изменений. GRANTs через blanket ON ALL TABLES в db/02_grants.sql — отдельных строк не добавлено.)
-- Базовая версия: v8.57 (26.06.2026 — RLS GUC hardening: ВСЕ 44 политики tenant_isolation приведены к NULLIF(current_setting('app.current_tenant_id', true), '')::bigint — устранён класс отказов на Managed PG/PgBouncer, когда GUC app.current_tenant_id пуст ('' → 22P02) либо не задан (→ 42704). Инцидент 26.06: вход в портал падал на резолве users (60 ошибок, все на users). 5 bootstrap-таблиц (users, auth_log, email_verifications, user_recovery_codes, user_sessions) дополнительно получили разрешающую ветку «NULLIF(...) IS NULL OR ...» — читаются/пишутся ДО tenant-контекста на auth-роутах без 'tenant' middleware; при ЗАДАННОМ tenant изоляция НЕ меняется (rls-reviewer APPROVE). Структурно БД НЕ меняется — переписаны только USING/WITH CHECK (счётчики таблиц/индексов/RLS=44/функций/триггеров без изменений). Миграция 2026_06_26_153000_rls_nullif_guc_hardening (идемпотентна). Применена на боевой кластер; lead_charges FORCE RLS сохранён.)
-- Базовая версия: v8.56 (26.06.2026 — Путь А, шов C: тело функции audit_block_mutation() пропускает пересчёт hash-цепочки по метке GUC app.audit_rebuild='on' (+ superuser ИЛИ член crm_migrator) ВМЕСТО session_replication_role (superuser-only, недоступен в Yandex Managed PG). Append-only сохранён: без метки UPDATE/DELETE аудита запрещён. Структурно БД НЕ меняется — только тело функции (счётчики таблиц/индексов/RLS/функций/триггеров без изменений, функций по-прежнему 5). Миграция 2026_06_26_140000_audit_block_mutation_guc_rebuild_flag. AuditRebuildChain команда переведена на SET LOCAL app.audit_rebuild в транзакции (Odyssey-safe).)
-- Базовая версия: v8.55 (25.06.2026 — Эпик 5 отчёт заливки: +1 таблица supplier_sync_runs (сводка по вечерней заливке проектов поставщику — групп/синк/ручная/отложено/упало + status; SaaS-level без RLS/tenant_id как supplier_csv_reconcile_log, пишет crm_supplier_worker BYPASSRLS, читает SaaS-admin) + 1 явный индекс idx_supplier_sync_runs_created. Миграция 2026_06_25_130000. Структурно +1 regular-таблица + 1 индекс. NB: сводные счётчики несут известный дрейф рантайм-счётчика (ср. сверка 23.06) — точная пересверка отдельным canon-sync. RLS/функций/триггеров без изменений.)
-- Базовая версия: v8.54 (25.06.2026 — Эпик 4 online-defer: +1 таблица supplier_deferred_sync (системная очередь отложенных онлайн-правок в окне 18:00→00:00 МСК, project_id PK → projects ON DELETE CASCADE, без RLS/tenant_id — доступ только crm_supplier_worker BYPASSRLS, покрыт blanket-грантом ON ALL TABLES в db/02_grants.sql как supplier_manual_sync_queue). Миграция 2026_06_25_120000. Структурно +1 regular-таблица; явных CREATE INDEX +0 (PK неявный). NB: сводные счётчики таблиц/индексов несут известный дрейф рантайм-счётчика (ср. сверка 23.06 RLS 42→44) — точная пересверка отдельным canon-sync. RLS/функций/триггеров без изменений.)
@@ -29,8 +30,9 @@
-- Базовая версия: v8.29 (22.05.2026 — webhook_log: supplier audit columns)
-- Базовая версия: v8.28 (22.05.2026 — tenant_operations_log: журнал тенант-уровневых операций вне сделок (проекты, API-ключи, webhook URL), append-only hash-chain, P2 operational journaling closure)
-- Базовая версия: v8.27 (21.05.2026 — drop projects.archived_at: feature архива заменена настоящим удалением с защитой по сделкам (ProjectService::delete()))
-- Метрики: 79 базовых таблиц (69 regular + 10 partitioned parents: deals + supplier_lead_costs + 6 audit + lead_region_resolution_log + project_routing_snapshots) + 16 партиций / 124 индекса (+1 явный: idx_supplier_sync_runs_created) / 44 RLS-политик / 5 функций / 15 триггеров
-- Метрики: 82 базовых таблицы (72 regular + 10 partitioned parents: deals + supplier_lead_costs + 6 audit + lead_region_resolution_log + project_routing_snapshots) + 16 партиций / 130 индексов (+6 autopodbor: 4 обычных + 2 UNIQUE dedup) / 47 RLS-политик / 5 функций / 15 триггеров
-- NB (23.06.2026 сверка-чистка перед запуском): RLS-политик исправлено 42→44 — реальное число активных CREATE POLICY в теле == 44 == на боевом liderra.ru (pg_policies). Расхождение было исторический недоучёт в бегущем счётчике шапки (tenant_requisites v8.43 и др. добавлены в тело без правки сводной метрики). Структурно БД не менялась — только корректность числа.
-- NB (28.06.2026): индексов 124→130 (+6 autopodbor). Счётчик шапки (124) соответствует заявленному в v8.57; реальное число CREATE INDEX/CREATE UNIQUE INDEX в теле — 127 (128 строк минус 1 закомментированная call_recordings). Дрейф +3 — исторический недоучёт дельта-миграций (аналогично дрейфу RLS 42→44 от 23.06). Цифра 130 в шапке = 124+6 (канон шапки + добавленные autopodbor); пересверка всего тела — отдельный canon-sync.
-- Базовая версия: v8.25 (19.05.2026 — supplier_manual_sync_queue: SaaS-level Tier 3 очередь резерва канала миграции проектов)
-- Базовая версия: v8.24 (18.05.2026 — supplier_leads.vid → nullable для CSV-recovered лидов (Путь 2))
-- Базовая версия: v8.20 (11.05.2026 — Plan 5 frontend projects UI: projects.archived_at TIMESTAMPTZ NULL для soft archive flow; tenants.limits JSONB NOT NULL DEFAULT '{}' для per-tenant project/user лимитов)
@@ -1346,6 +1348,102 @@ CREATE TABLE supplier_deferred_sync (
);
-- -----------------------------------------------------------------------------
-- autopodbor_runs — платный прогон агента автоподбора конкурентов (v8.58)
-- -----------------------------------------------------------------------------
-- Три вида прогонов: search (поиск конкурентов), study (изучение сайта/звонков),
-- resolve (разрешение неопределённости). Статусы: queued|running|done|empty|failed.
-- tenant-isolated. GRANTs — через blanket ON ALL TABLES в db/02_grants.sql.
-- Миграция 2026_06_28_100000.
-- -----------------------------------------------------------------------------
CREATE TABLE autopodbor_runs (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
kind VARCHAR(16) NOT NULL, -- search | study | resolve
status VARCHAR(16) NOT NULL DEFAULT 'queued', -- queued|running|done|empty|failed
region_code SMALLINT,
params JSONB NOT NULL DEFAULT '{}'::jsonb,
competitor_id BIGINT, -- ссылка на autopodbor_competitors (без FK — до создания таблицы)
price_rub_charged NUMERIC(12, 2),
balance_transaction_id BIGINT,
error_code VARCHAR(64),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ
);
CREATE INDEX ON autopodbor_runs (tenant_id, status);
CREATE INDEX ON autopodbor_runs (tenant_id, kind, status);
ALTER TABLE autopodbor_runs ENABLE ROW LEVEL SECURITY;
ALTER TABLE autopodbor_runs FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON autopodbor_runs
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::bigint);
-- -----------------------------------------------------------------------------
-- autopodbor_competitors — конкурент, найденный агентом (v8.58)
-- -----------------------------------------------------------------------------
-- Каждый конкурент привязан к прогону search (search_run_id, SET NULL при удалении).
-- Дедупликация по dedup_key внутри (tenant_id, search_run_id).
-- NB: search_run_id может быть NULL (конкурент добавлен вручную) — postgres допускает
-- несколько NULL в UNIQUE (деды «своих» обеспечивается кодом AutopodborDedup).
-- Миграция 2026_06_28_100100.
-- -----------------------------------------------------------------------------
CREATE TABLE autopodbor_competitors (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
search_run_id BIGINT REFERENCES autopodbor_runs(id) ON DELETE SET NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
is_federal BOOLEAN NOT NULL DEFAULT false,
relevance_pct SMALLINT,
origin VARCHAR(16) NOT NULL DEFAULT 'auto', -- auto|manual|resolve
site_url VARCHAR(255),
directory_urls JSONB NOT NULL DEFAULT '[]'::jsonb,
provenance JSONB NOT NULL DEFAULT '{}'::jsonb,
dedup_key VARCHAR(255) NOT NULL,
study_run_id BIGINT REFERENCES autopodbor_runs(id) ON DELETE SET NULL,
studied_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON autopodbor_competitors (tenant_id, search_run_id);
CREATE UNIQUE INDEX autopodbor_competitor_dedup ON autopodbor_competitors (tenant_id, search_run_id, dedup_key);
ALTER TABLE autopodbor_competitors ENABLE ROW LEVEL SECURITY;
ALTER TABLE autopodbor_competitors FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON autopodbor_competitors
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::bigint);
-- -----------------------------------------------------------------------------
-- autopodbor_sources — источники (сайт/звонковый номер) конкурента (v8.58)
-- -----------------------------------------------------------------------------
-- Каждый источник привязан к конкуренту (competitor_id CASCADE) и прогону study
-- (study_run_id CASCADE). created_project_id: если по источнику создан проект —
-- ссылка на него (SET NULL при удалении проекта).
-- Дедупликация по dedup_key внутри конкурента.
-- Миграция 2026_06_28_100200.
-- -----------------------------------------------------------------------------
CREATE TABLE autopodbor_sources (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
competitor_id BIGINT NOT NULL REFERENCES autopodbor_competitors(id) ON DELETE CASCADE,
study_run_id BIGINT NOT NULL REFERENCES autopodbor_runs(id) ON DELETE CASCADE,
signal_type VARCHAR(8) NOT NULL, -- site | call
identifier VARCHAR(255) NOT NULL, -- голова домена / 7xxxxxxxxxx
phone_kind VARCHAR(12), -- real | substitute | NULL (для site)
provenance_url VARCHAR(500),
provenance_label VARCHAR(255),
dedup_key VARCHAR(255) NOT NULL,
created_project_id BIGINT REFERENCES projects(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX autopodbor_source_dedup ON autopodbor_sources (competitor_id, dedup_key);
CREATE INDEX ON autopodbor_sources (tenant_id, competitor_id);
ALTER TABLE autopodbor_sources ENABLE ROW LEVEL SECURITY;
ALTER TABLE autopodbor_sources FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON autopodbor_sources
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::bigint);
-- -----------------------------------------------------------------------------
-- project_suppliers — m2m связь "проект ↔ поставщики" (НОВАЯ в v8.2)
-- -----------------------------------------------------------------------------