`UNIQUE (doc_group, doc_num)` вводился, чтобы склеивать ОДИН документ,
пришедший из двух схем портала. Замер 20.08.2026 показал, что задача,
ради которой ключ введён, почти отсутствует, а побочный эффект огромен:
docNum у ГИСОГД НЕ уникален — разрешение и изменения к нему носят один
номер.
группа документов различных key различных docNum схлопывается
DocRS 6098 6096 4305 1793
DocRV 5419 5415 4969 450
DocIZ 548 547 393 155
общих docNum между схемами (DocRS): 2 ← ради этого ключ и вводился
общих key между схемами (DocRS): 2 ← те же два
На проде 9182 строки против 12 065 документов на портале — нет 23.9 %
реестра. Пример 66-06-06-2026: портал отдаёт два документа (key …719586 —
само разрешение, key …752293 — изменения к нему), а UPSERT с
предпочтением позднего date_reg оставлял только изменение. Так вытеснено
598 из 4320 строк РНС (13.8 %) — в §6 на месте разрешения показывается
изменение к нему, без признака подмены.
Ключ стал `UNIQUE (source_key)`: разделяет разрешение и изменения (разные
key) и по-прежнему склеивает настоящие межсхемные дубли (у них key
ОБЩИЙ — ровно 7 записей по всем группам). Дедуп перед сменой не нужен:
source_key на проде уже уникален (9182 из 9182, NOT NULL).
Заодно группа DocIZ добавлена в GROUP_CODE — её не было вовсе, 548
документов не грузились. CHECK расширен значением 'IZ'.
§6 сужена до РНС/РВЭ ЯВНО: агрегат обещает total_count = rs_count +
rv_count, а строки 'IZ' попадали бы в total и ни в один счётчик.
Показывать ли изменения отдельной строкой — вопрос продуктовый (#2986);
до его решения сужение стоит в запросе, а не держится на том, что таких
строк «пока нет».
Проверки:
- два гейта на лоадер (GROUP_CODE и цель ON CONFLICT) — БЕЗ базы,
двусторонние: на origin/main дают конкретные неверные значения
({'DocRS','DocRV'} и старый ON CONFLICT в тексте запроса);
- гейт на §6 и контроль инварианта total = rs + rv на данных — красные
на origin/main;
- герметичная репетиция миграции на временной копии: со старым ключом
разрешение и изменение схлопываются в одну строку (и остаётся именно
изменение — как на проде), после миграции живут раздельно; межсхемный
дубль по-прежнему склеивается; CHECK принимает 'IZ' и отвергает мусор.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
167 lines
16 KiB
Text
167 lines
16 KiB
Text
# Объявленные пропуски сьюта backend/.
|
||
#
|
||
# Пропуск, которого здесь нет, роняет прогон (хук в tests/conftest.py). Смысл: не
|
||
# запретить пропуски, а запретить НЕЗАМЕТНЫЕ. Проверка, которая тихо перестала
|
||
# исполняться, со временем расходится с кодом, и узнают об этом ровно тогда,
|
||
# когда на неё понадобилось опереться (#2722, #2729, #2740).
|
||
#
|
||
# Формат: один nodeid в строке, без «[параметров]»; `#` — комментарий.
|
||
# Прежде чем добавить строку, ответь: почему эту проверку нельзя выполнить ЗДЕСЬ
|
||
# и где она выполняется вместо этого. Нет ответа — это не пропуск, а дыра.
|
||
#
|
||
# Список — НАДмножество сред: на CI (ubuntu + libpango + postgres-сервис) часть
|
||
# записей не срабатывает, на ноутбуке без них — срабатывает. Запись, чей пропуск
|
||
# не случился, безвредна; пропуск без записи — нет.
|
||
|
||
# ── WeasyPrint: нужны native-либы (Pango/cairo/GObject) ───────────────────────
|
||
# В CI ЭТИ ТЕСТЫ ИДУТ: ci.yml ставит libcairo2/libpango-1.0-0/libpangoft2-1.0-0.
|
||
# Записи нужны только для macOS-dev, где libgobject-2.0-0 нет.
|
||
tests/api/v1/test_parcels_forecast.py::test_export_forecast_pdf_returns_pdf
|
||
tests/services/exporters/test_report_pdf.py::TestConfidenceSectionParity::test_dict_shaped_factor_produces_valid_pdf
|
||
tests/services/exporters/test_report_pdf.py::TestFullReportExport::test_accepts_as_dict_input
|
||
tests/services/exporters/test_report_pdf.py::TestFullReportExport::test_returns_non_trivial_length
|
||
tests/services/exporters/test_report_pdf.py::TestFullReportExport::test_returns_pdf_magic_bytes
|
||
tests/services/exporters/test_report_pdf.py::TestGracefulPartialReport::test_empty_report_still_valid_pdf
|
||
tests/services/exporters/test_report_pdf.py::TestGracefulPartialReport::test_garbage_input_does_not_crash
|
||
tests/services/exporters/test_report_pdf.py::TestGracefulPartialReport::test_partial_report_some_sections
|
||
tests/services/generative/test_exporters.py::test_pdf_export_produces_pdf_bytes
|
||
tests/test_layout_tz_pdf.py
|
||
|
||
# ── phantom-column gate: нужна КОПИЯ ПРОДОВОЙ схемы ───────────────────────────
|
||
# Смысл этих тестов — поймать колонку, которой нет в реальной БД, поэтому пустой
|
||
# сервис-контейнер их не заменяет: против пустой схемы они дали бы красноту, не
|
||
# относящуюся к делу. Условие: TEST_DATABASE_URL на SSH-туннель к проду
|
||
# (`ssh -N gendesign` → localhost:15432), см. tests/integration/conftest.py.
|
||
# ЗАПУСКАТЬ ВРУЧНУЮ после правок SQL-запросов в app/services/**.
|
||
tests/integration/test_analyze_parcels_sql.py::TestIrdOverlapSql::test_explain_ird_overlap
|
||
tests/integration/test_analyze_parcels_sql.py::TestVelocityCompetitorsSql::test_explain_competitors
|
||
tests/integration/test_analyze_parcels_sql.py::TestNeighborsSummarySql::test_explain_neighbors_summary
|
||
tests/integration/test_phantom_columns.py::TestCadGeoTables::test_parcel_centroid_query
|
||
tests/integration/test_phantom_columns.py::TestDomrfKnFlats::test_avg_price_query
|
||
tests/integration/test_phantom_columns.py::TestDomrfKnFlats::test_supply_batch_query
|
||
tests/integration/test_phantom_columns.py::TestDomrfKnObjects::test_competitors_full_cte_query
|
||
tests/integration/test_phantom_columns.py::TestDomrfKnObjects::test_competitors_radius_query
|
||
tests/integration/test_phantom_columns.py::TestDomrfKnObjects::test_velocity_competitor_query_columns
|
||
tests/integration/test_phantom_columns.py::TestEkburgConstructionPermits::test_permits_rns_columns
|
||
tests/integration/test_phantom_columns.py::TestEkburgConstructionPermits::test_permits_rve_columns
|
||
tests/integration/test_phantom_columns.py::TestEkburgConstructionPermits::test_recent_permits_query
|
||
tests/integration/test_phantom_columns.py::TestMvLayoutVelocity::test_velocity_by_room_bucket
|
||
tests/integration/test_phantom_columns.py::TestObjectiveTables::test_objective_corpus_room_month_columns
|
||
tests/integration/test_phantom_columns.py::TestObjectiveTables::test_objective_mapping_columns
|
||
tests/integration/test_phantom_columns.py::TestObjectiveTables::test_sold_count_query
|
||
|
||
# ── SQL-логика на живой Postgres ──────────────────────────────────────────────
|
||
# В CI ЭТИ ТЕСТЫ ИДУТ с #2745 (postgres-сервис в ci.yml); до него не бежали ни
|
||
# разу с момента написания. Схема не нужна — строят себе временные таблицы.
|
||
# Локально: docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=... postgres:16
|
||
tests/sql/test_ddu_price_indicator.py::test_basis_and_previous_index
|
||
tests/sql/test_ddu_price_indicator.py::test_packaged_deal_bucketed_by_per_unit_area
|
||
tests/sql/test_ddu_price_indicator.py::test_prev_period_value_skips_filtered_quarter
|
||
tests/sql/test_mv_layout_velocity_weighted_avg.py::TestWeightedAvgFormula::test_hand_computed_weighted_average
|
||
tests/sql/test_mv_layout_velocity_weighted_avg.py::TestWeightedAvgFormula::test_no_zero_months_weighted_equals_naive
|
||
tests/sql/test_mv_layout_velocity_weighted_avg.py::TestWeightedAvgFormula::test_nullif_prevents_division_by_zero
|
||
tests/sql/test_mv_layout_velocity_weighted_avg.py::TestWeightedAvgFormula::test_weighted_differs_from_naive_sparse_project
|
||
tests/sql/test_mv_layout_velocity_weighted_avg.py::TestWeightedAvgFormula::test_zero_deal_months_skew_naive_avg
|
||
tests/sql/test_velocity_alerts.py::test_lookback_anchors_to_latest_data_month
|
||
tests/sql/test_velocity_alerts.py::test_sharp_drop_is_flagged
|
||
|
||
# ── Живая сеть NSPD (nspd.gov.ru) ─────────────────────────────────────────────
|
||
# Ходят в внешний сервис: в гейте это флейк и зависимость от чужого аптайма.
|
||
# Запуск вручную: uv run pytest tests/scrapers/ -m slow / -m integration -s
|
||
tests/scrapers/test_nspd_bulk_client.py::test_list_objects_in_building_real
|
||
tests/scrapers/test_nspd_bulk_client.py::test_search_by_quarter_empty
|
||
tests/scrapers/test_nspd_bulk_client.py::test_search_by_quarter_real_ekb
|
||
tests/scrapers/test_nspd_bulk_client.py::test_wms_feature_info_real
|
||
tests/scrapers/test_nspd_grid_walk.py::test_live_nspd_grid_walk_skipped
|
||
|
||
# ── Образец, которого нет в git ───────────────────────────────────────────────
|
||
# ДОЛГ, а не норма. Было 8 записей; 2 из них (test_ekb_krt_registry.py::
|
||
# TestMultiSiteCountFromDump) сняты в #2778: дампа `krt_page_raw.html` не
|
||
# оказалось НИГДЕ — ни в репозитории, ни на дисках, ни на проде (в БД лежит
|
||
# `raw_block` = уже очищенный текст, вход парсера из него не восстановить), —
|
||
# зато свойство, которое они проверяли (несколько блоков на странице, уникальные
|
||
# site_name), выразимо без дампа и теперь проверяется на многоблочной странице,
|
||
# собранной из блоков, уже лежащих в том же тест-файле.
|
||
#
|
||
# Оставшиеся 6 так не лечатся: они проверяют извлечение таблиц из НАСТОЯЩЕГО PDF
|
||
# через pdfplumber, соседний класс TestParsePptTep уже мокает pdfplumber целиком.
|
||
# Нужен файл `ppt2018_22823_poyasnit_PPT.pdf` (или рабочий URL на ГИСОГД ЕКБ) —
|
||
# запрошен у Anton в #2778, там же список того, что оживёт. Причин не коммитить
|
||
# нет: официальный градостроительный документ, без персональных данных.
|
||
# После получения — положить в tests/fixtures/, поправить `_REAL_PDF_PATH`,
|
||
# убрать эти шесть строк.
|
||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_phasing_has_area
|
||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_phasing_has_rows
|
||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_raw_tables_not_empty
|
||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_tep_has_rows
|
||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_zone_balance_has_itogo
|
||
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_zone_balance_has_rows
|
||
|
||
# ── #2956: свежесть data-table источника считается по успешным строкам ────────
|
||
# Нужен живой Postgres: тест создаёт ВРЕМЕННЫЕ копии всех таблиц реестра freshness
|
||
# и гоняет по ним настоящий SQL compute_freshness. В CI ЭТИ ТЕСТЫ ИДУТ — postgres-
|
||
# сервис поднят (#2745), как и для соседних tests/sql/*. Записи нужны только для
|
||
# машины без БД и без туннеля на 15432.
|
||
tests/sql/test_2956_freshness_ignores_failed_dumps.py::test_failed_dumps_do_not_refresh_the_clock
|
||
tests/sql/test_2956_freshness_ignores_failed_dumps.py::test_successful_dump_still_counts_as_fresh
|
||
tests/sql/test_2956_freshness_ignores_failed_dumps.py::test_attempt_is_still_recorded
|
||
tests/sql/test_2956_freshness_ignores_failed_dumps.py::test_only_failures_means_no_success_at_all
|
||
|
||
# ── #2464: контракт суффиксов в /admin/leads/stats ────────────────────────────
|
||
# Нужен живой Postgres: тест создаёт ВРЕМЕННЫЕ prinzip_leads/prinzip_deals и
|
||
# вызывает leads_stats на данных, где итог заведомо не равен окну. В CI ЭТИ ТЕСТЫ
|
||
# ИДУТ (postgres-сервис, #2745); записи нужны для машины без БД и без туннеля.
|
||
tests/sql/test_2464_leads_stats_suffix_contract.py::test_window_suffixed_fields_match_the_window
|
||
tests/sql/test_2464_leads_stats_suffix_contract.py::test_total_suffixed_fields_are_all_time
|
||
tests/sql/test_2464_leads_stats_suffix_contract.py::test_revenue_and_deals_are_named_by_their_scope
|
||
tests/sql/test_2464_leads_stats_suffix_contract.py::test_window_months_present_on_empty_data
|
||
# ── #2464: дедуп land_reservation (миграция 189) ──────────────────────────────
|
||
# Нужен живой Postgres: тесты создают ВРЕМЕННУЮ копию таблицы, проверяют семантику
|
||
# UNIQUE NULLS NOT DISTINCT и репетируют миграцию на засеянных дублях. В CI ИДУТ
|
||
# (postgres-сервис, #2745); записи нужны для машины без БД и без туннеля.
|
||
tests/sql/test_2464_land_reservation_dedup.py::test_nulls_not_distinct_deduplicates
|
||
tests/sql/test_2464_land_reservation_dedup.py::test_plain_unique_does_not_deduplicate
|
||
tests/sql/test_2464_land_reservation_dedup.py::test_records_with_act_number_still_deduplicate
|
||
tests/sql/test_2464_land_reservation_dedup.py::test_different_parcels_are_not_collapsed
|
||
tests/sql/test_2464_land_reservation_dedup.py::test_migration_dedup_statement_matches_the_key
|
||
tests/sql/test_2464_land_reservation_dedup.py::test_migration_body_runs_on_a_prod_shaped_replica
|
||
|
||
# ── #2464: единственный дефолтный профиль весов (миграция 190) ────────────────
|
||
# Нужен живой Postgres: тесты создают ВРЕМЕННУЮ user_weight_profiles, применяют
|
||
# миграцию и проверяют, что вторая установка дефолта отвергается базой. В CI ИДУТ
|
||
# (postgres-сервис, #2745); записи нужны для машины без БД и без туннеля.
|
||
tests/sql/test_2464_default_profile_unique.py::test_second_default_is_rejected_by_the_database
|
||
tests/sql/test_2464_default_profile_unique.py::test_without_migration_two_defaults_slip_through
|
||
tests/sql/test_2464_default_profile_unique.py::test_reassigning_default_still_works
|
||
tests/sql/test_2464_default_profile_unique.py::test_different_users_keep_their_own_defaults
|
||
|
||
# ── #2464: корзина площади «н/д» в supply-only (best_layouts) ─────────────────
|
||
# Нужен живой Postgres: тесты создают ВРЕМЕННУЮ таблицу objective_lots, которая
|
||
# затеняет боевую в пределах сессии, и прогоняют НАСТОЯЩИЙ _SUPPLY_ONLY_LOTS_SQL
|
||
# на засеянных лотах. В CI ИДУТ (postgres-сервис, #2745); записи нужны для машины
|
||
# без БД и без туннеля.
|
||
tests/sql/test_2464_area_bin_unknown.py::test_lot_without_area_is_not_counted_as_tiny
|
||
tests/sql/test_2464_area_bin_unknown.py::test_unknown_area_gets_its_own_bucket
|
||
tests/sql/test_2464_area_bin_unknown.py::test_total_lots_preserved
|
||
tests/sql/test_2464_area_bin_unknown.py::test_normal_buckets_unchanged
|
||
# ── #2464: backfill act_date (миграция 191) ──────────────────────────────────
|
||
# Нужен живой Postgres: тесты создают ВРЕМЕННУЮ копию land_reservation в прод-форме
|
||
# (9+2 строки с датой Генплана + контрольные посторонние) и прогоняют ТЕЛО миграции
|
||
# целиком. В CI ИДУТ (postgres-сервис, #2745); записи нужны для машины без БД.
|
||
tests/sql/test_2464_act_date_backfill.py::test_genplan_dates_are_replaced_with_real_ones
|
||
tests/sql/test_2464_act_date_backfill.py::test_no_2004_rows_left_in_target_documents
|
||
tests/sql/test_2464_act_date_backfill.py::test_other_documents_with_same_date_are_untouched
|
||
tests/sql/test_2464_act_date_backfill.py::test_correct_rows_do_not_move
|
||
tests/sql/test_2464_act_date_backfill.py::test_migration_is_idempotent
|
||
tests/sql/test_2464_act_date_backfill.py::test_without_the_migration_rows_stay_wrong
|
||
|
||
# ── #2986: бизнес-ключ gisogd_permits по source_key (миграция 191) ────────────
|
||
# Нужен живой Postgres: тесты создают ВРЕМЕННУЮ копию таблицы в прод-форме (со СТАРЫМ
|
||
# ключом), прогоняют тело миграции и проверяют, что разрешение и изменения к нему
|
||
# живут раздельно, а межсхемный дубль по-прежнему склеивается. Два гейта на лоадер
|
||
# (GROUP_CODE и цель ON CONFLICT) базы НЕ требуют и в этот список НЕ входят.
|
||
# В CI ИДУТ (postgres-сервис, #2745); записи нужны для машины без БД.
|
||
tests/sql/test_2986_permits_source_key.py::test_old_key_collapses_permit_and_its_amendment
|
||
tests/sql/test_2986_permits_source_key.py::test_after_migration_both_documents_survive
|
||
tests/sql/test_2986_permits_source_key.py::test_cross_schema_duplicate_still_merges
|
||
tests/sql/test_2986_permits_source_key.py::test_migration_allows_the_izmeneniya_group
|