[object Object]

← back to La Socrata Ingester

Fix: dedupe rows by conflict key within upsert batch (last-wins)

4c680aa7db44e96b55645d16ddcd2c2c600cc90e · 2026-08-10 22:47:55 -0700 · steve

Postgres rejects INSERT..ON CONFLICT touching the same key twice per statement.
Code-enforcement (apno) and WeHo film (permit_no) repeat keys within a page.
Surfaced during full backfill; all sources now load clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 4c680aa7db44e96b55645d16ddcd2c2c600cc90e
Author: steve <steve@designerwallcoverings.com>
Date:   Mon Aug 10 22:47:55 2026 -0700

    Fix: dedupe rows by conflict key within upsert batch (last-wins)
    
    Postgres rejects INSERT..ON CONFLICT touching the same key twice per statement.
    Code-enforcement (apno) and WeHo film (permit_no) repeat keys within a page.
    Surfaced during full backfill; all sources now load clean.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 src/db.js | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/db.js b/src/db.js
index 529903c..c334957 100644
--- a/src/db.js
+++ b/src/db.js
@@ -24,6 +24,17 @@ export async function q(text, params) {
 // Returns the number of rows sent.
 export async function upsert(table, conflict, rows) {
   if (!rows.length) return 0;
+
+  // Postgres rejects an INSERT ... ON CONFLICT that targets the same conflict key
+  // twice in one statement ("cannot affect row a second time"). Some datasets
+  // repeat a key within a single page (e.g. one code-enforcement apno or one film
+  // permit_no spanning multiple rows), so collapse by conflict key first — last wins.
+  if (rows.length > 1) {
+    const seen = new Map();
+    for (const r of rows) seen.set(conflict.map((k) => r[k]).join(''), r);
+    if (seen.size !== rows.length) rows = [...seen.values()];
+  }
+
   const cols = Object.keys(rows[0]);
   const updatable = cols.filter((c) => !conflict.includes(c));
   const setClause = updatable.length

← 899b4fc LA Socrata/ArcGIS public-data ingester → realestate DB (TK-1  ·  back to La Socrata Ingester  ·  assessor --full now scans all roll years (matches docs); def b045794 →