This repository has no description
1package db
2
3import (
4 "context"
5 "database/sql"
6 "log/slog"
7 "strings"
8
9 _ "github.com/mattn/go-sqlite3"
10 "tangled.org/core/log"
11 "tangled.org/core/orm"
12)
13
14type DB struct {
15 *sql.DB
16 logger *slog.Logger
17}
18
19type Execer interface {
20 Query(query string, args ...any) (*sql.Rows, error)
21 QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
22 QueryRow(query string, args ...any) *sql.Row
23 QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
24 Exec(query string, args ...any) (sql.Result, error)
25 ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
26 Prepare(query string) (*sql.Stmt, error)
27 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
28}
29
30func Make(ctx context.Context, dbPath string) (*DB, error) {
31 // https://github.com/mattn/go-sqlite3#connection-string
32 opts := []string{
33 "_foreign_keys=1",
34 "_journal_mode=WAL",
35 "_synchronous=NORMAL",
36 "_auto_vacuum=incremental",
37 "_busy_timeout=5000",
38 }
39
40 logger := log.FromContext(ctx)
41 logger = log.SubLogger(logger, "db")
42
43 db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
44 if err != nil {
45 return nil, err
46 }
47
48 conn, err := db.Conn(ctx)
49 if err != nil {
50 return nil, err
51 }
52 defer conn.Close()
53
54 _, err = conn.ExecContext(ctx, `
55 create table if not exists registrations (
56 id integer primary key autoincrement,
57 domain text not null unique,
58 did text not null,
59 secret text not null,
60 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
61 registered text
62 );
63 create table if not exists public_keys (
64 id integer primary key autoincrement,
65 did text not null,
66 name text not null,
67 key text not null,
68 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
69 unique(did, name, key)
70 );
71 create table if not exists repos (
72 id integer primary key autoincrement,
73 did text not null,
74 name text not null,
75 knot text not null,
76 rkey text not null,
77 at_uri text not null unique,
78 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
79 unique(did, name, knot, rkey)
80 );
81 create table if not exists collaborators (
82 id integer primary key autoincrement,
83 did text not null,
84 repo integer not null,
85 foreign key (repo) references repos(id) on delete cascade
86 );
87 create table if not exists follows (
88 user_did text not null,
89 subject_did text not null,
90 rkey text not null,
91 followed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
92 primary key (user_did, subject_did),
93 check (user_did <> subject_did)
94 );
95 create table if not exists vouches (
96 did text not null,
97 subject_did text not null,
98 cid text not null,
99 kind text not null default 'vouch',
100 reason text,
101 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
102 primary key (did, subject_did),
103 check (did <> subject_did),
104 check (kind in ('vouch', 'denounce'))
105 );
106 create table if not exists issues (
107 id integer primary key autoincrement,
108 owner_did text not null,
109 repo_at text not null,
110 issue_id integer not null,
111 title text not null,
112 body text not null,
113 open integer not null default 1,
114 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
115 issue_at text,
116 unique(repo_at, issue_id),
117 foreign key (repo_at) references repos(at_uri) on delete cascade
118 );
119 create table if not exists comments (
120 id integer primary key autoincrement,
121 owner_did text not null,
122 issue_id integer not null,
123 repo_at text not null,
124 comment_id integer not null,
125 comment_at text not null,
126 body text not null,
127 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
128 unique(issue_id, comment_id),
129 foreign key (repo_at, issue_id) references issues(repo_at, issue_id) on delete cascade
130 );
131 create table if not exists pulls (
132 -- identifiers
133 id integer primary key autoincrement,
134 pull_id integer not null,
135
136 -- at identifiers
137 repo_at text not null,
138 owner_did text not null,
139 rkey text not null,
140 pull_at text,
141
142 -- content
143 title text not null,
144 body text not null,
145 target_branch text not null,
146 state integer not null default 0 check (state in (0, 1, 2)), -- open, merged, closed
147
148 -- meta
149 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
150
151 -- constraints
152 unique(repo_at, pull_id),
153 foreign key (repo_at) references repos(at_uri) on delete cascade
154 );
155
156 -- every pull must have atleast 1 submission: the initial submission
157 create table if not exists pull_submissions (
158 -- identifiers
159 id integer primary key autoincrement,
160 pull_id integer not null,
161
162 -- at identifiers
163 repo_at text not null,
164
165 -- content, these are immutable, and require a resubmission to update
166 round_number integer not null default 0,
167 patch text,
168
169 -- meta
170 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
171
172 -- constraints
173 unique(repo_at, pull_id, round_number),
174 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade
175 );
176
177 create table if not exists pull_comments (
178 -- identifiers
179 id integer primary key autoincrement,
180 pull_id integer not null,
181 submission_id integer not null,
182
183 -- at identifiers
184 repo_at text not null,
185 owner_did text not null,
186 comment_at text not null,
187
188 -- content
189 body text not null,
190
191 -- meta
192 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
193
194 -- constraints
195 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade,
196 foreign key (submission_id) references pull_submissions(id) on delete cascade
197 );
198
199 create table if not exists _jetstream (
200 id integer primary key autoincrement,
201 last_time_us integer not null
202 );
203
204 create table if not exists repo_issue_seqs (
205 repo_at text primary key,
206 next_issue_id integer not null default 1
207 );
208
209 create table if not exists repo_pull_seqs (
210 repo_at text primary key,
211 next_pull_id integer not null default 1
212 );
213
214 create table if not exists stars (
215 id integer primary key autoincrement,
216 starred_by_did text not null,
217 repo_at text not null,
218 rkey text not null,
219 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
220 foreign key (repo_at) references repos(at_uri) on delete cascade,
221 unique(starred_by_did, repo_at)
222 );
223
224 create table if not exists reactions (
225 id integer primary key autoincrement,
226 reacted_by_did text not null,
227 thread_at text not null,
228 kind text not null,
229 rkey text not null,
230 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
231 unique(reacted_by_did, thread_at, kind)
232 );
233
234 create table if not exists emails (
235 id integer primary key autoincrement,
236 did text not null,
237 email text not null,
238 verified integer not null default 0,
239 verification_code text not null,
240 last_sent text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
241 is_primary integer not null default 0,
242 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
243 unique(did, email)
244 );
245
246 create table if not exists artifacts (
247 -- id
248 id integer primary key autoincrement,
249 did text not null,
250 rkey text not null,
251
252 -- meta
253 repo_at text not null,
254 tag binary(20) not null,
255 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
256
257 -- data
258 blob_cid text not null,
259 name text not null,
260 size integer not null default 0,
261 mimetype string not null default "*/*",
262
263 -- constraints
264 unique(did, rkey), -- record must be unique
265 unique(repo_at, tag, name), -- for a given tag object, each file must be unique
266 foreign key (repo_at) references repos(at_uri) on delete cascade
267 );
268
269 create table if not exists profile (
270 -- id
271 id integer primary key autoincrement,
272 did text not null,
273
274 -- data
275 description text not null,
276 include_bluesky integer not null default 0,
277 location text,
278
279 -- constraints
280 unique(did)
281 );
282 create table if not exists profile_links (
283 -- id
284 id integer primary key autoincrement,
285 did text not null,
286
287 -- data
288 link text not null,
289
290 -- constraints
291 foreign key (did) references profile(did) on delete cascade
292 );
293 create table if not exists profile_stats (
294 -- id
295 id integer primary key autoincrement,
296 did text not null,
297
298 -- data
299 kind text not null check (kind in (
300 "merged-pull-request-count",
301 "closed-pull-request-count",
302 "open-pull-request-count",
303 "open-issue-count",
304 "closed-issue-count",
305 "repository-count"
306 )),
307
308 -- constraints
309 foreign key (did) references profile(did) on delete cascade
310 );
311 create table if not exists profile_pinned_repositories (
312 -- id
313 id integer primary key autoincrement,
314 did text not null,
315
316 -- data
317 at_uri text not null,
318
319 -- constraints
320 unique(did, at_uri),
321 foreign key (did) references profile(did) on delete cascade,
322 foreign key (at_uri) references repos(at_uri) on delete cascade
323 );
324
325 create table if not exists oauth_requests (
326 id integer primary key autoincrement,
327 auth_server_iss text not null,
328 state text not null,
329 did text not null,
330 handle text not null,
331 pds_url text not null,
332 pkce_verifier text not null,
333 dpop_auth_server_nonce text not null,
334 dpop_private_jwk text not null
335 );
336
337 create table if not exists oauth_sessions (
338 id integer primary key autoincrement,
339 did text not null,
340 handle text not null,
341 pds_url text not null,
342 auth_server_iss text not null,
343 access_jwt text not null,
344 refresh_jwt text not null,
345 dpop_pds_nonce text,
346 dpop_auth_server_nonce text not null,
347 dpop_private_jwk text not null,
348 expiry text not null
349 );
350
351 create table if not exists punchcard (
352 did text not null,
353 date text not null, -- yyyy-mm-dd
354 count integer,
355 primary key (did, date)
356 );
357
358 create table if not exists spindles (
359 id integer primary key autoincrement,
360 owner text not null,
361 instance text not null,
362 verified text, -- time of verification
363 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
364
365 unique(owner, instance)
366 );
367
368 create table if not exists spindle_members (
369 -- identifiers for the record
370 id integer primary key autoincrement,
371 did text not null,
372 rkey text not null,
373
374 -- data
375 instance text not null,
376 subject text not null,
377 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
378
379 -- constraints
380 unique (did, instance, subject)
381 );
382
383 create table if not exists pipelines (
384 -- identifiers
385 id integer primary key autoincrement,
386 knot text not null,
387 rkey text not null,
388
389 repo_owner text not null,
390 repo_name text not null,
391
392 -- every pipeline must be associated with exactly one commit
393 sha text not null check (length(sha) = 40),
394 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
395
396 -- trigger data
397 trigger_id integer not null,
398
399 unique(knot, rkey),
400 foreign key (trigger_id) references triggers(id) on delete cascade
401 );
402
403 create table if not exists triggers (
404 -- primary key
405 id integer primary key autoincrement,
406
407 -- top-level fields
408 kind text not null,
409
410 -- pushTriggerData fields
411 push_ref text,
412 push_new_sha text check (length(push_new_sha) = 40),
413 push_old_sha text check (length(push_old_sha) = 40),
414
415 -- pullRequestTriggerData fields
416 pr_source_branch text,
417 pr_target_branch text,
418 pr_source_sha text check (length(pr_source_sha) = 40),
419 pr_action text
420 );
421
422 create table if not exists pipeline_statuses (
423 -- identifiers
424 id integer primary key autoincrement,
425 spindle text not null,
426 rkey text not null,
427
428 -- referenced pipeline. these form the (did, rkey) pair
429 pipeline_knot text not null,
430 pipeline_rkey text not null,
431
432 -- content
433 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
434 workflow text not null,
435 status text not null,
436 error text,
437 exit_code integer not null default 0,
438
439 unique (spindle, rkey),
440 foreign key (pipeline_knot, pipeline_rkey)
441 references pipelines (knot, rkey)
442 on delete cascade
443 );
444
445 create table if not exists repo_languages (
446 -- identifiers
447 id integer primary key autoincrement,
448
449 -- repo identifiers
450 repo_at text not null,
451 ref text not null,
452 is_default_ref integer not null default 0,
453
454 -- language breakdown
455 language text not null,
456 bytes integer not null check (bytes >= 0),
457
458 unique(repo_at, ref, language)
459 );
460
461 create table if not exists signups_inflight (
462 id integer primary key autoincrement,
463 email text not null unique,
464 invite_code text not null,
465 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
466 );
467
468 create table if not exists strings (
469 -- identifiers
470 did text not null,
471 rkey text not null,
472
473 -- content
474 filename text not null,
475 description text,
476 content text not null,
477 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
478 edited text,
479
480 primary key (did, rkey)
481 );
482
483 create table if not exists label_definitions (
484 -- identifiers
485 id integer primary key autoincrement,
486 did text not null,
487 rkey text not null,
488 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.label.definition' || '/' || rkey) stored,
489
490 -- content
491 name text not null,
492 value_type text not null check (value_type in (
493 "null",
494 "boolean",
495 "integer",
496 "string"
497 )),
498 value_format text not null default "any",
499 value_enum text, -- comma separated list
500 scope text not null, -- comma separated list of nsid
501 color text,
502 multiple integer not null default 0,
503 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
504
505 -- constraints
506 unique (did, rkey)
507 unique (at_uri)
508 );
509
510 -- ops are flattened, a record may contain several additions and deletions, but the table will include one row per add/del
511 create table if not exists label_ops (
512 -- identifiers
513 id integer primary key autoincrement,
514 did text not null,
515 rkey text not null,
516 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.label.op' || '/' || rkey) stored,
517
518 -- content
519 subject text not null,
520 operation text not null check (operation in ("add", "del")),
521 operand_key text not null,
522 operand_value text not null,
523 -- we need two time values: performed is declared by the user, indexed is calculated by the av
524 performed text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
525 indexed text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
526
527 -- constraints
528 -- traditionally (did, rkey) pair should be unique, but not in this case
529 -- operand_key should reference a label definition
530 foreign key (operand_key) references label_definitions (at_uri) on delete cascade,
531 unique (did, rkey, subject, operand_key, operand_value)
532 );
533
534 create table if not exists repo_labels (
535 -- identifiers
536 id integer primary key autoincrement,
537
538 -- repo identifiers
539 repo_at text not null,
540
541 -- label to subscribe to
542 label_at text not null,
543
544 unique (repo_at, label_at)
545 );
546
547 create table if not exists notifications (
548 id integer primary key autoincrement,
549 recipient_did text not null,
550 actor_did text not null,
551 type text not null,
552 entity_type text not null,
553 entity_id text not null,
554 read integer not null default 0,
555 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
556 repo_id integer references repos(id),
557 issue_id integer references issues(id),
558 pull_id integer references pulls(id)
559 );
560
561 create table if not exists notification_preferences (
562 id integer primary key autoincrement,
563 user_did text not null unique,
564 repo_starred integer not null default 1,
565 issue_created integer not null default 1,
566 issue_commented integer not null default 1,
567 pull_created integer not null default 1,
568 pull_commented integer not null default 1,
569 followed integer not null default 1,
570 pull_merged integer not null default 1,
571 issue_closed integer not null default 1,
572 email_notifications integer not null default 0
573 );
574
575 create table if not exists reference_links (
576 id integer primary key autoincrement,
577 from_at text not null,
578 to_at text not null,
579 unique (from_at, to_at)
580 );
581
582 create table if not exists webhooks (
583 id integer primary key autoincrement,
584 repo_at text not null,
585 url text not null,
586 secret text,
587 active integer not null default 1,
588 events text not null, -- comma-separated list of events
589 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
590 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
591
592 foreign key (repo_at) references repos(at_uri) on delete cascade
593 );
594
595 create table if not exists webhook_deliveries (
596 id integer primary key autoincrement,
597 webhook_id integer not null,
598 event text not null,
599 delivery_id text not null,
600 url text not null,
601 request_body text not null,
602 response_code integer,
603 response_body text,
604 success integer not null default 0,
605 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
606
607 foreign key (webhook_id) references webhooks(id) on delete cascade
608 );
609
610 create table if not exists bluesky_posts (
611 rkey text primary key,
612 text text not null,
613 created_at text not null,
614 langs text,
615 facets text,
616 embed text,
617 like_count integer not null default 0,
618 reply_count integer not null default 0,
619 repost_count integer not null default 0,
620 quote_count integer not null default 0
621 );
622
623 create table if not exists domain_claims (
624 id integer primary key autoincrement,
625 did text not null unique,
626 domain text not null unique,
627 deleted text -- timestamp when the domain was released/unclaimed; null means actively claimed
628 );
629
630 create table if not exists repo_sites (
631 id integer primary key autoincrement,
632 repo_at text not null unique,
633 branch text not null,
634 dir text not null default '/',
635 is_index integer not null default 0,
636 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
637 updated text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
638 foreign key (repo_at) references repos(at_uri) on delete cascade
639 );
640
641 create table if not exists site_deploys (
642 id integer primary key autoincrement,
643 repo_at text not null,
644 branch text not null,
645 dir text not null default '/',
646 commit_sha text not null default '',
647 status text not null check (status in ('success', 'failure')),
648 trigger text not null check (trigger in ('config_change', 'push')),
649 error text not null default '',
650 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
651 foreign key (repo_at) references repos(at_uri) on delete cascade
652 );
653
654 create table if not exists punchcard_preferences (
655 id integer primary key autoincrement,
656 user_did text not null unique,
657 hide_mine integer default 0,
658 hide_others integer default 0
659 );
660
661 create table if not exists newsletter_preferences (
662 id integer primary key autoincrement,
663 user_did text not null unique,
664 status text not null check (status in ('subscribed', 'dismissed')),
665 email text,
666 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
667 );
668
669 create table if not exists vouch_evidences (
670 id integer primary key autoincrement,
671 vouch_id integer not null,
672 at_uri text not null,
673 unique(vouch_id, at_uri),
674 foreign key (vouch_id) references vouches(id) on delete cascade
675 );
676
677 create table if not exists migrations (
678 id integer primary key autoincrement,
679 name text unique
680 );
681
682 -- indexes for better performance
683 create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc);
684 create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read);
685 create index if not exists idx_references_from_at on reference_links(from_at);
686 create index if not exists idx_references_to_at on reference_links(to_at);
687 create index if not exists idx_webhooks_repo_at on webhooks(repo_at);
688 create index if not exists idx_webhook_deliveries_webhook_id on webhook_deliveries(webhook_id);
689 create index if not exists idx_site_deploys_repo_at on site_deploys(repo_at);
690 create index if not exists idx_newsletter_prefs_user_did on newsletter_preferences(user_did);
691 `)
692 if err != nil {
693 return nil, err
694 }
695
696 // run migrations
697 orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
698 tx.Exec(`
699 alter table repos add column description text check (length(description) <= 200);
700 `)
701 return nil
702 })
703
704 orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
705 // add unconstrained column
706 _, err := tx.Exec(`
707 alter table public_keys
708 add column rkey text;
709 `)
710 if err != nil {
711 return err
712 }
713
714 // backfill
715 _, err = tx.Exec(`
716 update public_keys
717 set rkey = ''
718 where rkey is null;
719 `)
720 if err != nil {
721 return err
722 }
723
724 return nil
725 })
726
727 orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
728 _, err := tx.Exec(`
729 alter table comments drop column comment_at;
730 alter table comments add column rkey text;
731 `)
732 return err
733 })
734
735 orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
736 _, err := tx.Exec(`
737 alter table comments add column deleted text; -- timestamp
738 alter table comments add column edited text; -- timestamp
739 `)
740 return err
741 })
742
743 orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
744 _, err := tx.Exec(`
745 alter table pulls add column source_branch text;
746 alter table pulls add column source_repo_at text;
747 alter table pull_submissions add column source_rev text;
748 `)
749 return err
750 })
751
752 orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
753 _, err := tx.Exec(`
754 alter table repos add column source text;
755 `)
756 return err
757 })
758
759 // disable foreign-keys for the next migration
760 // NOTE: this cannot be done in a transaction, so it is run outside [0]
761 //
762 // [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
763 conn.ExecContext(ctx, "pragma foreign_keys = off;")
764 orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
765 _, err := tx.Exec(`
766 create table pulls_new (
767 -- identifiers
768 id integer primary key autoincrement,
769 pull_id integer not null,
770
771 -- at identifiers
772 repo_at text not null,
773 owner_did text not null,
774 rkey text not null,
775
776 -- content
777 title text not null,
778 body text not null,
779 target_branch text not null,
780 state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted
781
782 -- source info
783 source_branch text,
784 source_repo_at text,
785
786 -- stacking
787 stack_id text,
788 change_id text,
789 parent_change_id text,
790
791 -- meta
792 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
793
794 -- constraints
795 unique(repo_at, pull_id),
796 foreign key (repo_at) references repos(at_uri) on delete cascade
797 );
798
799 insert into pulls_new (
800 id, pull_id,
801 repo_at, owner_did, rkey,
802 title, body, target_branch, state,
803 source_branch, source_repo_at,
804 created
805 )
806 select
807 id, pull_id,
808 repo_at, owner_did, rkey,
809 title, body, target_branch, state,
810 source_branch, source_repo_at,
811 created
812 FROM pulls;
813
814 drop table pulls;
815 alter table pulls_new rename to pulls;
816 `)
817 return err
818 })
819 conn.ExecContext(ctx, "pragma foreign_keys = on;")
820
821 orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
822 tx.Exec(`
823 alter table repos add column spindle text;
824 `)
825 return nil
826 })
827
828 // drop all knot secrets, add unique constraint to knots
829 //
830 // knots will henceforth use service auth for signed requests
831 orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
832 _, err := tx.Exec(`
833 create table registrations_new (
834 id integer primary key autoincrement,
835 domain text not null,
836 did text not null,
837 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
838 registered text,
839 read_only integer not null default 0,
840 unique(domain, did)
841 );
842
843 insert into registrations_new (id, domain, did, created, registered, read_only)
844 select id, domain, did, created, registered, 1 from registrations
845 where registered is not null;
846
847 drop table registrations;
848 alter table registrations_new rename to registrations;
849 `)
850 return err
851 })
852
853 // recreate and add rkey + created columns with default constraint
854 orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
855 // create new table
856 // - repo_at instead of repo integer
857 // - rkey field
858 // - created field
859 _, err := tx.Exec(`
860 create table collaborators_new (
861 -- identifiers for the record
862 id integer primary key autoincrement,
863 did text not null,
864 rkey text,
865
866 -- content
867 subject_did text not null,
868 repo_at text not null,
869
870 -- meta
871 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
872
873 -- constraints
874 foreign key (repo_at) references repos(at_uri) on delete cascade
875 )
876 `)
877 if err != nil {
878 return err
879 }
880
881 // copy data
882 _, err = tx.Exec(`
883 insert into collaborators_new (id, did, rkey, subject_did, repo_at)
884 select
885 c.id,
886 r.did,
887 '',
888 c.did,
889 r.at_uri
890 from collaborators c
891 join repos r on c.repo = r.id
892 `)
893 if err != nil {
894 return err
895 }
896
897 // drop old table
898 _, err = tx.Exec(`drop table collaborators`)
899 if err != nil {
900 return err
901 }
902
903 // rename new table
904 _, err = tx.Exec(`alter table collaborators_new rename to collaborators`)
905 return err
906 })
907
908 orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
909 _, err := tx.Exec(`
910 alter table issues add column rkey text not null default '';
911
912 -- get last url section from issue_at and save to rkey column
913 update issues
914 set rkey = replace(issue_at, rtrim(issue_at, replace(issue_at, '/', '')), '');
915 `)
916 return err
917 })
918
919 // repurpose the read-only column to "needs-upgrade"
920 orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
921 _, err := tx.Exec(`
922 alter table registrations rename column read_only to needs_upgrade;
923 `)
924 return err
925 })
926
927 // require all knots to upgrade after the release of total xrpc
928 orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
929 _, err := tx.Exec(`
930 update registrations set needs_upgrade = 1;
931 `)
932 return err
933 })
934
935 // require all knots to upgrade after the release of total xrpc
936 orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
937 _, err := tx.Exec(`
938 alter table spindles add column needs_upgrade integer not null default 0;
939 `)
940 return err
941 })
942
943 // remove issue_at from issues and replace with generated column
944 //
945 // this requires a full table recreation because stored columns
946 // cannot be added via alter
947 //
948 // couple other changes:
949 // - columns renamed to be more consistent
950 // - adds edited and deleted fields
951 //
952 // disable foreign-keys for the next migration
953 conn.ExecContext(ctx, "pragma foreign_keys = off;")
954 orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
955 _, err := tx.Exec(`
956 create table if not exists issues_new (
957 -- identifiers
958 id integer primary key autoincrement,
959 did text not null,
960 rkey text not null,
961 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) stored,
962
963 -- at identifiers
964 repo_at text not null,
965
966 -- content
967 issue_id integer not null,
968 title text not null,
969 body text not null,
970 open integer not null default 1,
971 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
972 edited text, -- timestamp
973 deleted text, -- timestamp
974
975 unique(did, rkey),
976 unique(repo_at, issue_id),
977 unique(at_uri),
978 foreign key (repo_at) references repos(at_uri) on delete cascade
979 );
980 `)
981 if err != nil {
982 return err
983 }
984
985 // transfer data
986 _, err = tx.Exec(`
987 insert into issues_new (id, did, rkey, repo_at, issue_id, title, body, open, created)
988 select
989 i.id,
990 i.owner_did,
991 i.rkey,
992 i.repo_at,
993 i.issue_id,
994 i.title,
995 i.body,
996 i.open,
997 i.created
998 from issues i;
999 `)
1000 if err != nil {
1001 return err
1002 }
1003
1004 // drop old table
1005 _, err = tx.Exec(`drop table issues`)
1006 if err != nil {
1007 return err
1008 }
1009
1010 // rename new table
1011 _, err = tx.Exec(`alter table issues_new rename to issues`)
1012 return err
1013 })
1014 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1015
1016 // - renames the comments table to 'issue_comments'
1017 // - rework issue comments to update constraints:
1018 // * unique(did, rkey)
1019 // * remove comment-id and just use the global ID
1020 // * foreign key (repo_at, issue_id)
1021 // - new columns
1022 // * column "reply_to" which can be any other comment
1023 // * column "at-uri" which is a generated column
1024 orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
1025 _, err := tx.Exec(`
1026 create table if not exists issue_comments (
1027 -- identifiers
1028 id integer primary key autoincrement,
1029 did text not null,
1030 rkey text,
1031 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue.comment' || '/' || rkey) stored,
1032
1033 -- at identifiers
1034 issue_at text not null,
1035 reply_to text, -- at_uri of parent comment
1036
1037 -- content
1038 body text not null,
1039 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1040 edited text,
1041 deleted text,
1042
1043 -- constraints
1044 unique(did, rkey),
1045 unique(at_uri),
1046 foreign key (issue_at) references issues(at_uri) on delete cascade
1047 );
1048 `)
1049 if err != nil {
1050 return err
1051 }
1052
1053 // transfer data
1054 _, err = tx.Exec(`
1055 insert into issue_comments (id, did, rkey, issue_at, body, created, edited, deleted)
1056 select
1057 c.id,
1058 c.owner_did,
1059 c.rkey,
1060 i.at_uri, -- get at_uri from issues table
1061 c.body,
1062 c.created,
1063 c.edited,
1064 c.deleted
1065 from comments c
1066 join issues i on c.repo_at = i.repo_at and c.issue_id = i.issue_id;
1067 `)
1068 if err != nil {
1069 return err
1070 }
1071
1072 // drop old table
1073 _, err = tx.Exec(`drop table comments`)
1074 return err
1075 })
1076
1077 // add generated at_uri column to pulls table
1078 //
1079 // this requires a full table recreation because stored columns
1080 // cannot be added via alter
1081 //
1082 // disable foreign-keys for the next migration
1083 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1084 orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
1085 _, err := tx.Exec(`
1086 create table if not exists pulls_new (
1087 -- identifiers
1088 id integer primary key autoincrement,
1089 pull_id integer not null,
1090 at_uri text generated always as ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) stored,
1091
1092 -- at identifiers
1093 repo_at text not null,
1094 owner_did text not null,
1095 rkey text not null,
1096
1097 -- content
1098 title text not null,
1099 body text not null,
1100 target_branch text not null,
1101 state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted
1102
1103 -- source info
1104 source_branch text,
1105 source_repo_at text,
1106
1107 -- stacking
1108 stack_id text,
1109 change_id text,
1110 parent_change_id text,
1111
1112 -- meta
1113 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1114
1115 -- constraints
1116 unique(repo_at, pull_id),
1117 unique(at_uri),
1118 foreign key (repo_at) references repos(at_uri) on delete cascade
1119 );
1120 `)
1121 if err != nil {
1122 return err
1123 }
1124
1125 // transfer data
1126 _, err = tx.Exec(`
1127 insert into pulls_new (
1128 id, pull_id, repo_at, owner_did, rkey,
1129 title, body, target_branch, state,
1130 source_branch, source_repo_at,
1131 stack_id, change_id, parent_change_id,
1132 created
1133 )
1134 select
1135 id, pull_id, repo_at, owner_did, rkey,
1136 title, body, target_branch, state,
1137 source_branch, source_repo_at,
1138 stack_id, change_id, parent_change_id,
1139 created
1140 from pulls;
1141 `)
1142 if err != nil {
1143 return err
1144 }
1145
1146 // drop old table
1147 _, err = tx.Exec(`drop table pulls`)
1148 if err != nil {
1149 return err
1150 }
1151
1152 // rename new table
1153 _, err = tx.Exec(`alter table pulls_new rename to pulls`)
1154 return err
1155 })
1156 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1157
1158 // remove repo_at and pull_id from pull_submissions and replace with pull_at
1159 //
1160 // this requires a full table recreation because stored columns
1161 // cannot be added via alter
1162 //
1163 // disable foreign-keys for the next migration
1164 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1165 orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1166 _, err := tx.Exec(`
1167 create table if not exists pull_submissions_new (
1168 -- identifiers
1169 id integer primary key autoincrement,
1170 pull_at text not null,
1171
1172 -- content, these are immutable, and require a resubmission to update
1173 round_number integer not null default 0,
1174 patch text,
1175 source_rev text,
1176
1177 -- meta
1178 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1179
1180 -- constraints
1181 unique(pull_at, round_number),
1182 foreign key (pull_at) references pulls(at_uri) on delete cascade
1183 );
1184 `)
1185 if err != nil {
1186 return err
1187 }
1188
1189 // transfer data, constructing pull_at from pulls table
1190 _, err = tx.Exec(`
1191 insert into pull_submissions_new (id, pull_at, round_number, patch, created)
1192 select
1193 ps.id,
1194 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey,
1195 ps.round_number,
1196 ps.patch,
1197 ps.created
1198 from pull_submissions ps
1199 join pulls p on ps.repo_at = p.repo_at and ps.pull_id = p.pull_id;
1200 `)
1201 if err != nil {
1202 return err
1203 }
1204
1205 // drop old table
1206 _, err = tx.Exec(`drop table pull_submissions`)
1207 if err != nil {
1208 return err
1209 }
1210
1211 // rename new table
1212 _, err = tx.Exec(`alter table pull_submissions_new rename to pull_submissions`)
1213 return err
1214 })
1215 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1216
1217 // knots may report the combined patch for a comparison, we can store that on the appview side
1218 // (but not on the pds record), because calculating the combined patch requires a git index
1219 orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1220 _, err := tx.Exec(`
1221 alter table pull_submissions add column combined text;
1222 `)
1223 return err
1224 })
1225
1226 orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1227 _, err := tx.Exec(`
1228 alter table profile add column pronouns text;
1229 `)
1230 return err
1231 })
1232
1233 orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1234 _, err := tx.Exec(`
1235 alter table repos add column website text;
1236 alter table repos add column topics text;
1237 `)
1238 return err
1239 })
1240
1241 orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1242 _, err := tx.Exec(`
1243 alter table notification_preferences add column user_mentioned integer not null default 1;
1244 `)
1245 return err
1246 })
1247
1248 // remove the foreign key constraints from stars.
1249 orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
1250 _, err := tx.Exec(`
1251 create table stars_new (
1252 id integer primary key autoincrement,
1253 did text not null,
1254 rkey text not null,
1255
1256 subject_at text not null,
1257
1258 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1259 unique(did, rkey),
1260 unique(did, subject_at)
1261 );
1262
1263 insert into stars_new (
1264 id,
1265 did,
1266 rkey,
1267 subject_at,
1268 created
1269 )
1270 select
1271 id,
1272 starred_by_did,
1273 rkey,
1274 repo_at,
1275 created
1276 from stars;
1277
1278 drop table stars;
1279 alter table stars_new rename to stars;
1280
1281 create index if not exists idx_stars_created on stars(created);
1282 create index if not exists idx_stars_subject_at_created on stars(subject_at, created);
1283 `)
1284 return err
1285 })
1286
1287 orm.RunMigration(conn, logger, "add-avatar-to-profile", func(tx *sql.Tx) error {
1288 _, err := tx.Exec(`
1289 alter table profile add column avatar text;
1290 `)
1291 return err
1292 })
1293
1294 orm.RunMigration(conn, logger, "remove-profile-stats-column-constraint", func(tx *sql.Tx) error {
1295 _, err := tx.Exec(`
1296 -- create new table without the check constraint
1297 create table profile_stats_new (
1298 id integer primary key autoincrement,
1299 did text not null,
1300 kind text not null, -- no constraint this time
1301 foreign key (did) references profile(did) on delete cascade
1302 );
1303
1304 -- copy data from old table
1305 insert into profile_stats_new (id, did, kind)
1306 select id, did, kind
1307 from profile_stats;
1308
1309 -- drop old table
1310 drop table profile_stats;
1311
1312 -- rename new table
1313 alter table profile_stats_new rename to profile_stats;
1314 `)
1315 return err
1316 })
1317
1318 orm.RunMigration(conn, logger, "add-preferred-handle-profile", func(tx *sql.Tx) error {
1319 _, err := tx.Exec(`
1320 alter table profile add column preferred_handle text;
1321 `)
1322 return err
1323 })
1324
1325 orm.RunMigration(conn, logger, "add-repo-did-column", func(tx *sql.Tx) error {
1326 _, err := tx.Exec(`
1327 alter table repos add column repo_did text;
1328 create unique index if not exists idx_repos_repo_did on repos(repo_did);
1329 `)
1330 return err
1331 })
1332
1333 orm.RunMigration(conn, logger, "add-pds-rewrite-status", func(tx *sql.Tx) error {
1334 _, err := tx.Exec(`
1335 create table if not exists pds_rewrite_status (
1336 id integer primary key autoincrement,
1337 user_did text not null,
1338 repo_did text not null,
1339 record_nsid text not null,
1340 record_rkey text not null,
1341 old_repo_at text not null,
1342 status text not null default 'pending',
1343 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1344 unique(user_did, record_nsid, record_rkey)
1345 );
1346 create index if not exists idx_pds_rewrite_user on pds_rewrite_status(user_did, status);
1347 `)
1348 return err
1349 })
1350
1351 orm.RunMigration(conn, logger, "add-pipelines-repo-did", func(tx *sql.Tx) error {
1352 _, err := tx.Exec(`
1353 alter table pipelines add column repo_did text;
1354 create index if not exists idx_pipelines_repo_did on pipelines(repo_did);
1355 `)
1356 return err
1357 })
1358
1359 orm.RunMigration(conn, logger, "migrate-knots-to-repo-dids", func(tx *sql.Tx) error {
1360 _, err := tx.Exec(`update registrations set needs_upgrade = 1`)
1361 return err
1362 })
1363
1364 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1365 orm.RunMigration(conn, logger, "drop-pinned-repos-at-uri-fk", func(tx *sql.Tx) error {
1366 _, err := tx.Exec(`
1367 create table if not exists profile_pinned_repositories_new (
1368 id integer primary key autoincrement,
1369 did text not null,
1370 pin text not null,
1371
1372 unique(did, pin),
1373 foreign key (did) references profile(did) on delete cascade
1374 );
1375
1376 insert into profile_pinned_repositories_new (id, did, pin)
1377 select id, did, at_uri from profile_pinned_repositories;
1378
1379 drop table profile_pinned_repositories;
1380
1381 alter table profile_pinned_repositories_new rename to profile_pinned_repositories;
1382 `)
1383 return err
1384 })
1385 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1386
1387 orm.RunMigration(conn, logger, "reset-profile-pin-rewrites", func(tx *sql.Tx) error {
1388 _, err := tx.Exec(`
1389 update pds_rewrite_status
1390 set status = 'pending',
1391 updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
1392 where record_nsid = 'sh.tangled.actor.profile'
1393 and status = 'done'
1394 `)
1395 return err
1396 })
1397
1398 orm.RunMigration(conn, logger, "add-blob-data-to-pull-submissions", func(tx *sql.Tx) error {
1399 _, err := tx.Exec(`
1400 alter table pull_submissions add column patch_blob_ref text;
1401 alter table pull_submissions add column patch_blob_mime text;
1402 alter table pull_submissions add column patch_blob_size integer;
1403 `)
1404 return err
1405 })
1406
1407 orm.RunMigration(conn, logger, "replace-parent-change-id-with-aturi", func(tx *sql.Tx) error {
1408 // add new column
1409 _, err := tx.Exec(`
1410 alter table pulls add column dependent_on text;
1411 `)
1412 if err != nil {
1413 return err
1414 }
1415
1416 // populate dependent_on with at_uri of the parent
1417 _, err = tx.Exec(`
1418 update pulls
1419 set dependent_on = (
1420 select at_uri
1421 from pulls as parent
1422 where parent.stack_id = pulls.stack_id
1423 and parent.change_id = pulls.parent_change_id
1424 )
1425 where parent_change_id is not null;
1426 `)
1427 if err != nil {
1428 return err
1429 }
1430
1431 // drop old columns
1432 _, err = tx.Exec(`
1433 alter table pulls drop column parent_change_id;
1434 alter table pulls drop column stack_id;
1435 `)
1436
1437 return err
1438 })
1439
1440 orm.RunMigration(conn, logger, "add-pds-migration", func(tx *sql.Tx) error {
1441 _, err := tx.Exec(`
1442 create table if not exists pds_migration (
1443 name text not null,
1444
1445 -- record at_uri
1446 did text not null,
1447 collection text not null,
1448 rkey text not null,
1449
1450 status text not null default 'pending',
1451 error_msg text,
1452 retry_count integer not null default 0,
1453 retry_after integer not null default 0,
1454 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1455
1456 unique(name, did, collection, rkey)
1457 );
1458 `)
1459 return err
1460 })
1461
1462 orm.RunMigration(conn, logger, "unify-pds-record-migration-table", func(tx *sql.Tx) error {
1463 _, err := tx.Exec(`
1464 insert into pds_migration (
1465 name,
1466 did,
1467 collection,
1468 rkey,
1469 status,
1470 updated_at
1471 )
1472 select
1473 'add-repo-did',
1474 user_did,
1475 record_nsid,
1476 record_rkey,
1477 status,
1478 updated_at
1479 from pds_rewrite_status;
1480
1481 drop table pds_rewrite_status;
1482 `)
1483 return err
1484 })
1485
1486 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1487 orm.RunMigration(conn, logger, "add-id-to-vouches", func(tx *sql.Tx) error {
1488 _, err := tx.Exec(`
1489 create table vouches_new (
1490 id integer primary key autoincrement,
1491 did text not null,
1492 subject_did text not null,
1493 cid text not null,
1494 kind text not null default 'vouch',
1495 reason text,
1496 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1497 unique(did, subject_did),
1498 check (did <> subject_did),
1499 check (kind in ('vouch', 'denounce'))
1500 );
1501
1502 insert into vouches_new (did, subject_did, cid, kind, reason, created_at)
1503 select did, subject_did, cid, kind, reason, created_at
1504 from vouches;
1505
1506 drop table vouches;
1507 alter table vouches_new rename to vouches;
1508 `)
1509 return err
1510 })
1511 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1512
1513 return &DB{
1514 db,
1515 logger,
1516 }, nil
1517}
1518
1519func (d *DB) Close() error {
1520 return d.DB.Close()
1521}