coverage~bigbes/sr-ht-spec64cae3afdb/space.go

Coverage
84.8% 39/46 statements
Δ
+0.0
Blob
88a2d88
1 package db
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "time"
9
10 "github.com/lib/pq"
11
12 "sourcecraft.dev/bigbes/sr-ht-spec/core"
13 )
14
15 // Space is one bare git repo: the unit of ownership, ACL and review policy.
16 // The repo on disk is the real thing; this row exists so spaces can be listed
17 // and so index bookkeeping has something to hang off. Ref.Owner never carries
18 // the leading '~'.
19 type Space struct {
20 ID int
21 Ref core.SpaceRef
22 Created time.Time
23 }
24
25 const spaceSelect = `SELECT id, owner, name, created FROM space`
26
27 13 func scanSpace(sc rowScanner) (*Space, error) {
28 13 var sp Space
29 13 if err := sc.Scan(&sp.ID, &sp.Ref.Owner, &sp.Ref.Name, &sp.Created); err != nil {
30 2 return nil, err
31 2 }
32 11 return &sp, nil
33 }
34
35 // CreateSpace inserts a space row. The name is validated with core before it
36 // reaches SQL: an owner or space name that is not a safe path segment would
37 // become a directory under the repos root, so it is rejected at every door
38 // rather than at whichever one happens to check.
39 //
40 // A uq_space_owner_name violation maps to ErrSpaceExists.
41 37 func (s *Store) CreateSpace(ctx context.Context, ref core.SpaceRef) (*Space, error) {
42 37 if err := core.ValidateOwner(ref.Owner); err != nil {
43 0 return nil, err
44 0 }
45 37 if err := core.ValidateSpaceName(ref.Name); err != nil {
46 1 return nil, err
47 1 }
48 36 const q = `
49 36 INSERT INTO space (owner, name, created)
50 36 VALUES ($1, $2, $3)
51 36 RETURNING id, created`
52 36 var sp Space
53 36 sp.Ref = ref
54 36 err := s.q.QueryRowContext(ctx, q, ref.Owner, ref.Name, time.Now().UTC()).
55 36 Scan(&sp.ID, &sp.Created)
56 36 if err != nil {
57 1 var pqErr *pq.Error
58 1 if errors.As(err, &pqErr) && pqErr.Code == "23505" {
59 1 return nil, fmt.Errorf("%w: %s", ErrSpaceExists, ref)
60 1 }
61 0 return nil, fmt.Errorf("insert space %s: %w", ref, err)
62 }
63 35 return &sp, nil
64 }
65
66 // GetSpace resolves a space by owner and name. Returns ErrNotFound if no such
67 // space exists.
68 2 func (s *Store) GetSpace(ctx context.Context, ref core.SpaceRef) (*Space, error) {
69 2 q := spaceSelect + ` WHERE owner = $1 AND name = $2`
70 2 sp, err := scanSpace(s.q.QueryRowContext(ctx, q, ref.Owner, ref.Name))
71 2 if errors.Is(err, sql.ErrNoRows) {
72 1 return nil, ErrNotFound
73 1 }
74 1 if err != nil {
75 0 return nil, fmt.Errorf("get space %s: %w", ref, err)
76 0 }
77 1 return sp, nil
78 }
79
80 // GetSpaceByID resolves a space by its primary key — the form every other table
81 // references it by. Returns ErrNotFound if no such space exists.
82 4 func (s *Store) GetSpaceByID(ctx context.Context, id int) (*Space, error) {
83 4 q := spaceSelect + ` WHERE id = $1`
84 4 sp, err := scanSpace(s.q.QueryRowContext(ctx, q, id))
85 4 if errors.Is(err, sql.ErrNoRows) {
86 1 return nil, ErrNotFound
87 1 }
88 3 if err != nil {
89 0 return nil, fmt.Errorf("get space %d: %w", id, err)
90 0 }
91 3 return sp, nil
92 }
93
94 // ListSpaces returns every space, ordered by owner then name. There is one human
95 // on this instance and no visibility levels, so there is nothing to filter by:
96 // the list is the whole corpus, which is also exactly what the meta-project (a
97 // filter that excludes nothing) needs.
98 1 func (s *Store) ListSpaces(ctx context.Context) ([]*Space, error) {
99 1 q := spaceSelect + ` ORDER BY owner, name`
100 1 rows, err := s.q.QueryContext(ctx, q)
101 1 if err != nil {
102 0 return nil, fmt.Errorf("list spaces: %w", err)
103 0 }
104 1 defer rows.Close()
105 1 var spaces []*Space
106 3 for rows.Next() {
107 3 sp, err := scanSpace(rows)
108 3 if err != nil {
109 0 return nil, fmt.Errorf("scan space: %w", err)
110 0 }
111 3 spaces = append(spaces, sp)
112 }
113 1 if err := rows.Err(); err != nil {
114 0 return nil, fmt.Errorf("iterate spaces: %w", err)
115 0 }
116 1 return spaces, nil
117 }