package storage import ( "context" "database/sql" "fmt" ) func (s *Store) ConfigCommit(ctx context.Context) (Checkpoint, error) { return s.checkpoint(ctx, "read config commit", "config_commit") } func (s *Store) UserbaseCommit(ctx context.Context) (Checkpoint, error) { return s.checkpoint(ctx, "read userbase commit", "userbase_commit") } func (s *Store) CompareAndSwapConfigCommit(ctx context.Context, expected Checkpoint, next string) error { return s.compareAndSwapCheckpoint(ctx, "compare and swap config commit", "config_commit", expected, next) } func (s *Store) CompareAndSwapUserbaseCommit(ctx context.Context, expected Checkpoint, next string) error { return s.compareAndSwapCheckpoint(ctx, "compare and swap userbase commit", "userbase_commit", expected, next) } func (s *Store) checkpoint(ctx context.Context, operation, column string) (Checkpoint, error) { if err := s.checkOpen(operation); err != nil { return Checkpoint{}, err } var value sql.NullString query := `SELECT ` + column + ` FROM settings WHERE id = 1` if err := s.sql.QueryRowContext(ctx, query).Scan(&value); err != nil { return Checkpoint{}, classify(operation, err) } if !value.Valid { return Checkpoint{}, nil } return Checkpoint{Value: value.String, Set: true}, nil } func (s *Store) compareAndSwapCheckpoint(ctx context.Context, operation, column string, expected Checkpoint, next string) error { if err := s.checkOpen(operation); err != nil { return err } if next == "" { return invalid(operation, fmt.Errorf("next checkpoint is empty")) } if !expected.Set && expected.Value != "" { return invalid(operation, fmt.Errorf("unset expected checkpoint has a value")) } var result sql.Result var err error if expected.Set { result, err = s.sql.ExecContext(ctx, `UPDATE settings SET `+column+` = ? WHERE id = 1 AND `+column+` = ?`, next, expected.Value) } else { result, err = s.sql.ExecContext(ctx, `UPDATE settings SET `+column+` = ? WHERE id = 1 AND `+column+` IS NULL`, next) } if err != nil { return classify(operation, err) } count, err := result.RowsAffected() if err != nil { return classify(operation+" rows affected", err) } if count == 0 { return fmt.Errorf("%s: %w", operation, ErrConflict) } return nil } func (s *Store) ResetConfigCommit(ctx context.Context, expected Checkpoint, next string) error { const operation = "reset config checkpoint" if err := s.checkOpen(operation); err != nil { return err } if !expected.Set || expected.Value == "" || next == "" { return invalid(operation, fmt.Errorf("both checkpoints must be set")) } tx, err := s.sql.BeginTx(ctx, nil) if err != nil { return classify(operation, err) } defer tx.Rollback() count, err := execCount(ctx, tx, operation, `UPDATE settings SET config_commit = ? WHERE id = 1 AND config_commit = ?`, next, expected.Value) if err != nil { return err } if count != 1 { return fmt.Errorf("%s: %w", operation, ErrConflict) } if err := writeAudit(ctx, tx, operation, "system", "config.checkpoint_reset", "config", canonicalTime(s.now()), map[string]any{"previous": expected.Value, "commit": next}); err != nil { return err } return classify(operation, tx.Commit()) }