gendesign/tradein-mvp/backend/data/sql/131_fix_diff_percent_overflow.sql
bot-backend 89cbddf318
All checks were successful
CI / changes (pull_request) Successful in 6s
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
fix(tradein/db): widen offer_price_history.diff_percent numeric(5,2)→(8,2) + clamp в триггере (краш full_load)
2026-06-20 11:43:23 +03:00

103 lines
4 KiB
PL/PgSQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Migration 131: fix NumericValueOutOfRange crash in record_listing_price_change()
--
-- Incident: prod run_id=260, 2026-06-20
-- A listing scrubbed by avito_full_load had an OLD.price_rub that was
-- garbage-small (data entry error or placeholder value). The computed
-- diff_percent = round((NEW-OLD)/OLD*100, 2) exceeded ±999.99, which
-- overflows numeric(5,2) (precision=5, scale=2, max value=±999.99).
-- PostgreSQL raised DataError: "numeric field overflow" inside the
-- trigger, which caused the INSERT into listings to abort, which caused
-- the entire avito_full_load batch to crash.
-- The trigger is shared across all non-cian sources → same crash would
-- occur for yandex/domclick full_load batches on similar data.
--
-- Fix (two parts):
-- 1. Widen diff_percent column to numeric(8,2) (max ±999999.99%).
-- Widening precision is safe and lossless for existing data.
-- 2. Replace the trigger function to clamp the computed value into
-- [999999.99, +999999.99] so even future extreme deltas cannot
-- overflow the column. All other logic is preserved exactly as in
-- migration 116.
--
-- Dependencies: offer_price_history table (023), listings table,
-- offer_price_history_listing_change_uq unique constraint,
-- trigger listings_price_change_trg (116).
--
-- Idempotent: ALTER TYPE widening is a no-op if already numeric(8,2);
-- CREATE OR REPLACE FUNCTION is always safe to re-run.
-- Trigger is NOT recreated — the existing trigger already points to
-- record_listing_price_change(); replacing the function body is enough.
--
-- Deploy order: standalone; auto-applied by deploy pipeline via
-- _schema_migrations. Do NOT apply manually on prod — causes drift.
BEGIN;
-- Part 1: widen the column.
-- numeric(5,2) → numeric(8,2): raises max from ±999.99 to ±999999.99.
-- PostgreSQL widens precision without rewriting rows (catalogue-only change
-- for heap tables when scale does not decrease and precision does not decrease).
ALTER TABLE offer_price_history
ALTER COLUMN diff_percent TYPE numeric(8,2);
-- Part 2: replace trigger function with clamped diff_percent computation.
-- All guards from migration 116 are preserved verbatim:
-- • same-value skip (IS NOT DISTINCT FROM)
-- • NULL new-price skip
-- • cian source skip (backfill exclusion — see 116 for rationale)
-- • OLD.price_rub > 0 gate (avoid division by zero / nonsensical pct)
-- • ON CONFLICT DO NOTHING (idempotent re-scrape)
CREATE OR REPLACE FUNCTION record_listing_price_change()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
-- Skip if price did not actually change (same-value upsert write).
IF NEW.price_rub IS NOT DISTINCT FROM OLD.price_rub THEN
RETURN NEW;
END IF;
-- Skip if the new price is NULL (defensive; price_rub is NOT NULL in practice).
IF NEW.price_rub IS NULL THEN
RETURN NEW;
END IF;
-- Skip cian: its history is source-provided via cian_price_history.py backfill.
-- That service selects listings WHERE oph.listing_id IS NULL (zero rows in
-- offer_price_history). Inserting a trigger row here would permanently
-- exclude those listings from the backfill, losing accurate historical timestamps.
IF NEW.source = 'cian' THEN
RETURN NEW;
END IF;
INSERT INTO offer_price_history (
listing_id,
change_time,
price_rub,
source,
diff_percent,
recorded_at
) VALUES (
NEW.id,
now(),
NEW.price_rub,
NEW.source,
CASE
WHEN OLD.price_rub IS NOT NULL AND OLD.price_rub > 0
THEN LEAST(GREATEST(
round(
(NEW.price_rub::numeric - OLD.price_rub::numeric)
/ OLD.price_rub::numeric * 100,
2
),
-999999.99), 999999.99)
ELSE NULL
END,
now()
)
ON CONFLICT ON CONSTRAINT offer_price_history_listing_change_uq
DO NOTHING;
RETURN NEW;
END;
$$;
COMMIT;