From 72a373b1daac1145d4235d41cad83c7ef0710ef0 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Sun, 26 Jul 2026 21:46:47 +0800 Subject: [PATCH] fix(db): add the orb_reuse_counters conflict key to the self-host Postgres dialect translator REPLACE_CONFLICT_KEYS requires an entry for every table used with INSERT OR REPLACE, or translateInsertOr throws "no known conflict key". Migration 0177 added orb_reuse_counters with a real INSERT OR REPLACE writer in src/orb/ingest.ts, but the map entry was missing, so a self-hosted Postgres instance's hourly ORB export threw on its first orb_reuse_counters write. Add `orb_reuse_counters: ["instance_id", "day"]`, matching the table's PRIMARY KEY. Closes #8893 Co-Authored-By: Claude Opus 4.8 --- src/selfhost/pg-dialect.ts | 3 +++ test/unit/selfhost-pg-dialect.test.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/selfhost/pg-dialect.ts b/src/selfhost/pg-dialect.ts index bd7b9ecbf4..07b0686f34 100644 --- a/src/selfhost/pg-dialect.ts +++ b/src/selfhost/pg-dialect.ts @@ -12,6 +12,9 @@ const REPLACE_CONFLICT_KEYS: Record = { tunables_overrides_shadow: ["project"], orb_export_cursor: ["instance_hash"], orb_signals: ["instance_id", "repo_hash", "pr_hash"], + // #8893: orb_reuse_counters (migrations/0177) is written with INSERT OR REPLACE by src/orb/ingest.ts; its + // PRIMARY KEY (instance_id, day) is the conflict target the hourly ORB export needs on self-host Postgres. + orb_reuse_counters: ["instance_id", "day"], // ams_signals (#8382): TWO columns here, deliberately — this must name the table's REAL unique constraint // (`UNIQUE (instance_id, pr_hash)`, migrations/0148_ams_signals.sql), or Postgres rejects the generated // `ON CONFLICT` with "no unique or exclusion constraint matching". The 3-column shape orb_signals needed diff --git a/test/unit/selfhost-pg-dialect.test.ts b/test/unit/selfhost-pg-dialect.test.ts index 02f542750b..cc1a29275d 100644 --- a/test/unit/selfhost-pg-dialect.test.ts +++ b/test/unit/selfhost-pg-dialect.test.ts @@ -138,6 +138,25 @@ describe("pg-dialect (#977 SQLite → Postgres)", () => { expect(translated).not.toContain("pr_hash=excluded.pr_hash"); }); + // #8893: src/orb/ingest.ts's hourly ORB export issues this INSERT OR REPLACE; without a + // REPLACE_CONFLICT_KEYS entry, translateInsertOr threw "no known conflict key" on the first + // orb_reuse_counters write on every self-host Postgres deployment. The statement below is verbatim. + it("REGRESSION (#8893): translates the real orb_reuse_counters ingest INSERT OR REPLACE without throwing", () => { + const translated = translateInsertOr( + `INSERT OR REPLACE INTO orb_reuse_counters (instance_id, day, hits, misses, received_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, + ); + expect(translated).toContain("INSERT INTO orb_reuse_counters"); + // PRIMARY KEY (instance_id, day) — migration 0177 — is the conflict target. + expect(translated).toContain("ON CONFLICT (instance_id, day) DO UPDATE SET"); + // Non-key columns are upserted; the key columns are excluded from the SET list. + expect(translated).toContain("hits=excluded.hits"); + expect(translated).toContain("misses=excluded.misses"); + expect(translated).toContain("received_at=excluded.received_at"); + expect(translated).not.toContain("instance_id=excluded.instance_id"); + expect(translated).not.toContain("day=excluded.day"); + }); + it("translateSql composes all passes; translateDdl handles the ISO-now default", () => { expect(translateSql("SELECT * FROM t WHERE updated_at > datetime('now', ?)")).toMatch(/\$1/); expect(translateDdl("created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))")).toContain("to_char(now() AT TIME ZONE 'UTC'");