fix(matching): Tier 2b alias sync + collapse double SAVEPOINT (#849) #870
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#870
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/849-matching-sync-savepoint"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
#774-2 Parts 2/3/4 (Part 1 UNIQUE/dedup deferred — cadnum data not flowing per analyst re-scope). Code-only matching-subsystem fixes.
Part 2 — fingerprint↔alias sync (
matching/houses.py)Two scrapers with the same address but coords beyond the 4-decimal fingerprint rounding (~11 m/unit) produced distinct fingerprints → Tier 2a miss → duplicate house. Added Tier 2b lookup by
normalized_addressafter Tier 2a (fingerprint) misses, and register the new fp via_insert_alias; alias upsert changedDO NOTHING→DO UPDATE SET fingerprintso the alias stays synced with the latest writer.Part 3 — collapse double-nested SAVEPOINT (
scrapers/base.py)Removed the redundant inner
db.begin_nested()aroundmatch_or_create_houseinside_link_listing_to_house. The outer per-row SAVEPOINT insave_listings(base.py:386) already isolates a failing lot from its siblings and from thelistingsINSERT (perbackend.mdSAVEPOINT-in-loop rule). Surviving structure: listings INSERT outside SAVEPOINTs (always persists) + one SAVEPOINT around the snapshot + one around the house-link hook.⚠️ BEHAVIOR CHANGE (please scrutinize): previously the inner SAVEPOINT let
upsert_listing_sourcesurvive a house-match failure; now a house-match failure aborts the whole hook for that lot (incl. thelisting_sourcelink) atomically. The listing row itself still persists. This is cleaner/atomic and DoD-compliant, but it does drop the source-link on house-match failure — flagging in case partial-commit was intended.Part 4 — match confidence/method → deferred to #774-3
match_or_create_listingreturns(id, confidence, method)but isn't on the ingestion path (_link_listing_to_housecallsmatch_or_create_house).listing_sources.confidence/matched_methodcolumns exist but are written assource_link=1.0. Wiring the fuzzy matcher in is a non-trivial restructure → comment added, deferred. No migration.Test plan
test_849_matching_savepoint.py(new): 2 listings same address → 1 house/1 alias key (Part 2); batch with one failing lot → siblings persist, no orphan (Part 3).test_matching.pyTier mocks (new Tier 2b execute sequence) +test_base_save_listings.py(Part 3 atomic-hook behavior).pytest -k "matching or house or save_listings"→ 276 passed;ruff+ruff-formatclean.Out of scope
estimator Tier A / headline (#774-3/#850), anchor (#755), UNIQUE(cadnum) migration (Part 1 deferred).
Refs #849, #774
✅ APPROVE — matching Tier 2b sync + collapse double-SAVEPOINT (#849 Parts 2/3/4). Все три части соответствуют DoD issue.
Part 2 — Tier 2b (houses.py): корректно. После Tier 2a (fingerprint) miss → fallback
WHERE normalized_address = :na(coord-independent) ловит cross-scraper coord-drift (>4-dec rounding) → возвращает существующий house_id вместо дубля._insert_aliasре-регистрирует fp;ON CONFLICT (normalized_address) DO UPDATE SET fingerprintсводит к одной alias-строке (house_id НЕ обновляется — first-writer-wins). None-safe:normalize_address(None)→""(normalize.py:53) + guardand norm_addr→ Tier 2b пропускается на coord-only пути, краша нет. Concurrent double-insert race (advisory lock на per-scraperfp→ не взаимоисключают) — pre-existing, не вводится здесь, явно deferred в Part 1 (merge dupes). Sequential-кейс (главный) починен.Part 3 — SAVEPOINT collapse (base.py): behavior change «source-link дропается при house-match DB-fail» — вынужденный, не произвольный: после raise в
match_or_create_houseтранзакция aborted, дойти доupsert_listing_sourceбез savepoint-rollback нельзя (это и была причина внутреннегоbegin_nested). Убрав его, единственный вариант — пропагировать exception. Безопасно: внешний per-row SAVEPOINT (base.py:386) изолирует lot (match_failures++, continue → batch не падает),listingsINSERT персистится (вне savepoint), source-link восстановим на следующем sweep. Триггерится лишь на редком match DB-fault. #849 DoD прямо требует «double-nested SAVEPOINT устранён, один уровень». ✓Part 4: deferred в #774-3 с inline-комментарием — DoD допускает «зафиксировано решение».
psycopg v3 чисто (bind-params,
CAST(:hid AS bigint), нет:x::type). 276 тестов pass (test_849_matching_savepoint: 2 листинга→1 house/1 alias + batch-fail→siblings persist). Reformatting (quote-style) — механический ruff-format, безопасен. Self-extending/секретов нет.Мержу. #849 → qa. Note для qa: заодно глянуть
save_listings:match_failedлог-частоту после deploy (source-link-drop trade-off).