Repair the 2026-07-30 review findings #1

Merged
kami merged 15 commits from repair/review-2026-07-30 into master 2026-07-30 22:48:15 +02:00
2 changed files with 91 additions and 1 deletions
Showing only changes of commit 3de1cfb4ca - Show all commits
+7 -1
View File
@@ -340,7 +340,13 @@ CREATE TABLE IF NOT EXISTS claims (
last_reinforced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
raw JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (subject_type, subject_id, predicate, object_type, object_id, source, user_id)
-- NULLS NOT DISTINCT (PG15+) is load-bearing: user_id is NULL for every
-- objective claim, and with default NULLS DISTINCT semantics the
-- ON CONFLICT clauses in upsertClaim() / MbSpineWriter never fire, so
-- re-enrichment inserts duplicates instead of reinforcing.
CONSTRAINT claims_edge_source_user_key
UNIQUE NULLS NOT DISTINCT
(subject_type, subject_id, predicate, object_type, object_id, source, user_id)
);
CREATE INDEX IF NOT EXISTS idx_claims_subject ON claims (subject_type, subject_id, predicate);
+84
View File
@@ -504,6 +504,90 @@ const MIGRATIONS: { id: string; sql: string }[] = [
JOIN artists a ON a.id = cf.object_id;
`,
},
{
id: '20260730_claims_dedup_nulls_not_distinct',
sql: `
-- The claims uniqueness constraint was declared as a plain
-- UNIQUE (subject_type, subject_id, predicate, object_type, object_id,
-- source, user_id). Every objective claim (MB, Discogs, tags) has
-- user_id IS NULL, and Postgres treats NULLs as distinct, so none of the
-- ON CONFLICT clauses in upsertClaim() / MbSpineWriter ever fired:
-- re-enrichment inserted a fresh duplicate row every time instead of
-- reinforcing. claim_fusion is SUM(trust * confidence * recency), so a
-- duplicated edge carried N times its intended weight.
--
-- Two phases, in this order (the constraint cannot be added while
-- duplicates exist):
-- 1. collapse each duplicate group into its most recently reinforced
-- row, carrying forward MAX(last_reinforced_at) / MAX(evidence_at) /
-- MAX(confidence) so reinforcement recency is not lost;
-- 2. replace the constraint with a NULLS NOT DISTINCT version (PG15+).
-- Phase 1: dedup.
CREATE TEMP TABLE claims_dedup ON COMMIT DROP AS
SELECT
id,
ROW_NUMBER() OVER ordered AS rn,
-- These MUST use the unordered window: an ORDER BY in the window spec
-- makes the default frame "UNBOUNDED PRECEDING TO CURRENT ROW", turning
-- MAX() into a running maximum rather than a per-group one.
MAX(last_reinforced_at) OVER grp AS max_last_reinforced_at,
MAX(evidence_at) OVER grp AS max_evidence_at,
MAX(confidence) OVER grp AS max_confidence
FROM claims
WINDOW
grp AS (
PARTITION BY subject_type, subject_id, predicate, object_type, object_id,
source,
COALESCE(user_id, '00000000-0000-0000-0000-000000000000'::uuid)
),
ordered AS (
grp ORDER BY last_reinforced_at DESC, evidence_at DESC, id
);
-- Keeper of each group absorbs the group's best values.
UPDATE claims c
SET last_reinforced_at = d.max_last_reinforced_at,
evidence_at = d.max_evidence_at,
confidence = d.max_confidence
FROM claims_dedup d
WHERE c.id = d.id
AND d.rn = 1;
Outdated
Review

might be a good idea to extract this in separate file.

might be a good idea to extract this in separate file.
DELETE FROM claims c
USING claims_dedup d
WHERE c.id = d.id
AND d.rn > 1;
-- Phase 2: replace the constraint. The live DB has drifted from
-- schema.sql, so find the existing constraint by its definition rather
-- than assuming Postgres' auto-generated name.
DO $mig$
DECLARE
cname TEXT;
BEGIN
FOR cname IN
SELECT con.conname
FROM pg_constraint con
WHERE con.conrelid = 'claims'::regclass
AND con.contype = 'u'
AND pg_get_constraintdef(con.oid) LIKE '%subject_type%'
AND pg_get_constraintdef(con.oid) NOT LIKE '%NULLS NOT DISTINCT%'
LOOP
EXECUTE format('ALTER TABLE claims DROP CONSTRAINT %I', cname);
END LOOP;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'claims'::regclass AND conname = 'claims_edge_source_user_key'
) THEN
ALTER TABLE claims ADD CONSTRAINT claims_edge_source_user_key
UNIQUE NULLS NOT DISTINCT
(subject_type, subject_id, predicate, object_type, object_id, source, user_id);
END IF;
END $mig$;
`,
},
];
export class DbService {