// 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.db.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.db.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
}