-- 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;