// Package store holds the data models and all SQL queries. // // Store talks to the control database (users, blog registry) and hands out a // BlogStore per blog, which is bound to that blog's own database — nothing a // BlogStore does can reach another blog's rows. package store import ( "context" "errors" "github.com/gramanas/blogspace/internal/db" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" ) var ErrNotFound = errors.New("not found") var ErrConflict = errors.New("already exists") type Store struct { db *pgxpool.Pool // control database cluster *db.Cluster } func New(cluster *db.Cluster) *Store { return &Store{db: cluster.Control(), cluster: cluster} } // BlogStore runs the content queries of one blog against its database. type BlogStore struct { db *pgxpool.Pool } // Open returns the blog's store and fills in the settings (title, tagline, // theme) kept in the blog database. func (s *Store) Open(ctx context.Context, b *Blog) (*BlogStore, error) { pool, err := s.cluster.Blog(ctx, b.DBName) if err != nil { return nil, err } bs := &BlogStore{db: pool} if err := bs.loadSettings(ctx, b); err != nil { // A database restored underneath a running app (dropdb --force, // createdb, psql < dump) leaves the pool holding dead connections; // drop them and try once more before giving up. pool.Reset() if err := bs.loadSettings(ctx, b); err != nil { return nil, err } } return bs, nil } // wrap maps driver errors onto the store's sentinel errors. func wrap(err error) error { if err == nil { return nil } if errors.Is(err, pgx.ErrNoRows) { return ErrNotFound } var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23505" { // unique_violation return ErrConflict } return err } // querier is what the helpers shared with transactions need: the pool or a pgx.Tx. type querier interface { Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) }