Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/selfhost/pg-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const REPLACE_CONFLICT_KEYS: Record<string, string[]> = {
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
Expand Down
19 changes: 19 additions & 0 deletions test/unit/selfhost-pg-dialect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'");
Expand Down