1package storage
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7)
8
9func (s *Store) ConfigCommit(ctx context.Context) (Checkpoint, error) {
10 return s.checkpoint(ctx, "read config commit", "config_commit")
11}
12
13func (s *Store) UserbaseCommit(ctx context.Context) (Checkpoint, error) {
14 return s.checkpoint(ctx, "read userbase commit", "userbase_commit")
15}
16
17func (s *Store) CompareAndSwapConfigCommit(ctx context.Context, expected Checkpoint, next string) error {
18 return s.compareAndSwapCheckpoint(ctx, "compare and swap config commit", "config_commit", expected, next)
19}
20
21func (s *Store) CompareAndSwapUserbaseCommit(ctx context.Context, expected Checkpoint, next string) error {
22 return s.compareAndSwapCheckpoint(ctx, "compare and swap userbase commit", "userbase_commit", expected, next)
23}
24
25func (s *Store) checkpoint(ctx context.Context, operation, column string) (Checkpoint, error) {
26 if err := s.checkOpen(operation); err != nil {
27 return Checkpoint{}, err
28 }
29 var value sql.NullString
30 query := `SELECT ` + column + ` FROM settings WHERE id = 1`
31 if err := s.sql.QueryRowContext(ctx, query).Scan(&value); err != nil {
32 return Checkpoint{}, classify(operation, err)
33 }
34 if !value.Valid {
35 return Checkpoint{}, nil
36 }
37 return Checkpoint{Value: value.String, Set: true}, nil
38}
39
40func (s *Store) compareAndSwapCheckpoint(ctx context.Context, operation, column string, expected Checkpoint, next string) error {
41 if err := s.checkOpen(operation); err != nil {
42 return err
43 }
44 if next == "" {
45 return invalid(operation, fmt.Errorf("next checkpoint is empty"))
46 }
47 if !expected.Set && expected.Value != "" {
48 return invalid(operation, fmt.Errorf("unset expected checkpoint has a value"))
49 }
50 var result sql.Result
51 var err error
52 if expected.Set {
53 result, err = s.sql.ExecContext(ctx, `UPDATE settings SET `+column+` = ? WHERE id = 1 AND `+column+` = ?`, next, expected.Value)
54 } else {
55 result, err = s.sql.ExecContext(ctx, `UPDATE settings SET `+column+` = ? WHERE id = 1 AND `+column+` IS NULL`, next)
56 }
57 if err != nil {
58 return classify(operation, err)
59 }
60 count, err := result.RowsAffected()
61 if err != nil {
62 return classify(operation+" rows affected", err)
63 }
64 if count == 0 {
65 return fmt.Errorf("%s: %w", operation, ErrConflict)
66 }
67 return nil
68}
69
70func (s *Store) ResetConfigCommit(ctx context.Context, expected Checkpoint, next string) error {
71 const operation = "reset config checkpoint"
72 if err := s.checkOpen(operation); err != nil {
73 return err
74 }
75 if !expected.Set || expected.Value == "" || next == "" {
76 return invalid(operation, fmt.Errorf("both checkpoints must be set"))
77 }
78 tx, err := s.sql.BeginTx(ctx, nil)
79 if err != nil {
80 return classify(operation, err)
81 }
82 defer tx.Rollback()
83 count, err := execCount(ctx, tx, operation, `UPDATE settings SET config_commit = ? WHERE id = 1 AND config_commit = ?`, next, expected.Value)
84 if err != nil {
85 return err
86 }
87 if count != 1 {
88 return fmt.Errorf("%s: %w", operation, ErrConflict)
89 }
90 if err := writeAudit(ctx, tx, operation, "system", "config.checkpoint_reset", "config", canonicalTime(s.now()), map[string]any{"previous": expected.Value, "commit": next}); err != nil {
91 return err
92 }
93 return classify(operation, tx.Commit())
94}