package personagen import ( "context" "database/sql" "errors" "fmt" "github.com/jmoiron/sqlx" "github.com/lib/pq" ) // ErrPersonaNotFound is returned when a persona ID does not exist. var ErrPersonaNotFound = errors.New("persona not found") // personaRow is the database scan target for persona queries. type personaRow struct { ID string `db:"id"` Name string `db:"name"` Handle string `db:"handle"` Gender string `db:"gender"` Description string `db:"description"` Tags pq.StringArray `db:"tags"` SpecJSON []byte `db:"spec_json"` AnchorURL *string `db:"anchor_url"` AvatarURL *string `db:"avatar_url"` BannerURL *string `db:"banner_url"` ImageURLs pq.StringArray `db:"image_urls"` VideoURLs pq.StringArray `db:"video_urls"` Status string `db:"status"` } func (r *personaRow) toRecord() *PersonaRecord { rec := &PersonaRecord{ ID: r.ID, Name: r.Name, Handle: r.Handle, Gender: r.Gender, Description: r.Description, Tags: []string(r.Tags), Status: r.Status, ImageURLs: []string(r.ImageURLs), VideoURLs: []string(r.VideoURLs), } if rec.Tags == nil { rec.Tags = []string{} } if rec.ImageURLs == nil { rec.ImageURLs = []string{} } if rec.VideoURLs == nil { rec.VideoURLs = []string{} } if len(r.SpecJSON) > 0 { rec.SpecJSON = r.SpecJSON } if r.AnchorURL != nil { rec.AnchorURL = *r.AnchorURL } if r.AvatarURL != nil { rec.AvatarURL = *r.AvatarURL } if r.BannerURL != nil { rec.BannerURL = *r.BannerURL } return rec } // PostgresPersonaStore implements PersonaStore using PostgreSQL/CockroachDB. type PostgresPersonaStore struct { db *sqlx.DB } // Compile-time interface check. var _ PersonaStore = (*PostgresPersonaStore)(nil) // NewPostgresPersonaStore creates a PersonaStore backed by a SQL database. func NewPostgresPersonaStore(db *sqlx.DB) *PostgresPersonaStore { return &PostgresPersonaStore{db: db} } func (s *PostgresPersonaStore) GetByID(ctx context.Context, id string) (*PersonaRecord, error) { var row personaRow err := s.db.QueryRowxContext(ctx, ` SELECT id, name, handle, gender, description, tags, spec_json, anchor_url, avatar_url, banner_url, image_urls, video_urls, status FROM personas WHERE id = $1 `, id).StructScan(&row) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrPersonaNotFound } return nil, fmt.Errorf("get persona: %w", err) } return row.toRecord(), nil } func (s *PostgresPersonaStore) Update(ctx context.Context, p *PersonaRecord) error { var specJSON []byte if p.SpecJSON != nil { specJSON = []byte(p.SpecJSON) } var anchorURL, avatarURL, bannerURL *string if p.AnchorURL != "" { anchorURL = &p.AnchorURL } if p.AvatarURL != "" { avatarURL = &p.AvatarURL } if p.BannerURL != "" { bannerURL = &p.BannerURL } result, err := s.db.ExecContext(ctx, ` UPDATE personas SET name = $2, handle = $3, tags = $4, spec_json = $5, anchor_url = $6, avatar_url = $7, banner_url = $8, image_urls = $9, video_urls = $10, status = $11 WHERE id = $1 `, p.ID, p.Name, p.Handle, pq.StringArray(p.Tags), specJSON, anchorURL, avatarURL, bannerURL, pq.StringArray(p.ImageURLs), pq.StringArray(p.VideoURLs), p.Status, ) if err != nil { return fmt.Errorf("update persona: %w", err) } rows, err := result.RowsAffected() if err != nil { return fmt.Errorf("update persona rows affected: %w", err) } if rows == 0 { return ErrPersonaNotFound } return nil }