diff --git a/internal/db/dbq/contextual_likes.sql.go b/internal/db/dbq/contextual_likes.sql.go new file mode 100644 index 00000000..7543cc6e --- /dev/null +++ b/internal/db/dbq/contextual_likes.sql.go @@ -0,0 +1,52 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: contextual_likes.sql + +package dbq + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const insertContextualLike = `-- name: InsertContextualLike :exec +INSERT INTO contextual_likes (user_id, track_id, session_vector, session_id) +VALUES ($1, $2, $3, $4) +` + +type InsertContextualLikeParams struct { + UserID pgtype.UUID + TrackID pgtype.UUID + SessionVector []byte + SessionID pgtype.UUID +} + +func (q *Queries) InsertContextualLike(ctx context.Context, arg InsertContextualLikeParams) error { + _, err := q.db.Exec(ctx, insertContextualLike, + arg.UserID, + arg.TrackID, + arg.SessionVector, + arg.SessionID, + ) + return err +} + +const softDeleteContextualLikesForUserTrack = `-- name: SoftDeleteContextualLikesForUserTrack :exec +UPDATE contextual_likes +SET deleted_at = now() +WHERE user_id = $1 AND track_id = $2 AND deleted_at IS NULL +` + +type SoftDeleteContextualLikesForUserTrackParams struct { + UserID pgtype.UUID + TrackID pgtype.UUID +} + +// Marks all currently-active rows for (user, track) as deleted. Idempotent — +// already-deleted rows aren't re-touched. +func (q *Queries) SoftDeleteContextualLikesForUserTrack(ctx context.Context, arg SoftDeleteContextualLikesForUserTrackParams) error { + _, err := q.db.Exec(ctx, softDeleteContextualLikesForUserTrack, arg.UserID, arg.TrackID) + return err +} diff --git a/internal/db/dbq/models.go b/internal/db/dbq/models.go index 4d6c4199..4de19781 100644 --- a/internal/db/dbq/models.go +++ b/internal/db/dbq/models.go @@ -29,6 +29,16 @@ type Artist struct { UpdatedAt pgtype.Timestamptz } +type ContextualLike struct { + ID pgtype.UUID + UserID pgtype.UUID + TrackID pgtype.UUID + LikedAt pgtype.Timestamptz + DeletedAt pgtype.Timestamptz + SessionVector []byte + SessionID pgtype.UUID +} + type GeneralLike struct { UserID pgtype.UUID TrackID pgtype.UUID diff --git a/internal/db/migrations/0007_contextual_likes.down.sql b/internal/db/migrations/0007_contextual_likes.down.sql new file mode 100644 index 00000000..942b4de2 --- /dev/null +++ b/internal/db/migrations/0007_contextual_likes.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS contextual_likes; diff --git a/internal/db/migrations/0007_contextual_likes.up.sql b/internal/db/migrations/0007_contextual_likes.up.sql new file mode 100644 index 00000000..93882b5d --- /dev/null +++ b/internal/db/migrations/0007_contextual_likes.up.sql @@ -0,0 +1,32 @@ +-- contextual_likes captures a per-session-context snapshot at the time of +-- each like. The recommendation engine in M3 sub-plan #3 uses these rows +-- to compute contextual_match_score per (current session, candidate track). +-- +-- Multiple rows per (user, track) are allowed and expected — each row +-- represents a like in a specific session context. Soft-deleted rows +-- remain in the table (deleted_at populated) but are filtered out of the +-- engine's queries via the partial index. Re-liking a previously-unliked +-- track adds a NEW row (does not undelete the old one). +-- +-- Note: the M2 events migration (0005) commented that contextual_likes +-- "ships nullable now" but the actual CREATE TABLE was missed. This slice +-- ships the table for the first time. + +CREATE TABLE contextual_likes ( + 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, + liked_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz, + session_vector jsonb, + session_id uuid REFERENCES play_sessions(id) ON DELETE SET NULL +); + +-- Hot path for both the engine's lookups and the soft-delete UPDATE. +CREATE INDEX contextual_likes_active_idx + ON contextual_likes (user_id, track_id) + WHERE deleted_at IS NULL; + +-- Vector similarity queries from M3 sub-plan #3 will use this. +CREATE INDEX contextual_likes_vector_idx + ON contextual_likes USING gin (session_vector); diff --git a/internal/db/queries/contextual_likes.sql b/internal/db/queries/contextual_likes.sql new file mode 100644 index 00000000..2a99793a --- /dev/null +++ b/internal/db/queries/contextual_likes.sql @@ -0,0 +1,10 @@ +-- name: InsertContextualLike :exec +INSERT INTO contextual_likes (user_id, track_id, session_vector, session_id) +VALUES ($1, $2, $3, $4); + +-- name: SoftDeleteContextualLikesForUserTrack :exec +-- Marks all currently-active rows for (user, track) as deleted. Idempotent — +-- already-deleted rows aren't re-touched. +UPDATE contextual_likes +SET deleted_at = now() +WHERE user_id = $1 AND track_id = $2 AND deleted_at IS NULL;