Commit 7af5f9d750

7af5f9d7503749b2e02d2200601ce6679a2ffaf8

parent: 14e8eb66f6

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 15:23 UTC

store: AUTOINCREMENT ids for accounts, orgs, repos, keys, tokens, queues

The rebuild keeps every row, index, trigger and foreign key, and starts
each sequence above the ids a deploy key scope, a grant, an audit row or
a page domain still names.

Ref #306

Layout: unified · split

internal/control/idreuse_test.go added +95
@@ -0,0 +1,95 @@
1package control
2
3import (
4 "fmt"
5 "testing"
6 "time"
7
8 "gitbay.org/gitbay/internal/policy"
9)
10
11// A deploy key names its repository by id in its scope, and deleting
12// the repository leaves the key. The next repository does not take the
13// id, so the key opens nothing (#306).
14func TestDeployKeyOfDeletedRepository(t *testing.T) {
15 st, _, uid := newQueueTestRepo(t)
16 goneID, err := st.CreateRepo("user", uid, "gone", "private")
17 if err != nil {
18 t.Fatal(err)
19 }
20 scope := fmt.Sprintf("deploy:%d:rw", goneID)
21 if err := st.AddSSHKey(uid, "SHA256:deploy", "ssh-ed25519", []byte("d"), scope, ""); err != nil {
22 t.Fatal(err)
23 }
24 if err := st.DeleteRepo(goneID); err != nil {
25 t.Fatal(err)
26 }
27 nextID, err := st.CreateRepo("user", uid, "next", "private")
28 if err != nil {
29 t.Fatal(err)
30 }
31 if policy.DeployScopeAllows(scope, nextID, false) {
32 t.Fatalf("the deleted repository's deploy key reads repository %d", nextID)
33 }
34}
35
36// A grant names its account by id and outlives the account. The next
37// account does not take the id, so it does not inherit the grant (#306).
38func TestGrantOfDeletedAccount(t *testing.T) {
39 st, repo, _ := newQueueTestRepo(t)
40 carol, err := st.CreateUser("carol", false)
41 if err != nil {
42 t.Fatal(err)
43 }
44 if err := st.GrantAccess(repo.ID, carol, "admin"); err != nil {
45 t.Fatal(err)
46 }
47 if err := st.DeleteUser(carol); err != nil {
48 t.Fatal(err)
49 }
50 dave, err := st.CreateUser("dave", false)
51 if err != nil {
52 t.Fatal(err)
53 }
54 if role, err := st.AccessRole(repo.ID, dave); err != nil || role != "" {
55 t.Fatalf("new account's role on the repository: %q, %v", role, err)
56 }
57}
58
59// A merge queued with a key that is then removed stays refused when a
60// new key is added: key_id is set to NULL on removal and the new key has
61// an id of its own (#289, #306).
62func TestQueuedMergeKeyRemovedThenNewKey(t *testing.T) {
63 st, repo, uid := newQueueTestRepo(t)
64 if err := st.AddSSHKey(uid, "SHA256:k1", "ssh-ed25519", []byte("k1"), "full", ""); err != nil {
65 t.Fatal(err)
66 }
67 k1, err := st.SSHKeyByFingerprint("SHA256:k1")
68 if err != nil {
69 t.Fatal(err)
70 }
71 if _, err := st.CreateMR(repo.ID, uid, repo.ID, "feature", "main", "t", "", "abc", "md", false); err != nil {
72 t.Fatal(err)
73 }
74 mr, err := st.MRByNumber(repo.ID, 1)
75 if err != nil {
76 t.Fatal(err)
77 }
78 mrID := mr.ID
79 if err := st.QueueMerge(mrID, uid, "", k1.ID, 0); err != nil {
80 t.Fatal(err)
81 }
82 if err := st.RemoveSSHKey(uid, "SHA256:k1"); err != nil {
83 t.Fatal(err)
84 }
85 if err := st.AddSSHKey(uid, "SHA256:k2", "ssh-ed25519", []byte("k2"), "full", ""); err != nil {
86 t.Fatal(err)
87 }
88 if k2, _ := st.SSHKeyByFingerprint("SHA256:k2"); k2.ID == k1.ID {
89 t.Fatalf("new key took the removed key's id %d", k1.ID)
90 }
91 reason, err := queueCredentialLapsed(st, mrID, time.Now())
92 if err != nil || reason != "the key it was queued with was removed" {
93 t.Fatalf("lapsed = %q, %v", reason, err)
94 }
95}
internal/httpd/lfsauth_test.go +38 −7
@@ -253,9 +253,10 @@ func TestLFSUploadTokenRefusedOnceArchived(t *testing.T) {
253253 }
254254}
255255
256// SQLite gives a new key the id of the highest deleted one. A token
257// minted for the deleted key is refused when presented against the new
258// key; the new key's own token works (#303).
256// A removed key's id is not handed to the next key (#306), so its token
257// names no live key. An id freed and taken before ids stopped being
258// reused is still refused by the fingerprint pin; the new key's own token
259// works (#303).
259260func TestLFSTokenRefusedOnReusedKeyID(t *testing.T) {
260261 s, st, u := newTokenTestServer(t)
261262 repo := lfsTestRepo(t, st, u.ID, "app", "private")
@@ -269,14 +270,44 @@ func TestLFSTokenRefusedOnReusedKeyID(t *testing.T) {
269270 if err := st.RemoveSSHKey(u.ID, "SHA256:old"); err != nil {
270271 t.Fatal(err)
271272 }
272 newID := lfsTestKey(t, st, u.ID, "SHA256:new", "full")
273 if newID != oldID {
274 t.Fatalf("new key got id %d, want the reused %d", newID, oldID)
273 nextID := lfsTestKey(t, st, u.ID, "SHA256:next", "full")
274 if nextID == oldID {
275 t.Fatalf("new key took the removed key's id %d", oldID)
276 }
277 if op, _ := s.lfsAuth(lfsRequest(old), repo); op != "" {
278 t.Errorf("removed key's token: %q", op)
279 }
280 if _, err := st.DB.Exec("INSERT INTO ssh_keys (id, user_id, fingerprint, algo, blob) VALUES (?, ?, 'SHA256:new', 'ssh-ed25519', x'00')",
281 oldID, u.ID); err != nil {
282 t.Fatal(err)
275283 }
276284 if op, _ := s.lfsAuth(lfsRequest(old), repo); op != "" {
277285 t.Errorf("old key's token on the reused id: %q", op)
278286 }
279 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, newID, "SHA256:new", "upload", now)), repo); op != "upload" {
287 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, oldID, "SHA256:new", "upload", now)), repo); op != "upload" {
280288 t.Errorf("new key's own token: %q", op)
281289 }
282290}
291
292// A token names its repository by id. Deleting the repository does not
293// let the next one take that id and the token with it (#306).
294func TestLFSTokenForDeletedRepository(t *testing.T) {
295 s, st, u := newTokenTestServer(t)
296 gone := lfsTestRepo(t, st, u.ID, "gone", "private")
297 secret, err := s.lfsSecret()
298 if err != nil {
299 t.Fatal(err)
300 }
301 key := lfsTestKey(t, st, u.ID, "SHA256:owner", "full")
302 tok := lfs.Sign(secret, gone.ID, key, "SHA256:owner", "upload", time.Now())
303 if err := st.DeleteRepo(gone.ID); err != nil {
304 t.Fatal(err)
305 }
306 next := lfsTestRepo(t, st, u.ID, "next", "private")
307 if next.ID == gone.ID {
308 t.Fatalf("new repository took the deleted one's id %d", gone.ID)
309 }
310 if op, _ := s.lfsAuth(lfsRequest(tok), next); op != "" {
311 t.Errorf("deleted repository's token on the next repository: %q", op)
312 }
313}
internal/lfs/lfs.go +4 −3
@@ -130,9 +130,10 @@ const TokenTTL = time.Hour
130130// Sign mints a token for op ("download" or "upload") on repoID, bound
131131// to keyID: the SSH key, user or deploy, that asked for it, or 0 for an
132132// anonymous download of a public repository. fingerprint is that key's
133// fingerprint, "" for key 0. SQLite reuses the id of a deleted key, so
134// the token carries a hash of the fingerprint as well and a new key
135// given the old id does not inherit the old key's tokens (#303).
133// fingerprint, "" for key 0. Key ids freed before they stopped being
134// reused (#306) may belong to a later key, so the token carries a hash
135// of the fingerprint as well and a new key given the old id does not
136// inherit the old key's tokens (#303).
136137func Sign(secret []byte, repoID, keyID int64, fingerprint, op string, now time.Time) string {
137138 payload := fmt.Sprintf("%d:%d:%s:%s:%d", repoID, keyID, KeyPin(fingerprint), op, now.Add(TokenTTL).Unix())
138139 mac := hmac.New(sha256.New, secret)
internal/mailin/mailin.go +3 −2
@@ -187,8 +187,9 @@ func (p *Processor) Handle(raw []byte) Result {
187187 case u.Pending:
188188 return p.refuse(u.ID, msgID, "account not active")
189189 }
190 // Ids are reused after a hard delete: an account created after the
191 // token was minted is not the one it named.
190 // An id freed before ids stopped being reused (#306) may have been
191 // taken: an account created after the token was minted is not the
192 // one it named.
192193 if code := p.createdAfter("users", u.ID, target, msgID, "account"); code != nil {
193194 return *code
194195 }
internal/mailin/mailin_test.go +25 −4
@@ -336,7 +336,26 @@ func TestDrainRetriesTransientFailure(t *testing.T) {
336336 }
337337}
338338
339// A repository id freed by a delete and taken by a later repository does
339// A deleted repository's id is not handed to the next repository
340// (#306), so a reply meant for it finds no repository.
341func TestDeletedRepositoryID(t *testing.T) {
342 f := setup(t)
343 m := []byte(f.message(t, "bob@example.test", "hi"))
344 if err := f.st.DeleteRepo(f.repo.ID); err != nil {
345 t.Fatal(err)
346 }
347 alice, _ := f.st.UserByUsername("alice")
348 id, err := f.st.CreateRepo("user", alice.ID, "other", "public")
349 if err != nil || id == f.repo.ID {
350 t.Fatalf("new repository has id %d (%v), the deleted one's", id, err)
351 }
352 res := f.p.Handle(m)
353 if res.Posted || !strings.Contains(res.Reason, "repository no longer exists") {
354 t.Fatalf("result %+v", res)
355 }
356}
357
358// A repository id freed and taken before ids stopped being reused does
340359// not accept replies meant for the old one.
341360func TestReusedRepositoryID(t *testing.T) {
342361 f := setup(t)
@@ -345,9 +364,11 @@ func TestReusedRepositoryID(t *testing.T) {
345364 t.Fatal(err)
346365 }
347366 alice, _ := f.st.UserByUsername("alice")
348 id, err := f.st.CreateRepo("user", alice.ID, "other", "public")
349 if err != nil || id != f.repo.ID {
350 t.Fatalf("new repository has id %d (%v), want the freed %d", id, err, f.repo.ID)
367 id := f.repo.ID
368 if _, err := f.st.DB.Exec(
369 "INSERT INTO repos (id, owner_kind, owner_id, name, visibility) VALUES (?, 'user', ?, 'other', 'public')",
370 id, alice.ID); err != nil {
371 t.Fatal(err)
351372 }
352373 f.st.CreateIssue(id, alice.ID, "t", "", "md")
353374 // Created after the token, as it would be outside a fast test.
internal/store/idreuse_test.go added +322
@@ -0,0 +1,322 @@
1package store
2
3import (
4 "strconv"
5 "strings"
6 "testing"
7)
8
9// idMigration is the version of the migration that makes ids
10// AUTOINCREMENT (#306), found by name so the test survives renumbering.
11func idMigration(t *testing.T) int {
12 t.Helper()
13 ms, err := loadMigrations()
14 if err != nil {
15 t.Fatal(err)
16 }
17 for _, m := range ms {
18 if m.name == "id_autoincrement" {
19 return m.version
20 }
21 }
22 t.Fatal("no id_autoincrement migration")
23 return 0
24}
25
26var autoincTables = []string{"users", "orgs", "repos", "api_tokens", "ssh_keys", "webhook_deliveries", "push_queue"}
27
28func mustExec(t *testing.T, s *Store, q string, args ...any) int64 {
29 t.Helper()
30 res, err := s.DB.Exec(q, args...)
31 if err != nil {
32 t.Fatalf("%s: %v", q, err)
33 }
34 id, _ := res.LastInsertId()
35 return id
36}
37
38func count(t *testing.T, s *Store, table string) int {
39 t.Helper()
40 var n int
41 if err := s.DB.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&n); err != nil {
42 t.Fatal(err)
43 }
44 return n
45}
46
47func fkClean(t *testing.T, s *Store) {
48 t.Helper()
49 rows, err := s.DB.Query("PRAGMA foreign_key_check")
50 if err != nil {
51 t.Fatal(err)
52 }
53 defer rows.Close()
54 if rows.Next() {
55 var table, parent string
56 var rowid, fk any
57 rows.Scan(&table, &rowid, &parent, &fk)
58 t.Fatalf("foreign_key_check: %s row %v -> %s", table, rowid, parent)
59 }
60}
61
62// seedForIDs fills every rebuilt table and the rows that name their ids,
63// then deletes the newest repository and the newest account while a
64// deploy key and a grant still name them.
65func seedForIDs(t *testing.T, s *Store) (goneRepo, goneUser int64) {
66 t.Helper()
67 alice := mustExec(t, s, "INSERT INTO users (username) VALUES ('alice')")
68 org := mustExec(t, s, "INSERT INTO orgs (name) VALUES ('acme')")
69 repo := mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'app', 'public')", alice)
70 mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility, fork_of) VALUES ('org', ?, 'fork', 'private', ?)", org, repo)
71 tok := mustExec(t, s, "INSERT INTO api_tokens (user_id, name, token_hash) VALUES (?, 't1', 'h1')", alice)
72 mustExec(t, s, "INSERT INTO api_tokens (user_id, name, token_hash, created_by_token) VALUES (?, 't2', 'h2', ?)", alice, tok)
73 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, created_by_token) VALUES (?, 'SHA256:a', 'ssh-ed25519', x'00', ?)", alice, tok)
74 hook := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/h')", repo)
75 ev := mustExec(t, s, "INSERT INTO events (repo_id, actor_id, kind) VALUES (?, ?, 'push')", repo, alice)
76 mustExec(t, s, "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)", hook, ev)
77 dev := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'devtok')", alice)
78 mustExec(t, s, "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')", dev)
79 mustExec(t, s, "INSERT INTO org_members (org_id, user_id, role) VALUES (?, ?, 'admin')", org, alice)
80
81 goneRepo = mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'gone', 'private')", alice)
82 mustExec(t, s, "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, 'SHA256:d', 'ssh-ed25519', x'00', ?)",
83 alice, "deploy:"+itoa(goneRepo)+":rw")
84 if err := s.DeleteRepo(goneRepo); err != nil {
85 t.Fatal(err)
86 }
87 goneUser = mustExec(t, s, "INSERT INTO users (username) VALUES ('carol')")
88 if err := s.GrantAccess(repo, goneUser, "write"); err != nil {
89 t.Fatal(err)
90 }
91 if err := s.DeleteUser(goneUser); err != nil {
92 t.Fatal(err)
93 }
94 return goneRepo, goneUser
95}
96
97func itoa(n int64) string { return strconv.FormatInt(n, 10) }
98
99func TestIDMigrationKeepsRowsAndForeignKeys(t *testing.T) {
100 v := idMigration(t)
101 s := open(t)
102 if err := s.MigrateTo(v - 1); err != nil {
103 t.Fatal(err)
104 }
105 goneRepo, goneUser := seedForIDs(t, s)
106 before := map[string]int{}
107 for _, tbl := range append(autoincTables, "org_members", "webhooks", "events", "push_devices", "repo_access") {
108 before[tbl] = count(t, s, tbl)
109 }
110 if err := s.MigrateTo(v); err != nil {
111 t.Fatal(err)
112 }
113 for tbl, n := range before {
114 if got := count(t, s, tbl); got != n {
115 t.Errorf("%s: %d rows after the migration, want %d", tbl, got, n)
116 }
117 }
118 for _, tbl := range autoincTables {
119 var sql string
120 if err := s.DB.QueryRow("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?", tbl).Scan(&sql); err != nil {
121 t.Fatal(err)
122 }
123 if !strings.Contains(sql, "AUTOINCREMENT") {
124 t.Errorf("%s is not AUTOINCREMENT", tbl)
125 }
126 }
127 fkClean(t, s)
128
129 // Children still name the rebuilt parents, not the *_old tables.
130 var n int
131 if err := s.DB.QueryRow(`SELECT COUNT(*) FROM sqlite_master m, pragma_foreign_key_list(m.name) f
132 WHERE m.type = 'table' AND f."table" LIKE '%_old'`).Scan(&n); err != nil {
133 t.Fatal(err)
134 }
135 if n != 0 {
136 t.Fatalf("%d foreign keys name a *_old table", n)
137 }
138 for _, c := range []struct{ child, parent string }{
139 {"ssh_keys", "users"}, {"ssh_keys", "api_tokens"}, {"api_tokens", "api_tokens"},
140 {"repos", "repos"}, {"issues", "repos"}, {"teams", "orgs"}, {"mr_merge_queue", "ssh_keys"},
141 {"push_queue", "push_devices"}, {"webhook_deliveries", "webhooks"},
142 } {
143 if err := s.DB.QueryRow(`SELECT COUNT(*) FROM pragma_foreign_key_list(?) WHERE "table" = ?`, c.child, c.parent).Scan(&n); err != nil || n == 0 {
144 t.Errorf("%s has no foreign key to %s (%v)", c.child, c.parent, err)
145 }
146 }
147 for _, idx := range []string{"ssh_keys_user", "webhook_deliveries_due", "push_queue_due"} {
148 if err := s.DB.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name = ?", idx).Scan(&n); err != nil || n != 1 {
149 t.Errorf("index %s missing", idx)
150 }
151 }
152
153 // The triggers still fire.
154 alice, err := s.UserByUsername("alice")
155 if err != nil {
156 t.Fatal(err)
157 }
158 if _, err := s.DB.Exec("DELETE FROM users WHERE id = ?", alice.ID); err == nil || !strings.Contains(err.Error(), "still owns repositories") {
159 t.Fatalf("deleting a repository owner: %v", err)
160 }
161 if _, err := s.DB.Exec("DELETE FROM orgs WHERE name = 'acme'"); err == nil || !strings.Contains(err.Error(), "still owns repositories") {
162 t.Fatalf("deleting an owning org: %v", err)
163 }
164 // Cascades still reach the rebuilt tables' children.
165 mustExec(t, s, "DELETE FROM push_devices WHERE token = 'devtok'")
166 if got := count(t, s, "push_queue"); got != 0 {
167 t.Fatalf("push_queue after its device went: %d rows", got)
168 }
169
170 // The sequences start above the ids a deploy key and a grant still
171 // name, though neither row survived.
172 var seq int64
173 s.DB.QueryRow("SELECT seq FROM sqlite_sequence WHERE name = 'repos'").Scan(&seq)
174 if seq < goneRepo {
175 t.Fatalf("repos sequence %d, want at least %d", seq, goneRepo)
176 }
177 r, err := s.CreateRepo("user", alice.ID, "new", "public")
178 if err != nil {
179 t.Fatal(err)
180 }
181 if r <= goneRepo {
182 t.Fatalf("new repository took id %d; a deploy key still names %d", r, goneRepo)
183 }
184 u, err := s.CreateUser("dave", false)
185 if err != nil {
186 t.Fatal(err)
187 }
188 if u <= goneUser {
189 t.Fatalf("new account took id %d; a grant still names %d", u, goneUser)
190 }
191
192 // Down and up again keep the rows.
193 if err := s.MigrateTo(v - 1); err != nil {
194 t.Fatal(err)
195 }
196 if got := count(t, s, "users"); got != before["users"]+1 {
197 t.Fatalf("users after down: %d", got)
198 }
199 if err := s.DB.QueryRow("SELECT COUNT(*) FROM sqlite_sequence WHERE name = 'users'").Scan(&n); err != nil || n != 0 {
200 t.Fatalf("sqlite_sequence keeps users after down: %d, %v", n, err)
201 }
202 fkClean(t, s)
203 if err := s.MigrateUp(); err != nil {
204 t.Fatal(err)
205 }
206 fkClean(t, s)
207}
208
209// A parked profile about text names its owner by kind and id with no
210// foreign key; the sequences start above the ids it names (#306).
211func TestIDMigrationSeedsFromAboutBackfill(t *testing.T) {
212 v := idMigration(t)
213 s := open(t)
214 if err := s.MigrateTo(v - 1); err != nil {
215 t.Fatal(err)
216 }
217 mustExec(t, s, "INSERT INTO users (username) VALUES ('alice')")
218 mustExec(t, s, "INSERT INTO orgs (name) VALUES ('acme')")
219 mustExec(t, s, `INSERT INTO profile_about_backfill (owner_kind, owner_id, about, about_format)
220 VALUES ('user', 50, 'a', 'md'), ('org', 40, 'b', 'md')`)
221 if err := s.MigrateTo(v); err != nil {
222 t.Fatal(err)
223 }
224 for table, want := range map[string]int64{"users": 50, "orgs": 40} {
225 var seq int64
226 if err := s.DB.QueryRow("SELECT seq FROM sqlite_sequence WHERE name = ?", table).Scan(&seq); err != nil {
227 t.Fatal(err)
228 }
229 if seq < want {
230 t.Errorf("%s sequence %d, want at least %d", table, seq, want)
231 }
232 }
233}
234
235// Deleting the row with the highest id does not free that id, in any of
236// the rebuilt tables.
237func TestIDsNotReusedAfterDelete(t *testing.T) {
238 s := open(t)
239 if err := s.MigrateUp(); err != nil {
240 t.Fatal(err)
241 }
242 u1 := mustExec(t, s, "INSERT INTO users (username) VALUES ('u1')")
243 u2 := mustExec(t, s, "INSERT INTO users (username) VALUES ('u2')")
244 repo := mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'r', 'public')", u1)
245 hook := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/h')", repo)
246 ev := mustExec(t, s, "INSERT INTO events (repo_id, kind) VALUES (?, 'push')", repo)
247 dev := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'devtok')", u1)
248
249 cases := []struct {
250 table, insert string
251 args func(i int) []any
252 }{
253 {"users", "INSERT INTO users (username) VALUES (?)", func(i int) []any { return []any{"x" + itoa(int64(i))} }},
254 {"orgs", "INSERT INTO orgs (name) VALUES (?)", func(i int) []any { return []any{"o" + itoa(int64(i))} }},
255 {"repos", "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, ?, 'public')",
256 func(i int) []any { return []any{u1, "n" + itoa(int64(i))} }},
257 {"api_tokens", "INSERT INTO api_tokens (user_id, name, token_hash) VALUES (?, ?, ?)",
258 func(i int) []any { return []any{u2, "t" + itoa(int64(i)), "h" + itoa(int64(i))} }},
259 {"ssh_keys", "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob) VALUES (?, ?, 'ssh-ed25519', x'00')",
260 func(i int) []any { return []any{u2, "SHA256:" + itoa(int64(i))} }},
261 {"webhook_deliveries", "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)",
262 func(int) []any { return []any{hook, ev} }},
263 {"push_queue", "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')",
264 func(int) []any { return []any{dev} }},
265 }
266 for _, c := range cases {
267 first := mustExec(t, s, c.insert, c.args(1)...)
268 mustExec(t, s, "DELETE FROM "+c.table+" WHERE id = ?", first)
269 second := mustExec(t, s, c.insert, c.args(2)...)
270 if second <= first {
271 t.Errorf("%s: id %d handed out again after its row was deleted (got %d)", c.table, first, second)
272 }
273 }
274}
275
276// A sender marks a webhook delivery or a push by id after its request
277// returns. If the hook or device was removed meanwhile, the cascade took
278// the row, and the next row must not take its id and its mark (#306).
279func TestInFlightMarksAfterCascade(t *testing.T) {
280 s := open(t)
281 if err := s.MigrateUp(); err != nil {
282 t.Fatal(err)
283 }
284 u := mustExec(t, s, "INSERT INTO users (username) VALUES ('u')")
285 repo := mustExec(t, s, "INSERT INTO repos (owner_kind, owner_id, name, visibility) VALUES ('user', ?, 'r', 'public')", u)
286 ev := mustExec(t, s, "INSERT INTO events (repo_id, kind) VALUES (?, 'push')", repo)
287 h1 := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/1')", repo)
288 h2 := mustExec(t, s, "INSERT INTO webhooks (repo_id, url) VALUES (?, 'https://example.org/2')", repo)
289 inFlight := mustExec(t, s, "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)", h1, ev)
290 if err := s.RemoveWebhook(repo, h1); err != nil {
291 t.Fatal(err)
292 }
293 next := mustExec(t, s, "INSERT INTO webhook_deliveries (webhook_id, event_id) VALUES (?, ?)", h2, ev)
294 if err := s.MarkDelivered(inFlight, 200); err != nil {
295 t.Fatal(err)
296 }
297 var delivered *string
298 if err := s.DB.QueryRow("SELECT delivered_at FROM webhook_deliveries WHERE id = ?", next).Scan(&delivered); err != nil {
299 t.Fatal(err)
300 }
301 if delivered != nil {
302 t.Fatal("the removed hook's delivery was marked on the next hook's")
303 }
304
305 d1 := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'd1')", u)
306 d2 := mustExec(t, s, "INSERT INTO push_devices (user_id, token) VALUES (?, 'd2')", u)
307 pushing := mustExec(t, s, "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')", d1)
308 if err := s.RemovePushDevice(u, d1); err != nil {
309 t.Fatal(err)
310 }
311 queued := mustExec(t, s, "INSERT INTO push_queue (device_id, title, body, path) VALUES (?, 't', 'b', 'p')", d2)
312 if err := s.MarkPushSent(pushing); err != nil {
313 t.Fatal(err)
314 }
315 var sent *string
316 if err := s.DB.QueryRow("SELECT sent_at FROM push_queue WHERE id = ?", queued).Scan(&sent); err != nil {
317 t.Fatal(err)
318 }
319 if sent != nil {
320 t.Fatal("the removed device's push was marked on the next device's")
321 }
322}
internal/store/migrations/0074_id_autoincrement.down.sql added +175
@@ -0,0 +1,175 @@
1-- foreign_keys: off
2-- Back to ids without AUTOINCREMENT: the same rebuild with the keyword
3-- dropped, and the tables' sqlite_sequence rows removed.
4PRAGMA legacy_alter_table = ON;
5
6DROP TRIGGER users_owning_repos;
7DROP TRIGGER orgs_owning_repos;
8
9ALTER TABLE users RENAME TO users_old;
10CREATE TABLE users (
11 id INTEGER PRIMARY KEY,
12 username TEXT NOT NULL UNIQUE,
13 is_admin INTEGER NOT NULL DEFAULT 0,
14 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
15 pending INTEGER NOT NULL DEFAULT 0,
16 description TEXT NOT NULL DEFAULT '',
17 website TEXT NOT NULL DEFAULT '',
18 disabled INTEGER NOT NULL DEFAULT 0,
19 links TEXT NOT NULL DEFAULT '',
20 repo_limit INTEGER,
21 byte_limit INTEGER,
22 notify_mail INTEGER NOT NULL DEFAULT 1,
23 notify_watch INTEGER NOT NULL DEFAULT 0,
24 theme TEXT NOT NULL DEFAULT 'system',
25 notify_push INTEGER NOT NULL DEFAULT 1,
26 diff_layout TEXT NOT NULL DEFAULT 'unified',
27 notify_reply INTEGER NOT NULL DEFAULT 0
28);
29INSERT INTO users (id, username, is_admin, created_at, pending, description, website, disabled,
30 links, repo_limit, byte_limit, notify_mail, notify_watch, theme, notify_push, diff_layout, notify_reply)
31 SELECT id, username, is_admin, created_at, pending, description, website, disabled,
32 links, repo_limit, byte_limit, notify_mail, notify_watch, theme, notify_push, diff_layout, notify_reply
33 FROM users_old;
34DROP TABLE users_old;
35
36ALTER TABLE orgs RENAME TO orgs_old;
37CREATE TABLE orgs (
38 id INTEGER PRIMARY KEY,
39 name TEXT NOT NULL UNIQUE,
40 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
41 description TEXT NOT NULL DEFAULT '',
42 website TEXT NOT NULL DEFAULT '',
43 members_role TEXT NOT NULL DEFAULT 'write'
44 CHECK (members_role IN ('write', 'read', 'none')),
45 links TEXT NOT NULL DEFAULT ''
46);
47INSERT INTO orgs (id, name, created_at, description, website, members_role, links)
48 SELECT id, name, created_at, description, website, members_role, links FROM orgs_old;
49DROP TABLE orgs_old;
50
51ALTER TABLE repos RENAME TO repos_old;
52CREATE TABLE repos (
53 id INTEGER PRIMARY KEY,
54 owner_kind TEXT NOT NULL CHECK (owner_kind IN ('user','org')),
55 owner_id INTEGER NOT NULL,
56 name TEXT NOT NULL,
57 visibility TEXT NOT NULL CHECK (visibility IN ('public','private')),
58 default_branch TEXT NOT NULL DEFAULT 'main',
59 fork_of INTEGER REFERENCES repos(id) ON DELETE SET NULL,
60 issue_counter INTEGER NOT NULL DEFAULT 0,
61 mr_counter INTEGER NOT NULL DEFAULT 0,
62 settings_json TEXT NOT NULL DEFAULT '{}',
63 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
64 build_counter INTEGER NOT NULL DEFAULT 0,
65 UNIQUE (owner_kind, owner_id, name)
66);
67INSERT INTO repos (id, owner_kind, owner_id, name, visibility, default_branch, fork_of,
68 issue_counter, mr_counter, settings_json, created_at, build_counter)
69 SELECT id, owner_kind, owner_id, name, visibility, default_branch, fork_of,
70 issue_counter, mr_counter, settings_json, created_at, build_counter
71 FROM repos_old;
72DROP TABLE repos_old;
73
74ALTER TABLE api_tokens RENAME TO api_tokens_old;
75CREATE TABLE api_tokens (
76 id INTEGER PRIMARY KEY,
77 user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
78 name TEXT NOT NULL,
79 token_hash TEXT NOT NULL UNIQUE,
80 scope TEXT NOT NULL DEFAULT 'full' CHECK (scope IN ('full','read')),
81 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
82 expires_at TEXT,
83 last_used_at TEXT,
84 created_by_token INTEGER REFERENCES api_tokens(id) ON DELETE SET NULL,
85 UNIQUE (user_id, name)
86);
87INSERT INTO api_tokens (id, user_id, name, token_hash, scope, created_at, expires_at,
88 last_used_at, created_by_token)
89 SELECT id, user_id, name, token_hash, scope, created_at, expires_at,
90 last_used_at, created_by_token
91 FROM api_tokens_old;
92DROP TABLE api_tokens_old;
93
94ALTER TABLE ssh_keys RENAME TO ssh_keys_old;
95CREATE TABLE ssh_keys (
96 id INTEGER PRIMARY KEY,
97 user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
98 fingerprint TEXT NOT NULL UNIQUE,
99 algo TEXT NOT NULL,
100 blob BLOB NOT NULL,
101 scope TEXT NOT NULL DEFAULT 'full',
102 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
103 last_used_at TEXT,
104 label TEXT NOT NULL DEFAULT '',
105 created_by_token INTEGER REFERENCES api_tokens(id) ON DELETE SET NULL,
106 expires_at TEXT
107);
108INSERT INTO ssh_keys (id, user_id, fingerprint, algo, blob, scope, created_at, last_used_at,
109 label, created_by_token, expires_at)
110 SELECT id, user_id, fingerprint, algo, blob, scope, created_at, last_used_at,
111 label, created_by_token, expires_at
112 FROM ssh_keys_old;
113DROP TABLE ssh_keys_old;
114CREATE INDEX ssh_keys_user ON ssh_keys(user_id);
115
116ALTER TABLE webhook_deliveries RENAME TO webhook_deliveries_old;
117CREATE TABLE webhook_deliveries (
118 id INTEGER PRIMARY KEY,
119 webhook_id INTEGER NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
120 event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE,
121 attempts INTEGER NOT NULL DEFAULT 0,
122 next_attempt_at TEXT,
123 delivered_at TEXT,
124 failed_at TEXT,
125 last_status INTEGER,
126 last_error TEXT,
127 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
128);
129INSERT INTO webhook_deliveries (id, webhook_id, event_id, attempts, next_attempt_at,
130 delivered_at, failed_at, last_status, last_error, created_at)
131 SELECT id, webhook_id, event_id, attempts, next_attempt_at,
132 delivered_at, failed_at, last_status, last_error, created_at
133 FROM webhook_deliveries_old;
134DROP TABLE webhook_deliveries_old;
135CREATE INDEX webhook_deliveries_due ON webhook_deliveries(next_attempt_at)
136 WHERE delivered_at IS NULL AND failed_at IS NULL;
137
138ALTER TABLE push_queue RENAME TO push_queue_old;
139CREATE TABLE push_queue (
140 id INTEGER PRIMARY KEY,
141 device_id INTEGER NOT NULL REFERENCES push_devices(id) ON DELETE CASCADE,
142 title TEXT NOT NULL,
143 body TEXT NOT NULL,
144 path TEXT NOT NULL,
145 attempts INTEGER NOT NULL DEFAULT 0,
146 next_attempt_at TEXT,
147 sent_at TEXT,
148 failed_at TEXT,
149 last_error TEXT,
150 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
151);
152INSERT INTO push_queue (id, device_id, title, body, path, attempts, next_attempt_at,
153 sent_at, failed_at, last_error, created_at)
154 SELECT id, device_id, title, body, path, attempts, next_attempt_at,
155 sent_at, failed_at, last_error, created_at
156 FROM push_queue_old;
157DROP TABLE push_queue_old;
158CREATE INDEX push_queue_due ON push_queue(next_attempt_at)
159 WHERE sent_at IS NULL AND failed_at IS NULL;
160
161CREATE TRIGGER users_owning_repos BEFORE DELETE ON users
162WHEN EXISTS (SELECT 1 FROM repos WHERE owner_kind = 'user' AND owner_id = OLD.id)
163BEGIN
164 SELECT RAISE(ABORT, 'user still owns repositories');
165END;
166CREATE TRIGGER orgs_owning_repos BEFORE DELETE ON orgs
167WHEN EXISTS (SELECT 1 FROM repos WHERE owner_kind = 'org' AND owner_id = OLD.id)
168BEGIN
169 SELECT RAISE(ABORT, 'organization still owns repositories');
170END;
171
172PRAGMA legacy_alter_table = OFF;
173
174DELETE FROM sqlite_sequence WHERE name IN
175 ('users', 'orgs', 'repos', 'api_tokens', 'ssh_keys', 'webhook_deliveries', 'push_queue');
internal/store/migrations/0074_id_autoincrement.up.sql added +211
@@ -0,0 +1,211 @@
1-- foreign_keys: off
2-- Ids that are named after their row is gone are never handed out again
3-- (#306). Without AUTOINCREMENT SQLite gives a new row MAX(id)+1, so
4-- deleting the newest account, organization, repository, key or token
5-- let the next one take its id, and with it whatever still named that
6-- id: a deploy key's scope, a repo_access grant, a signed LFS or
7-- reply-by-mail token, a hook's environment. webhook_deliveries and
8-- push_queue rows are named by id by a sender that is mid-request when
9-- a cascade can remove them.
10--
11-- Each table is rebuilt the way 0052 rebuilds labels: foreign keys off
12-- for the step, legacy_alter_table so the children keep naming the
13-- table through the rename and bind to the new one, and the runner's
14-- foreign_key_check before commit. Rows keep their ids; indexes and
15-- triggers are recreated. sqlite_sequence starts at the highest id in
16-- the table or named anywhere else, so an id already freed and still
17-- named (a deploy key for a deleted repository, a grant, an audit row or
18-- a parked profile about text for a deleted owner) is not handed out
19-- either.
20PRAGMA legacy_alter_table = ON;
21
22DROP TRIGGER users_owning_repos;
23DROP TRIGGER orgs_owning_repos;
24
25ALTER TABLE users RENAME TO users_old;
26CREATE TABLE users (
27 id INTEGER PRIMARY KEY AUTOINCREMENT,
28 username TEXT NOT NULL UNIQUE,
29 is_admin INTEGER NOT NULL DEFAULT 0,
30 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
31 pending INTEGER NOT NULL DEFAULT 0,
32 description TEXT NOT NULL DEFAULT '',
33 website TEXT NOT NULL DEFAULT '',
34 disabled INTEGER NOT NULL DEFAULT 0,
35 links TEXT NOT NULL DEFAULT '',
36 repo_limit INTEGER,
37 byte_limit INTEGER,
38 notify_mail INTEGER NOT NULL DEFAULT 1,
39 notify_watch INTEGER NOT NULL DEFAULT 0,
40 theme TEXT NOT NULL DEFAULT 'system',
41 notify_push INTEGER NOT NULL DEFAULT 1,
42 diff_layout TEXT NOT NULL DEFAULT 'unified',
43 notify_reply INTEGER NOT NULL DEFAULT 0
44);
45INSERT INTO users (id, username, is_admin, created_at, pending, description, website, disabled,
46 links, repo_limit, byte_limit, notify_mail, notify_watch, theme, notify_push, diff_layout, notify_reply)
47 SELECT id, username, is_admin, created_at, pending, description, website, disabled,
48 links, repo_limit, byte_limit, notify_mail, notify_watch, theme, notify_push, diff_layout, notify_reply
49 FROM users_old;
50DROP TABLE users_old;
51
52ALTER TABLE orgs RENAME TO orgs_old;
53CREATE TABLE orgs (
54 id INTEGER PRIMARY KEY AUTOINCREMENT,
55 name TEXT NOT NULL UNIQUE,
56 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
57 description TEXT NOT NULL DEFAULT '',
58 website TEXT NOT NULL DEFAULT '',
59 members_role TEXT NOT NULL DEFAULT 'write'
60 CHECK (members_role IN ('write', 'read', 'none')),
61 links TEXT NOT NULL DEFAULT ''
62);
63INSERT INTO orgs (id, name, created_at, description, website, members_role, links)
64 SELECT id, name, created_at, description, website, members_role, links FROM orgs_old;
65DROP TABLE orgs_old;
66
67ALTER TABLE repos RENAME TO repos_old;
68CREATE TABLE repos (
69 id INTEGER PRIMARY KEY AUTOINCREMENT,
70 owner_kind TEXT NOT NULL CHECK (owner_kind IN ('user','org')),
71 owner_id INTEGER NOT NULL,
72 name TEXT NOT NULL,
73 visibility TEXT NOT NULL CHECK (visibility IN ('public','private')),
74 default_branch TEXT NOT NULL DEFAULT 'main',
75 fork_of INTEGER REFERENCES repos(id) ON DELETE SET NULL,
76 issue_counter INTEGER NOT NULL DEFAULT 0,
77 mr_counter INTEGER NOT NULL DEFAULT 0,
78 settings_json TEXT NOT NULL DEFAULT '{}',
79 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
80 build_counter INTEGER NOT NULL DEFAULT 0,
81 UNIQUE (owner_kind, owner_id, name)
82);
83INSERT INTO repos (id, owner_kind, owner_id, name, visibility, default_branch, fork_of,
84 issue_counter, mr_counter, settings_json, created_at, build_counter)
85 SELECT id, owner_kind, owner_id, name, visibility, default_branch, fork_of,
86 issue_counter, mr_counter, settings_json, created_at, build_counter
87 FROM repos_old;
88DROP TABLE repos_old;
89
90ALTER TABLE api_tokens RENAME TO api_tokens_old;
91CREATE TABLE api_tokens (
92 id INTEGER PRIMARY KEY AUTOINCREMENT,
93 user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
94 name TEXT NOT NULL,
95 token_hash TEXT NOT NULL UNIQUE,
96 scope TEXT NOT NULL DEFAULT 'full' CHECK (scope IN ('full','read')),
97 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
98 expires_at TEXT,
99 last_used_at TEXT,
100 created_by_token INTEGER REFERENCES api_tokens(id) ON DELETE SET NULL,
101 UNIQUE (user_id, name)
102);
103INSERT INTO api_tokens (id, user_id, name, token_hash, scope, created_at, expires_at,
104 last_used_at, created_by_token)
105 SELECT id, user_id, name, token_hash, scope, created_at, expires_at,
106 last_used_at, created_by_token
107 FROM api_tokens_old;
108DROP TABLE api_tokens_old;
109
110ALTER TABLE ssh_keys RENAME TO ssh_keys_old;
111CREATE TABLE ssh_keys (
112 id INTEGER PRIMARY KEY AUTOINCREMENT,
113 user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
114 fingerprint TEXT NOT NULL UNIQUE,
115 algo TEXT NOT NULL,
116 blob BLOB NOT NULL,
117 scope TEXT NOT NULL DEFAULT 'full',
118 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
119 last_used_at TEXT,
120 label TEXT NOT NULL DEFAULT '',
121 created_by_token INTEGER REFERENCES api_tokens(id) ON DELETE SET NULL,
122 expires_at TEXT
123);
124INSERT INTO ssh_keys (id, user_id, fingerprint, algo, blob, scope, created_at, last_used_at,
125 label, created_by_token, expires_at)
126 SELECT id, user_id, fingerprint, algo, blob, scope, created_at, last_used_at,
127 label, created_by_token, expires_at
128 FROM ssh_keys_old;
129DROP TABLE ssh_keys_old;
130CREATE INDEX ssh_keys_user ON ssh_keys(user_id);
131
132ALTER TABLE webhook_deliveries RENAME TO webhook_deliveries_old;
133CREATE TABLE webhook_deliveries (
134 id INTEGER PRIMARY KEY AUTOINCREMENT,
135 webhook_id INTEGER NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
136 event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE,
137 attempts INTEGER NOT NULL DEFAULT 0,
138 next_attempt_at TEXT,
139 delivered_at TEXT,
140 failed_at TEXT,
141 last_status INTEGER,
142 last_error TEXT,
143 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
144);
145INSERT INTO webhook_deliveries (id, webhook_id, event_id, attempts, next_attempt_at,
146 delivered_at, failed_at, last_status, last_error, created_at)
147 SELECT id, webhook_id, event_id, attempts, next_attempt_at,
148 delivered_at, failed_at, last_status, last_error, created_at
149 FROM webhook_deliveries_old;
150DROP TABLE webhook_deliveries_old;
151CREATE INDEX webhook_deliveries_due ON webhook_deliveries(next_attempt_at)
152 WHERE delivered_at IS NULL AND failed_at IS NULL;
153
154ALTER TABLE push_queue RENAME TO push_queue_old;
155CREATE TABLE push_queue (
156 id INTEGER PRIMARY KEY AUTOINCREMENT,
157 device_id INTEGER NOT NULL REFERENCES push_devices(id) ON DELETE CASCADE,
158 title TEXT NOT NULL,
159 body TEXT NOT NULL,
160 path TEXT NOT NULL,
161 attempts INTEGER NOT NULL DEFAULT 0,
162 next_attempt_at TEXT,
163 sent_at TEXT,
164 failed_at TEXT,
165 last_error TEXT,
166 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
167);
168INSERT INTO push_queue (id, device_id, title, body, path, attempts, next_attempt_at,
169 sent_at, failed_at, last_error, created_at)
170 SELECT id, device_id, title, body, path, attempts, next_attempt_at,
171 sent_at, failed_at, last_error, created_at
172 FROM push_queue_old;
173DROP TABLE push_queue_old;
174CREATE INDEX push_queue_due ON push_queue(next_attempt_at)
175 WHERE sent_at IS NULL AND failed_at IS NULL;
176
177CREATE TRIGGER users_owning_repos BEFORE DELETE ON users
178WHEN EXISTS (SELECT 1 FROM repos WHERE owner_kind = 'user' AND owner_id = OLD.id)
179BEGIN
180 SELECT RAISE(ABORT, 'user still owns repositories');
181END;
182CREATE TRIGGER orgs_owning_repos BEFORE DELETE ON orgs
183WHEN EXISTS (SELECT 1 FROM repos WHERE owner_kind = 'org' AND owner_id = OLD.id)
184BEGIN
185 SELECT RAISE(ABORT, 'organization still owns repositories');
186END;
187
188PRAGMA legacy_alter_table = OFF;
189
190-- The copies above set each sequence to the table's own highest id; an
191-- empty table has no row yet.
192INSERT INTO sqlite_sequence (name, seq)
193 SELECT t.name, 0 FROM (SELECT 'users' AS name UNION ALL SELECT 'orgs' UNION ALL SELECT 'repos'
194 UNION ALL SELECT 'api_tokens' UNION ALL SELECT 'ssh_keys'
195 UNION ALL SELECT 'webhook_deliveries' UNION ALL SELECT 'push_queue') t
196 WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence s WHERE s.name = t.name);
197
198UPDATE sqlite_sequence SET seq = MAX(seq,
199 (SELECT COALESCE(MAX(subject_id), 0) FROM repo_access WHERE subject_kind = 'user'),
200 (SELECT COALESCE(MAX(actor_ref), 0) FROM audit_log),
201 (SELECT COALESCE(MAX(user_id), 0) FROM page_domains),
202 (SELECT COALESCE(MAX(owner_id), 0) FROM profile_about_backfill WHERE owner_kind = 'user'))
203 WHERE name = 'users';
204UPDATE sqlite_sequence SET seq = MAX(seq,
205 (SELECT COALESCE(MAX(subject_id), 0) FROM repo_access WHERE subject_kind = 'org'),
206 (SELECT COALESCE(MAX(owner_id), 0) FROM profile_about_backfill WHERE owner_kind = 'org'))
207 WHERE name = 'orgs';
208UPDATE sqlite_sequence SET seq = MAX(seq,
209 (SELECT COALESCE(MAX(CAST(substr(scope, 8, instr(substr(scope, 8), ':') - 1) AS INTEGER)), 0)
210 FROM ssh_keys WHERE scope LIKE 'deploy:%'))
211 WHERE name = 'repos';