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

Coverage
87.5% 28/32 statements
Δ
+0.0
Blob
9eb0fba
1 package db
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "time"
9 )
10
11 // IndexStamp records which revision of a space's approved branch the global
12 // bleve index currently reflects.
13 //
14 // It is the reconciler's staleness comparison, and it is why a crash between
15 // merge and reindex is a defined repair rather than a bespoke one: stamp rev
16 // != approved head means reindex. The index is a pure cache, so a missing or
17 // wrong stamp costs a rebuild, never data.
18 type IndexStamp struct {
19 SpaceID int
20 Rev string
21 IndexedAt time.Time
22 }
23
24 // GetIndexStamp returns the space's index stamp. Returns ErrNotFound when the
25 // space has never been indexed — which the reconciler must treat as "stale",
26 // not as "up to date"; that is exactly why this is an error rather than a zero
27 // value.
28 4 func (s *Store) GetIndexStamp(ctx context.Context, spaceID int) (*IndexStamp, error) {
29 4 const q = `SELECT space_id, rev, indexed_at FROM index_stamp WHERE space_id = $1`
30 4 var st IndexStamp
31 4 err := s.q.QueryRowContext(ctx, q, spaceID).Scan(&st.SpaceID, &st.Rev, &st.IndexedAt)
32 4 if errors.Is(err, sql.ErrNoRows) {
33 2 return nil, ErrNotFound
34 2 }
35 2 if err != nil {
36 0 return nil, fmt.Errorf("get index stamp space=%d: %w", spaceID, err)
37 0 }
38 2 return &st, nil
39 }
40
41 // SetIndexStamp records that the index now reflects rev for this space. Called
42 // after a rebuild completes, never before: a stamp written ahead of the rebuild
43 // would make a crash look like a fresh index.
44 3 func (s *Store) SetIndexStamp(ctx context.Context, spaceID int, rev string) (*IndexStamp, error) {
45 3 if rev == "" {
46 1 return nil, fmt.Errorf("set index stamp space=%d: rev is required", spaceID)
47 1 }
48 2 const q = `
49 2 INSERT INTO index_stamp (space_id, rev, indexed_at)
50 2 VALUES ($1, $2, $3)
51 2 ON CONFLICT (space_id) DO UPDATE
52 2 SET rev = EXCLUDED.rev, indexed_at = EXCLUDED.indexed_at
53 2 RETURNING space_id, rev, indexed_at`
54 2 var st IndexStamp
55 2 err := s.q.QueryRowContext(ctx, q, spaceID, rev, time.Now().UTC()).
56 2 Scan(&st.SpaceID, &st.Rev, &st.IndexedAt)
57 2 if err != nil {
58 0 return nil, fmt.Errorf("set index stamp space=%d: %w", spaceID, err)
59 0 }
60 2 return &st, nil
61 }
62
63 // GetDigestMark returns when the owner last looked at the digest of
64 // policy-merged content. Returns ErrNotFound if they never have.
65 //
66 // Auto-merged content that never appears in any view is write-only and rots
67 // invisibly — the exact failure this service exists to prevent, just relocated.
68 // This one timestamp is the whole of "what landed since you last looked".
69 4 func (s *Store) GetDigestMark(ctx context.Context, owner string) (time.Time, error) {
70 4 const q = `SELECT seen_at FROM digest_mark WHERE owner = $1`
71 4 var seen time.Time
72 4 err := s.q.QueryRowContext(ctx, q, owner).Scan(&seen)
73 4 if errors.Is(err, sql.ErrNoRows) {
74 2 return time.Time{}, ErrNotFound
75 2 }
76 2 if err != nil {
77 0 return time.Time{}, fmt.Errorf("get digest mark %q: %w", owner, err)
78 0 }
79 2 return seen, nil
80 }
81
82 // SetDigestMark moves the owner's "last looked at" timestamp. seenAt is passed
83 // in rather than defaulted to now() so the caller can mark the digest as of the
84 // moment it rendered the page, not the moment the write happened — anything
85 // that lands in between must still show up next time.
86 4 func (s *Store) SetDigestMark(ctx context.Context, owner string, seenAt time.Time) error {
87 4 if owner == "" {
88 1 return fmt.Errorf("set digest mark: owner is required")
89 1 }
90 3 if seenAt.IsZero() {
91 1 return fmt.Errorf("set digest mark %q: seen_at is required", owner)
92 1 }
93 2 const q = `
94 2 INSERT INTO digest_mark (owner, seen_at)
95 2 VALUES ($1, $2)
96 2 ON CONFLICT (owner) DO UPDATE SET seen_at = EXCLUDED.seen_at`
97 2 if _, err := s.q.ExecContext(ctx, q, owner, seenAt.UTC()); err != nil {
98 0 return fmt.Errorf("set digest mark %q: %w", owner, err)
99 0 }
100 2 return nil
101 }