package beads import ( "context" "regexp" "sort" "strings" "time" "sourcecraft.dev/bigbes/sr-ht-dolt/browse" ) // --- which database owns "-" -------------------------------------- // // A global-tracker issue that says "blocked by artifacts-nex.2" is naming a row // in another database, and without this the reader has to work out which one and // go there by hand. Two levels of tracker only pay for themselves if they point // at each other. // // This file answers the one question that needs several databases: given an // id-shaped string in some text, which database — of the ones this caller may // browse — owns it. Whether the answer becomes a link, and what that link looks // like, is the renderer's business; this package renders nothing. // // Authorization is not here, exactly as it is not in ReadyAcross: handing this // function a database is the statement that the caller may read it. A database // the caller may not browse is one the caller's index has never heard of, which // is what makes an id belonging to it plain text and not a hint that it exists. // prefixKey is the config row every beads database stores its own id prefix in // ("global", "artifacts", "sr-ht-dolt", …). It sits in the same config table the // memories do, beside compact_tier2_days and the rest of the tracker's settings. const prefixKey = "issue_prefix" // PrefixCache is the prefix index's projection cache: one prefix per database, // gated on the head hash and expiring on ReadyCacheTTL — /ready's bounds and not // a second set (see cache.go). The zero value is usable, so a holder that has no // constructor can carry one as a field. type PrefixCache struct { projectionCache[string] } // PrefixIndex is prefix → the database that owns it, over the databases one // caller may browse. It is built per caller and must not be shared between them: // what it holds is precisely the set of databases that caller is allowed to know // exists. type PrefixIndex struct { byPrefix map[string]ReadyDatabase // Failed lists the databases that could not be read. It is here for the // caller's log and for nothing else: a database that could not be read costs // the ids it owns their link, and a page may not say more than that. Failed []ReadyFailure } // Reference is one id found in a text: where it sits, what it says, and which // database owns it. type Reference struct { Start, End int // byte offsets of the id within the scanned text ID string // the id exactly as written Database ReadyDatabase } // idPattern matches an id-shaped token: a prefix of one or more lowercase // alphanumeric segments joined by hyphens, then a hyphen, then the suffix bd // generates — `[0-9a-z]+` with optional dotted parts, which is the `46c.2` // subtask form. // // The suffix carries no hyphen, so the last hyphen of a token is always the // prefix/suffix boundary and the split below is unambiguous even for a prefix // that is itself hyphenated ("sr-ht-dolt-44n.6" is sr-ht-dolt's 44n.6, never // sr-ht's dolt-44n.6). The word boundaries keep a token from being found inside // a longer word, so "Xartifacts-46c" names nothing. // // The token being id-shaped is not what makes it an id: only a prefix the index // knows makes it one. Everything else is left as it was written. var idPattern = regexp.MustCompile(`\b[0-9a-z_]+(?:-[0-9a-z_]+)*-[0-9a-z]+(?:\.[0-9a-z]+)*\b`) // PrefixesAcross builds the index over the databases it is handed: for each, the // issue_prefix its config names. // // It is bounded exactly as ReadyAcross is — the same head-hash gate, the same // TTL, the same ceiling — because it is the same read pattern: N stores opened // for one request. It is cheaper per database, though: the projection is one row // of a dozen-row table, and the fingerprint is not asked for separately, since a // config table carrying issue_prefix is itself the statement that this is a bd // tracker. A database with no such row simply owns no prefix, and that answer is // cached like any other. // // now is the clock the TTL is measured against, passed in rather than read here // for the reason ReadyAcross takes one: this package reads no hidden clock. // // It returns no error. A database that cannot be opened or read costs the ids it // owns their link and nothing else; it lands in Failed for the caller's log. func PrefixesAcross( ctx context.Context, dbs []ReadyDatabase, open ReadyOpener, cache *PrefixCache, now time.Time, ) *PrefixIndex { index := &PrefixIndex{byPrefix: map[string]ReadyDatabase{}} if len(dbs) > ReadyMaxDatabases { // The first Max in the order the caller listed them. The caller puts the // database whose page this is first, so the one prefix a page cannot do // without is the one the ceiling can never drop. dbs = dbs[:ReadyMaxDatabases] } // A prefix two databases both claim is dropped rather than awarded to // whichever was listed first: linking to one of two candidates would be a // guess rendered as a fact, and the id is readable as text either way. ambiguous := map[string]bool{} for _, d := range dbs { prefix, err := databasePrefix(ctx, d, open, cache, now) if err != nil { index.Failed = append(index.Failed, ReadyFailure{Database: d, Err: err}) continue } if prefix == "" { continue } if ambiguous[prefix] { continue } if _, taken := index.byPrefix[prefix]; taken { ambiguous[prefix] = true delete(index.byPrefix, prefix) continue } index.byPrefix[prefix] = d } return index } // databasePrefix returns one database's issue prefix, from the cache when the // head has not moved and by reading config otherwise. // // The order is the whole point of the gate: open, list branches, and only then // consult the cache. Opening a session and reading the branch list is cheap; // reading rows is not, and on a hit no row is touched. func databasePrefix( ctx context.Context, d ReadyDatabase, open ReadyOpener, cache *PrefixCache, now time.Time, ) (string, error) { sess, err := open(ctx, d) if err != nil { return "", err } defer sess.Close() branches, err := sess.Branches(ctx) if err != nil { return "", err } ref := browse.DefaultBranch(branches) if ref == "" { // A store with no branches carries no config either: nothing to read and // nothing to cache. return "", nil } head := headHashOf(branches, ref) if prefix, ok := cache.lookup(d.ID, head, now); ok { return prefix, nil } prefix, err := readPrefix(ctx, sess, ref) if err != nil { return "", err } cache.store(d.ID, head, now, prefix) return prefix, nil } // readPrefix reads the issue_prefix row out of a database's config table. A // missing table — this is not a bd tracker — is no prefix, the treatment every // optional table gets here. func readPrefix(ctx context.Context, sess BrowseSession, ref string) (string, error) { rows, _, err := readRowsOptional(ctx, sess, ref, memoryTable) if err != nil { return "", err } if rows == nil { return "", nil } cols := indexCols(rows.Columns) for _, r := range rows.Rows { if cell(cols, r, "key") != prefixKey { continue } // Lowercased because that is the case the ids themselves are written in, // and the index is looked up by what the text says. return strings.ToLower(strings.TrimSpace(cell(cols, r, "value"))), nil } return "", nil } // Lookup returns the database owning prefix. A nil index — no database was // readable, or the caller did not build one for this page — knows no prefix, // which is the same answer as an unknown one: not linked. func (ix *PrefixIndex) Lookup(prefix string) (ReadyDatabase, bool) { if ix == nil { return ReadyDatabase{}, false } d, ok := ix.byPrefix[prefix] return d, ok } // Prefixes lists the prefixes the index knows, in order. It exists for the // caller's log line and for tests. func (ix *PrefixIndex) Prefixes() []string { if ix == nil { return nil } out := make([]string, 0, len(ix.byPrefix)) for p := range ix.byPrefix { out = append(out, p) } sort.Strings(out) return out } // Scan returns every id in text whose prefix this index knows, in order and // non-overlapping. // // It answers where the ids are and nothing about how they should be rendered: // the caller escapes the text it was handed and wraps these ranges, which is the // only order in which stored text can become HTML safely. // // An id-shaped token whose prefix the index does not know yields nothing at all, // which is what makes an id in a database the caller may not browse // indistinguishable from one that matches nothing. func (ix *PrefixIndex) Scan(text string) []Reference { if ix == nil || len(ix.byPrefix) == 0 || text == "" { return nil } var out []Reference for _, m := range idPattern.FindAllStringIndex(text, -1) { token := text[m[0]:m[1]] cut := strings.LastIndex(token, "-") if cut <= 0 { continue } d, ok := ix.byPrefix[token[:cut]] if !ok { continue } out = append(out, Reference{Start: m[0], End: m[1], ID: token, Database: d}) } return out }