fix claim_fusion MV duplicate-key failure on concurrent refresh
The MV SELECTed COALESCE(user_id, zero-uuid) but GROUPed BY raw user_id, so a global enrichment claim (user_id NULL) and a default-user behavior claim (user_id = zero-uuid) for the same edge produced two rows that collide on idx_claim_fusion_pk, breaking REFRESH ... CONCURRENTLY. Exposed by the #219 listener_behavior same_scene_as/alias_of writes. New migration rebuilds the MV grouping by the COALESCE'd user_id so the two fuse into one row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -443,6 +443,67 @@ const MIGRATIONS: { id: string; sql: string }[] = [
|
|||||||
ON artists (mbid) WHERE mbid IS NOT NULL;
|
ON artists (mbid) WHERE mbid IS NOT NULL;
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '20260717_claim_fusion_group_by_coalesce',
|
||||||
|
sql: `
|
||||||
|
-- Fix duplicate-key failures in REFRESH MATERIALIZED VIEW CONCURRENTLY.
|
||||||
|
-- The MV SELECTs COALESCE(user_id, zero-uuid) but GROUPed BY the raw
|
||||||
|
-- user_id, so a global enrichment claim (user_id NULL) and a default-user
|
||||||
|
-- behavior claim (user_id = zero-uuid, e.g. listener_behavior same_scene_as)
|
||||||
|
-- for the same edge fell into separate groups yet collapsed to the same
|
||||||
|
-- output key → two rows violating idx_claim_fusion_pk. Group by the same
|
||||||
|
-- COALESCE'd expression so they fuse into one row.
|
||||||
|
DROP MATERIALIZED VIEW IF EXISTS claim_fusion CASCADE;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW claim_fusion AS
|
||||||
|
SELECT
|
||||||
|
c.subject_type,
|
||||||
|
c.subject_id,
|
||||||
|
c.predicate,
|
||||||
|
c.object_type,
|
||||||
|
c.object_id,
|
||||||
|
COALESCE(c.user_id, '00000000-0000-0000-0000-000000000000'::uuid) AS user_id,
|
||||||
|
SUM(
|
||||||
|
st.trust * c.confidence *
|
||||||
|
GREATEST(0.1, 1.0 - EXTRACT(DAY FROM NOW() - c.last_reinforced_at) / 180.0)
|
||||||
|
) AS fused_value,
|
||||||
|
COUNT(*) AS claim_count,
|
||||||
|
MAX(c.last_reinforced_at) AS last_reinforced_at
|
||||||
|
FROM claims c
|
||||||
|
JOIN source_trust st ON st.key = c.source
|
||||||
|
GROUP BY c.subject_type, c.subject_id, c.predicate, c.object_type, c.object_id,
|
||||||
|
COALESCE(c.user_id, '00000000-0000-0000-0000-000000000000'::uuid);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_claim_fusion_pk
|
||||||
|
ON claim_fusion (subject_type, subject_id, predicate, object_type, object_id, user_id);
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW track_artists_v2 AS
|
||||||
|
SELECT t.id AS track_id,
|
||||||
|
a.id AS artist_id,
|
||||||
|
a.name AS artist_name,
|
||||||
|
CASE cf.predicate WHEN 'credited_main_on' THEN 'main' ELSE 'featured' END AS role,
|
||||||
|
cf.fused_value AS confidence
|
||||||
|
FROM tracks t
|
||||||
|
JOIN claim_fusion cf
|
||||||
|
ON cf.subject_type = 'track' AND cf.subject_id = t.id
|
||||||
|
AND cf.predicate IN ('credited_main_on', 'featured_on')
|
||||||
|
AND cf.object_type = 'artist'
|
||||||
|
JOIN artists a ON a.id = cf.object_id;
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW album_artists_v2 AS
|
||||||
|
SELECT al.id AS album_id,
|
||||||
|
a.id AS artist_id,
|
||||||
|
a.name AS artist_name,
|
||||||
|
CASE cf.predicate WHEN 'credited_main_on_album' THEN 'main' ELSE 'featured' END AS role,
|
||||||
|
cf.fused_value AS confidence
|
||||||
|
FROM albums al
|
||||||
|
JOIN claim_fusion cf
|
||||||
|
ON cf.subject_type = 'album' AND cf.subject_id = al.id
|
||||||
|
AND cf.predicate IN ('credited_main_on_album', 'featured_on_album')
|
||||||
|
AND cf.object_type = 'artist'
|
||||||
|
JOIN artists a ON a.id = cf.object_id;
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export class DbService {
|
export class DbService {
|
||||||
|
|||||||
Reference in New Issue
Block a user