Skip to content
quarry Artifact [7b5b0c8582]
Login

Artifact [7b5b0c8582]

Artifact 7b5b0c858266cc7830388a23a29de7058228e0cf31c3af21a7d12db39d1b9a22:


package oltp

import (
	"database/sql"
	"errors"
	"time"

	"craftdesign.group/quarry/internal/uv"
)

// unversionedSchema is RFC 0020's catalog of a repository's
// unversioned files: one row per name, the newest version or the
// tombstone that removed it.
//
// The bucket is the record and this is a copy of it, kept so a listing
// and the sync front's uv-hash never list the bucket on a request path.
// unversioned_source says which bucket the copy was built from: a
// repository with no source row, or one whose bound bucket is not the
// one named, is rebuilt from a listing before it is read (RFC 0001
// ยง11's disposability, passed the way RFC 0016 passes it).
const unversionedSchema = `
CREATE TABLE IF NOT EXISTS unversioned (
	repo    TEXT NOT NULL,
	name    TEXT NOT NULL,
	mtime   INTEGER NOT NULL,          -- fossil's, seconds since 1970
	hash    TEXT NOT NULL DEFAULT '',  -- '' for a tombstone
	size    INTEGER NOT NULL DEFAULT 0,
	deleted INTEGER NOT NULL DEFAULT 0,
	who     TEXT NOT NULL DEFAULT '',  -- uploader, sync login, or mirror:<host>
	at      TEXT NOT NULL,             -- when quarry recorded it
	PRIMARY KEY (repo, name)
);
CREATE TABLE IF NOT EXISTS unversioned_source (
	repo     TEXT PRIMARY KEY,
	source   TEXT NOT NULL,            -- endpoint and bucket the catalog was built from
	built_at TEXT NOT NULL
);`

// UVFiles is a repository's catalog, tombstones included, by name.
func (d *DB) UVFiles(repo string) ([]uv.File, error) {
	rows, err := d.db.Query(`SELECT name, mtime, hash, size, deleted, who, at
		FROM unversioned WHERE repo = ? ORDER BY name`, repo)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []uv.File{}
	for rows.Next() {
		var f uv.File
		var deleted int
		if err := rows.Scan(&f.Name, &f.Mtime, &f.Hash, &f.Size, &deleted, &f.Who, &f.At); err != nil {
			return nil, err
		}
		f.Deleted = deleted != 0
		out = append(out, f)
	}
	return out, rows.Err()
}

// UVFile is one name's row.
func (d *DB) UVFile(repo, name string) (uv.File, bool, error) {
	return uvFile(d.db, repo, name)
}

type queryRower interface {
	QueryRow(query string, args ...any) *sql.Row
}

func uvFile(q queryRower, repo, name string) (uv.File, bool, error) {
	var f uv.File
	var deleted int
	err := q.QueryRow(`SELECT name, mtime, hash, size, deleted, who, at
		FROM unversioned WHERE repo = ? AND name = ?`, repo, name).
		Scan(&f.Name, &f.Mtime, &f.Hash, &f.Size, &deleted, &f.Who, &f.At)
	if errors.Is(err, sql.ErrNoRows) {
		return uv.File{}, false, nil
	}
	f.Deleted = deleted != 0
	return f, err == nil, err
}

// PutUVFile records a version of one name if fossil's rule says it
// replaces what the catalog holds (uv.Replaces), and reports whether it
// did. The read and the write share one transaction, so two writers of
// one name cannot both win.
func (d *DB) PutUVFile(repo string, f uv.File) (bool, error) {
	tx, err := d.w.Begin()
	if err != nil {
		return false, err
	}
	defer tx.Rollback()
	have, exists, err := uvFile(tx, repo, f.Name)
	if err != nil {
		return false, err
	}
	if !uv.Replaces(f, have, exists) {
		return false, nil
	}
	if err := insertUVFile(tx, repo, f); err != nil {
		return false, err
	}
	return true, tx.Commit()
}

func insertUVFile(tx *sql.Tx, repo string, f uv.File) error {
	if f.Deleted {
		f.Hash, f.Size = "", 0
	}
	if f.At == "" {
		f.At = time.Now().UTC().Format(time.RFC3339)
	}
	_, err := tx.Exec(`INSERT OR REPLACE INTO unversioned (repo, name, mtime, hash, size, deleted, who, at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, repo, f.Name, f.Mtime, f.Hash, f.Size, boolInt(f.Deleted), f.Who, f.At)
	return err
}

// UVSource is the bucket a repository's catalog was built from; false
// when it was never built, which is what makes the next read rebuild it.
func (d *DB) UVSource(repo string) (string, bool, error) {
	var src string
	err := d.db.QueryRow(`SELECT source FROM unversioned_source WHERE repo = ?`, repo).Scan(&src)
	if errors.Is(err, sql.ErrNoRows) {
		return "", false, nil
	}
	return src, err == nil, err
}

// ReplaceUVCatalog swaps a repository's whole catalog for one read
// from source, in one transaction, so a reader sees the old catalog or
// the new one and never half of each.
func (d *DB) ReplaceUVCatalog(repo, source string, files []uv.File) error {
	tx, err := d.w.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()
	if _, err := tx.Exec(`DELETE FROM unversioned WHERE repo = ?`, repo); err != nil {
		return err
	}
	for _, f := range files {
		if err := insertUVFile(tx, repo, f); err != nil {
			return err
		}
	}
	if _, err := tx.Exec(`INSERT OR REPLACE INTO unversioned_source (repo, source, built_at) VALUES (?, ?, ?)`,
		repo, source, time.Now().UTC().Format(time.RFC3339)); err != nil {
		return err
	}
	return tx.Commit()
}

// DropUVCatalog forgets a repository's catalog and where it came from.
// Nothing in the bucket changes; the next read rebuilds from it.
func (d *DB) DropUVCatalog(repo string) error {
	tx, err := d.w.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()
	if _, err := tx.Exec(`DELETE FROM unversioned WHERE repo = ?`, repo); err != nil {
		return err
	}
	if _, err := tx.Exec(`DELETE FROM unversioned_source WHERE repo = ?`, repo); err != nil {
		return err
	}
	return tx.Commit()
}