-- 0059_duplicate_groups.up.sql — proposed duplicates and the sweeps that find -- them (Scribe milestone #400: #3910). -- -- The sweep compares fingerprints (track_fingerprints, 0058) and proposes groups -- of tracks that hold one recording. Nothing here merges anything: a group is a -- proposal the operator reviews, and the merge (#3911) is a separate act. -- One row per sweep. Lets the report tell "the sweep has never run" apart from -- "it ran and found nothing", and gives the in-flight guard something to check, -- the same way scan_runs does for the library scan. CREATE TABLE duplicate_sweeps ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), started_at timestamptz NOT NULL DEFAULT now(), finished_at timestamptz, candidates integer, groups_found integer, oversize_clusters integer, error_message text ); CREATE INDEX duplicate_sweeps_started_at_idx ON duplicate_sweeps (started_at DESC); CREATE TABLE duplicate_groups ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- The group's identity: its member track ids, sorted and joined. A sweep -- that finds the same tracks again updates this row rather than proposing -- them twice, and a dismissal stays attached to the set it was made about. member_key text NOT NULL UNIQUE, -- Rule 36: a new value for either CHECK swaps the constraint in the same -- migration. tier text NOT NULL CHECK (tier IN ('exact', 'acoustic')), -- Largest disagreement between any two members; NULL for exact groups, -- which have no score. worst_bit_error_rate real, status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'dismissed', 'merged')), detected_at timestamptz NOT NULL DEFAULT now(), last_seen_sweep_id uuid REFERENCES duplicate_sweeps (id) ON DELETE SET NULL, resolved_at timestamptz ); CREATE INDEX duplicate_groups_status_idx ON duplicate_groups (status); CREATE TABLE duplicate_group_members ( group_id uuid NOT NULL REFERENCES duplicate_groups (id) ON DELETE CASCADE, -- CASCADE is right here: a track that genuinely leaves the library has no -- place in a proposal about its duplicates. track_id uuid NOT NULL REFERENCES tracks (id) ON DELETE CASCADE, PRIMARY KEY (group_id, track_id) ); CREATE INDEX duplicate_group_members_track_idx ON duplicate_group_members (track_id); -- The sweep streams candidates in (duration_ms, id) order, keyset-paged, so it -- only ever holds a few seconds' worth of durations in memory. Without this each -- page would sort the whole library again. CREATE INDEX tracks_duration_id_idx ON tracks (duration_ms, id);