66 lines
4.6 KiB
PL/PgSQL
66 lines
4.6 KiB
PL/PgSQL
-- 124_domrf_kn_free_flats.sql
|
||
-- #950 (Site Finder v2 / 3-layer склад, EPIC 6) — PR A: persist free_flats.
|
||
--
|
||
-- Добавляет domrf_kn_objects.free_flats = freeFlatsInfo.numberFlats из kn-API dom.рф
|
||
-- (кол-во квартир ЖК В ТЕКУЩЕЙ ПРОДАЖЕ). Это единственный детерминированный сигнал,
|
||
-- разблокирующий object-level Layer2: hidden = flat_count − free_flats
|
||
-- (flat_count = всего квартир по проектной декларации; free_flats = выставлено сейчас).
|
||
--
|
||
-- До #950 payload-блок freeFlatsInfo парсился ТОЛЬКО ради priceMin → price_min_rub
|
||
-- (миграция 113); numberFlats отбрасывался. Поле free_flats — sibling price_min_rub:
|
||
-- bigint→int, та же null-safety, тот же EXCLUDED-overwrite в UPSERT_OBJECT_SQL.
|
||
--
|
||
-- Backfill: сырой list объектов kn-API сохраняется как JSONB в domrf_raw_endpoints
|
||
-- (section='kn_api', endpoint='kn_object_place_<place>'; см. domrf_kn._insert_raw),
|
||
-- каждый элемент массива несёт objId + freeFlatsInfo.numberFlats. Поэтому historical
|
||
-- строки можно заполнить БЕЗ повторного скрейпа — join raw-элементов к
|
||
-- domrf_kn_objects по (obj_id, snapshot_date). Детерминированно, без LLM.
|
||
--
|
||
-- Deploy: auto-applied by deploy.yml через _schema_migrations tracking (ровно один раз).
|
||
-- Idempotent: ADD COLUMN IF NOT EXISTS + backfill только WHERE free_flats IS NULL.
|
||
-- Migration order: эта SQL-миграция применяется ПЕРВОЙ, backend domrf_kn.py — следом.
|
||
-- free_flats НЕnullable НЕ делаем: historical строки до бекфилла/без raw остаются NULL,
|
||
-- L2-сервис в этом случае падает на fallback (flat_count − sold).
|
||
|
||
BEGIN;
|
||
|
||
-- ── 1. Колонка ─────────────────────────────────────────────────────────────────
|
||
ALTER TABLE domrf_kn_objects
|
||
ADD COLUMN IF NOT EXISTS free_flats integer;
|
||
|
||
COMMENT ON COLUMN domrf_kn_objects.free_flats IS
|
||
'Кол-во квартир в текущей продаже (freeFlatsInfo.numberFlats из kn-API dom.рф). '
|
||
'NULL = не заполнено (старый снапшот до #950). Для Layer2 hidden = flat_count − free_flats.';
|
||
|
||
-- ── 2. Backfill из сохранённого raw-payload ────────────────────────────────────
|
||
-- domrf_raw_endpoints.payload — JSONB-массив сырых object-строк kn-API. Разворачиваем
|
||
-- его, тянем objId + freeFlatsInfo.numberFlats и матчим к уже записанным строкам по
|
||
-- (obj_id, snapshot_date). Только WHERE free_flats IS NULL — повторный прогон no-op.
|
||
-- Guard: payload должен быть массивом; objId и numberFlats — числовые строки (регексп
|
||
-- отсекает null/мусор ДО CAST, иначе invalid input syntax for type integer). Digit-cap
|
||
-- {1,9}/{1,18} держит numberFlats в int4, objId в bigint — отсекает overflow ДО CAST
|
||
-- (числа-кандидаты семантически ограничены квартирами одного ЖК, но guard явный).
|
||
-- DISTINCT ON (obj_id, snapshot_date): один objId не дублируется в одном снапшоте, но
|
||
-- на случай нескольких place-endpoint'ов на дату берём первый детерминированно.
|
||
WITH raw_flats AS (
|
||
SELECT DISTINCT ON (obj_id, r.snapshot_date)
|
||
CAST(elem->>'objId' AS bigint) AS obj_id,
|
||
r.snapshot_date AS snapshot_date,
|
||
CAST(elem->'freeFlatsInfo'->>'numberFlats' AS integer) AS free_flats
|
||
FROM domrf_raw_endpoints AS r
|
||
CROSS JOIN LATERAL jsonb_array_elements(r.payload) AS elem
|
||
WHERE r.section = 'kn_api'
|
||
AND r.endpoint LIKE 'kn_object_place_%'
|
||
AND jsonb_typeof(r.payload) = 'array'
|
||
AND elem->>'objId' ~ '^[0-9]{1,18}$'
|
||
AND elem->'freeFlatsInfo'->>'numberFlats' ~ '^[0-9]{1,9}$'
|
||
ORDER BY obj_id, r.snapshot_date
|
||
)
|
||
UPDATE domrf_kn_objects AS o
|
||
SET free_flats = rf.free_flats
|
||
FROM raw_flats AS rf
|
||
WHERE o.obj_id = rf.obj_id
|
||
AND o.snapshot_date = rf.snapshot_date
|
||
AND o.free_flats IS NULL;
|
||
|
||
COMMIT;
|