gendesign/data/sql/74_dedupe_unified_views.sql
bot-backend f2e6a59c68
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m5s
CI / backend-tests (pull_request) Successful in 17m46s
fix(gendesign): FDW-роль не могла читать районы ЕКБ — витрина сделок МЕРЫ теряла район
foreign table tradein.gendesign_ekb_districts_geom падала с «permission denied for
view ekb_districts_geom». Замер: has_table_privilege('tradein_fdw_reader',
'public.ekb_districts_geom','SELECT') = false, при том что все четыре соседних
объекта того же FDW-сервера (rosreestr_deals, mv_quarter_price_index,
v_tradein_cad_buildings, v_tradein_osm_poi_ekb) грант имеют.

Грант не «потерялся» — его не выдавали никогда: 74_dedupe_unified_views.sql
меняет тип объекта (DROP TABLE + CREATE OR REPLACE VIEW), то есть создаёт его
заново, а строки GRANT рядом не было. Тот же класс, что #2583.

Приёмка на проде после деплоя: тот же has_table_privilege обязан вернуть true,
а SELECT count(*) FROM gendesign_ekb_districts_geom — 8 вместо ошибки.
2026-08-29 18:56:11 +05:00

272 lines
14 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Phase B + D: унификация дублирующихся таблиц через VIEW (UNION ALL).
--
-- Стратегия: НЕ дропаем источники (они продолжают писать своими скраперами/
-- импортёрами), но создаём поверх единые view для удобных запросов из
-- аналитики и UI. Это zero-risk миграция: можно делать в любой момент
-- и откатывать DROP VIEW без потери данных.
--
-- После того как код в backend перейдёт на use views — старые имена таблиц
-- можно будет либо превратить в views поверх objединённой структуры, либо
-- мигрировать данные целиком. Сейчас — promotion path.
--
-- Применено в проде через mcp postgres 2026-05-10.
-- Идемпотентно через CREATE OR REPLACE VIEW.
-- ── 1. ekb_districts: merge geom-таблицы в основную ─────────────────────────
-- ekb_districts (9, dimension с метриками) + ekb_districts_geom (8, polygons)
-- → одна таблица ekb_districts с PostGIS-колонкой geom
-- + view-alias ekb_districts_geom для backward-compat.
ALTER TABLE ekb_districts
ADD COLUMN IF NOT EXISTS geom geometry(MultiPolygon, 4326),
ADD COLUMN IF NOT EXISTS osm_id BIGINT,
ADD COLUMN IF NOT EXISTS geom_fetched_at timestamptz;
UPDATE ekb_districts d
SET geom = g.geom, osm_id = g.osm_id, geom_fetched_at = g.fetched_at
FROM ekb_districts_geom g
WHERE g.district_name = d.district_name;
CREATE INDEX IF NOT EXISTS ekb_districts_geom_idx
ON ekb_districts USING GIST(geom) WHERE geom IS NOT NULL;
DROP TABLE IF EXISTS ekb_districts_geom;
CREATE OR REPLACE VIEW ekb_districts_geom AS
SELECT district_name, geom, osm_id, geom_fetched_at AS fetched_at
FROM ekb_districts WHERE geom IS NOT NULL;
-- ── 2. v_mortgage_unified — 4 источника ипотечных метрик в long-формате ─────
CREATE OR REPLACE VIEW v_mortgage_unified AS
SELECT
'cbr' AS source,
NULL::date AS snapshot_date,
period AS period_label,
region,
NULL::text AS bank_name,
title AS metric_kind,
'total' AS segment,
value,
NULL::numeric AS delta_pct,
series_id AS metric_id
FROM cbr_mortgage_series
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'avg_rate_pct', 'total', total_credit_avg_rate, total_credit_avg_rate_delta, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'avg_rate_pct', 'primary', primary_credit_avg_rate, primary_credit_avg_rate_delta, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'avg_rate_pct', 'secondary', secondary_credit_avg_rate, secondary_credit_avg_rate_delta, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_count', 'total', total_credit_count::numeric, total_credit_count_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_count', 'primary', primary_credit_count::numeric, primary_credit_count_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_count', 'secondary', secondary_credit_count::numeric, secondary_credit_count_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_amount_rub', 'total', total_credit_amount, total_credit_amount_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_amount_rub', 'primary', primary_credit_amount, primary_credit_amount_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_dashboard', snapshot_date, NULL, 'РФ', NULL,
'credit_amount_rub', 'secondary', secondary_credit_amount, secondary_credit_amount_delta_pct, NULL
FROM domrf_mortgage_dashboard
UNION ALL
SELECT 'domrf_details', snapshot_date, NULL, 'РФ', NULL,
'credit_amount_avg_rub', currency, credit_amount_avg, credit_amount_avg_delta_pct, NULL
FROM domrf_mortgage_details
UNION ALL
SELECT 'domrf_details', snapshot_date, NULL, 'РФ', NULL,
'credit_avg_period_months', currency, credit_avg_period, credit_avg_period_delta_pct, NULL
FROM domrf_mortgage_details
UNION ALL
SELECT 'domrf_details', snapshot_date, NULL, 'РФ', NULL,
'credit_debts_amount_rub', currency, credit_debts_amount, credit_debts_amount_delta_pct, NULL
FROM domrf_mortgage_details
UNION ALL
SELECT 'domrf_details', snapshot_date, NULL, 'РФ', NULL,
'overdue_pct', currency, credit_debts_overdue_percent, credit_debts_overdue_percent_delta, NULL
FROM domrf_mortgage_details
UNION ALL
SELECT 'domrf_rates', snapshot_date, NULL, 'РФ', bank_name,
'rate_pct', 'primary', primary_rate, NULL, NULL FROM domrf_mortgage_rates
UNION ALL
SELECT 'domrf_rates', snapshot_date, NULL, 'РФ', bank_name,
'rate_pct', 'secondary', secondary_rate, NULL, NULL FROM domrf_mortgage_rates
UNION ALL
SELECT 'domrf_rates', snapshot_date, NULL, 'РФ', bank_name,
'rate_pct', 'refinance', refinance_rate, NULL, NULL FROM domrf_mortgage_rates;
COMMENT ON VIEW v_mortgage_unified IS
'Long-формат всех 4 источников ипотечных метрик. metric_kind: avg_rate_pct '
'/ credit_count / credit_amount_rub / overdue_pct / etc. segment: primary / '
'secondary / total. ~3385 rows total. Источники не дропаются.';
-- ── 3. v_construction_metrics_unified — 5 launch/commissioning ──────────────
CREATE OR REPLACE VIEW v_construction_metrics_unified AS
SELECT 'planned_commissioning' AS source, snapshot_date, plan_year::text AS period_label,
'РФ' AS region, 'planned_total_th_sqm' AS metric_kind,
NULL::text AS breakdown_kind, NULL::text AS breakdown_value,
total_th_sqm AS value,
jsonb_build_object('escrow', escrow_th_sqm, 'fund', fund_th_sqm,
'no_attraction', no_attraction_th_sqm) AS extra
FROM domrf_planned_commissioning
UNION ALL
SELECT 'commissioning', snapshot_date, rep_year::text, region_name,
'fact_area_total', NULL, NULL, accumulated_fact_area,
jsonb_build_object('multifamily', accumulated_fact_area_multifamily,
'private', accumulated_fact_area_private,
'change', accumulated_fact_area_change,
'change_share', accumulated_fact_area_change_share)
FROM domrf_commissioning
UNION ALL
SELECT 'launch_monthly', snapshot_date,
(rep_year::text || '-' || LPAD(rep_month::text, 2, '0')),
'РФ', 'launch_app_value', 'calc_type', calc_type, app_value,
jsonb_build_object('rnv_value', rnv_value)
FROM domrf_launch_monthly
UNION ALL
SELECT 'launch_monthly', snapshot_date,
(rep_year::text || '-' || LPAD(rep_month::text, 2, '0')),
'РФ', 'launch_rnv_value', 'calc_type', calc_type, rnv_value, NULL
FROM domrf_launch_monthly
UNION ALL
SELECT 'launch_top', snapshot_date, rep_year::text, entity_name,
metric_type, dim_type, entity_id, value,
jsonb_build_object('calc_type', calc_type)
FROM domrf_launch_top
UNION ALL
SELECT 'launch_obj_class', snapshot_date, rep_year::text, 'РФ',
'launch_app_value', 'obj_class', obj_class_desc, app_value,
jsonb_build_object('rnv_value', rnv_value, 'calc_type', calc_type,
'obj_class_cd', obj_class_cd)
FROM domrf_launch_obj_class
UNION ALL
SELECT 'launch_obj_class', snapshot_date, rep_year::text, 'РФ',
'launch_rnv_value', 'obj_class', obj_class_desc, rnv_value,
jsonb_build_object('calc_type', calc_type)
FROM domrf_launch_obj_class;
COMMENT ON VIEW v_construction_metrics_unified IS
'Long-формат для 5 таблиц про launch/commissioning. metric_kind: '
'planned_total_th_sqm / fact_area_total / launch_app_value / launch_rnv_value. '
'breakdown_kind: calc_type / obj_class / dim_type. ~1579 rows.';
-- ── 4. v_sold_ready_unified — 4 sold_ready / sold_out ──────────────────────
CREATE OR REPLACE VIEW v_sold_ready_unified AS
SELECT 'sold_out' AS source, snapshot_date, NULL::text AS period_label,
territory_name AS scope, NULL::text AS breakdown_kind, NULL::text AS breakdown_value,
'sold_pct' AS metric_kind, sold_pct AS value,
jsonb_build_object('total_area_th_sqm', total_area_th_sqm,
'sold_area_th_sqm', sold_area_th_sqm,
'unsold_pct', unsold_pct,
'avg_price_per_sqm', avg_price_per_sqm,
'attracted_funds_mln_rub', attracted_funds_mln_rub) AS extra
FROM domrf_sold_out
UNION ALL
SELECT 'sold_ready_index', snapshot_date,
(rep_year::text || '-' || LPAD(rep_month::text, 2, '0')),
'РФ', NULL, NULL, 'sold_ready_perc', sold_ready_perc,
jsonb_build_object('square_sum', square_sum, 'sold_sum', sold_sum,
'sold_perc', sold_perc, 'ready_perc', ready_perc)
FROM domrf_sold_ready_index
UNION ALL
SELECT 'sold_ready_dynamics', snapshot_date,
(rep_year::text || '-' || LPAD(rep_month::text, 2, '0')),
'РФ', 'dynamic_chart_type', dynamic_chart_type,
dynamic_chart_type, value, NULL
FROM domrf_sold_ready_dynamics
UNION ALL
SELECT 'sold_ready_breakdown', snapshot_date,
(rep_year::text || '-' || LPAD(COALESCE(rep_month, 0)::text, 2, '0')),
COALESCE(NULLIF(dev_group_value, ''), 'РФ'),
chart_type, entity_key, 'sold_perc', sold_perc,
jsonb_build_object('ready_perc', ready_perc, 'sold_ready_perc', sold_ready_perc,
'square_sum', square_sum, 'fo_cd', fo_cd, 'region_cd', region_cd,
'obj_class', obj_class, 'ready_year', ready_year,
'city_population', city_population)
FROM domrf_sold_ready_breakdown;
COMMENT ON VIEW v_sold_ready_unified IS
'Long-формат 4 sold_ready таблиц. metric_kind: sold_pct / sold_ready_perc / '
'<dynamic_chart_type>. Big source: sold_ready_breakdown (~39k) — фильтруй '
'по chart_type/entity_key. Total: ~42858 rows.';
-- ── 5. v_scrape_runs_unified — 3 скрапера в одном журнале ───────────────────
CREATE OR REPLACE VIEW v_scrape_runs_unified AS
SELECT 'kn' AS scraper_type, run_id, started_at, finished_at, heartbeat_at,
status, error, NULL::text AS triggered_by,
region_codes::text AS scope, requests_count,
objects_count AS items_ok, NULL::integer AS items_failed,
flats_count AS sub_items_ok,
progress_obj_index AS progress_idx, total_obj_count AS progress_total,
jsonb_build_object('developers', developer_ids, 'snapshot_date', snapshot_date,
'resumed_from', resumed_from_run_id, 'params', params) AS extra
FROM kn_scrape_runs
UNION ALL
SELECT 'nspd', run_id, started_at, finished_at, heartbeat_at, status, error,
triggered_by, region_code::text, requests_count,
quarters_ok, quarters_failed, buildings_ok,
NULL, pending_count,
jsonb_build_object('waf_429_count', waf_429_count)
FROM nspd_scrape_runs
UNION ALL
SELECT 'objective', run_id, started_at, finished_at, heartbeat_at, status, error,
triggered_by, group_name, requests_count,
reports_ok, reports_failed, rows_lots,
NULL, NULL,
jsonb_build_object('rows_corpus_room', rows_corpus_room, 'rows_history', rows_history)
FROM objective_scrape_runs;
COMMENT ON VIEW v_scrape_runs_unified IS
'Унифицированный журнал прогонов для всех 3 скраперов. Полиморфные общие '
'поля (items_ok / sub_items_ok / scope) — для UI; специфичные — в extra '
'jsonb. Используй для общего admin /scrape dashboard вместо 3 отдельных endpoint-ов.';
-- ── 6. v_scrape_log_unified — kn + nspd логи ────────────────────────────────
CREATE OR REPLACE VIEW v_scrape_log_unified AS
SELECT 'kn' AS scraper_type, log_id, run_id, ts, level, stage,
obj_id::text AS entity_id, message
FROM kn_scrape_log
UNION ALL
SELECT 'nspd', log_id, run_id, ts, level, stage, cad_number, message
FROM nspd_scrape_log;
COMMENT ON VIEW v_scrape_log_unified IS
'Per-step log двух скраперов. entity_id = obj_id для kn / cad_number для nspd. '
'objective scraper пока не пишет log — добавим если понадобится debug.';
-- ── ГРАНТ ЖИВЁТ ЗДЕСЬ, А НЕ ОТДЕЛЬНО (29.08.2026) ──────────────────────────
-- Выше `DROP TABLE ekb_districts_geom` + `CREATE OR REPLACE VIEW` меняет тип
-- объекта, то есть создаёт его заново — а вместе с объектом исчезают и его
-- гранты. Из-за этого tradein-сторона (foreign table
-- gendesign_ekb_districts_geom, роль tradein_fdw_reader) читала вьюху с
-- `permission denied` и витрина сделок МЕРЫ теряла район.
-- Тот же урок, что в 188_regrant_quarter_price_index_fdw.sql: грант, лежащий в
-- отдельной миграции, переживает ровно до следующего пересоздания объекта.
-- Держим его рядом с CREATE, чтобы объект и его права ехали вместе.
--
-- ЧЕСТНО О ДЕЙСТВИИ: на ДЕЙСТВУЮЩЕМ проде эта строка не выполнится никогда —
-- деплой применяет только файлы, которых ещё нет в _schema_migrations, а 74-я
-- давно записана. Работу делает 194_grant_ekb_districts_fdw.sql. Здесь строка
-- нужна для чистой базы и для случая, когда файл прогоняют руками.
GRANT SELECT ON public.ekb_districts_geom TO tradein_fdw_reader;