Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions api/v1_users_feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,30 +50,49 @@ func (app *ApiServer) v1UsersFeed(c *fiber.Ctx) error {
),
history as (

-- Track-type reposts. Splitting from playlist-type reposts so each
-- branch can use a per-row JOIN against the entity instead of forcing
-- the planner to hash every public playlist (~94k rows) just to filter
-- a handful of repost rows.
(
SELECT
repost_type as entity_type,
'track' as entity_type,
repost_item_id as entity_id,
min(reposts.created_at) as created_at
FROM reposts
JOIN follow_set using (user_id)
LEFT JOIN tracks
ON repost_type = 'track'
AND repost_item_id = track_id
JOIN tracks ON repost_item_id = tracks.track_id
AND tracks.is_delete = false
AND tracks.is_unlisted = false
AND tracks.is_available = true
LEFT JOIN playlists
ON repost_type != 'track'
AND repost_item_id = playlist_id
WHERE
@filter in ('all', 'repost')
AND reposts.repost_type = 'track'
AND reposts.created_at < @before
AND reposts.created_at >= @before - INTERVAL '1 YEAR'
AND reposts.is_delete = false
GROUP BY entity_id
)

UNION ALL

-- Playlist/album-type reposts.
(
SELECT
reposts.repost_type::text as entity_type,
repost_item_id as entity_id,
min(reposts.created_at) as created_at
FROM reposts
JOIN follow_set using (user_id)
JOIN playlists ON repost_item_id = playlists.playlist_id
AND playlists.is_delete = false
AND playlists.is_private = false
WHERE
@filter in ('all', 'repost')
AND reposts.repost_type <> 'track'
AND reposts.created_at < @before
AND reposts.created_at >= @before - INTERVAL '1 YEAR'
AND reposts.is_delete = false
AND (tracks.track_id IS NOT NULL OR playlists.playlist_id IS NOT NULL)
GROUP BY entity_type, entity_id
)

Expand Down
66 changes: 66 additions & 0 deletions api/v1_users_feed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package api

import (
"context"
"testing"

"api.audius.co/trashid"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)

// Regression coverage for the feed query. The query was rewritten to split
// repost handling into separate track-type and playlist-type branches so the
// planner stops building a hash over every public playlist on every call —
// this test guards both branches plus the owned-track and owned-playlist
// branches.
func TestUsersFeed(t *testing.T) {
app := testAppWithFixtures(t)
app.skipAuthCheck = true

// Seed a playlist repost so Branch 1b (playlist/album reposts) executes
// alongside the existing track repost in RepostFixtures (user 1 → track
// 200). Reposts pkey is (user_id, repost_item_id, repost_type, txhash).
_, err := app.pool.Exec(context.Background(), `
INSERT INTO reposts (user_id, repost_type, repost_item_id, txhash, blockhash, blocknumber, created_at, is_delete, is_current)
VALUES (3, 'playlist', 1, 'feed-test-tx-1', 'block1', 101, now() - interval '1 hour', false, true)
`)
assert.NoError(t, err)

// User 2 follows user 1 (track repost path) and user 3 (playlist repost
// path) per fixtures. Feed should surface both.
var resp struct {
Data []struct {
Type string `json:"type"`
}
}
status, body := testGet(t, app, "/v1/users/"+trashid.MustEncodeHashID(2)+"/feed?limit=50", &resp)
assert.Equal(t, 200, status)
assert.NotEmpty(t, resp.Data, "feed for user 2 (2 followees) should not be empty")

// Spot-check the items the data contains.
titles := []string{}
for _, m := range gjson.GetBytes(body, "data.#.item.title").Array() {
titles = append(titles, m.String())
}
playlistNames := []string{}
for _, m := range gjson.GetBytes(body, "data.#.item.playlist_name").Array() {
playlistNames = append(playlistNames, m.String())
}

// Track 200 (Culca Canyon) is owned by user 2 themselves but reposted by
// user 1; it should appear via the track-repost branch.
assert.Contains(t, titles, "Culca Canyon", "feed should include track reposted by a followee")

// Playlist 1 (First) is owned by user 1 and now reposted by user 3; it
// should appear via the playlist-repost branch.
assert.Contains(t, playlistNames, "First", "feed should include playlist reposted by a followee")

// Sanity: a user with zero followees gets an empty feed.
var empty struct {
Data []any
}
status, _ = testGet(t, app, "/v1/users/"+trashid.MustEncodeHashID(99999)+"/feed?limit=10", &empty)
assert.Equal(t, 200, status)
assert.Empty(t, empty.Data)
}
Loading