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