Skip to content
quarry Artifact [acb646861a]
Login

Artifact [acb646861a]

Artifact acb646861a3c475f8c7e3cc29630ca116deb57f78601e764f2d68d4c6ce57c6e:


// ticketnumber.go is the forge's ticket numbering (54c5175f, option C):
// one sequence per repository the forge owns, in creation order across
// the local tier and the store. The forge is the allocator because a
// store ticket is minted by whoever holds a clone, with no authority in
// the middle; this database is one. The hex id stays the address; the
// number is what people say.
package oltp

import (
	"database/sql"
	"errors"
)

const ticketNumberSchema = `
CREATE TABLE IF NOT EXISTS ticket_number (
	repo   TEXT NOT NULL,
	number INTEGER NOT NULL,
	ticket TEXT NOT NULL,
	PRIMARY KEY (repo, number),
	UNIQUE (repo, ticket)
);`

// TicketNumbersAssign numbers, in the order given, every ticket in ids
// that has no number yet, continuing the repository's sequence. Already
// numbered tickets keep theirs, so calling it again is harmless.
func (d *DB) TicketNumbersAssign(repo string, ids []string) error {
	if len(ids) == 0 {
		return nil
	}
	tx, err := d.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()
	var max int
	if err := tx.QueryRow(`SELECT COALESCE(MAX(number), 0) FROM ticket_number WHERE repo = ?`, repo).Scan(&max); err != nil {
		return err
	}
	for _, id := range ids {
		var have int
		err := tx.QueryRow(`SELECT number FROM ticket_number WHERE repo = ? AND ticket = ?`, repo, id).Scan(&have)
		if err == nil {
			continue
		}
		if !errors.Is(err, sql.ErrNoRows) {
			return err
		}
		max++
		if _, err := tx.Exec(`INSERT INTO ticket_number (repo, number, ticket) VALUES (?, ?, ?)`, repo, max, id); err != nil {
			return err
		}
	}
	return tx.Commit()
}

// TicketNumberClaim gives a ticket a particular number, if that number
// is free here and the ticket has none: how a forge adopts the number
// another forge already recorded on a ticket it now owns. It answers
// whether the claim took.
func (d *DB) TicketNumberClaim(repo, ticket string, n int) (bool, error) {
	res, err := d.db.Exec(`INSERT INTO ticket_number (repo, number, ticket) VALUES (?, ?, ?)
		ON CONFLICT DO NOTHING`, repo, n, ticket)
	if err != nil {
		return false, err
	}
	k, err := res.RowsAffected()
	return k == 1, err
}

// TicketNumbers is every numbered ticket of a repository, id → number.
func (d *DB) TicketNumbers(repo string) (map[string]int, error) {
	rows, err := d.db.Query(`SELECT ticket, number FROM ticket_number WHERE repo = ?`, repo)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := map[string]int{}
	for rows.Next() {
		var id string
		var n int
		if err := rows.Scan(&id, &n); err != nil {
			return nil, err
		}
		out[id] = n
	}
	return out, rows.Err()
}

// TicketNumbered is the ticket a repository's number names.
func (d *DB) TicketNumbered(repo string, n int) (string, bool, error) {
	var id string
	err := d.db.QueryRow(`SELECT ticket FROM ticket_number WHERE repo = ? AND number = ?`, repo, n).Scan(&id)
	if errors.Is(err, sql.ErrNoRows) {
		return "", false, nil
	}
	return id, err == nil, err
}

// TicketNumberRekey moves a number from one id to another — a local
// ticket's number to the artifact it was promoted into, so the name
// people used keeps naming the same ticket.
func (d *DB) TicketNumberRekey(repo, from, to string) error {
	_, err := d.db.Exec(`UPDATE ticket_number SET ticket = ? WHERE repo = ? AND ticket = ?`, to, repo, from)
	return err
}