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;
|
||||
`,
|
||||
},
|
||||
{
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user