fix(likes): dedupe latest state with DISTINCT ON

max(created_at)+equality-join returned both rows when two like events shared
an identical created_at (realistic: likes carry a client-supplied timestamp
from offline sync), double-counting the track. Replace with DISTINCT ON
(track_id) + deterministic tiebreaker (created_at desc, id desc) across
get_latest_state / liked-tracks / count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Цвылев Александр Вадимович
2026-07-29 10:53:34 +03:00
parent 591a938e71
commit ed77acf0fe
@@ -6,7 +6,7 @@ Likes are an append-only event log. Current state = latest event per (user, trac
import datetime as dt import datetime as dt
import uuid import uuid
from sqlalchemy import func, select from sqlalchemy import Subquery, func, select
from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -105,31 +105,52 @@ class SqlAlchemyLikeRepository:
rows = (await self._session.execute(stmt)).scalars().all() rows = (await self._session.execute(stmt)).scalars().all()
return [_to_entity(r) for r in rows] 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( async def get_latest_state(
self, *, user_id: uuid.UUID, track_ids: list[uuid.UUID] self, *, user_id: uuid.UUID, track_ids: list[uuid.UUID]
) -> list[Like]: ) -> list[Like]:
if not track_ids: if not track_ids:
return [] 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 = ( rows = (
( (
await self._session.execute( await self._session.execute(
select(LikeModel) select(LikeModel)
.join( .where(
max_sq, LikeModel.user_id == user_id,
(LikeModel.track_id == max_sq.c.track_id) LikeModel.track_id.in_(track_ids),
& (LikeModel.created_at == max_sq.c.latest), )
.distinct(LikeModel.track_id)
.order_by(
LikeModel.track_id,
LikeModel.created_at.desc(),
LikeModel.id.desc(),
) )
.where(LikeModel.user_id == user_id)
) )
) )
.scalars() .scalars()
@@ -141,31 +162,14 @@ class SqlAlchemyLikeRepository:
self, *, user_id: uuid.UUID, limit: int, offset: int self, *, user_id: uuid.UUID, limit: int, offset: int
) -> list[Track]: ) -> list[Track]:
# Tracks where the latest like event has value='like', ordered by like time desc # Tracks where the latest like event has value='like', ordered by like time desc
max_sq = ( latest_sq = self._latest_events_sq(user_id, None)
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()
)
rows = ( rows = (
( (
await self._session.execute( await self._session.execute(
select(TrackModel) select(TrackModel)
.join(liked_sq, TrackModel.id == liked_sq.c.track_id) .join(latest_sq, TrackModel.id == latest_sq.c.track_id)
.order_by(liked_sq.c.created_at.desc()) .where(latest_sq.c.value == "like")
.order_by(latest_sq.c.created_at.desc())
.limit(limit) .limit(limit)
.offset(offset) .offset(offset)
) )
@@ -176,25 +180,11 @@ class SqlAlchemyLikeRepository:
return [_track_to_entity(r) for r in rows] return [_track_to_entity(r) for r in rows]
async def count_liked_tracks(self, *, user_id: uuid.UUID) -> int: async def count_liked_tracks(self, *, user_id: uuid.UUID) -> int:
max_sq = ( latest_sq = self._latest_events_sq(user_id, None)
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()
)
return ( 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() ).scalar_one()