Raw
 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}