package postgres import ( "context" "errors" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/metadata-agregator/internal/domain" "github.com/metadata-agregator/internal/repository" ) type AlbumRepository struct { pool *pgxpool.Pool } func NewAlbumRepository(pool *pgxpool.Pool) *AlbumRepository { return &AlbumRepository{pool: pool} } func (r *AlbumRepository) GetByID(ctx context.Context, id string) (*domain.Album, error) { query := ` SELECT id, title, album_type, release_date, upc, total_tracks, total_discs, cover_url, source, source_id FROM albums WHERE id = $1` album, err := r.scanAlbum(ctx, query, id) if err != nil { return nil, err } if err := r.loadRelations(ctx, album); err != nil { return nil, err } return album, nil } func (r *AlbumRepository) GetByExternalID(ctx context.Context, source, sourceID string) (*domain.Album, error) { query := ` SELECT a.id, a.title, a.album_type, a.release_date, a.upc, a.total_tracks, a.total_discs, a.cover_url, a.source, a.source_id FROM albums a JOIN album_external_ids e ON a.id = e.album_id WHERE e.source = $1 AND e.source_id = $2` album, err := r.scanAlbum(ctx, query, source, sourceID) if err != nil { return nil, err } if err := r.loadRelations(ctx, album); err != nil { return nil, err } return album, nil } func (r *AlbumRepository) GetByArtistID(ctx context.Context, artistID string, limit, offset int) (*domain.SearchResult[domain.Album], error) { countQuery := ` SELECT COUNT(DISTINCT a.id) FROM albums a JOIN album_artists aa ON a.id = aa.album_id JOIN artist_external_ids ae ON aa.artist_id = ae.artist_id WHERE ae.source_id = $1` searchQuery := ` SELECT DISTINCT a.id, a.title, a.album_type, a.release_date, a.upc, a.total_tracks, a.total_discs, a.cover_url, a.source, a.source_id FROM albums a JOIN album_artists aa ON a.id = aa.album_id JOIN artist_external_ids ae ON aa.artist_id = ae.artist_id WHERE ae.source_id = $1 ORDER BY a.release_date DESC NULLS LAST LIMIT $2 OFFSET $3` var total int if err := r.pool.QueryRow(ctx, countQuery, artistID).Scan(&total); err != nil { return nil, err } rows, err := r.pool.Query(ctx, searchQuery, artistID, limit, offset) if err != nil { return nil, err } defer rows.Close() var albums []domain.Album for rows.Next() { album, err := r.scanAlbumFromRow(rows) if err != nil { return nil, err } albums = append(albums, *album) } return &domain.SearchResult[domain.Album]{ Items: albums, Total: total, Limit: limit, Offset: offset, }, nil } func (r *AlbumRepository) Save(ctx context.Context, album *domain.Album) error { tx, err := r.pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) var source, sourceID string if len(album.ExternalIDs) > 0 { source = album.ExternalIDs[0].Source sourceID = album.ExternalIDs[0].SourceID } query := ` INSERT INTO albums (id, title, album_type, release_date, upc, total_tracks, total_discs, cover_url, source, source_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title, album_type = EXCLUDED.album_type, release_date = EXCLUDED.release_date, upc = EXCLUDED.upc, total_tracks = EXCLUDED.total_tracks, total_discs = EXCLUDED.total_discs, cover_url = EXCLUDED.cover_url, updated_at = now()` _, err = tx.Exec(ctx, query, album.ID, album.Title, nullString(album.Type), album.ReleaseDate, nullString(album.UPC), album.TotalTracks, album.TotalDiscs, nullString(album.CoverURL), source, sourceID) if err != nil { return err } for _, ext := range album.ExternalIDs { extQuery := ` INSERT INTO album_external_ids (album_id, source, source_id, url) VALUES ($1, $2, $3, $4) ON CONFLICT (album_id, source, source_id) DO UPDATE SET url = EXCLUDED.url, fetched_at = now()` _, err = tx.Exec(ctx, extQuery, album.ID, ext.Source, ext.SourceID, nullString(ext.URL)) if err != nil { return err } } for _, ac := range album.Artists { artistQuery := ` INSERT INTO album_artists (album_id, artist_id, role, position) VALUES ($1, $2, $3, $4) ON CONFLICT (album_id, artist_id, role) DO NOTHING` _, err = tx.Exec(ctx, artistQuery, album.ID, ac.Artist.ID, ac.Role, ac.Position) if err != nil { return err } } return tx.Commit(ctx) } func (r *AlbumRepository) scanAlbum(ctx context.Context, query string, args ...any) (*domain.Album, error) { row := r.pool.QueryRow(ctx, query, args...) return r.scanAlbumRow(row) } func (r *AlbumRepository) scanAlbumFromRow(row pgx.Row) (*domain.Album, error) { return r.scanAlbumRow(row) } func (r *AlbumRepository) scanAlbumRow(row pgx.Row) (*domain.Album, error) { var ( album domain.Album albumType *string releaseDate *time.Time upc *string totalTracks *int totalDiscs *int coverURL *string source string sourceID *string ) err := row.Scan( &album.ID, &album.Title, &albumType, &releaseDate, &upc, &totalTracks, &totalDiscs, &coverURL, &source, &sourceID, ) if errors.Is(err, pgx.ErrNoRows) { return nil, repository.ErrNotFound } if err != nil { return nil, err } album.Type = derefString(albumType) album.ReleaseDate = releaseDate album.UPC = derefString(upc) if totalTracks != nil { album.TotalTracks = *totalTracks } if totalDiscs != nil { album.TotalDiscs = *totalDiscs } album.CoverURL = derefString(coverURL) return &album, nil } func (r *AlbumRepository) loadRelations(ctx context.Context, album *domain.Album) error { extQuery := `SELECT source, source_id, url FROM album_external_ids WHERE album_id = $1` rows, err := r.pool.Query(ctx, extQuery, album.ID) if err != nil { return err } defer rows.Close() for rows.Next() { var ext domain.ExternalID var url *string if err := rows.Scan(&ext.Source, &ext.SourceID, &url); err != nil { return err } ext.URL = derefString(url) album.ExternalIDs = append(album.ExternalIDs, ext) } return rows.Err() }