// Package store contains small, parameterized SQLite queries used by the service. package store import ( "context" "database/sql" "fmt" "havenllo/internal/domain" ) type DBTX interface { ExecContext(context.Context, string, ...any) (sql.Result, error) QueryContext(context.Context, string, ...any) (*sql.Rows, error) QueryRowContext(context.Context, string, ...any) *sql.Row } func Lists(ctx context.Context, q DBTX, counts bool) ([]domain.List, error) { query := `SELECT l.id, l.name, l.position` if counts { query += `, COUNT(c.id) FROM lists l LEFT JOIN cards c ON c.list_id = l.id GROUP BY l.id, l.name, l.position ORDER BY l.position, l.id` } else { query += ` FROM lists l ORDER BY l.position, l.id` } rows, err := q.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() lists := make([]domain.List, 0) for rows.Next() { var list domain.List if counts { err = rows.Scan(&list.ID, &list.Name, &list.Position, &list.CardCount) } else { err = rows.Scan(&list.ID, &list.Name, &list.Position) } if err != nil { return nil, err } lists = append(lists, list) } return lists, rows.Err() } func List(ctx context.Context, q DBTX, id int64) (domain.List, error) { var list domain.List err := q.QueryRowContext(ctx, "SELECT id, name, position FROM lists WHERE id = ?", id).Scan(&list.ID, &list.Name, &list.Position) if err == sql.ErrNoRows { return domain.List{}, domain.ErrNotFound } return list, err } func Cards(ctx context.Context, q DBTX, listID int64) ([]domain.Card, error) { rows, err := q.QueryContext(ctx, `SELECT id, list_id, title, description, position, created_at, updated_at FROM cards WHERE list_id = ? ORDER BY position, id`, listID) if err != nil { return nil, err } defer rows.Close() cards := make([]domain.Card, 0) for rows.Next() { card, err := scanCard(rows) if err != nil { return nil, err } cards = append(cards, card) } return cards, rows.Err() } func Card(ctx context.Context, q DBTX, id int64) (domain.Card, error) { row := q.QueryRowContext(ctx, `SELECT id, list_id, title, description, position, created_at, updated_at FROM cards WHERE id = ?`, id) card, err := scanCard(row) if err == sql.ErrNoRows { return domain.Card{}, domain.ErrNotFound } return card, err } type scanner interface{ Scan(...any) error } func scanCard(row scanner) (domain.Card, error) { var card domain.Card return card, row.Scan(&card.ID, &card.ListID, &card.Title, &card.Description, &card.Position, &card.CreatedAt, &card.UpdatedAt) } func Snapshot(ctx context.Context, q DBTX) (domain.Snapshot, error) { lists, err := Lists(ctx, q, false) if err != nil { return domain.Snapshot{}, err } snapshot := domain.Snapshot{Lists: make([]domain.ListWithCards, 0, len(lists))} for _, list := range lists { cards, err := Cards(ctx, q, list.ID) if err != nil { return domain.Snapshot{}, err } snapshot.Lists = append(snapshot.Lists, domain.ListWithCards{ID: list.ID, Name: list.Name, Position: list.Position, Cards: cards}) } return snapshot, nil } func MaxListPosition(ctx context.Context, q DBTX) (float64, error) { var position float64 err := q.QueryRowContext(ctx, "SELECT COALESCE(MAX(position), 0) FROM lists").Scan(&position) return position, err } func MaxCardPosition(ctx context.Context, q DBTX, listID int64) (float64, error) { var position float64 err := q.QueryRowContext(ctx, "SELECT COALESCE(MAX(position), 0) FROM cards WHERE list_id = ?", listID).Scan(&position) return position, err } func CountLists(ctx context.Context, q DBTX) (int, error) { var count int err := q.QueryRowContext(ctx, "SELECT COUNT(*) FROM lists").Scan(&count) return count, err } func MustRows(result sql.Result, err error) error { if err != nil { return err } n, err := result.RowsAffected() if err != nil { return err } if n == 0 { return domain.ErrNotFound } return nil } func Wrap(action string, err error) error { if err == nil || err == domain.ErrNotFound { return err } return fmt.Errorf("%s: %w", action, err) }