feat: add db functions for counting new items

This commit is contained in:
Gabe Farrell 2025-12-29 17:53:01 -05:00
parent ef27dce5e3
commit 3b585f748a
9 changed files with 235 additions and 24 deletions

View file

@ -4,7 +4,7 @@ VALUES ($1, $2, $3)
RETURNING *;
-- name: GetArtist :one
SELECT
SELECT
a.*,
array_agg(aa.alias)::text[] AS aliases
FROM artists_with_name a
@ -13,7 +13,7 @@ WHERE a.id = $1
GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name;
-- name: GetTrackArtists :many
SELECT
SELECT
a.*,
at.is_primary as is_primary
FROM artists_with_name a
@ -25,7 +25,7 @@ GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name, at.is_primary;
SELECT * FROM artists WHERE image = $1 LIMIT 1;
-- name: GetReleaseArtists :many
SELECT
SELECT
a.*,
ar.is_primary as is_primary
FROM artists_with_name a
@ -35,7 +35,7 @@ GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name, ar.is_primary;
-- name: GetArtistByName :one
WITH artist_with_aliases AS (
SELECT
SELECT
a.*,
COALESCE(array_agg(aa.alias), '{}')::text[] AS aliases
FROM artists_with_name a
@ -48,7 +48,7 @@ WITH artist_with_aliases AS (
SELECT * FROM artist_with_aliases;
-- name: GetArtistByMbzID :one
SELECT
SELECT
a.*,
array_agg(aa.alias)::text[] AS aliases
FROM artists_with_name a
@ -78,6 +78,17 @@ FROM listens l
JOIN artist_tracks at ON l.track_id = at.track_id
WHERE l.listened_at BETWEEN $1 AND $2;
-- name: CountNewArtists :one
SELECT COUNT(*) AS total_count
FROM (
SELECT at.artist_id
FROM listens l
JOIN tracks t ON l.track_id = t.id
JOIN artist_tracks at ON t.id = at.track_id
GROUP BY at.artist_id
HAVING MIN(l.listened_at) BETWEEN $1 AND $2
) first_appearances;
-- name: UpdateArtistMbzID :exec
UPDATE artists SET musicbrainz_id = $2
WHERE id = $1;
@ -111,4 +122,4 @@ SET artist_id = $2
WHERE artist_id = $1;
-- name: DeleteArtist :exec
DELETE FROM artists WHERE id = $1;
DELETE FROM artists WHERE id = $1;

View file

@ -4,7 +4,7 @@ VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: GetRelease :one
SELECT
SELECT
*,
get_artists_for_release(id) AS artists
FROM releases_with_title
@ -69,10 +69,20 @@ WHERE l.listened_at BETWEEN $1 AND $2;
-- name: CountReleasesFromArtist :one
SELECT COUNT(*)
FROM releases r
FROM releases r
JOIN artist_releases ar ON r.id = ar.release_id
WHERE ar.artist_id = $1;
-- name: CountNewReleases :one
SELECT COUNT(*) AS total_count
FROM (
SELECT t.release_id
FROM listens l
JOIN tracks t ON l.track_id = t.id
GROUP BY t.release_id
HAVING MIN(l.listened_at) BETWEEN $1 AND $2
) first_appearances;
-- name: AssociateArtistToRelease :exec
INSERT INTO artist_releases (artist_id, release_id, is_primary)
VALUES ($1, $2, $3)
@ -82,8 +92,8 @@ ON CONFLICT DO NOTHING;
SELECT
r.*,
get_artists_for_release(r.id) AS artists
FROM releases_with_title r
WHERE r.image IS NULL
FROM releases_with_title r
WHERE r.image IS NULL
AND r.id > $2
ORDER BY r.id ASC
LIMIT $1;
@ -107,8 +117,8 @@ WHERE id = $1;
-- name: DeleteRelease :exec
DELETE FROM releases WHERE id = $1;
-- name: DeleteReleasesFromArtist :exec
-- name: DeleteReleasesFromArtist :exec
DELETE FROM releases r
USING artist_releases ar
WHERE ar.release_id = r.id
AND ar.artist_id = $1;
AND ar.artist_id = $1;

View file

@ -9,7 +9,7 @@ VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING;
-- name: GetTrack :one
SELECT
SELECT
t.*,
get_artists_for_track(t.id) AS artists,
r.image
@ -109,6 +109,15 @@ JOIN tracks t ON l.track_id = t.id
WHERE l.listened_at BETWEEN $1 AND $2
AND t.release_id = $3;
-- name: CountNewTracks :one
SELECT COUNT(*) AS total_count
FROM (
SELECT track_id
FROM listens
GROUP BY track_id
HAVING MIN(listened_at) BETWEEN $1 AND $2
) first_appearances;
-- name: UpdateTrackMbzID :exec
UPDATE tracks SET musicbrainz_id = $2
WHERE id = $1;
@ -126,4 +135,4 @@ UPDATE artist_tracks SET is_primary = $3
WHERE artist_id = $1 AND track_id = $2;
-- name: DeleteTrack :exec
DELETE FROM tracks WHERE id = $1;
DELETE FROM tracks WHERE id = $1;

View file

@ -67,6 +67,9 @@ type DB interface {
CountTracks(ctx context.Context, timeframe Timeframe) (int64, error)
CountAlbums(ctx context.Context, timeframe Timeframe) (int64, error)
CountArtists(ctx context.Context, timeframe Timeframe) (int64, error)
CountNewTracks(ctx context.Context, timeframe Timeframe) (int64, error)
CountNewAlbums(ctx context.Context, timeframe Timeframe) (int64, error)
CountNewArtists(ctx context.Context, timeframe Timeframe) (int64, error)
CountTimeListened(ctx context.Context, timeframe Timeframe) (int64, error)
CountTimeListenedToItem(ctx context.Context, opts TimeListenedOpts) (int64, error)
CountUsers(ctx context.Context) (int64, error)

View file

@ -142,3 +142,60 @@ func (p *Psql) CountTimeListenedToItem(ctx context.Context, opts db.TimeListened
}
return 0, errors.New("CountTimeListenedToItem: an id must be provided")
}
func (p *Psql) CountNewTracks(ctx context.Context, timeframe db.Timeframe) (int64, error) {
var t1, t2 time.Time
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now()
t1 = db.StartTimeFromPeriod(timeframe.Period)
} else {
t1 = time.Unix(timeframe.T1u, 0)
t2 = time.Unix(timeframe.T2u, 0)
}
count, err := p.q.CountNewTracks(ctx, repository.CountNewTracksParams{
ListenedAt: t1,
ListenedAt_2: t2,
})
if err != nil {
return 0, fmt.Errorf("CountNewTracks: %w", err)
}
return count, nil
}
func (p *Psql) CountNewAlbums(ctx context.Context, timeframe db.Timeframe) (int64, error) {
var t1, t2 time.Time
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now()
t1 = db.StartTimeFromPeriod(timeframe.Period)
} else {
t1 = time.Unix(timeframe.T1u, 0)
t2 = time.Unix(timeframe.T2u, 0)
}
count, err := p.q.CountNewReleases(ctx, repository.CountNewReleasesParams{
ListenedAt: t1,
ListenedAt_2: t2,
})
if err != nil {
return 0, fmt.Errorf("CountNewAlbums: %w", err)
}
return count, nil
}
func (p *Psql) CountNewArtists(ctx context.Context, timeframe db.Timeframe) (int64, error) {
var t1, t2 time.Time
if timeframe.T1u == 0 && timeframe.T2u == 0 {
t2 = time.Now()
t1 = db.StartTimeFromPeriod(timeframe.Period)
} else {
t1 = time.Unix(timeframe.T1u, 0)
t2 = time.Unix(timeframe.T2u, 0)
}
count, err := p.q.CountNewArtists(ctx, repository.CountNewArtistsParams{
ListenedAt: t1,
ListenedAt_2: t2,
})
if err != nil {
return 0, fmt.Errorf("CountNewArtists: %w", err)
}
return count, nil
}

View file

@ -3,6 +3,7 @@ package psql_test
import (
"context"
"testing"
"time"
"github.com/gabehf/koito/internal/db"
"github.com/stretchr/testify/assert"
@ -35,6 +36,23 @@ func TestCountTracks(t *testing.T) {
truncateTestData(t)
}
func TestCountNewTracks(t *testing.T) {
ctx := context.Background()
testDataForTopItems(t)
testDataAbsoluteListenTimes(t)
// Test CountTracks
t1, _ := time.Parse(time.DateOnly, "2025-01-01")
t1u := t1.Unix()
t2, _ := time.Parse(time.DateOnly, "2025-12-31")
t2u := t2.Unix()
count, err := store.CountNewTracks(ctx, db.Timeframe{T1u: t1u, T2u: t2u})
require.NoError(t, err)
assert.Equal(t, int64(1), count, "expected tracks count to match inserted data")
truncateTestData(t)
}
func TestCountAlbums(t *testing.T) {
ctx := context.Background()
testDataForTopItems(t)
@ -48,6 +66,23 @@ func TestCountAlbums(t *testing.T) {
truncateTestData(t)
}
func TestCountNewAlbums(t *testing.T) {
ctx := context.Background()
testDataForTopItems(t)
testDataAbsoluteListenTimes(t)
// Test CountTracks
t1, _ := time.Parse(time.DateOnly, "2025-01-01")
t1u := t1.Unix()
t2, _ := time.Parse(time.DateOnly, "2025-12-31")
t2u := t2.Unix()
count, err := store.CountNewAlbums(ctx, db.Timeframe{T1u: t1u, T2u: t2u})
require.NoError(t, err)
assert.Equal(t, int64(1), count, "expected albums count to match inserted data")
truncateTestData(t)
}
func TestCountArtists(t *testing.T) {
ctx := context.Background()
testDataForTopItems(t)
@ -61,6 +96,23 @@ func TestCountArtists(t *testing.T) {
truncateTestData(t)
}
func TestCountNewArtists(t *testing.T) {
ctx := context.Background()
testDataForTopItems(t)
testDataAbsoluteListenTimes(t)
// Test CountTracks
t1, _ := time.Parse(time.DateOnly, "2025-01-01")
t1u := t1.Unix()
t2, _ := time.Parse(time.DateOnly, "2025-12-31")
t2u := t2.Unix()
count, err := store.CountNewArtists(ctx, db.Timeframe{T1u: t1u, T2u: t2u})
require.NoError(t, err)
assert.Equal(t, int64(1), count, "expected artists count to match inserted data")
truncateTestData(t)
}
func TestCountTimeListened(t *testing.T) {
ctx := context.Background()
testDataForTopItems(t)

View file

@ -13,6 +13,30 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const countNewArtists = `-- name: CountNewArtists :one
SELECT COUNT(*) AS total_count
FROM (
SELECT at.artist_id
FROM listens l
JOIN tracks t ON l.track_id = t.id
JOIN artist_tracks at ON t.id = at.track_id
GROUP BY at.artist_id
HAVING MIN(l.listened_at) BETWEEN $1 AND $2
) first_appearances
`
type CountNewArtistsParams struct {
ListenedAt time.Time
ListenedAt_2 time.Time
}
func (q *Queries) CountNewArtists(ctx context.Context, arg CountNewArtistsParams) (int64, error) {
row := q.db.QueryRow(ctx, countNewArtists, arg.ListenedAt, arg.ListenedAt_2)
var total_count int64
err := row.Scan(&total_count)
return total_count, err
}
const countTopArtists = `-- name: CountTopArtists :one
SELECT COUNT(DISTINCT at.artist_id) AS total_count
FROM listens l
@ -78,7 +102,7 @@ func (q *Queries) DeleteConflictingArtistTracks(ctx context.Context, arg DeleteC
}
const getArtist = `-- name: GetArtist :one
SELECT
SELECT
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
array_agg(aa.alias)::text[] AS aliases
FROM artists_with_name a
@ -127,7 +151,7 @@ func (q *Queries) GetArtistByImage(ctx context.Context, image *uuid.UUID) (Artis
}
const getArtistByMbzID = `-- name: GetArtistByMbzID :one
SELECT
SELECT
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
array_agg(aa.alias)::text[] AS aliases
FROM artists_with_name a
@ -161,7 +185,7 @@ func (q *Queries) GetArtistByMbzID(ctx context.Context, musicbrainzID *uuid.UUID
const getArtistByName = `-- name: GetArtistByName :one
WITH artist_with_aliases AS (
SELECT
SELECT
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
COALESCE(array_agg(aa.alias), '{}')::text[] AS aliases
FROM artists_with_name a
@ -198,7 +222,7 @@ func (q *Queries) GetArtistByName(ctx context.Context, alias string) (GetArtistB
}
const getReleaseArtists = `-- name: GetReleaseArtists :many
SELECT
SELECT
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
ar.is_primary as is_primary
FROM artists_with_name a
@ -307,7 +331,7 @@ func (q *Queries) GetTopArtistsPaginated(ctx context.Context, arg GetTopArtistsP
}
const getTrackArtists = `-- name: GetTrackArtists :many
SELECT
SELECT
a.id, a.musicbrainz_id, a.image, a.image_source, a.name,
at.is_primary as is_primary
FROM artists_with_name a

View file

@ -30,9 +30,32 @@ func (q *Queries) AssociateArtistToRelease(ctx context.Context, arg AssociateArt
return err
}
const countNewReleases = `-- name: CountNewReleases :one
SELECT COUNT(*) AS total_count
FROM (
SELECT t.release_id
FROM listens l
JOIN tracks t ON l.track_id = t.id
GROUP BY t.release_id
HAVING MIN(l.listened_at) BETWEEN $1 AND $2
) first_appearances
`
type CountNewReleasesParams struct {
ListenedAt time.Time
ListenedAt_2 time.Time
}
func (q *Queries) CountNewReleases(ctx context.Context, arg CountNewReleasesParams) (int64, error) {
row := q.db.QueryRow(ctx, countNewReleases, arg.ListenedAt, arg.ListenedAt_2)
var total_count int64
err := row.Scan(&total_count)
return total_count, err
}
const countReleasesFromArtist = `-- name: CountReleasesFromArtist :one
SELECT COUNT(*)
FROM releases r
FROM releases r
JOIN artist_releases ar ON r.id = ar.release_id
WHERE ar.artist_id = $1
`
@ -86,7 +109,7 @@ func (q *Queries) DeleteReleasesFromArtist(ctx context.Context, artistID int32)
}
const getRelease = `-- name: GetRelease :one
SELECT
SELECT
id, musicbrainz_id, image, various_artists, image_source, title,
get_artists_for_release(id) AS artists
FROM releases_with_title
@ -213,8 +236,8 @@ const getReleasesWithoutImages = `-- name: GetReleasesWithoutImages :many
SELECT
r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title,
get_artists_for_release(r.id) AS artists
FROM releases_with_title r
WHERE r.image IS NULL
FROM releases_with_title r
WHERE r.image IS NULL
AND r.id > $2
ORDER BY r.id ASC
LIMIT $1

View file

@ -29,6 +29,28 @@ func (q *Queries) AssociateArtistToTrack(ctx context.Context, arg AssociateArtis
return err
}
const countNewTracks = `-- name: CountNewTracks :one
SELECT COUNT(*) AS total_count
FROM (
SELECT track_id
FROM listens
GROUP BY track_id
HAVING MIN(listened_at) BETWEEN $1 AND $2
) first_appearances
`
type CountNewTracksParams struct {
ListenedAt time.Time
ListenedAt_2 time.Time
}
func (q *Queries) CountNewTracks(ctx context.Context, arg CountNewTracksParams) (int64, error) {
row := q.db.QueryRow(ctx, countNewTracks, arg.ListenedAt, arg.ListenedAt_2)
var total_count int64
err := row.Scan(&total_count)
return total_count, err
}
const countTopTracks = `-- name: CountTopTracks :one
SELECT COUNT(DISTINCT l.track_id) AS total_count
FROM listens l
@ -343,7 +365,7 @@ func (q *Queries) GetTopTracksPaginated(ctx context.Context, arg GetTopTracksPag
}
const getTrack = `-- name: GetTrack :one
SELECT
SELECT
t.id, t.musicbrainz_id, t.duration, t.release_id, t.title,
get_artists_for_track(t.id) AS artists,
r.image