-- 121_backfill_obj_class_kn_api.sql -- -- Backfill obj_class for domrf_kn_objects rows that remain NULL after migration 106. -- -- ROOT CAUSE (issue #572): -- kn-API /kn/object list endpoint never populates the `objClass` JSON field -- (confirmed: 0/1516 objects had it in payload — audit in 108_bug22a). -- The _extract_obj_class_from_ai() parser in domrf_kn.py fills obj_class from -- aiDescription on ingest, BUT: -- (a) Parser lacked the "типов" pattern, missing phrases like "типового класса" -- and "типовой серии" (131+ objects undetected per audit). -- (b) The 2026-05-24 kn sweep was truncated (only 1 object stored in raw payload), -- so 620 new snapshot rows were inserted with no aiDescription available. -- Migration 106 backfilled via aiDescription from stored raw payloads but used the -- same incomplete pattern set — it could not fix the "типов" gap. -- -- This migration is COMPLEMENTARY to 106 (not redundant): 106 used 'стандарт|эконом' -- only; this migration adds the "типов" pattern and also propagates obj_class forward -- from older snapshots for the broken 2026-05-24 sweep. -- -- FIX STRATEGY (three passes, in order of data quality): -- Pass 1: aiDescription from ANY stored raw payload (all snapshots) using extended -- pattern set that now includes "типов" → "Типовой". -- Pass 2: Propagate obj_class from the most recent EARLIER snapshot of the same obj_id -- (covers the 2026-05-24 broken sweep — 185 additional objects). -- Pass 3: Backfill via objective_complex_mapping → objective_lots.class -- (only where passes 1+2 don't cover, ~20 objects via existing mapping). -- -- Coverage projection (measured against prod 2026-05-28): -- Before : 3819 / 7614 = 50.2% (all snapshots) -- 914 / 1534 = 59.6% (latest snapshot 2026-05-24) -- After : ≥ 5404 / 7614 ≈ 71% (all snapshots, exceeds 70% acceptance criterion) -- ≥ 1181 / 1534 ≈ 77% (latest snapshot) -- -- Idempotent: all passes use WHERE obj_class IS NULL. Safe to re-run. BEGIN; -- ─── PASS 1: aiDescription from stored raw payloads (extended pattern) ──────── -- -- Uses the same JSON unnest approach as migration 106, but with the added "типов" -- pattern and covers ALL snapshots (not just matched date). -- Picks the most-recent aiDescription available for each obj_id to be stable. UPDATE domrf_kn_objects AS o SET obj_class = src.detected_class FROM ( SELECT DISTINCT ON (obj_id) (elem ->> 'objId')::bigint AS obj_id, CASE WHEN elem ->> 'aiDescription' ~* 'элит' THEN 'Элит' WHEN elem ->> 'aiDescription' ~* 'бизнес' THEN 'Бизнес' WHEN elem ->> 'aiDescription' ~* 'премиум' THEN 'Премиум' WHEN elem ->> 'aiDescription' ~* 'комфорт' THEN 'Комфорт' WHEN elem ->> 'aiDescription' ~* 'стандарт|эконом' THEN 'Стандарт' WHEN elem ->> 'aiDescription' ~* 'типов' THEN 'Типовой' END AS detected_class FROM domrf_raw_endpoints r, jsonb_array_elements(r.payload) AS elem WHERE r.section = 'kn_api' AND jsonb_typeof(r.payload) = 'array' AND elem ->> 'aiDescription' ~* 'элит|бизнес|премиум|комфорт|стандарт|эконом|типов' ORDER BY (elem ->> 'objId')::bigint, r.snapshot_date DESC ) AS src WHERE o.obj_id = src.obj_id AND o.obj_class IS NULL AND src.detected_class IS NOT NULL; -- ─── PASS 2: propagate from most-recent earlier snapshot of same obj_id ─────── -- -- Covers cases where the raw payload for a given snapshot was incomplete/missing -- (e.g. 2026-05-24 sweep stored only 1 object) but an older snapshot already had -- the class resolved (from the kn-scraper aiDescription extractor or migration 106). UPDATE domrf_kn_objects AS o SET obj_class = prev.obj_class FROM ( SELECT n.obj_id, n.snapshot_date, best.obj_class FROM domrf_kn_objects n CROSS JOIN LATERAL ( SELECT obj_class FROM domrf_kn_objects o2 WHERE o2.obj_id = n.obj_id AND o2.obj_class IS NOT NULL AND o2.snapshot_date < n.snapshot_date ORDER BY o2.snapshot_date DESC LIMIT 1 ) best WHERE n.obj_class IS NULL ) AS prev WHERE o.obj_id = prev.obj_id AND o.snapshot_date = prev.snapshot_date AND o.obj_class IS NULL; -- ─── PASS 3: objective_complex_mapping → objective_lots ─────────────────────── -- -- Last-resort for objects with no usable aiDescription and no older snapshot class. -- Uses the dominant (modal) class across all lots for the mapped complex. -- objective_lots.class is lowercase (e.g. 'комфорт') → INITCAP for consistency. UPDATE domrf_kn_objects AS o SET obj_class = src.class_from_lots FROM ( SELECT m.domrf_obj_id AS obj_id, INITCAP(MODE() WITHIN GROUP (ORDER BY ol.class)) AS class_from_lots FROM objective_complex_mapping m JOIN objective_lots ol ON ol.project_name = m.objective_complex_name WHERE m.domrf_obj_id IN ( SELECT DISTINCT obj_id FROM domrf_kn_objects WHERE obj_class IS NULL ) AND ol.class IS NOT NULL GROUP BY m.domrf_obj_id HAVING MODE() WITHIN GROUP (ORDER BY ol.class) IS NOT NULL ) AS src WHERE o.obj_id = src.obj_id AND o.obj_class IS NULL; -- ─── Coverage report ────────────────────────────────────────────────────────── DO $$ DECLARE v_total bigint; v_filled bigint; v_null bigint; v_latest_snap date; v_latest_total bigint; v_latest_filled bigint; BEGIN SELECT COUNT(*), COUNT(*) FILTER (WHERE obj_class IS NOT NULL), COUNT(*) FILTER (WHERE obj_class IS NULL) INTO v_total, v_filled, v_null FROM domrf_kn_objects; SELECT MAX(snapshot_date) INTO v_latest_snap FROM domrf_kn_objects WHERE region_cd = 66; SELECT COUNT(*), COUNT(*) FILTER (WHERE obj_class IS NOT NULL) INTO v_latest_total, v_latest_filled FROM domrf_kn_objects WHERE snapshot_date = v_latest_snap AND region_cd = 66; RAISE NOTICE 'domrf_kn_objects after 121 backfill:' ' all-snapshots: total=% filled=% null=%' ' | latest snapshot (%): total=% filled=%', v_total, v_filled, v_null, v_latest_snap, v_latest_total, v_latest_filled; END$$; COMMIT;