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 | } | |