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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user