package db import ( "context" "database/sql" "errors" "fmt" "time" "github.com/lib/pq" "sourcecraft.dev/bigbes/sr-ht-spec/core" ) // Space is one bare git repo: the unit of ownership, ACL and review policy. // The repo on disk is the real thing; this row exists so spaces can be listed // and so index bookkeeping has something to hang off. Ref.Owner never carries // the leading '~'. type Space struct { ID int Ref core.SpaceRef Created time.Time } const spaceSelect = `SELECT id, owner, name, created FROM space` func scanSpace(sc rowScanner) (*Space, error) { var sp Space if err := sc.Scan(&sp.ID, &sp.Ref.Owner, &sp.Ref.Name, &sp.Created); err != nil { return nil, err } return &sp, nil } // CreateSpace inserts a space row. The name is validated with core before it // reaches SQL: an owner or space name that is not a safe path segment would // become a directory under the repos root, so it is rejected at every door // rather than at whichever one happens to check. // // A uq_space_owner_name violation maps to ErrSpaceExists. func (s *Store) CreateSpace(ctx context.Context, ref core.SpaceRef) (*Space, error) { if err := core.ValidateOwner(ref.Owner); err != nil { return nil, err } if err := core.ValidateSpaceName(ref.Name); err != nil { return nil, err } const q = ` INSERT INTO space (owner, name, created) VALUES ($1, $2, $3) RETURNING id, created` var sp Space sp.Ref = ref err := s.q.QueryRowContext(ctx, q, ref.Owner, ref.Name, time.Now().UTC()). Scan(&sp.ID, &sp.Created) if err != nil { var pqErr *pq.Error if errors.As(err, &pqErr) && pqErr.Code == "23505" { return nil, fmt.Errorf("%w: %s", ErrSpaceExists, ref) } return nil, fmt.Errorf("insert space %s: %w", ref, err) } return &sp, nil } // GetSpace resolves a space by owner and name. Returns ErrNotFound if no such // space exists. func (s *Store) GetSpace(ctx context.Context, ref core.SpaceRef) (*Space, error) { q := spaceSelect + ` WHERE owner = $1 AND name = $2` sp, err := scanSpace(s.q.QueryRowContext(ctx, q, ref.Owner, ref.Name)) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, fmt.Errorf("get space %s: %w", ref, err) } return sp, nil } // GetSpaceByID resolves a space by its primary key — the form every other table // references it by. Returns ErrNotFound if no such space exists. func (s *Store) GetSpaceByID(ctx context.Context, id int) (*Space, error) { q := spaceSelect + ` WHERE id = $1` sp, err := scanSpace(s.q.QueryRowContext(ctx, q, id)) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, fmt.Errorf("get space %d: %w", id, err) } return sp, nil } // ListSpaces returns every space, ordered by owner then name. There is one human // on this instance and no visibility levels, so there is nothing to filter by: // the list is the whole corpus, which is also exactly what the meta-project (a // filter that excludes nothing) needs. func (s *Store) ListSpaces(ctx context.Context) ([]*Space, error) { q := spaceSelect + ` ORDER BY owner, name` rows, err := s.q.QueryContext(ctx, q) if err != nil { return nil, fmt.Errorf("list spaces: %w", err) } defer rows.Close() var spaces []*Space for rows.Next() { sp, err := scanSpace(rows) if err != nil { return nil, fmt.Errorf("scan space: %w", err) } spaces = append(spaces, sp) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate spaces: %w", err) } return spaces, nil }