diff --git a/app/infrastructure/db/repositories/like_repository.py b/app/infrastructure/db/repositories/like_repository.py index 25c7916..5a0ed9c 100644 --- a/app/infrastructure/db/repositories/like_repository.py +++ b/app/infrastructure/db/repositories/like_repository.py @@ -6,7 +6,7 @@ Likes are an append-only event log. Current state = latest event per (user, trac import datetime as dt import uuid -from sqlalchemy import func, select +from sqlalchemy import Subquery, func, select from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession @@ -105,31 +105,52 @@ class SqlAlchemyLikeRepository: rows = (await self._session.execute(stmt)).scalars().all() return [_to_entity(r) for r in rows] + def _latest_events_sq( + self, user_id: uuid.UUID, track_ids: list[uuid.UUID] | None + ) -> Subquery: + """The latest like event per ``track_id`` for a user, as a subquery. + + ``DISTINCT ON (track_id)`` with a deterministic tiebreaker (``created_at`` + then ``id``) picks exactly one row per track even when two events share an + identical ``created_at`` — likes carry a client-supplied timestamp from + offline sync, so ties are realistic and a plain ``max()``+equality-join + would return both rows (double-counting the track).""" + stmt = select( + LikeModel.track_id, + LikeModel.value.label("value"), + LikeModel.created_at.label("created_at"), + ).where(LikeModel.user_id == user_id) + if track_ids is not None: + stmt = stmt.where(LikeModel.track_id.in_(track_ids)) + return ( + stmt.distinct(LikeModel.track_id) + .order_by( + LikeModel.track_id, + LikeModel.created_at.desc(), + LikeModel.id.desc(), + ) + .subquery() + ) + async def get_latest_state( self, *, user_id: uuid.UUID, track_ids: list[uuid.UUID] ) -> list[Like]: if not track_ids: return [] - # Subquery: max(created_at) per track for this user - max_sq = ( - select( - LikeModel.track_id, - func.max(LikeModel.created_at).label("latest"), - ) - .where(LikeModel.user_id == user_id, LikeModel.track_id.in_(track_ids)) - .group_by(LikeModel.track_id) - .subquery() - ) rows = ( ( await self._session.execute( select(LikeModel) - .join( - max_sq, - (LikeModel.track_id == max_sq.c.track_id) - & (LikeModel.created_at == max_sq.c.latest), + .where( + LikeModel.user_id == user_id, + LikeModel.track_id.in_(track_ids), + ) + .distinct(LikeModel.track_id) + .order_by( + LikeModel.track_id, + LikeModel.created_at.desc(), + LikeModel.id.desc(), ) - .where(LikeModel.user_id == user_id) ) ) .scalars() @@ -141,31 +162,14 @@ class SqlAlchemyLikeRepository: self, *, user_id: uuid.UUID, limit: int, offset: int ) -> list[Track]: # Tracks where the latest like event has value='like', ordered by like time desc - max_sq = ( - select( - LikeModel.track_id, - func.max(LikeModel.created_at).label("latest"), - ) - .where(LikeModel.user_id == user_id) - .group_by(LikeModel.track_id) - .subquery() - ) - liked_sq = ( - select(LikeModel.track_id, LikeModel.created_at) - .join( - max_sq, - (LikeModel.track_id == max_sq.c.track_id) - & (LikeModel.created_at == max_sq.c.latest), - ) - .where(LikeModel.user_id == user_id, LikeModel.value == "like") - .subquery() - ) + latest_sq = self._latest_events_sq(user_id, None) rows = ( ( await self._session.execute( select(TrackModel) - .join(liked_sq, TrackModel.id == liked_sq.c.track_id) - .order_by(liked_sq.c.created_at.desc()) + .join(latest_sq, TrackModel.id == latest_sq.c.track_id) + .where(latest_sq.c.value == "like") + .order_by(latest_sq.c.created_at.desc()) .limit(limit) .offset(offset) ) @@ -176,25 +180,11 @@ class SqlAlchemyLikeRepository: return [_track_to_entity(r) for r in rows] async def count_liked_tracks(self, *, user_id: uuid.UUID) -> int: - max_sq = ( - select( - LikeModel.track_id, - func.max(LikeModel.created_at).label("latest"), - ) - .where(LikeModel.user_id == user_id) - .group_by(LikeModel.track_id) - .subquery() - ) - liked_sq = ( - select(LikeModel.track_id) - .join( - max_sq, - (LikeModel.track_id == max_sq.c.track_id) - & (LikeModel.created_at == max_sq.c.latest), - ) - .where(LikeModel.user_id == user_id, LikeModel.value == "like") - .subquery() - ) + latest_sq = self._latest_events_sq(user_id, None) return ( - await self._session.execute(select(func.count()).select_from(liked_sq)) + await self._session.execute( + select(func.count()) + .select_from(latest_sq) + .where(latest_sq.c.value == "like") + ) ).scalar_one()