initial state: muzick music player + recommendation engine
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
-- Enums
|
||||
-- Guarded so this file is idempotent and can be re-applied on every backend boot
|
||||
-- (CREATE TYPE has no IF NOT EXISTS; swallow the duplicate_object error instead).
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE track_state AS ENUM ('LIBRARY', 'RECOMMENDED', 'HIDDEN', 'MISSING', 'DELETED');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE track_source_type AS ENUM ('MANUAL', 'RECOMMENDATION');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE recommendation_status AS ENUM ('ACTIVE', 'RESOLVED', 'FAILED');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE dislike_state AS ENUM ('HIDDEN', 'WARNED', 'DELETED');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$;
|
||||
|
||||
-- Reduce an artist string to its PRIMARY (first-billed) artist, so that every
|
||||
-- form of a collaboration maps to the same canonical identity:
|
||||
-- "Artist feat. Guest", "Artist ft. Guest", "Artist x Guest",
|
||||
-- "Artist & Guest", "Artist; Guest", "Artist, Guest", "Artist / Guest"
|
||||
-- all map to "Artist". This is the identity used by the normalized_name /
|
||||
-- normalized_artist generated columns, dedup, and the Vibe engine. The full
|
||||
-- list of co-billed artists is preserved separately in the track_artists table
|
||||
-- (populated by the scanner and the split-collab-artists migration); this
|
||||
-- function intentionally only yields the main artist.
|
||||
-- Also handles parenthesized feature forms like "(feat. X)".
|
||||
CREATE OR REPLACE FUNCTION normalize_artist(artist TEXT) RETURNS TEXT AS $$
|
||||
DECLARE
|
||||
result TEXT;
|
||||
BEGIN
|
||||
-- 1. Strip feat/ft/vs/x feature suffixes (+ everything after them), incl.
|
||||
-- parenthesized forms like "(feat. X)" / "(Feat. X)".
|
||||
result := REGEXP_REPLACE(
|
||||
artist,
|
||||
'\s*\(?\s*([fF]eat(uring)?\.?|[fF]t\.?|[vV]s\.?|[xX])\s+.*$',
|
||||
''
|
||||
);
|
||||
-- 2. Cut at the first collaboration separator ( ; & / , ) and keep the part
|
||||
-- before it: "$bunny, Metox" -> "$bunny", "Booker & ЗАМАЙ" -> "Booker".
|
||||
result := REGEXP_REPLACE(result, '\s*[;&/,].*$', '');
|
||||
result := BTRIM(result);
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE STRICT;
|
||||
|
||||
-- Tables
|
||||
CREATE TABLE IF NOT EXISTS artists (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
-- Canonical display name (e.g., "P!nk" not "Pink")
|
||||
canonical_name TEXT NOT NULL,
|
||||
-- Sort name for alphabetical ordering (e.g., "Pink, P!" or "Beatles, The")
|
||||
sort_name TEXT,
|
||||
-- MusicBrainz ID: the canonical identity. NULL for artists not in MB.
|
||||
-- Unique when present so we never create duplicate MB artists.
|
||||
mbid UUID UNIQUE,
|
||||
-- Fallback: legacy name used before MBID resolution.
|
||||
-- Not unique; multiple rows can have same name before dedup.
|
||||
name TEXT NOT NULL,
|
||||
normalized_name TEXT
|
||||
GENERATED ALWAYS AS (normalize_artist(name)) STORED,
|
||||
discogs_id TEXT,
|
||||
image_path TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artists_mbid ON artists(mbid) WHERE mbid IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_artists_normalized_name ON artists(normalized_name);
|
||||
|
||||
-- Artist aliases: alternative names for the same artist.
|
||||
-- Enables matching "Pink", "P!nk", "PINK" to the same artist_id.
|
||||
CREATE TABLE IF NOT EXISTS artist_aliases (
|
||||
artist_id UUID NOT NULL REFERENCES artists(id) ON DELETE CASCADE,
|
||||
alias TEXT NOT NULL,
|
||||
alias_normalized TEXT GENERATED ALWAYS AS (normalize_artist(alias)) STORED,
|
||||
PRIMARY KEY (artist_id, alias)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artist_aliases_normalized ON artist_aliases(alias_normalized);
|
||||
|
||||
-- Artist lookup cache: avoids repeated MusicBrainz queries.
|
||||
-- Keyed by normalized artist name.
|
||||
CREATE TABLE IF NOT EXISTS artist_lookup_cache (
|
||||
normalized_name TEXT PRIMARY KEY,
|
||||
mbid UUID,
|
||||
canonical_name TEXT,
|
||||
sort_name TEXT,
|
||||
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
-- Track if we looked up and found nothing (negative cache)
|
||||
not_found BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS albums (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
artist_id UUID REFERENCES artists(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
year INTEGER,
|
||||
-- Full release date from MusicBrainz first-release-date (YYYY-MM-DD).
|
||||
-- More precise than `year` (which can also come from Discogs). Used as a
|
||||
-- deterministic tiebreaker in album dedup (earlier release = keeper).
|
||||
release_date DATE,
|
||||
artwork_id TEXT,
|
||||
mbid UUID UNIQUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(artist_id, title)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albums_mbid ON albums(mbid) WHERE mbid IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tracks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
path TEXT UNIQUE NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
artist TEXT NOT NULL,
|
||||
album_id UUID REFERENCES albums(id) ON DELETE CASCADE,
|
||||
duration REAL NOT NULL,
|
||||
state track_state DEFAULT 'LIBRARY',
|
||||
play_count INTEGER DEFAULT 0,
|
||||
skip_count INTEGER DEFAULT 0,
|
||||
dislike_count INTEGER DEFAULT 0,
|
||||
last_played_at TIMESTAMP,
|
||||
mtime REAL,
|
||||
source_type track_source_type DEFAULT 'MANUAL',
|
||||
quarantined_at TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
release_date DATE
|
||||
);
|
||||
|
||||
-- Add normalized columns as generated columns for existing databases where the
|
||||
-- CREATE TABLE IF NOT EXISTS above was a no-op (column didn't exist before).
|
||||
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'tracks' AND column_name = 'release_date'
|
||||
) THEN
|
||||
ALTER TABLE tracks ADD COLUMN release_date DATE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_release_date ON tracks (release_date) WHERE release_date IS NOT NULL;
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'tracks' AND column_name = 'normalized_artist'
|
||||
) THEN
|
||||
ALTER TABLE tracks ADD COLUMN normalized_artist TEXT
|
||||
GENERATED ALWAYS AS (normalize_artist(artist)) STORED;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'artists' AND column_name = 'normalized_name'
|
||||
) THEN
|
||||
ALTER TABLE artists ADD COLUMN normalized_name TEXT
|
||||
GENERATED ALWAYS AS (normalize_artist(name)) STORED;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_hash ON tracks(hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_normalized_artist ON tracks(normalized_artist);
|
||||
CREATE INDEX IF NOT EXISTS idx_artists_normalized_name ON artists(normalized_name);
|
||||
|
||||
-- Tracks integrity issues found by the periodic integrity-sweep worker.
|
||||
-- Created with IF NOT EXISTS so the worker can self-provision this table at
|
||||
-- runtime on databases that predate this schema change (see IntegrityService.ensureSchema).
|
||||
CREATE TABLE IF NOT EXISTS track_integrity_issues (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
track_id UUID REFERENCES tracks(id) ON DELETE CASCADE,
|
||||
issue_type TEXT NOT NULL, -- 'CORRUPT_METADATA' | 'MISSING_FILE'
|
||||
status TEXT NOT NULL DEFAULT 'OPEN', -- 'OPEN' | 'FIXED' | 'NEEDS_REVIEW'
|
||||
details TEXT, -- human-readable: e.g. the corrupted value
|
||||
detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
resolved_at TIMESTAMP,
|
||||
UNIQUE(track_id, issue_type)
|
||||
);
|
||||
|
||||
-- Last.fm artist similarity, feeds Vibe discovery. Populated by the worker's
|
||||
-- `artist_similarity` job (see EnrichmentService.refreshArtistSimilarity).
|
||||
-- Created with IF NOT EXISTS so the worker can self-provision this table at
|
||||
-- runtime on databases that predate this schema change.
|
||||
--
|
||||
-- mbid storage decision: artists.mbid is typed UUID and MusicBrainz MBIDs are
|
||||
-- themselves UUID-format strings, so the worker stores the artist MBID directly
|
||||
-- in the existing artists.mbid column (guarded with a UUID-shape check before
|
||||
-- the write). No mbid_text column was needed.
|
||||
CREATE TABLE IF NOT EXISTS artist_similar (
|
||||
artist_id UUID REFERENCES artists(id) ON DELETE CASCADE,
|
||||
similar_name TEXT NOT NULL,
|
||||
match REAL NOT NULL DEFAULT 0,
|
||||
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (artist_id, similar_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS genre (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
parent_id UUID REFERENCES genre(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS track_genre (
|
||||
track_id UUID REFERENCES tracks(id) ON DELETE CASCADE,
|
||||
genre_id UUID REFERENCES genre(id) ON DELETE CASCADE,
|
||||
weight DECIMAL NOT NULL DEFAULT 1.0,
|
||||
PRIMARY KEY (track_id, genre_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dislikes (
|
||||
track_id UUID PRIMARY KEY REFERENCES tracks(id) ON DELETE CASCADE,
|
||||
disliked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
warned_at TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
grace_hours INTEGER DEFAULT 48,
|
||||
state dislike_state DEFAULT 'HIDDEN'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
user_id UUID NOT NULL,
|
||||
track_id UUID PRIMARY KEY REFERENCES tracks(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS recommendation_batch (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
status recommendation_status DEFAULT 'ACTIVE',
|
||||
last_interaction_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
seed_track_id UUID REFERENCES tracks(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS recommendation_batch_track (
|
||||
batch_id UUID REFERENCES recommendation_batch(id) ON DELETE CASCADE,
|
||||
track_id UUID REFERENCES tracks(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (batch_id, track_id)
|
||||
);
|
||||
|
||||
-- Play history: one row per playback event. Feeds the Vibe engine's
|
||||
-- "Success-Driven Center" rule (a completed play moves the active batch's center)
|
||||
-- and the feedback learning loop. Created with IF NOT EXISTS so it can be
|
||||
-- self-provisioned on databases that predate this schema change.
|
||||
CREATE TABLE IF NOT EXISTS play_history (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
track_id UUID REFERENCES tracks(id) ON DELETE CASCADE,
|
||||
batch_id UUID REFERENCES recommendation_batch(id) ON DELETE SET NULL,
|
||||
completed BOOLEAN NOT NULL DEFAULT false,
|
||||
played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_play_history_user_played_at ON play_history(user_id, played_at DESC);
|
||||
|
||||
-- Feedback: explicit user signals consumed by the Vibe scorer's feedback
|
||||
-- learning loop. action is one of 'promoted' | 'disliked' | 'skipped' | 'deleted_permanent'.
|
||||
CREATE TABLE IF NOT EXISTS feedback (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
track_id UUID REFERENCES tracks(id) ON DELETE CASCADE,
|
||||
action TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_feedback_user_action ON feedback(user_id, action);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS track_audio_features (
|
||||
track_id UUID PRIMARY KEY REFERENCES tracks(id) ON DELETE CASCADE,
|
||||
bpm REAL,
|
||||
key TEXT,
|
||||
energy REAL,
|
||||
danceability REAL,
|
||||
valence REAL,
|
||||
acousticness REAL,
|
||||
instrumentalness REAL,
|
||||
liveness REAL,
|
||||
valence_score REAL,
|
||||
tempo REAL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS track_lyrics (
|
||||
track_id UUID PRIMARY KEY REFERENCES tracks(id) ON DELETE CASCADE,
|
||||
lyrics_text TEXT,
|
||||
provider TEXT,
|
||||
language VARCHAR(10),
|
||||
synced_lyrics JSONB
|
||||
);
|
||||
|
||||
-- Enrichment settings: toggles that control which external-enrichment steps the
|
||||
-- worker runs. Default all to true (best-effort, credentials-permitting).
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO settings (key, value) VALUES ('enrich_metadata', 'true') ON CONFLICT (key) DO NOTHING;
|
||||
INSERT INTO settings (key, value) VALUES ('enrich_cover_art', 'true') ON CONFLICT (key) DO NOTHING;
|
||||
INSERT INTO settings (key, value) VALUES ('enrich_genres', 'true') ON CONFLICT (key) DO NOTHING;
|
||||
INSERT INTO settings (key, value) VALUES ('enrich_lyrics', 'true') ON CONFLICT (key) DO NOTHING;
|
||||
INSERT INTO settings (key, value) VALUES ('enrich_artist_similarity', 'true') ON CONFLICT (key) DO NOTHING;
|
||||
INSERT INTO settings (key, value) VALUES ('enrich_audio_analysis', 'false') ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
|
||||
-- ==========================================================================
|
||||
-- v2 Recommendation Engine — System A: Knowledge Graph (probabilistic fusion)
|
||||
-- ==========================================================================
|
||||
|
||||
-- Source trust weights. One row per source of claims. Tunable.
|
||||
CREATE TABLE IF NOT EXISTS source_trust (
|
||||
key TEXT PRIMARY KEY,
|
||||
trust REAL NOT NULL CHECK (trust >= 0 AND trust <= 1.0),
|
||||
description TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO source_trust (key, trust, description) VALUES
|
||||
('curated', 1.00, 'Manual / human-curated claim. Never decayed.'),
|
||||
('mb', 0.90, 'MusicBrainz structural spine. High-trust seed; not infallible.'),
|
||||
('cover_art_archive', 0.85, 'Cover Art Archive, MB-backed.'),
|
||||
('discogs', 0.75, 'Discogs release/artist credits.'),
|
||||
('lastfm', 0.50, 'Last.fm tags + similar. Noisy; used as weak signal.'),
|
||||
('listener_behavior', 0.40, 'Derived from observed play patterns. User-keyed.'),
|
||||
('tag', 0.30, 'File-tag-derived via scanner heuristic. Lowest trust.')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- Claims: the spine of the graph. One row per (subject, predicate, object, source).
|
||||
-- user_id is NULL for objective claims (MB, Discogs, tags), non-NULL for
|
||||
-- listener-behavior-derived claims.
|
||||
CREATE TABLE IF NOT EXISTS claims (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID,
|
||||
subject_type TEXT NOT NULL,
|
||||
subject_id UUID NOT NULL,
|
||||
predicate TEXT NOT NULL,
|
||||
object_type TEXT NOT NULL,
|
||||
object_id UUID NOT NULL,
|
||||
source TEXT NOT NULL REFERENCES source_trust(key),
|
||||
confidence REAL NOT NULL DEFAULT 1.0 CHECK (confidence >= 0 AND confidence <= 1.0),
|
||||
evidence_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
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)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_subject ON claims (subject_type, subject_id, predicate);
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_object ON claims (object_type, object_id, predicate);
|
||||
CREATE INDEX IF NOT EXISTS idx_claims_user ON claims (user_id) WHERE user_id IS NOT NULL;
|
||||
|
||||
-- recording_mbid on tracks — the structural spine anchor for the graph
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'tracks' AND column_name = 'recording_mbid'
|
||||
) THEN
|
||||
ALTER TABLE tracks ADD COLUMN recording_mbid UUID;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_recording_mbid ON tracks (recording_mbid) WHERE recording_mbid IS NOT NULL;
|
||||
|
||||
-- ==========================================================================
|
||||
-- System B: Listener Model
|
||||
-- ==========================================================================
|
||||
|
||||
-- Evidence: every observed interaction that should influence a belief.
|
||||
-- Append-only. Never edited or deleted (purge policy separate).
|
||||
CREATE TABLE IF NOT EXISTS evidence (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id UUID NOT NULL,
|
||||
signal TEXT NOT NULL,
|
||||
profile TEXT NOT NULL,
|
||||
weight REAL NOT NULL,
|
||||
context JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_evidence_user_entity ON evidence (user_id, entity_type, entity_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_evidence_user_profile ON evidence (user_id, profile, created_at DESC);
|
||||
|
||||
-- Listener beliefs: the derived state. Continuously decayed; reinforced by evidence.
|
||||
CREATE TABLE IF NOT EXISTS listener_beliefs (
|
||||
user_id UUID NOT NULL,
|
||||
profile TEXT NOT NULL,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id UUID NOT NULL,
|
||||
dimension TEXT NOT NULL,
|
||||
value REAL NOT NULL CHECK (value >= -1.0 AND value <= 1.0),
|
||||
confidence REAL NOT NULL CHECK (confidence >= 0 AND confidence <= 1.0),
|
||||
evidence_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_reinforced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_decayed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (user_id, profile, entity_type, entity_id, dimension)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_listener_beliefs_user_profile ON listener_beliefs (user_id, profile, entity_type, entity_id);
|
||||
|
||||
-- ==========================================================================
|
||||
-- System D: Session Director
|
||||
-- ==========================================================================
|
||||
|
||||
-- Per-session state; persisted across heartbeats so resumes stay coherent.
|
||||
CREATE TABLE IF NOT EXISTS session_state (
|
||||
session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_interaction TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
context TEXT,
|
||||
state_vector JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_state_user ON session_state (user_id, last_interaction DESC);
|
||||
|
||||
-- Diversity budgets for the session director's planner.
|
||||
CREATE TABLE IF NOT EXISTS diversity_budgets (
|
||||
user_id UUID NOT NULL,
|
||||
dimension TEXT NOT NULL,
|
||||
budget_share REAL NOT NULL,
|
||||
horizon_min INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, dimension, horizon_min)
|
||||
);
|
||||
|
||||
-- Adaptive minimum-distance repetition rules.
|
||||
CREATE TABLE IF NOT EXISTS repetition_rules (
|
||||
user_id UUID NOT NULL,
|
||||
dimension TEXT NOT NULL,
|
||||
min_distance INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, dimension)
|
||||
);
|
||||
|
||||
-- ==========================================================================
|
||||
-- System E: Acquisition Pipeline
|
||||
-- ==========================================================================
|
||||
|
||||
-- Discovery candidates: tracks not yet in the library, identified by E.
|
||||
CREATE TABLE IF NOT EXISTS discovery_candidates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
source TEXT NOT NULL,
|
||||
external_id TEXT NOT NULL,
|
||||
title TEXT,
|
||||
artist_credit JSONB,
|
||||
notes JSONB,
|
||||
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_eval_at TIMESTAMPTZ,
|
||||
status TEXT NOT NULL DEFAULT 'candidate',
|
||||
UNIQUE (source, external_id)
|
||||
);
|
||||
|
||||
-- Probation status for acquired tracks.
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'tracks' AND column_name = 'probation_status'
|
||||
) THEN
|
||||
ALTER TABLE tracks ADD COLUMN probation_status TEXT
|
||||
DEFAULT 'retained'
|
||||
CHECK (probation_status IN ('probation', 'retained', 'retired'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'tracks' AND column_name = 'probation_entered_at'
|
||||
) THEN
|
||||
ALTER TABLE tracks ADD COLUMN probation_entered_at TIMESTAMPTZ;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_probation ON tracks (probation_status) WHERE probation_status = 'probation';
|
||||
|
||||
-- ==========================================================================
|
||||
-- Phase 4 (preserved): Image candidates
|
||||
-- ==========================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS image_candidates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
entity_type TEXT NOT NULL CHECK (entity_type IN ('artist', 'album')),
|
||||
entity_id UUID NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
url TEXT,
|
||||
width INTEGER,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (entity_type, entity_id, source)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_image_candidates_entity ON image_candidates (entity_type, entity_id);
|
||||
Reference in New Issue
Block a user