From efc965a257b55cbdd6769d647432417e0162260d Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 20 Aug 2026 23:31:46 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix(db/ci):=20=D1=87=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=81=D1=82=D0=B0=D1=80=D1=82=20=D0=91=D0=94=20?= =?UTF-8?q?=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=B5=D1=82=20=D0=BD=D0=B0=20077=20=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=BC=D0=BE=D0=B6=D0=B5=D1=82=20=D1=83=D0=B5?= =?UTF-8?q?=D1=85=D0=B0=D1=82=D1=8C=20=D0=BD=D0=B0=20=D0=BF=D1=83=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=B9=20=D1=81=D1=85=D0=B5=D0=BC=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Блокер переезда (#2990). Чистый старт на пустом томе падал: 077 читает foreign table gendesign_rosreestr_deals, а USER MAPPING создаёт бэкенд при старте (app/core/fdw.py), то есть ПОСЛЕ docker-entrypoint-initdb.d. Контейнер не поднимался вообще. Путь «пустой том» на реальном железе не исполнялся ни разу, а CI этот файл явно пропускал — гейт, который должен был поймать, был ослаблен. Проверено по всем 14 миграциям, упоминающим FDW-таблицы: читает ровно одна — 077. Остальные только CREATE/DROP FOREIGN TABLE и COMMENT, им ни USER MAPPING, ни связь с чужой БД не нужны. 077 не удалена, а сделана самозащитной: гард считает строки в md5-форме и выходит раньше обращения к FDW, если мигрировать нечего. На чистой БД таких строк нет по определению. Удаление файла было бы неверным — прод помнит миграции по bare-filename в _schema_migrations, и test_applied_migration_is_not_renamed_or_deleted падает на удалении. Исключение в ci-tradein.yml снято: теперь цепочка применяется целиком, то есть CI сам стал репетицией чистого старта. Отдельно закрыт тихий отказ в deploy-tradein.yml. Ветка baseline срабатывала по одному лишь отсутствию _schema_migrations, а это состояние неоднозначно: так выглядит и наполненный прод до внедрения tracking, и пустая БД нового сервера. Во втором случае baseline пометил бы все миграции применёнными, ни одной не прогнав, и деплой уехал бы зелёным на пустой схеме. Добавлен sentinel по listings: пусто → baseline пропускается, цепочка применяется с нуля. Refs #2990, #2989 --- .forgejo/workflows/ci-tradein.yml | 13 ++- .forgejo/workflows/deploy-tradein.yml | 29 +++++-- .../sql/077_dedup_hash_plain_key_backfill.sql | 81 +++++++++++++------ 3 files changed, 86 insertions(+), 37 deletions(-) diff --git a/.forgejo/workflows/ci-tradein.yml b/.forgejo/workflows/ci-tradein.yml index da65bb17..d5764313 100644 --- a/.forgejo/workflows/ci-tradein.yml +++ b/.forgejo/workflows/ci-tradein.yml @@ -179,16 +179,13 @@ jobs: "CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE ROLE gendesign_reader;" + # Исключений НЕТ (#2990). Раньше здесь пропускалась 077 — единственная + # миграция, читающая foreign table через postgres_fdw, которой в CI нет. + # Пропуск означал, что гейт не проверял ровно тот файл, который потом + # ронял чистый старт на реальном железе. Теперь 077 сама выходит раньше + # обращения к FDW, если мигрировать нечего, и в CI проходит честно. for sql_file in $(ls -1 tradein-mvp/backend/data/sql/*.sql | sort); do fname=$(basename "$sql_file") - # ЕДИНСТВЕННОЕ исключение, и оно названо вслух: 077 — не DDL, а - # backfill, читающий foreign table gendesign_rosreestr_deals из БД - # ДРУГОГО стека через postgres_fdw. В CI второй БД нет, USER MAPPING - # создать не из чего. На пустых таблицах backfill всё равно no-op. - if [ "$fname" = "077_dedup_hash_plain_key_backfill.sql" ]; then - echo "⚠ пропускаю $fname — postgres_fdw к БД gendesign, которой в CI нет" - continue - fi docker exec -i "$CI_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -q < "$sql_file" \ || { echo "::error::миграция $fname не применилась"; docker logs --tail 20 "$CI_PG" 2>&1 || true; exit 1; } done diff --git a/.forgejo/workflows/deploy-tradein.yml b/.forgejo/workflows/deploy-tradein.yml index 76d332bf..39fd12e5 100644 --- a/.forgejo/workflows/deploy-tradein.yml +++ b/.forgejo/workflows/deploy-tradein.yml @@ -748,6 +748,19 @@ jobs: psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \ "SELECT to_regclass('public._schema_migrations') IS NOT NULL;" | tr -d '[:space:]') + # Sentinel (#2990): отсутствия _schema_migrations НЕДОСТАТОЧНО, чтобы + # заключить «схема уже накачена». Ровно два разных состояния дают одно + # и то же отсутствие таблицы: + # 1) наполненный прод до внедрения tracking → baseline корректен; + # 2) ПУСТАЯ БД на новом сервере → baseline пометил бы все миграции + # применёнными, ни одной не прогнав, и деплой уехал бы зелёным + # на пустой схеме. Отказ тихий и обнаружился бы уже под нагрузкой. + # Различаем по живой таблице listings: она есть только если схема реально + # применялась (initdb или предыдущим циклом миграций). + schema_already_present=$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ + psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \ + "SELECT to_regclass('public.listings') IS NOT NULL;" | tr -d '[:space:]') + docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -v ON_ERROR_STOP=on -c " CREATE TABLE IF NOT EXISTS _schema_migrations ( @@ -756,12 +769,18 @@ jobs: ); " - if [ "$migrations_table_existed" != "t" ]; then - # BASELINE: таблицы не было → seed ВСЕ текущие миграции как applied - # БЕЗ их прогона. prod уже работает на этой схеме; помечаем её - # текущим состоянием, чтобы под строгий gate попадали только НОВЫЕ + if [ "$migrations_table_existed" != "t" ] && [ "$schema_already_present" != "t" ]; then + # ПУСТАЯ БД: baseline пропускаем намеренно. Цикл ниже применит всю + # цепочку с нуля под ON_ERROR_STOP — это и есть штатный путь чистого + # старта на новом сервере. + echo "→ БД пуста (нет ни _schema_migrations, ни listings) — baseline ПРОПУЩЕН," + echo " вся цепочка миграций будет применена циклом ниже." + elif [ "$migrations_table_existed" != "t" ]; then + # BASELINE: таблицы не было, но схема есть → seed ВСЕ текущие миграции + # как applied БЕЗ их прогона. prod уже работает на этой схеме; помечаем + # её текущим состоянием, чтобы под строгий gate попадали только НОВЫЕ # (077+) миграции. INSERT ... ON CONFLICT DO NOTHING — идемпотентно. - echo "→ _schema_migrations отсутствовала — baseline существующих миграций (без прогона)" + echo "→ _schema_migrations отсутствовала, но схема на месте — baseline существующих миграций (без прогона)" for sql_file in $(ls -1 backend/data/sql/*.sql 2>/dev/null | sort); do fname=$(basename "$sql_file") echo " baseline: $fname" diff --git a/tradein-mvp/backend/data/sql/077_dedup_hash_plain_key_backfill.sql b/tradein-mvp/backend/data/sql/077_dedup_hash_plain_key_backfill.sql index 16f839cf..b64fac43 100644 --- a/tradein-mvp/backend/data/sql/077_dedup_hash_plain_key_backfill.sql +++ b/tradein-mvp/backend/data/sql/077_dedup_hash_plain_key_backfill.sql @@ -13,10 +13,23 @@ -- не могут совпасть с md5-hex (разный формат), поэтому транзиентного UNIQUE-violation -- по deals_dedup_hash_key во время UPDATE не возникает. -- --- FDW dependency: читает foreign table gendesign_rosreestr_deals (SERVER gendesign_remote, --- создан в 060_postgres_fdw_extension.sql; foreign table — в 072_..._cian_rosreestr.sql; --- USER MAPPING — backend startup core/fdw.py). Деплой применяет миграции ПОСЛЕ `compose up -d`, --- т.е. FDW-сервер уже поднят и foreign table читаема. +-- ── Гард чистого старта (#2990) ─────────────────────────────────────────────── +-- Это ЕДИНСТВЕННАЯ миграция во всей цепочке, которая ЧИТАЕТ foreign table +-- (проверено 2026-08-20 по всем 14 файлам, упоминающим FDW-таблицы: остальные — +-- только CREATE/DROP FOREIGN TABLE и COMMENT, им ни USER MAPPING, ни связь не нужны). +-- +-- А USER MAPPING создаёт бэкенд при старте (app/core/fdw.py), то есть ПОСЛЕ initdb. +-- На чистом томе (новый сервер, CI, локальная разработка) FDW ещё не отображён и +-- SELECT падал бы с "user mapping not found", обрывая docker-entrypoint-initdb.d +-- и не давая контейнеру подняться вообще. +-- +-- Поэтому backfill выполняется ТОЛЬКО если есть что мигрировать. На чистой БД строк +-- в md5-форме нет по определению (их создавал исторический импорт), гард выходит +-- раньше обращения к FDW, и миграция становится честным no-op вместо падения. +-- +-- Файл НЕ удалён намеренно: прод помнит его по bare-filename в _schema_migrations, +-- а tests/test_migration_numbering.py::test_applied_migration_is_not_renamed_or_deleted +-- падает на удалении применённой миграции. -- -- Фильтр src CTE — байт-в-байт совпадает с live-импортом (scheduler.py:437-444 / -- import-rosreestr.sh), чтобы каждая импортированная сделка нашлась и сконвертировалась. @@ -24,25 +37,45 @@ BEGIN; -WITH src AS ( - SELECT - id, - md5('ros:dkp:' || CAST(id AS text)) AS h - FROM gendesign_rosreestr_deals - WHERE region_code = 66 - AND city ILIKE '%катеринбург%' - AND realestate_type_code = '002001003000' - AND area BETWEEN 18 AND 200 - AND deal_price BETWEEN 1000000 AND 100000000 - AND street IS NOT NULL AND trim(street) <> '' - AND doc_type = 'ДКП' - AND period_start_date >= DATE '2024-01-01' -) -UPDATE deals d -SET dedup_hash = 'ros:dkp:' || CAST(src.id AS text), - source_id = CAST(src.id AS text) -FROM src -WHERE d.source = 'rosreestr' - AND d.dedup_hash = src.h; +DO $migration_077$ +DECLARE + pending bigint; +BEGIN + -- Гард: считаем строки, оставшиеся в md5-форме. Плоский ключ 'ros:dkp:N' + -- под этот шаблон не подходит, поэтому после успешного прогона pending = 0. + SELECT count(*) INTO pending + FROM deals + WHERE source = 'rosreestr' + AND dedup_hash ~ '^[0-9a-f]{32}$'; + + IF pending = 0 THEN + RAISE NOTICE '077: строк в md5-форме нет — backfill пропущен, FDW не читается'; + RETURN; + END IF; + + RAISE NOTICE '077: строк к конвертации: %', pending; + + WITH src AS ( + SELECT + id, + md5('ros:dkp:' || CAST(id AS text)) AS h + FROM gendesign_rosreestr_deals + WHERE region_code = 66 + AND city ILIKE '%катеринбург%' + AND realestate_type_code = '002001003000' + AND area BETWEEN 18 AND 200 + AND deal_price BETWEEN 1000000 AND 100000000 + AND street IS NOT NULL AND trim(street) <> '' + AND doc_type = 'ДКП' + AND period_start_date >= DATE '2024-01-01' + ) + UPDATE deals d + SET dedup_hash = 'ros:dkp:' || CAST(src.id AS text), + source_id = CAST(src.id AS text) + FROM src + WHERE d.source = 'rosreestr' + AND d.dedup_hash = src.h; +END +$migration_077$; COMMIT; -- 2.45.3 From a3ccbbd0452adc4ef4c6c43381fba8f9ee49029f Mon Sep 17 00:00:00 2001 From: bot-backend Date: Fri, 21 Aug 2026 15:35:26 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(db/ci):=20077=20=D0=B3=D0=B0=D1=80?= =?UTF-8?q?=D0=B4=D0=B8=D1=82=D1=81=D1=8F=20=D0=BF=D0=BE=20USER=20MAPPING,?= =?UTF-8?q?=20=D0=B4=D0=B5=D0=BF=D0=BB=D0=BE=D0=B9=20=D0=B6=D0=B4=D1=91?= =?UTF-8?q?=D1=82=20=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=20=D0=91=D0=94=20=D0=BF=D0=BE=20TCP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deep-review BLOCK на PR #3011: обе правки чинили заявленный симптом только частично. 077: гард считал pending-строки по source='rosreestr' AND dedup_hash ~ md5-паттерн и пропускал backfill, только если таких строк 0. На чистой БД они есть — 003_seed_deals.sql сеет синтетические сделки с тем же паттерном, значит pending > 0 уже на пустом томе, и миграция всё равно падала на "user mapping not found" (воспроизведено в CI run 8257). Первичный гард теперь проверяет напрямую наличие USER MAPPING для gendesign_remote (идиома из app/core/fdw.py:57-62), счётчик pending оставлен вторым — экономит обращение к FDW, когда мигрировать уже нечего. deploy-tradein.yml: цикл ожидания готовности postgres ходил по unix-сокету (pg_isready без -h). На пустом томе временный init-сервер отвечает на сокете, пока docker-entrypoint-initdb.d ещё прогоняет цепочку миграций — проба зеленела посреди initdb. Добавлен -h 127.0.0.1 (тот же приём уже есть в ci-tradein.yml:157) — TCP открывается только после полного завершения initdb.d. Отдельно ужесточён sentinel baseline-детекции: раньше «схема уже накачена» проверялась одной таблицей listings (миграция 002, почти голова цепочки). Если бы гонка готовности когда-нибудь вернулась, listings был бы уже создан, а хвост цепочки — ещё нет, и baseline тихо пометил бы недостающие миграции применёнными без прогона. Теперь проверяются оба конца — listings (голова) и houses_geog_gist_idx, индекс из миграции 270 (хвост); при несовпадении (ровно один конец на месте) деплой падает громко с explicit ошибкой вместо угадывания. --- .forgejo/workflows/deploy-tradein.yml | 86 +++++++++++++++---- .../sql/077_dedup_hash_plain_key_backfill.sql | 34 ++++++-- 2 files changed, 98 insertions(+), 22 deletions(-) diff --git a/.forgejo/workflows/deploy-tradein.yml b/.forgejo/workflows/deploy-tradein.yml index 39fd12e5..fef908ce 100644 --- a/.forgejo/workflows/deploy-tradein.yml +++ b/.forgejo/workflows/deploy-tradein.yml @@ -713,11 +713,29 @@ jobs: docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --no-deps postgres # (2) Ждём готовности postgres (pg_isready в цикле, НЕ тупой sleep). + # + # `-h 127.0.0.1` ОБЯЗАТЕЛЕН (#2990) — по тому же образцу, что уже в + # ci-tradein.yml:157. На пустом томе образ postgres поднимает + # ВРЕМЕННЫЙ сервер с listen_addresses='' на время прогона + # docker-entrypoint-initdb.d (сюда смонтирован весь + # backend/data/sql/*.sql, см. docker-compose.prod.yml). Этот временный + # сервер отвечает "accepting connections" по unix-сокету уже через + # пару секунд — а pg_isready БЕЗ -h ходит именно по сокету через + # `docker compose exec`. Проба зеленела посреди initdb, до того как + # цепочка миграций реально доехала до конца, и код ниже (детект + # baseline vs пустая БД) видел частично накаченную схему. TCP-порт + # 5432 открывается только когда initdb.d полностью отработал и + # postgres перезапустился как настоящий сервер — проба по 127.0.0.1 + # зеленеет ровно тогда, когда БД реально готова. + # + # 90 попыток × 2с = до 3 минут: на пустом томе postgres прогоняет + # ВСЮ цепочку миграций (270+ файлов) внутри initdb, это медленнее, + # чем ожидание живого сервера на непустом томе (обычный деплой). echo "→ Ожидание готовности postgres..." pg_ready="" - for i in $(seq 1 30); do + for i in $(seq 1 90); do if docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ - pg_isready -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein >/dev/null 2>&1; then + pg_isready -h 127.0.0.1 -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein >/dev/null 2>&1; then pg_ready="yes"; break fi sleep 2 @@ -755,11 +773,27 @@ jobs: # 2) ПУСТАЯ БД на новом сервере → baseline пометил бы все миграции # применёнными, ни одной не прогнав, и деплой уехал бы зелёным # на пустой схеме. Отказ тихий и обнаружился бы уже под нагрузкой. - # Различаем по живой таблице listings: она есть только если схема реально - # применялась (initdb или предыдущим циклом миграций). - schema_already_present=$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ + # + # Раньше различали одной живой таблицей listings — она создаётся + # миграцией 002, то есть почти в САМОМ НАЧАЛЕ цепочки. Этого мало: на + # пустом томе до фикса ожидания готовности (см. выше, -h 127.0.0.1) + # проба зеленела ПОСРЕДИ initdb, когда listings уже создан, а хвост + # цепочки — ещё нет; результат — тихий baseline недокачанной схемы. + # Фикс готовности эту гонку убирает (TCP открывается только после + # полного прохода initdb.d), но сентинел всё равно проверяем по ОБОИМ + # концам цепочки как defense-in-depth: если голова и хвост когда-нибудь + # разъедутся — это тот самый гоночный симптом, и его надо ловить явно, + # а не гадать. + # + # Хвост — houses_geog_gist_idx, индекс из миграции 270 (#2997, самая + # свежая на момент правки #2990). При добавлении новых миграций после + # 270 обнови этот сентинел на объект из новой последней миграции. + schema_head_present=$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \ "SELECT to_regclass('public.listings') IS NOT NULL;" | tr -d '[:space:]') + schema_tail_present=$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ + psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \ + "SELECT to_regclass('public.houses_geog_gist_idx') IS NOT NULL;" | tr -d '[:space:]') docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -v ON_ERROR_STOP=on -c " @@ -769,18 +803,23 @@ jobs: ); " - if [ "$migrations_table_existed" != "t" ] && [ "$schema_already_present" != "t" ]; then - # ПУСТАЯ БД: baseline пропускаем намеренно. Цикл ниже применит всю - # цепочку с нуля под ON_ERROR_STOP — это и есть штатный путь чистого - # старта на новом сервере. - echo "→ БД пуста (нет ни _schema_migrations, ни listings) — baseline ПРОПУЩЕН," + if [ "$migrations_table_existed" != "t" ] && [ "$schema_head_present" != "t" ] && [ "$schema_tail_present" != "t" ]; then + # ПУСТАЯ БД: ни головы, ни хвоста цепочки — baseline пропускаем + # намеренно. Цикл ниже применит всю цепочку с нуля под + # ON_ERROR_STOP — это и есть штатный путь чистого старта на новом + # сервере (initdb.d уже должен был всё применить сам; этот цикл — + # подстраховка на случай, если монтирование почему-то не сработало). + echo "→ БД пуста (нет ни _schema_migrations, ни listings, ни хвоста цепочки) — baseline ПРОПУЩЕН," echo " вся цепочка миграций будет применена циклом ниже." - elif [ "$migrations_table_existed" != "t" ]; then - # BASELINE: таблицы не было, но схема есть → seed ВСЕ текущие миграции - # как applied БЕЗ их прогона. prod уже работает на этой схеме; помечаем - # её текущим состоянием, чтобы под строгий gate попадали только НОВЫЕ - # (077+) миграции. INSERT ... ON CONFLICT DO NOTHING — идемпотентно. - echo "→ _schema_migrations отсутствовала, но схема на месте — baseline существующих миграций (без прогона)" + elif [ "$migrations_table_existed" != "t" ] && [ "$schema_head_present" = "t" ] && [ "$schema_tail_present" = "t" ]; then + # BASELINE: таблицы не было, но и голова, и хвост цепочки на месте → + # seed ВСЕ текущие миграции как applied БЕЗ их прогона. Это либо + # наполненный прод до внедрения tracking, либо чистый старт, где + # initdb.d уже честно доехал до конца сам (ожидание готовности это + # теперь гарантирует). В обоих случаях повторный прогон не нужен — + # помечаем текущим состоянием, чтобы под строгий gate попадали + # только НОВЫЕ миграции. INSERT ... ON CONFLICT DO NOTHING — идемпотентно. + echo "→ _schema_migrations отсутствовала, но схема на месте целиком (голова + хвост) — baseline существующих миграций (без прогона)" for sql_file in $(ls -1 backend/data/sql/*.sql 2>/dev/null | sort); do fname=$(basename "$sql_file") echo " baseline: $fname" @@ -789,6 +828,21 @@ jobs: "INSERT INTO _schema_migrations (filename) VALUES ('$fname') ON CONFLICT DO NOTHING;" done echo "Baseline complete — existing schema marked as applied." + elif [ "$migrations_table_existed" != "t" ]; then + # НЕОДНОЗНАЧНО: ровно один из концов цепочки на месте, второго нет, + # а _schema_migrations отсутствует. Это и есть симптом гонки + # готовности (см. комментарий выше) — молча баселайнить тут нельзя: + # либо схема реально недокачана (baseline пометил бы недостающий + # хвост как применённый без прогона), либо и то и другое пусто, но + # тогда tail-check не должен был сработать. Падаем громко, а не + # гадаем — новый app-код НЕ поднят, старые контейнеры не тронуты. + echo "ERROR: неоднозначное состояние схемы — _schema_migrations нет," + echo " listings присутствует=${schema_head_present}, houses_geog_gist_idx присутствует=${schema_tail_present}." + echo " Похоже на недокачанную схему (гонка готовности postgres) —" + echo " baseline пропущен намеренно, чтобы не пометить недостающие" + echo " миграции применёнными без прогона. Прерываю деплой; нужен" + echo " ручной разбор состояния тома перед повторным запуском." + exit 1 fi for sql_file in $(ls -1 backend/data/sql/*.sql 2>/dev/null | sort); do diff --git a/tradein-mvp/backend/data/sql/077_dedup_hash_plain_key_backfill.sql b/tradein-mvp/backend/data/sql/077_dedup_hash_plain_key_backfill.sql index b64fac43..c655754b 100644 --- a/tradein-mvp/backend/data/sql/077_dedup_hash_plain_key_backfill.sql +++ b/tradein-mvp/backend/data/sql/077_dedup_hash_plain_key_backfill.sql @@ -23,9 +23,12 @@ -- SELECT падал бы с "user mapping not found", обрывая docker-entrypoint-initdb.d -- и не давая контейнеру подняться вообще. -- --- Поэтому backfill выполняется ТОЛЬКО если есть что мигрировать. На чистой БД строк --- в md5-форме нет по определению (их создавал исторический импорт), гард выходит --- раньше обращения к FDW, и миграция становится честным no-op вместо падения. +-- ВАЖНО: гард по количеству pending-строк тут НЕ защищает от FDW-падения — +-- 003_seed_deals.sql на чистой БД сеет синтетические сделки с source='rosreestr' +-- и md5-хэшем в dedup_hash, то есть pending > 0 уже на пустом томе. Первичный +-- гард обязан проверять именно наличие USER MAPPING (pg_user_mappings), а не +-- количество строк. Счётчик pending оставлен вторым, уже после проверки +-- мэппинга — он просто экономит обращение к FDW, когда мигрировать нечего. -- -- Файл НЕ удалён намеренно: прод помнит его по bare-filename в _schema_migrations, -- а tests/test_migration_numbering.py::test_applied_migration_is_not_renamed_or_deleted @@ -41,15 +44,34 @@ DO $migration_077$ DECLARE pending bigint; BEGIN - -- Гард: считаем строки, оставшиеся в md5-форме. Плоский ключ 'ros:dkp:N' - -- под этот шаблон не подходит, поэтому после успешного прогона pending = 0. + -- Гард 1 (первичный, обязателен): USER MAPPING создаётся бэкендом при + -- старте (app/core/fdw.py), то есть ПОСЛЕ docker-entrypoint-initdb.d. На + -- чистом томе его физически ещё не существует, а количество pending-строк + -- тут ни при чём — счётчик ниже не защищает от FDW-падения, потому что + -- сидовые данные (003_seed_deals.sql) генерируют строки с + -- source='rosreestr' и md5-хэшем, то есть pending > 0 уже на пустой БД. + -- Идиома совпадает с проверкой в app/core/fdw.py:57-62. + IF NOT EXISTS ( + SELECT 1 FROM pg_user_mappings + WHERE srvname = 'gendesign_remote' + AND (usename = current_user OR usename IS NULL) + ) THEN + RAISE NOTICE '077: USER MAPPING для gendesign_remote нет — backfill пропущен'; + RETURN; + END IF; + + -- Гард 2 (вторичный): считаем строки, оставшиеся в md5-форме. Плоский + -- ключ 'ros:dkp:N' под этот шаблон не подходит, поэтому после успешного + -- прогона pending = 0. Ранний выход тут — просто чтобы не трогать FDW + -- зря, когда мигрировать нечего (мэппинг уже есть, но backfill уже + -- применён). SELECT count(*) INTO pending FROM deals WHERE source = 'rosreestr' AND dedup_hash ~ '^[0-9a-f]{32}$'; IF pending = 0 THEN - RAISE NOTICE '077: строк в md5-форме нет — backfill пропущен, FDW не читается'; + RAISE NOTICE '077: строк в md5-форме нет — backfill пропущен'; RETURN; END IF; -- 2.45.3