Skip to content
quarry Artifact [b4e4a4f7a4]
Login

Artifact [b4e4a4f7a4]

Artifact b4e4a4f7a4a231c717bab031c5b252a6c635a854b62b3497fafed7ba25603ebc:


// localattach.go holds the attachments of a ticket on the FORGE tier.
//
// A local ticket lives in this database and never enters the artifact
// log until somebody promotes it, while an attachment is an A card in
// the log naming its target. So the default tier's tickets — most
// tickets on most repositories — could carry nothing: the store would
// accept an A card for a ticket the forge would never resolve, and the
// forge answered an empty list for a ticket that plainly existed
// (9e628be2). dmestas's rule: a forge-tier ticket may carry
// attachments; hold them here, and write the A cards when the ticket is
// promoted. The bytes go to the bucket exactly as for any upload; what
// this table keeps is the stub that the A card will carry — the same
// four facts attach.Stub records — plus who uploaded it and when.
package oltp

import (
	"database/sql"
	"errors"
)

const localAttachSchema = `
CREATE TABLE IF NOT EXISTS local_attachment (
	repo    TEXT NOT NULL,
	ticket  TEXT NOT NULL,      -- the local ticket's id
	name    TEXT NOT NULL,
	size    INTEGER NOT NULL,
	type    TEXT NOT NULL,
	sha256  TEXT NOT NULL,      -- the object's key in the bucket
	who     TEXT NOT NULL DEFAULT '',
	created TEXT NOT NULL,
	PRIMARY KEY (repo, ticket, name)
);`

// LocalAttachment is one file held against a forge-tier ticket: the
// stub an A card will carry at promotion, and who put it there.
type LocalAttachment struct {
	Repo    string `json:"repo"`
	Ticket  string `json:"ticket"`
	Name    string `json:"name"`
	Size    int64  `json:"size"`
	Type    string `json:"type"`
	SHA256  string `json:"sha256"`
	Who     string `json:"who"`
	Created string `json:"created"`
}

// LocalAttachmentAdd records a file against a local ticket. A second
// file of the same name replaces the first, as a second A card of the
// same name would supersede it in the log.
func (d *DB) LocalAttachmentAdd(a LocalAttachment) error {
	if a.Repo == "" || a.Ticket == "" || a.Name == "" || a.SHA256 == "" {
		return errors.New("oltp: a local attachment needs a repository, a ticket, a name and a hash")
	}
	_, err := d.w.Exec(`INSERT OR REPLACE INTO local_attachment (repo, ticket, name, size, type, sha256, who, created)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, a.Repo, a.Ticket, a.Name, a.Size, a.Type, a.SHA256, a.Who, now())
	return err
}

const localAttachCols = `repo, ticket, name, size, type, sha256, who, created`

func scanLocalAttachment(rows interface{ Scan(...any) error }) (LocalAttachment, error) {
	var a LocalAttachment
	err := rows.Scan(&a.Repo, &a.Ticket, &a.Name, &a.Size, &a.Type, &a.SHA256, &a.Who, &a.Created)
	return a, err
}

// LocalAttachments lists what a local ticket holds, oldest first.
func (d *DB) LocalAttachments(repo, ticket string) ([]LocalAttachment, error) {
	rows, err := d.db.Query(`SELECT `+localAttachCols+` FROM local_attachment
		WHERE repo = ? AND ticket = ? ORDER BY created, name`, repo, ticket)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []LocalAttachment
	for rows.Next() {
		a, err := scanLocalAttachment(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, a)
	}
	return out, rows.Err()
}

// LocalAttachmentBySHA finds a held file by the object it names — the
// address the read ops take for an upload, since a held file has no
// artifact yet.
func (d *DB) LocalAttachmentBySHA(repo, sha string) (LocalAttachment, bool, error) {
	a, err := scanLocalAttachment(d.db.QueryRow(`SELECT `+localAttachCols+` FROM local_attachment
		WHERE repo = ? AND sha256 = ? ORDER BY created LIMIT 1`, repo, sha))
	if errors.Is(err, sql.ErrNoRows) {
		return LocalAttachment{}, false, nil
	}
	return a, err == nil, err
}

// LocalAttachmentRemove drops one held file. Nothing in the log is
// touched, because nothing in the log was written.
func (d *DB) LocalAttachmentRemove(repo, ticket, name string) error {
	_, err := d.w.Exec(`DELETE FROM local_attachment WHERE repo = ? AND ticket = ? AND name = ?`, repo, ticket, name)
	return err
}

// LocalSHACount says how many held files across the repository name the
// same object, for the removal that may also delete the object.
func (d *DB) LocalSHACount(repo, sha string) (int, error) {
	var n int
	err := d.db.QueryRow(`SELECT COUNT(*) FROM local_attachment WHERE repo = ? AND sha256 = ?`, repo, sha).Scan(&n)
	return n, err
}