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
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 вместо ошибки.
272 lines
14 KiB
SQL
272 lines
14 KiB
SQL
-- 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;
|