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