Skip to content
quarry Artifact [3f014371c2]
Login

Artifact [3f014371c2]

Artifact 3f014371c2e69ccac72f16be7315961ea36095d96d6264e8d997f74fa743e3b4:


// chatref.go records that a chat message referenced an artifact
// (RFC 0011 ยง14). Chat is a window, not the DAG: these rows live and
// die with the messages they describe, which is why an artifact page
// shows their count as a window-bounded "chat" row beside the durable
// MentionRefs the index derives โ€” the two look different because they
// ARE different, and a reader who cannot tell them apart will read a
// vanished count as data loss.
package oltp

const chatRefSchema = `
CREATE TABLE IF NOT EXISTS chat_ref (
	repo   TEXT NOT NULL,
	target TEXT NOT NULL, -- artifact hash or thread root
	seq    INTEGER NOT NULL,
	at     TEXT NOT NULL,
	PRIMARY KEY (repo, target, seq)
);
CREATE INDEX IF NOT EXISTS chat_ref_seq ON chat_ref (repo, seq);`

// RecordChatRef notes one reference, idempotently on (repo, target,
// seq): the same message replayed says the same thing, and a replay
// must never double a count.
func (d *DB) RecordChatRef(repo, target string, seq uint64, at string) error {
	if repo == "" || target == "" || seq == 0 {
		return nil
	}
	_, err := d.w.Exec(`INSERT OR IGNORE INTO chat_ref (repo, target, seq, at)
		VALUES (?, ?, ?, ?)`, repo, target, seq, at)
	return err
}

// PruneChatRefs drops rows for messages the channel window no longer
// holds. It runs on the post path rather than on a timer: the window
// only ever moves when something is posted, so a sweep with no post to
// trigger it would have nothing to do.
func (d *DB) PruneChatRefs(repo string, firstSeq uint64) error {
	if repo == "" || firstSeq == 0 {
		return nil
	}
	_, err := d.w.Exec(`DELETE FROM chat_ref WHERE repo = ? AND seq < ?`, repo, firstSeq)
	return err
}

// ChatRefs answers how often a target has been named in chat and when
// last โ€” count, the latest message's seq (the permalink to land on),
// and its time. A target nobody has mentioned answers zero, not an
// error: "no chat about this" is an answer.
func (d *DB) ChatRefs(repo, target string) (count int, lastSeq uint64, lastAt string, err error) {
	row := d.db.QueryRow(`SELECT count(*), COALESCE(max(seq), 0),
			COALESCE((SELECT at FROM chat_ref i WHERE i.repo = o.repo AND i.target = o.target
				ORDER BY i.seq DESC LIMIT 1), '')
		FROM chat_ref o WHERE repo = ? AND target = ? GROUP BY repo, target`, repo, target)
	err = row.Scan(&count, &lastSeq, &lastAt)
	if err != nil && err.Error() == "sql: no rows in result set" {
		return 0, 0, "", nil
	}
	return count, lastSeq, lastAt, err
}

const chatReplySchema = `
CREATE TABLE IF NOT EXISTS chat_reply (
	repo    TEXT NOT NULL,
	ref_seq INTEGER NOT NULL, -- the message being answered
	seq     INTEGER NOT NULL, -- the answer
	PRIMARY KEY (repo, ref_seq, seq)
);
CREATE INDEX IF NOT EXISTS chat_reply_seq ON chat_reply (repo, seq);`

// RecordChatReply notes that seq answers refSeq. The row exists so a
// preview card can say "3 replies" without reading the channel to its
// tip: a hover is anonymous and constant, and an unbounded scan per
// hover is a channel drain wearing a card's clothes.
func (d *DB) RecordChatReply(repo string, refSeq, seq uint64) error {
	if repo == "" || refSeq == 0 || seq == 0 {
		return nil
	}
	_, err := d.w.Exec(`INSERT OR IGNORE INTO chat_reply (repo, ref_seq, seq)
		VALUES (?, ?, ?)`, repo, refSeq, seq)
	return err
}

// DropChatReply forgets one answer โ€” a deleted message is not a reply
// anybody can see, and a count that kept it would disagree with the
// folded log the page renders.
func (d *DB) DropChatReply(repo string, seq uint64) error {
	if repo == "" || seq == 0 {
		return nil
	}
	_, err := d.w.Exec(`DELETE FROM chat_reply WHERE repo = ? AND seq = ?`, repo, seq)
	return err
}

// PruneChatReplies drops rows the channel window no longer holds, on
// the same rule as PruneChatRefs.
func (d *DB) PruneChatReplies(repo string, firstSeq uint64) error {
	if repo == "" || firstSeq == 0 {
		return nil
	}
	_, err := d.w.Exec(`DELETE FROM chat_reply WHERE repo = ? AND seq < ?`, repo, firstSeq)
	return err
}

// ChatReplyCount answers how many visible replies a message has.
func (d *DB) ChatReplyCount(repo string, refSeq uint64) (int, error) {
	var n int
	err := d.db.QueryRow(`SELECT count(*) FROM chat_reply WHERE repo = ? AND ref_seq = ?`,
		repo, refSeq).Scan(&n)
	return n, err
}