~bigbes/sr-ht-dolt

ref: 35875ff76df1efe232d436a3041a8e621cdab418 sr-ht-dolt/beads/prefixes.go -rw-r--r-- 8.9 KiB
35875ff7 — Eugene Blikh web: stop printing store paths to the owner 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
package beads

import (
	"context"
	"regexp"
	"sort"
	"strings"
	"time"

	"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
)

// --- which database owns "<prefix>-<id>" --------------------------------------
//
// 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
}