All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m57s
1. cian_history_backfill.py listings block was missing the same db.rollback() the houses block already has: save_detail_enrichment() runs several unprotected db.execute() and only commits at the end. A single bad row (e.g. a non-standard Cian change_time hitting CAST(:ct AS timestamptz)) leaves the session in a failed-transaction state, and every subsequent listing in the batch (up to 49) then fails with PendingRollbackError -- one real failure looked like N independent ones in the logs. 2. account_estimate_usage.used had no floor. Prod audit: praktika 2026-06 shows used=-3 against 42 real estimates. Migration 185 already fixed this class of bug for user2 (negative used from a retired SQL-runbook bonus hack) but only reset that one username. No decrement path exists anywhere in app.services.account_quota -- increment() only ever does used+1 under a WHERE used < lim guard (#747) -- so the minus is external (manual UPDATE), not an app bug. Migration 189 resets all remaining negative used rows and adds CHECK (used >= 0) so a future manual UPDATE can't reintroduce it.
51 lines
3 KiB
PL/PgSQL
51 lines
3 KiB
PL/PgSQL
-- Migration 189: account_estimate_usage.used >= 0 — защита от бонус-хака (negative used)
|
||
--
|
||
-- WHY:
|
||
-- Прежний SQL-runbook хак раздачи бонусных попыток (`UPDATE account_estimate_usage
|
||
-- SET used = used - N`) уводил used в отрицательные значения. Migration 185 сбросила
|
||
-- это ТОЛЬКО для user2 (used=-35 -> 0, WHERE username = 'user2'). На проде остаётся
|
||
-- минимум ещё один затронутый аккаунт тем же классом порчи (praktika, период 2026-06:
|
||
-- used=-3 при 42 фактических успешных оценках — расхождение объясняется именно этим
|
||
-- хаком, не кодовым багом).
|
||
--
|
||
-- Аудит app.services.account_quota подтверждает: декремента `used` в текущем коде
|
||
-- НЕТ. increment() делает только `used + 1` под предикатом `WHERE used < :lim`
|
||
-- (#747, atomic conditional increment) — этот путь не может уйти в минус. Значит
|
||
-- источник отрицательных значений исключительно внешний (ручной UPDATE через
|
||
-- runbook), а не баг в приложении.
|
||
--
|
||
-- WHAT:
|
||
-- 1. Сброс ВСЕХ оставшихся negative used -> 0 (не только user2, как в 185) —
|
||
-- закрывает praktika и любой другой пропущенный аккаунт.
|
||
-- 2. CHECK (used >= 0) — защита на уровне схемы: любой будущий ручной UPDATE/хак,
|
||
-- уводящий used < 0, теперь падает на уровне БД вместо тихой порчи /quota
|
||
-- (GET /quota мог отдать remaining > limit — «Осталось 50 из 15», см. 185).
|
||
--
|
||
-- IDEMPOTENCY:
|
||
-- - UPDATE ... WHERE used < 0 — no-op при повторном прогоне (после первого раза
|
||
-- условие больше не матчит).
|
||
-- - ADD CONSTRAINT через DO-блок с проверкой pg_constraint — Postgres не
|
||
-- поддерживает `ADD CONSTRAINT IF NOT EXISTS` напрямую для CHECK, поэтому
|
||
-- оборачиваем в идемпотентную проверку по имени constraint.
|
||
--
|
||
-- Dependencies: 076_account_estimate_quota.sql (account_estimate_usage),
|
||
-- 185_account_quota_overrides.sql (первый частичный сброс, только user2).
|
||
|
||
BEGIN;
|
||
|
||
UPDATE account_estimate_usage
|
||
SET used = 0, updated_at = now()
|
||
WHERE used < 0;
|
||
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_constraint
|
||
WHERE conname = 'account_estimate_usage_used_nonnegative'
|
||
) THEN
|
||
ALTER TABLE account_estimate_usage
|
||
ADD CONSTRAINT account_estimate_usage_used_nonnegative CHECK (used >= 0);
|
||
END IF;
|
||
END $$;
|
||
|
||
COMMIT;
|