Skip to content
quarry Artifact [b797a67fc2]
Login

Artifact [b797a67fc2]

Artifact b797a67fc27b3df768b42f6cca91870377a09f898be42e8b7eeb665af09fbcf3:


// praise.go stores praise — the forge's "this is worth your attention"
// signal. Forge-local state, never part of the DAG (0001 §8), for the
// same reason forum votes are: a clone of the store must carry the work,
// not the applause.
package oltp

import (
	"errors"
	"time"
)

const praiseSchema = `
CREATE TABLE IF NOT EXISTS praise (
	repo    TEXT NOT NULL,
	account TEXT NOT NULL,
	at      TEXT NOT NULL,
	-- The key IS the idempotency, exactly as forum_vote's is: one row per
	-- account per repository. Praising twice is the same fact, and
	-- withdrawing is a delete rather than a second row saying no.
	PRIMARY KEY (repo, account)
);
CREATE INDEX IF NOT EXISTS praise_by_account ON praise (account, at DESC);`

// SetPraise records or withdraws one account's praise. Returns whether
// the repository is praised by that account afterwards, so a caller can
// name the event it raises without asking again.
func (d *DB) SetPraise(repo, account string, on bool) (bool, error) {
	if repo == "" || account == "" {
		return false, errors.New("oltp: praise needs a repo and an account")
	}
	if !on {
		_, err := d.db.Exec(`DELETE FROM praise WHERE repo = ? AND account = ?`, repo, account)
		return false, err
	}
	// Insert-or-ignore, not upsert: re-praising must not move the date,
	// or a listing ordered by when you praised something would reshuffle
	// every time somebody double-clicked.
	_, err := d.db.Exec(`INSERT OR IGNORE INTO praise (repo, account, at) VALUES (?, ?, ?)`,
		repo, account, time.Now().UTC().Format(time.RFC3339))
	return true, err
}

// Praised reports whether one account praises one repository.
func (d *DB) Praised(repo, account string) bool {
	if account == "" {
		return false
	}
	var one int
	err := d.db.QueryRow(`SELECT 1 FROM praise WHERE repo = ? AND account = ?`,
		repo, account).Scan(&one)
	return err == nil
}

// PraiseCount is how many accounts praise a repository.
func (d *DB) PraiseCount(repo string) int {
	var n int
	if err := d.db.QueryRow(`SELECT count(*) FROM praise WHERE repo = ?`, repo).Scan(&n); err != nil {
		return 0
	}
	return n
}

// PraisedBy lists the repositories one account praises, newest first.
//
// It returns names only. Deciding which of them the VIEWER may see is
// the caller's job and must not be skipped: praise is public, but a
// private repository's existence is not, and the two meet here. An
// account's own list and a stranger's view of it are different lists.
func (d *DB) PraisedBy(account string) ([]string, error) {
	if account == "" {
		return nil, nil
	}
	rows, err := d.db.Query(`SELECT repo FROM praise WHERE account = ? ORDER BY at DESC`, account)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []string
	for rows.Next() {
		var repo string
		if err := rows.Scan(&repo); err != nil {
			return nil, err
		}
		out = append(out, repo)
	}
	return out, rows.Err()
}