fix(ptica): фильтр класса в velocity ссылался на алиас, которого нет в CTE (#2464-G) #2865

Merged
bot-backend merged 4 commits from fix/2464-velocity-class-filter into main 2026-08-13 17:46:29 +00:00
Collaborator

Что

compute_velocity собирает фильтр класса как
AND COALESCE(o.obj_class, o.obj_class_fallback) = :obj_class и подставляет его
внутрь CTE latest_obj, где FROM domrf_kn_objects — без алиаса. Алиас o
появляется только во внешнем SELECT ... FROM latest_obj o.

Прод-EXPLAIN 13.08:

ERROR:  missing FROM-clause entry for table "o"
LINE 11:        AND COALESCE(o.obj_class, o.obj_class_fallback) = 'ко...

Насколько это горит

Не горит. Ветка мёртвая: единственный вызывающий — analyze_parcel
(parcels.py:3797) — obj_class не передаёт, значит class_filter всегда пустой
и в прод уходит валидный запрос. Так что это не «ломает сейчас», а «сломается
у первого, кто включит».

И сломается тихо: исключение ловит except Exception внутри compute_velocity,
функция возвращает None, вызывающий получает velocity_data = None — блок темпа
продаж просто исчезает из отчёта, оставив одну строку в логе.

Как

  • Убран алиас: AND COALESCE(obj_class, obj_class_fallback) = :obj_class.
    Колонки внутри CTE неквалифицированы, domrf_kn_objects — единственный FROM.
  • SQL вынесен в модульную константу _COMPETITORS_SQL_TMPL с {class_filter}
    ровно затем, чтобы тест мог прогнать EXPLAIN по обеим подстановкам, а не
    только по той, что сегодня исполняется. Шаблон байт-в-байт прежний.

Проверка — реальная, не текстовая

tests/integration/test_analyze_parcels_sql.py делает EXPLAIN против живой прод-схемы.
Прогнал через SSH-туннель с TEST_DATABASE_URL, обе стороны:

со строкой из main (алиас o.)     1 failed, 3 passed
    psycopg.errors.UndefinedTable: missing FROM-clause entry for table "o"
после правки                      4 passed

То есть падает именно новая параметризация with_class_filter, а три контрольных
проверки зелёные с обеих сторон.

⚠️ В CI этот тест скипаетсяrequires_test_db без TEST_DATABASE_URL. Прогон
выше сделан вручную против прод-PG; в пайплайне регрессию поймать нечем, пока
у CI нет базы.

  • pytest tests/test_velocity.py tests/integration/test_analyze_parcels_sql.py — 29 passed, 4 skipped
  • pytest -m integration через туннель — 4 passed
  • ruff check — clean

Побочная находка, оставлена как комментарий

Правка делает ветку работоспособной — и первый, кто ей воспользуется, упрётся
в следующую ловушку: сравнение точное и регистрозависимое, а в проде классы
с заглавной и словарь шире ожидаемого (замер 13.08, region_cd = 66):

Комфорт   870      Премиум  13
(NULL)    504      Элит     12
Типовой   224      Стандарт  9
Бизнес     95      Элитный   4

Менять сравнение не стал — вызывающих нет, и любое «улучшение» было бы догадкой
о намерении. Написал это прямо в коде рядом с фильтром.

Шум в диффе

Один хунк в тесте — не мой: pre-commit пинит ruff v0.7.4, в backend/.venv 0.15.12
(#2864).

Refs #2464

## Что `compute_velocity` собирает фильтр класса как `AND COALESCE(o.obj_class, o.obj_class_fallback) = :obj_class` и подставляет его **внутрь CTE `latest_obj`**, где `FROM domrf_kn_objects` — без алиаса. Алиас `o` появляется только во внешнем `SELECT ... FROM latest_obj o`. Прод-EXPLAIN 13.08: ``` ERROR: missing FROM-clause entry for table "o" LINE 11: AND COALESCE(o.obj_class, o.obj_class_fallback) = 'ко... ``` ## Насколько это горит **Не горит.** Ветка мёртвая: единственный вызывающий — `analyze_parcel` (`parcels.py:3797`) — `obj_class` не передаёт, значит `class_filter` всегда пустой и в прод уходит валидный запрос. Так что это не «ломает сейчас», а «сломается у первого, кто включит». И сломается **тихо**: исключение ловит `except Exception` внутри `compute_velocity`, функция возвращает `None`, вызывающий получает `velocity_data = None` — блок темпа продаж просто исчезает из отчёта, оставив одну строку в логе. ## Как - Убран алиас: `AND COALESCE(obj_class, obj_class_fallback) = :obj_class`. Колонки внутри CTE неквалифицированы, `domrf_kn_objects` — единственный FROM. - SQL вынесен в модульную константу `_COMPETITORS_SQL_TMPL` с `{class_filter}` — ровно затем, чтобы тест мог прогнать EXPLAIN по **обеим** подстановкам, а не только по той, что сегодня исполняется. Шаблон байт-в-байт прежний. ## Проверка — реальная, не текстовая `tests/integration/test_analyze_parcels_sql.py` делает EXPLAIN против живой прод-схемы. Прогнал через SSH-туннель с `TEST_DATABASE_URL`, обе стороны: ``` со строкой из main (алиас o.) 1 failed, 3 passed psycopg.errors.UndefinedTable: missing FROM-clause entry for table "o" после правки 4 passed ``` То есть падает именно новая параметризация `with_class_filter`, а три контрольных проверки зелёные с обеих сторон. ⚠️ **В CI этот тест скипается** — `requires_test_db` без `TEST_DATABASE_URL`. Прогон выше сделан вручную против прод-PG; в пайплайне регрессию поймать нечем, пока у CI нет базы. - [x] `pytest tests/test_velocity.py tests/integration/test_analyze_parcels_sql.py` — 29 passed, 4 skipped - [x] `pytest -m integration` через туннель — 4 passed - [x] `ruff check` — clean ## Побочная находка, оставлена как комментарий Правка делает ветку **работоспособной** — и первый, кто ей воспользуется, упрётся в следующую ловушку: сравнение точное и регистрозависимое, а в проде классы с заглавной и словарь шире ожидаемого (замер 13.08, `region_cd = 66`): ``` Комфорт 870 Премиум 13 (NULL) 504 Элит 12 Типовой 224 Стандарт 9 Бизнес 95 Элитный 4 ``` Менять сравнение не стал — вызывающих нет, и любое «улучшение» было бы догадкой о намерении. Написал это прямо в коде рядом с фильтром. ## Шум в диффе Один хунк в тесте — не мой: pre-commit пинит ruff v0.7.4, в `backend/.venv` 0.15.12 (#2864). Refs #2464
bot-backend added 1 commit 2026-08-13 11:44:37 +00:00
fix(ptica): фильтр класса в velocity ссылался на алиас, которого нет в CTE
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
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 2m18s
CI / backend-tests (pull_request) Failing after 16m26s
e7d112b028
class_filter подставляется ВНУТРЬ latest_obj, где FROM — голый
domrf_kn_objects, а алиас `o` появляется только во внешнем SELECT.
Прод-EXPLAIN 13.08: `missing FROM-clause entry for table "o"`.

Ветка мёртвая — единственный вызывающий (analyze_parcel) obj_class не
передаёт, поэтому в проде это не стреляло. Стрельнуло бы тихо: исключение
глотает except в compute_velocity, функция возвращает None, и блок
velocity просто исчезает из отчёта с одной строкой в логе.

SQL вынесен в модульную константу _COMPETITORS_SQL_TMPL — чтобы
integration-тест мог прогнать EXPLAIN по ОБЕИМ подстановкам, а не только
по той, что сегодня исполняется.

Один хунк в тесте — не мой: pre-commit ruff v0.7.4 против 0.15.12 в venv (#2864).

Refs #2464
bot-backend closed this pull request 2026-08-13 12:19:04 +00:00
bot-backend reopened this pull request 2026-08-13 12:19:09 +00:00
Author
Collaborator

Первый прогон backend-tests покрасился, но не на тестах. Достал лог задания
(actions/runs/7493/jobs/1):

Required test coverage of 65% reached. Total coverage: 74.57%
4646 passed, 28 skipped, 7 warnings in 911.53s (0:15:11)
...
12:01:24  docker exec cmd=[node .../hashfiles/index.js]
12:01:25  Skipping step 'Cache uv packages' due to 'success()'
12:01:31  🏁  Job failed

То есть шаг с pytest прошёл целиком, гейт покрытия пройден, а красным job стал
после него — в одном из двух if: always()-шагов (Coverage summary /
Снести тестовый Postgres), причём ни один из них не напечатал ни строки.

Локально полный сьют на этой же ветке: 4622 passed, 48 skipped.

Что я НЕ утверждаю: что это «просто флак». Причина шага не видна, вывода нет.
Что известно точно — падение не в тестах и не в покрытии. Перезапускаю прогон
(close/reopen), чтобы отличить воспроизводимое от разового. Если покраснеет так же —
заведу отдельную задачу на этот участок пайплайна.

Соседний сигнал: на PR #2868 в то же окно упал openapi-codegen-check (2m20s, на
установке). Проверил отдельно — схема app.openapi() на той ветке байт-в-байт
совпадает с main-эквивалентом, то есть правка тут ни при чём. Три прогона CI и сборка
деплоя одновременно на трёх раннерах одного VPS — похоже на нехватку ресурсов, но это
гипотеза, а не диагноз.

Первый прогон `backend-tests` покрасился, но **не на тестах**. Достал лог задания (`actions/runs/7493/jobs/1`): ``` Required test coverage of 65% reached. Total coverage: 74.57% 4646 passed, 28 skipped, 7 warnings in 911.53s (0:15:11) ... 12:01:24 docker exec cmd=[node .../hashfiles/index.js] 12:01:25 Skipping step 'Cache uv packages' due to 'success()' 12:01:31 🏁 Job failed ``` То есть шаг с pytest прошёл целиком, гейт покрытия пройден, а красным job стал **после** него — в одном из двух `if: always()`-шагов (`Coverage summary` / `Снести тестовый Postgres`), причём ни один из них не напечатал ни строки. Локально полный сьют на этой же ветке: **4622 passed, 48 skipped**. Что я НЕ утверждаю: что это «просто флак». Причина шага не видна, вывода нет. Что известно точно — падение не в тестах и не в покрытии. Перезапускаю прогон (close/reopen), чтобы отличить воспроизводимое от разового. Если покраснеет так же — заведу отдельную задачу на этот участок пайплайна. Соседний сигнал: на PR #2868 в то же окно упал `openapi-codegen-check` (2m20s, на установке). Проверил отдельно — схема `app.openapi()` на той ветке **байт-в-байт** совпадает с main-эквивалентом, то есть правка тут ни при чём. Три прогона CI и сборка деплоя одновременно на трёх раннерах одного VPS — похоже на нехватку ресурсов, но это гипотеза, а не диагноз.
Author
Collaborator

Причина найдена, и это не «флак» и не нехватка ЦПУ, как я предположил выше.

Диск на VPS был заполнен на 94%. Лог упавшего деплоя main:

#15 ERROR: mkdir /var/lib/buildkit/.../fs/app: no space left on device

Один дефицит объясняет все три сегодняшних покраснения: этот backend-tests (упал ПОСЛЕ
успешных тестов, в шаге без вывода), openapi-codegen-check на #2868 и обе сборки деплоя.

Накопитель — buildx-билдеры: каждый прогон CI создаёт новый и не убирает, на хосте их
скопилось 20 штук возрастом до двух месяцев, суммарно ~19 ГБ в _state-томах.
Вычистил кэш в восьми старых (buildctl prune --all, только регенерируемый кэш, ничего
не удалял): 132G → 117G использовано, свободно 13G → 28G. Разбор и план корневой
починки — #2869.

Прогон перезапущен уже на разгруженном хосте.

Причина найдена, и это не «флак» и не нехватка ЦПУ, как я предположил выше. **Диск на VPS был заполнен на 94%.** Лог упавшего деплоя main: ``` #15 ERROR: mkdir /var/lib/buildkit/.../fs/app: no space left on device ``` Один дефицит объясняет все три сегодняшних покраснения: этот `backend-tests` (упал ПОСЛЕ успешных тестов, в шаге без вывода), `openapi-codegen-check` на #2868 и обе сборки деплоя. Накопитель — buildx-билдеры: каждый прогон CI создаёт новый и не убирает, на хосте их скопилось **20 штук** возрастом до двух месяцев, суммарно ~19 ГБ в `_state`-томах. Вычистил кэш в восьми старых (`buildctl prune --all`, только регенерируемый кэш, ничего не удалял): **132G → 117G использовано, свободно 13G → 28G**. Разбор и план корневой починки — #2869. Прогон перезапущен уже на разгруженном хосте.
bot-backend added 1 commit 2026-08-13 12:40:00 +00:00
Merge branch 'main' into fix/2464-velocity-class-filter
Some checks failed
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / backend-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 2m12s
CI / backend-tests (pull_request) Failing after 16m25s
ef6813d670
Author
Collaborator

Поправка к предыдущему комментарию: дефицит диска эту красноту не объясняет.

Перезапуск прошёл уже на разгруженном хосте (28 ГБ свободно) и упал точно так же:

run 7503, vps-runner-2, 12:19-12:35
Required test coverage of 65% reached. Total coverage: 74.57%
4646 passed, 28 skipped, 7 warnings in 893.16s (0:14:53)
   ← девять секунд без единой строки
Skipping step 'Cache uv packages' due to 'success()'
🏁  Job failed

Диск объяснил падение деплоя и openapi-codegen-check — это подтвердилось (после чистки
оба зелёные). А вот про этот job я поспешил и записал его в ту же корзину. Он
воспроизводится.

Что проверил дальше и что этим исключил:

  • Не пропал тест. Счёт 4647→4646 passed и 26→28 skipped смутил, но это объясняется
    базой: ветка отрезана от старого main, до мержа #2863, поэтому в ней нет
    test_price_avg_has_sanity_bounds и есть мои два новых (скипаются без БД).
    Сверил pytest --collect-only на обеих ветках — расхождение ровно эти три теста,
    ничего не исчезло.
  • Не раннер как таковой. Тот же vps-runner-2 в 11:38 отработал зелёным (run 7491).
  • Не шаг с покрытием и не удаление тестового Postgres по логу. Сравнил с зелёным
    прогоном: там после pytest идут Post Cache uv packages / Post Set up Python,
    здесь — сразу Skipping ... due to 'success()', то есть job уже НЕ success к моменту
    post-фазы. Оба шага if: always() в обоих прогонах не печатают ничего, так что по
    логу их различить нельзя — act не выводит баннеры для обычных run:-шагов.

Итог: падение после успешного pytest, в невидимом шаге, воспроизводимо на этой ветке
и не воспроизводится на трёх других за то же окно. Обновил ветку от base (в ней теперь
все три сегодняшних мержа) — если покраснеет и на свежей базе, заведу отдельную задачу
на диагностируемость этого участка: сейчас шаги if: always() молчат, и отличить
«упал шаг покрытия» от «упала уборка контейнера» нечем.

Поправка к предыдущему комментарию: **дефицит диска эту красноту не объясняет.** Перезапуск прошёл уже на разгруженном хосте (28 ГБ свободно) и упал **точно так же**: ``` run 7503, vps-runner-2, 12:19-12:35 Required test coverage of 65% reached. Total coverage: 74.57% 4646 passed, 28 skipped, 7 warnings in 893.16s (0:14:53) ← девять секунд без единой строки Skipping step 'Cache uv packages' due to 'success()' 🏁 Job failed ``` Диск объяснил падение деплоя и `openapi-codegen-check` — это подтвердилось (после чистки оба зелёные). А вот про этот job я поспешил и записал его в ту же корзину. Он **воспроизводится**. Что проверил дальше и что этим исключил: - **Не пропал тест.** Счёт 4647→4646 passed и 26→28 skipped смутил, но это объясняется базой: ветка отрезана от старого main, до мержа #2863, поэтому в ней нет `test_price_avg_has_sanity_bounds` и есть мои два новых (скипаются без БД). Сверил `pytest --collect-only` на обеих ветках — расхождение ровно эти три теста, ничего не исчезло. - **Не раннер как таковой.** Тот же `vps-runner-2` в 11:38 отработал зелёным (run 7491). - **Не шаг с покрытием и не удаление тестового Postgres по логу.** Сравнил с зелёным прогоном: там после pytest идут `Post Cache uv packages` / `Post Set up Python`, здесь — сразу `Skipping ... due to 'success()'`, то есть job уже НЕ success к моменту post-фазы. Оба шага `if: always()` в обоих прогонах не печатают ничего, так что по логу их различить нельзя — act не выводит баннеры для обычных `run:`-шагов. Итог: падение после успешного pytest, в невидимом шаге, воспроизводимо на этой ветке и не воспроизводится на трёх других за то же окно. Обновил ветку от base (в ней теперь все три сегодняшних мержа) — если покраснеет и на свежей базе, заведу отдельную задачу на диагностируемость этого участка: сейчас шаги `if: always()` молчат, и отличить «упал шаг покрытия» от «упала уборка контейнера» нечем.
bot-backend added 1 commit 2026-08-13 16:53:53 +00:00
Merge branch 'main' into fix/2464-velocity-class-filter
Some checks failed
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
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 2m6s
CI / backend-tests (pull_request) Failing after 16m14s
8aacd1385e
Light1YT added 1 commit 2026-08-13 17:28:22 +00:00
test(ptica): объявить пропуск нового EXPLAIN-теста в skip_allowlist
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
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 2m30s
CI / backend-tests (pull_request) Successful in 16m41s
c9c7aab228
Причина четырёх красных прогонов backend-tests найдена, и это не инфраструктура:
сторож в tests/conftest.py (pytest_sessionfinish) роняет прогон, если тест
пропустился, но не объявлен в skip_allowlist.txt. Мои два новых
параметризованных EXPLAIN-теста скипаются без TEST_DATABASE_URL и в списке
не значились → session.exitstatus = 1 при зелёной сводке «4647 passed».

Сторож сработал ПРАВИЛЬНО и ровно за тем, зачем написан: пропуск без записи
неотличим от пройденной проверки. Его сообщение всё это время лежало в логе
на 1014-й строке, за две секунды до сводки, — я его не увидел, потому что
искал по английским «FAILED/ERROR» и смотрел хвост лога.

Почему проверку нельзя выполнить здесь (как требует шапка файла): EXPLAIN идёт
против КОПИИ ПРОДОВОЙ схемы через SSH-туннель, CI намеренно не задаёт
TEST_DATABASE_URL (комментарий в ci.yml у backend-tests). Где выполняется
вместо этого: вручную с туннелем — прогнан в этой ветке, 4 passed, и со
строкой из main падает 1 из 3.

Плюс backend/.gitignore: coverage.xml. Локальный прогон с --cov-report=xml
кладёт 1.2 МБ рядом с кодом; в этом заходе файл чуть не уехал в коммит и был
пойман только хуком check-added-large-files.

Refs #2464, #2871
bot-backend merged commit c40261cb16 into main 2026-08-13 17:46:29 +00:00
bot-backend deleted branch fix/2464-velocity-class-filter 2026-08-13 17:46:30 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2865
No description provided.