[object Object]

← back to Commercialrealestate

fix(sfr): upsert arbiter must be source (URL), not id — kills ~4,900 sfr_source_key dupe-key errors

be0c49da929ceb8fbca1a4127b2f53c9b4d6743f · 2026-09-18 13:59:06 -0700 · Steve

The sfr table has two unique constraints (id PK, source UNIQUE). The Redfin
upsert declared ON CONFLICT(id), but id drifts across re-sweeps (MLS# appears
late / addr+zip synthetic hash shifts) while the URL stays constant. A same-URL
new-id row slipped past the id check and violated sfr_source_key; the error was
swallowed by .catch(() => null), silently dropping the fresh price/status update
(stale listings). source is the schema's stated idempotency key. DO UPDATE never
rewrites id, so broker_sfr's FK to sfr.id stays stable. Verified in a rolled-back
txn: old arbiter throws the exact error, new arbiter updates in place, id kept,
no duplicate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hi31Ew38GE15TY3oCFtz5d

Files touched

Diff

commit be0c49da929ceb8fbca1a4127b2f53c9b4d6743f
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Sep 18 13:59:06 2026 -0700

    fix(sfr): upsert arbiter must be source (URL), not id — kills ~4,900 sfr_source_key dupe-key errors
    
    The sfr table has two unique constraints (id PK, source UNIQUE). The Redfin
    upsert declared ON CONFLICT(id), but id drifts across re-sweeps (MLS# appears
    late / addr+zip synthetic hash shifts) while the URL stays constant. A same-URL
    new-id row slipped past the id check and violated sfr_source_key; the error was
    swallowed by .catch(() => null), silently dropping the fresh price/status update
    (stale listings). source is the schema's stated idempotency key. DO UPDATE never
    rewrites id, so broker_sfr's FK to sfr.id stays stable. Verified in a rolled-back
    txn: old arbiter throws the exact error, new arbiter updates in place, id kept,
    no duplicate.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Hi31Ew38GE15TY3oCFtz5d
---
 scripts/fetch-sfr-redfin.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/scripts/fetch-sfr-redfin.js b/scripts/fetch-sfr-redfin.js
index 9415034..63ac875 100644
--- a/scripts/fetch-sfr-redfin.js
+++ b/scripts/fetch-sfr-redfin.js
@@ -152,13 +152,17 @@ function loadRegions() {
   let added = 0, captured = 0;
   const num = v => { const n = +String(v == null ? '' : v).replace(/[^0-9.]/g, ''); return Number.isFinite(n) && n > 0 ? Math.round(n) : null; };
 
-  // Upsert one SFR (idempotent on id; source/url is also UNIQUE).
+  // Upsert one SFR. The idempotency key is the Redfin URL (source, UNIQUE) — id can drift
+  // across re-sweeps (MLS# appears late, or the addr+zip synthetic hash shifts), so we MUST
+  // arbiter on source, not id. Arbitering on id let a same-URL/new-id row slip past the ON
+  // CONFLICT check and slam into sfr_source_key (~4,900 swallowed dupe-key errors → stale rows).
+  // id is never re-written on conflict, so broker_sfr's FK to sfr.id stays stable.
   const upsert = async (s) => {
     if (!s.source) return;
     const ins = await pool.query(
       `INSERT INTO sfr(id,address,city,zip,price,beds,baths,sqft,year_built,listing_text,source,last_seen,status,days_on_market,market_status,listed_date,lat,lng)
        VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,now(),'active',$12::int,$13, CASE WHEN $12::int IS NOT NULL THEN CURRENT_DATE - $12::int ELSE NULL END,$14::float8,$15::float8)
-       ON CONFLICT(id) DO UPDATE SET price=EXCLUDED.price, beds=EXCLUDED.beds, baths=EXCLUDED.baths,
+       ON CONFLICT(source) DO UPDATE SET price=EXCLUDED.price, beds=EXCLUDED.beds, baths=EXCLUDED.baths,
          sqft=EXCLUDED.sqft, year_built=EXCLUDED.year_built,
          last_seen=now(), status='active', off_market_at=NULL, disposition=NULL, sold_price=NULL, sold_date=NULL,
          days_on_market=EXCLUDED.days_on_market, listed_date=EXCLUDED.listed_date,

← aa6fd57 afternoon CRE update 2026-09-18  ·  back to Commercialrealestate  ·  auto-data-snapshot: 2026-09-18T15:26:25 (2 data files) — dat e021548 →