Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Index the rest of the metadata branch

7f547ca3b8da218249120d3c910c73c9a1a1e52d · Keenan Nemetz · 10d ago · parent c9202f7f

Index the rest of the metadata branch

The index held issues and their numbers. Everything else on the branch —
change requests, their comments, approvals and closes, and release
artifacts — was read from git on every page that needed it, and the
repository header read the whole requests subtree just to print a count.

Add the tables and derive them from the same commit:

- requests, with the body preserved for search;
- request_closes and request_approvals, each in its own table so a
  backlink and a current-approval count are queries rather than a walk of
  every request;
- comments for both kinds of entity, with the anchor a review comment
  carries;
- artifacts, the release pointers, with the size from the pointer.

The refresh is still driven by the path delta between two commits: a
changed issue or request replaces its own rows, its comments and its sets,
and a deleted one removes them while keeping its `#42` number. Rebuild and
the first read go through IndexRepo, which is the definition the
incremental path has to reproduce; a rebuild now clears the derived tables
first so a deleted repository leaves nothing behind.

Schema version 4 drops the old rows on first open, which is the existing
recovery path: the index is disposable and built from the branch.

forge/index/index.go +91−8

@@ -108,7 +108,75 @@ CREATE TABLE IF NOT EXISTS entity_numbers (
108 108 id TEXT NOT NULL,
109 109 PRIMARY KEY (repo_id, number)
110 110 );
111 CREATE INDEX IF NOT EXISTS entity_numbers_id ON entity_numbers(repo_id, id);`); err != nil {
111 CREATE INDEX IF NOT EXISTS entity_numbers_id ON entity_numbers(repo_id, id);
112
113 CREATE TABLE IF NOT EXISTS requests (
114 repo_id TEXT NOT NULL,
115 id TEXT NOT NULL,
116 title TEXT NOT NULL,
117 state TEXT NOT NULL,
118 author TEXT NOT NULL,
119 created INTEGER NOT NULL,
120 base TEXT NOT NULL DEFAULT '',
121 head TEXT NOT NULL DEFAULT '',
122 head_ref TEXT NOT NULL DEFAULT '',
123 merged_base TEXT NOT NULL DEFAULT '',
124 merged_head TEXT NOT NULL DEFAULT '',
125 merged_by TEXT NOT NULL DEFAULT '',
126 merged_at INTEGER NOT NULL DEFAULT 0,
127 body TEXT NOT NULL DEFAULT '',
128 comments INTEGER NOT NULL DEFAULT 0,
129 PRIMARY KEY (repo_id, id)
130 );
131 CREATE TABLE IF NOT EXISTS request_closes (
132 repo_id TEXT NOT NULL,
133 request_id TEXT NOT NULL,
134 issue_id TEXT NOT NULL,
135 PRIMARY KEY (repo_id, request_id, issue_id)
136 );
137 CREATE INDEX IF NOT EXISTS request_closes_issue ON request_closes(repo_id, issue_id);
138 CREATE TABLE IF NOT EXISTS request_approvals (
139 repo_id TEXT NOT NULL,
140 request_id TEXT NOT NULL,
141 principal TEXT NOT NULL,
142 head TEXT NOT NULL,
143 base TEXT NOT NULL,
144 created INTEGER NOT NULL DEFAULT 0,
145 PRIMARY KEY (repo_id, request_id, principal)
146 );
147
148 -- comments holds the discussion on both kinds of entity. The entity row carries
149 -- the count a list shows; these rows are what search reads and what a future
150 -- thread renderer can page through.
151 CREATE TABLE IF NOT EXISTS comments (
152 repo_id TEXT NOT NULL,
153 kind TEXT NOT NULL,
154 parent TEXT NOT NULL,
155 id TEXT NOT NULL,
156 author TEXT NOT NULL,
157 created INTEGER NOT NULL,
158 body TEXT NOT NULL DEFAULT '',
159 reply_to TEXT NOT NULL DEFAULT '',
160 deleted INTEGER NOT NULL DEFAULT 0,
161 path TEXT NOT NULL DEFAULT '',
162 side TEXT NOT NULL DEFAULT '',
163 line INTEGER NOT NULL DEFAULT 0,
164 blob TEXT NOT NULL DEFAULT '',
165 PRIMARY KEY (repo_id, kind, parent, id)
166 );
167 CREATE INDEX IF NOT EXISTS comments_parent ON comments(repo_id, kind, parent);
168
169 -- artifacts are the release pointers: releases/<tag>/<name> on the metadata
170 -- branch. The size is the pointer's, which is the artifact's real size, so a
171 -- release page lists its files without reading the branch.
172 CREATE TABLE IF NOT EXISTS artifacts (
173 repo_id TEXT NOT NULL,
174 tag TEXT NOT NULL,
175 name TEXT NOT NULL,
176 oid TEXT NOT NULL,
177 size INTEGER NOT NULL DEFAULT 0,
178 PRIMARY KEY (repo_id, tag, name)
179 );`); err != nil {
112 180 db.Close()
113 181 return nil, err
114 182 }
@@ -120,11 +188,11 @@ CREATE INDEX IF NOT EXISTS entity_numbers_id ON entity_numbers(repo_id, id);`);
120 188 }
121 189
122 190 // schemaVersion is the shape the rows in this file were derived from. It is not
123 // a migration chain: the index is disposable, so a bump drops the issue rows and
124 // leaves every repo with an empty issues_sha, which makes the next read rebuild
125 // that repo from git. A stale row is worse than a rebuild, and a rebuild is the
191 // a migration chain: the index is disposable, so a bump drops the derived rows
192 // and leaves every repo with an empty issues_sha, which makes the next read
193 // rebuild that repo from git. A stale row is worse than a rebuild, and a rebuild is the
126 194 // operation the index already has to survive.
127 const schemaVersion = 3
195 const schemaVersion = 4
128 196
129 197 func (s *Store) migrate() error {
130 198 var v int
@@ -139,6 +207,11 @@ DELETE FROM issues;
139 207 DELETE FROM issue_labels;
140 208 DELETE FROM issue_assignees;
141 209 DELETE FROM entity_numbers;
210 DELETE FROM requests;
211 DELETE FROM request_closes;
212 DELETE FROM request_approvals;
213 DELETE FROM comments;
214 DELETE FROM artifacts;
142 215 UPDATE repos SET issues_sha = '';`); err != nil {
143 216 return err
144 217 }
@@ -179,6 +252,16 @@ func (s *Store) Rebuild(st *repo.Store) (int, error) {
179 252 if _, err := tx.Exec(`DELETE FROM repos`); err != nil {
180 253 return 0, err
181 254 }
255 // A rebuild is a replacement, not a merge: rows for a repository that no
256 // longer exists must not survive it.
257 for _, table := range []string{
258 "issues", "issue_labels", "issue_assignees", "requests", "request_closes",
259 "request_approvals", "comments", "artifacts", "entity_numbers",
260 } {
261 if _, err := tx.Exec(`DELETE FROM ` + table); err != nil {
262 return 0, err
263 }
264 }
182 265 for _, m := range repos {
183 266 e, err := entryFor(m)
184 267 if err != nil {
@@ -191,10 +274,10 @@ func (s *Store) Rebuild(st *repo.Store) (int, error) {
191 274 if err := tx.Commit(); err != nil {
192 275 return 0, err
193 276 }
194 // Issue rows are indexed per repo after the repo rows exist, because the
195 // issues table is keyed by repo id.
277 // Derived rows are indexed per repo after the repo rows exist, because every
278 // table is keyed by repo id.
196 279 for _, m := range repos {
197 if err := s.IndexIssues(m); err != nil {
280 if err := s.IndexRepo(m); err != nil {
198 281 return 0, err
199 282 }
200 283 }