-- M2 listening telemetry: play_sessions, play_events, skip_events. -- Note the table is named play_sessions (not sessions — that table already -- exists from migration 0004 and holds HTTP auth sessions). Listening -- sessions are an entirely separate concept driven by spec §6. -- -- session_vector_at_play and contextual_likes ship as nullable now even -- though M2 writes NULL; M3 will populate them without a follow-up -- migration. CREATE TABLE play_sessions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, started_at timestamptz NOT NULL, ended_at timestamptz, last_event_at timestamptz NOT NULL, track_count integer NOT NULL DEFAULT 0, client_id text ); CREATE INDEX play_sessions_user_last_event_idx ON play_sessions (user_id, last_event_at DESC); CREATE TABLE play_events ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, track_id uuid NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, session_id uuid NOT NULL REFERENCES play_sessions(id) ON DELETE CASCADE, started_at timestamptz NOT NULL, ended_at timestamptz, duration_played_ms integer, completion_ratio double precision, was_skipped boolean NOT NULL DEFAULT false, client_id text, session_vector_at_play jsonb, scrobbled_at timestamptz ); CREATE INDEX play_events_user_started_idx ON play_events (user_id, started_at DESC); CREATE INDEX play_events_user_track_idx ON play_events (user_id, track_id); CREATE TABLE skip_events ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, track_id uuid NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, session_id uuid NOT NULL REFERENCES play_sessions(id) ON DELETE CASCADE, skipped_at timestamptz NOT NULL, position_ms integer NOT NULL ); CREATE INDEX skip_events_user_skipped_idx ON skip_events (user_id, skipped_at DESC);