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 ArtistRepository struct { pool *pgxpool.Pool } func NewArtistRepository(pool *pgxpool.Pool) *ArtistRepository { return &ArtistRepository{pool: pool} } func (r *ArtistRepository) GetByID(ctx context.Context, id string) (*domain.Artist, error) { query := ` SELECT id, name, sort_name, artist_type, country, formed_date, disbanded_date, description, image_url, source, source_id FROM artists WHERE id = $1` artist, err := r.scanArtist(ctx, query, id) if err != nil { return nil, err } if err := r.loadExternalIDs(ctx, artist); err != nil { return nil, err } return artist, nil } func (r *ArtistRepository) GetByExternalID(ctx context.Context, source, sourceID string) (*domain.Artist, error) { query := ` SELECT a.id, a.name, a.sort_name, a.artist_type, a.country, a.formed_date, a.disbanded_date, a.description, a.image_url, a.source, a.source_id FROM artists a JOIN artist_external_ids e ON a.id = e.artist_id WHERE e.source = $1 AND e.source_id = $2` artist, err := r.scanArtist(ctx, query, source, sourceID) if err != nil { return nil, err } if err := r.loadExternalIDs(ctx, artist); err != nil { return nil, err } return artist, nil } func (r *ArtistRepository) Search(ctx context.Context, query string, limit, offset int) (*domain.SearchResult[domain.Artist], error) { countQuery := `SELECT COUNT(*) FROM artists WHERE name ILIKE $1` searchQuery := ` SELECT id, name, sort_name, artist_type, country, formed_date, disbanded_date, description, image_url, source, source_id FROM artists WHERE name ILIKE $1 ORDER BY name LIMIT $2 OFFSET $3` pattern := "%" + query + "%" var total int if err := r.pool.QueryRow(ctx, countQuery, pattern).Scan(&total); err != nil { return nil, err } rows, err := r.pool.Query(ctx, searchQuery, pattern, limit, offset) if err != nil { return nil, err } defer rows.Close() var artists []domain.Artist for rows.Next() { artist, err := r.scanArtistFromRow(rows) if err != nil { return nil, err } artists = append(artists, *artist) } return &domain.SearchResult[domain.Artist]{ Items: artists, Total: total, Limit: limit, Offset: offset, }, nil } func (r *ArtistRepository) Save(ctx context.Context, artist *domain.Artist) error { tx, err := r.pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) var source, sourceID string if len(artist.ExternalIDs) > 0 { source = artist.ExternalIDs[0].Source sourceID = artist.ExternalIDs[0].SourceID } query := ` INSERT INTO artists (id, name, sort_name, artist_type, country, formed_date, disbanded_date, description, image_url, source, source_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, sort_name = EXCLUDED.sort_name, artist_type = EXCLUDED.artist_type, country = EXCLUDED.country, formed_date = EXCLUDED.formed_date, disbanded_date = EXCLUDED.disbanded_date, description = EXCLUDED.description, image_url = EXCLUDED.image_url, updated_at = now()` _, err = tx.Exec(ctx, query, artist.ID, artist.Name, nullString(artist.SortName), nullString(artist.Type), nullString(artist.Country), artist.FormedDate, artist.DisbandedDate, nullString(artist.Description), nullString(artist.ImageURL), source, sourceID) if err != nil { return err } for _, ext := range artist.ExternalIDs { extQuery := ` INSERT INTO artist_external_ids (artist_id, source, source_id, url) VALUES ($1, $2, $3, $4) ON CONFLICT (artist_id, source, source_id) DO UPDATE SET url = EXCLUDED.url, fetched_at = now()` _, err = tx.Exec(ctx, extQuery, artist.ID, ext.Source, ext.SourceID, nullString(ext.URL)) if err != nil { return err } } return tx.Commit(ctx) } func (r *ArtistRepository) scanArtist(ctx context.Context, query string, args ...any) (*domain.Artist, error) { row := r.pool.QueryRow(ctx, query, args...) var ( artist domain.Artist sortName *string artistType *string country *string formedDate *time.Time disbandDate *time.Time description *string imageURL *string source string sourceID *string ) err := row.Scan( &artist.ID, &artist.Name, &sortName, &artistType, &country, &formedDate, &disbandDate, &description, &imageURL, &source, &sourceID, ) if errors.Is(err, pgx.ErrNoRows) { return nil, repository.ErrNotFound } if err != nil { return nil, err } artist.SortName = derefString(sortName) artist.Type = derefString(artistType) artist.Country = derefString(country) artist.FormedDate = formedDate artist.DisbandedDate = disbandDate artist.Description = derefString(description) artist.ImageURL = derefString(imageURL) return &artist, nil } func (r *ArtistRepository) scanArtistFromRow(row pgx.Row) (*domain.Artist, error) { var ( artist domain.Artist sortName *string artistType *string country *string formedDate *time.Time disbandDate *time.Time description *string imageURL *string source string sourceID *string ) err := row.Scan( &artist.ID, &artist.Name, &sortName, &artistType, &country, &formedDate, &disbandDate, &description, &imageURL, &source, &sourceID, ) if err != nil { return nil, err } artist.SortName = derefString(sortName) artist.Type = derefString(artistType) artist.Country = derefString(country) artist.FormedDate = formedDate artist.DisbandedDate = disbandDate artist.Description = derefString(description) artist.ImageURL = derefString(imageURL) return &artist, nil } func (r *ArtistRepository) loadExternalIDs(ctx context.Context, artist *domain.Artist) error { query := `SELECT source, source_id, url FROM artist_external_ids WHERE artist_id = $1` rows, err := r.pool.Query(ctx, query, artist.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) artist.ExternalIDs = append(artist.ExternalIDs, ext) } return rows.Err() } func nullString(s string) *string { if s == "" { return nil } return &s } func derefString(s *string) string { if s == nil { return "" } return *s }