This repository has no description
1package db
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "log/slog"
8 "strings"
9
10 _ "github.com/mattn/go-sqlite3"
11 "tangled.org/core/log"
12 "tangled.org/core/orm"
13)
14
15type DB struct {
16 *sql.DB
17 logger *slog.Logger
18}
19
20type Execer interface {
21 Query(query string, args ...any) (*sql.Rows, error)
22 QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
23 QueryRow(query string, args ...any) *sql.Row
24 QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
25 Exec(query string, args ...any) (sql.Result, error)
26 ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
27 Prepare(query string) (*sql.Stmt, error)
28 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
29}
30
31func Make(ctx context.Context, dbPath string) (*DB, error) {
32 // https://github.com/mattn/go-sqlite3#connection-string
33 opts := []string{
34 "_foreign_keys=1",
35 "_journal_mode=WAL",
36 "_synchronous=NORMAL",
37 "_auto_vacuum=incremental",
38 "_busy_timeout=5000",
39 }
40
41 logger := log.FromContext(ctx)
42 logger = log.SubLogger(logger, "db")
43
44 db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
45 if err != nil {
46 return nil, err
47 }
48
49 conn, err := db.Conn(ctx)
50 if err != nil {
51 return nil, err
52 }
53 defer conn.Close()
54
55 _, err = conn.ExecContext(ctx, `
56 create table if not exists registrations (
57 id integer primary key autoincrement,
58 domain text not null unique,
59 did text not null,
60 secret text not null,
61 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
62 registered text
63 );
64 create table if not exists public_keys (
65 id integer primary key autoincrement,
66 did text not null,
67 name text not null,
68 key text not null,
69 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
70 unique(did, name, key)
71 );
72 create table if not exists repos (
73 id integer primary key autoincrement,
74 did text not null,
75 name text not null,
76 knot text not null,
77 rkey text not null,
78 at_uri text not null unique,
79 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
80 unique(did, name, knot, rkey)
81 );
82 create table if not exists collaborators (
83 id integer primary key autoincrement,
84 did text not null,
85 repo integer not null,
86 foreign key (repo) references repos(id) on delete cascade
87 );
88 create table if not exists follows (
89 user_did text not null,
90 subject_did text not null,
91 rkey text not null,
92 followed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
93 primary key (user_did, subject_did),
94 check (user_did <> subject_did)
95 );
96 create table if not exists vouches (
97 did text not null,
98 subject_did text not null,
99 cid text not null,
100 kind text not null default 'vouch',
101 reason text,
102 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
103 primary key (did, subject_did),
104 check (did <> subject_did),
105 check (kind in ('vouch', 'denounce'))
106 );
107 create table if not exists issues (
108 id integer primary key autoincrement,
109 owner_did text not null,
110 repo_at text not null,
111 issue_id integer not null,
112 title text not null,
113 body text not null,
114 open integer not null default 1,
115 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
116 issue_at text,
117 unique(repo_at, issue_id),
118 foreign key (repo_at) references repos(at_uri) on delete cascade
119 );
120 create table if not exists pulls (
121 -- identifiers
122 id integer primary key autoincrement,
123 pull_id integer not null,
124
125 -- at identifiers
126 repo_at text not null,
127 owner_did text not null,
128 rkey text not null,
129 pull_at text,
130
131 -- content
132 title text not null,
133 body text not null,
134 target_branch text not null,
135 state integer not null default 0 check (state in (0, 1, 2)), -- open, merged, closed
136
137 -- meta
138 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
139
140 -- constraints
141 unique(repo_at, pull_id),
142 foreign key (repo_at) references repos(at_uri) on delete cascade
143 );
144
145 -- every pull must have atleast 1 submission: the initial submission
146 create table if not exists pull_submissions (
147 -- identifiers
148 id integer primary key autoincrement,
149 pull_id integer not null,
150
151 -- at identifiers
152 repo_at text not null,
153
154 -- content, these are immutable, and require a resubmission to update
155 round_number integer not null default 0,
156 patch text,
157
158 -- meta
159 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
160
161 -- constraints
162 unique(repo_at, pull_id, round_number),
163 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade
164 );
165
166 create table if not exists pull_comments (
167 -- identifiers
168 id integer primary key autoincrement,
169 pull_id integer not null,
170 submission_id integer not null,
171
172 -- at identifiers
173 repo_at text not null,
174 owner_did text not null,
175 comment_at text not null,
176
177 -- content
178 body text not null,
179
180 -- meta
181 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
182
183 -- constraints
184 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade,
185 foreign key (submission_id) references pull_submissions(id) on delete cascade
186 );
187
188 create table if not exists _jetstream (
189 id integer primary key autoincrement,
190 last_time_us integer not null
191 );
192
193 create table if not exists repo_issue_seqs (
194 repo_at text primary key,
195 next_issue_id integer not null default 1
196 );
197
198 create table if not exists repo_pull_seqs (
199 repo_at text primary key,
200 next_pull_id integer not null default 1
201 );
202
203 create table if not exists stars (
204 id integer primary key autoincrement,
205 starred_by_did text not null,
206 repo_at text not null,
207 rkey text not null,
208 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
209 foreign key (repo_at) references repos(at_uri) on delete cascade,
210 unique(starred_by_did, repo_at)
211 );
212
213 create table if not exists reactions (
214 id integer primary key autoincrement,
215 reacted_by_did text not null,
216 thread_at text not null,
217 kind text not null,
218 rkey text not null,
219 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
220 unique(reacted_by_did, thread_at, kind)
221 );
222
223 create table if not exists emails (
224 id integer primary key autoincrement,
225 did text not null,
226 email text not null,
227 verified integer not null default 0,
228 verification_code text not null,
229 last_sent text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
230 is_primary integer not null default 0,
231 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
232 unique(did, email)
233 );
234
235 create table if not exists artifacts (
236 -- id
237 id integer primary key autoincrement,
238 did text not null,
239 rkey text not null,
240
241 -- meta
242 repo_at text not null,
243 tag binary(20) not null,
244 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
245
246 -- data
247 blob_cid text not null,
248 name text not null,
249 size integer not null default 0,
250 mimetype string not null default "*/*",
251
252 -- constraints
253 unique(did, rkey), -- record must be unique
254 unique(repo_at, tag, name), -- for a given tag object, each file must be unique
255 foreign key (repo_at) references repos(at_uri) on delete cascade
256 );
257
258 create table if not exists profile (
259 -- id
260 id integer primary key autoincrement,
261 did text not null,
262
263 -- data
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 domain_claims (
613 id integer primary key autoincrement,
614 did text not null unique,
615 domain text not null unique,
616 deleted text -- timestamp when the domain was released/unclaimed; null means actively claimed
617 );
618
619 create table if not exists repo_sites (
620 id integer primary key autoincrement,
621 repo_at text not null unique,
622 branch text not null,
623 dir text not null default '/',
624 is_index integer not null default 0,
625 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
626 updated text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
627 foreign key (repo_at) references repos(at_uri) on delete cascade
628 );
629
630 create table if not exists site_deploys (
631 id integer primary key autoincrement,
632 repo_at text not null,
633 branch text not null,
634 dir text not null default '/',
635 commit_sha text not null default '',
636 status text not null check (status in ('success', 'failure')),
637 trigger text not null check (trigger in ('config_change', 'push')),
638 error text not null default '',
639 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
640 foreign key (repo_at) references repos(at_uri) on delete cascade
641 );
642
643 create table if not exists punchcard_preferences (
644 id integer primary key autoincrement,
645 user_did text not null unique,
646 hide_mine integer default 0,
647 hide_others integer default 0
648 );
649
650 create table if not exists newsletter_preferences (
651 id integer primary key autoincrement,
652 user_did text not null unique,
653 status text not null check (status in ('subscribed', 'dismissed')),
654 email text,
655 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
656 );
657
658 create table if not exists vouch_evidences (
659 id integer primary key autoincrement,
660 vouch_id integer not null,
661 at_uri text not null,
662 unique(vouch_id, at_uri),
663 foreign key (vouch_id) references vouches(id) on delete cascade
664 );
665
666 create table if not exists vouch_skips (
667 did text not null,
668 subject_did text not null,
669 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
670 primary key (did, subject_did),
671 check (did <> subject_did)
672 );
673
674
675 create table if not exists migrations (
676 id integer primary key autoincrement,
677 name text unique
678 );
679
680 -- indexes for better performance
681 create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc);
682 create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read);
683 create index if not exists idx_references_from_at on reference_links(from_at);
684 create index if not exists idx_references_to_at on reference_links(to_at);
685 create index if not exists idx_webhook_deliveries_webhook_id on webhook_deliveries(webhook_id);
686 create index if not exists idx_newsletter_prefs_user_did on newsletter_preferences(user_did);
687 `)
688 if err != nil {
689 return nil, err
690 }
691
692 // run migrations
693 orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
694 tx.Exec(`
695 alter table repos add column description text check (length(description) <= 200);
696 `)
697 return nil
698 })
699
700 orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
701 // add unconstrained column
702 _, err := tx.Exec(`
703 alter table public_keys
704 add column rkey text;
705 `)
706 if err != nil {
707 return err
708 }
709
710 // backfill
711 _, err = tx.Exec(`
712 update public_keys
713 set rkey = ''
714 where rkey is null;
715 `)
716 if err != nil {
717 return err
718 }
719
720 return nil
721 })
722
723 orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
724 _, err := tx.Exec(`
725 alter table comments drop column comment_at;
726 alter table comments add column rkey text;
727 `)
728 return err
729 })
730
731 orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
732 _, err := tx.Exec(`
733 alter table comments add column deleted text; -- timestamp
734 alter table comments add column edited text; -- timestamp
735 `)
736 return err
737 })
738
739 orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
740 _, err := tx.Exec(`
741 alter table pulls add column source_branch text;
742 alter table pulls add column source_repo_at text;
743 alter table pull_submissions add column source_rev text;
744 `)
745 return err
746 })
747
748 orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
749 _, err := tx.Exec(`
750 alter table repos add column source text;
751 `)
752 return err
753 })
754
755 // disable foreign-keys for the next migration
756 // NOTE: this cannot be done in a transaction, so it is run outside [0]
757 //
758 // [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
759 conn.ExecContext(ctx, "pragma foreign_keys = off;")
760 orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
761 _, err := tx.Exec(`
762 create table pulls_new (
763 -- identifiers
764 id integer primary key autoincrement,
765 pull_id integer not null,
766
767 -- at identifiers
768 repo_at text not null,
769 owner_did text not null,
770 rkey text not null,
771
772 -- content
773 title text not null,
774 body text not null,
775 target_branch text not null,
776 state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted
777
778 -- source info
779 source_branch text,
780 source_repo_at text,
781
782 -- stacking
783 stack_id text,
784 change_id text,
785 parent_change_id text,
786
787 -- meta
788 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
789
790 -- constraints
791 unique(repo_at, pull_id),
792 foreign key (repo_at) references repos(at_uri) on delete cascade
793 );
794
795 insert into pulls_new (
796 id, pull_id,
797 repo_at, owner_did, rkey,
798 title, body, target_branch, state,
799 source_branch, source_repo_at,
800 created
801 )
802 select
803 id, pull_id,
804 repo_at, owner_did, rkey,
805 title, body, target_branch, state,
806 source_branch, source_repo_at,
807 created
808 FROM pulls;
809
810 drop table pulls;
811 alter table pulls_new rename to pulls;
812 `)
813 return err
814 })
815 conn.ExecContext(ctx, "pragma foreign_keys = on;")
816
817 orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
818 tx.Exec(`
819 alter table repos add column spindle text;
820 `)
821 return nil
822 })
823
824 // drop all knot secrets, add unique constraint to knots
825 //
826 // knots will henceforth use service auth for signed requests
827 orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
828 _, err := tx.Exec(`
829 create table registrations_new (
830 id integer primary key autoincrement,
831 domain text not null,
832 did text not null,
833 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
834 registered text,
835 read_only integer not null default 0,
836 unique(domain, did)
837 );
838
839 insert into registrations_new (id, domain, did, created, registered, read_only)
840 select id, domain, did, created, registered, 1 from registrations
841 where registered is not null;
842
843 drop table registrations;
844 alter table registrations_new rename to registrations;
845 `)
846 return err
847 })
848
849 // recreate and add rkey + created columns with default constraint
850 orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
851 // create new table
852 // - repo_at instead of repo integer
853 // - rkey field
854 // - created field
855 _, err := tx.Exec(`
856 create table collaborators_new (
857 -- identifiers for the record
858 id integer primary key autoincrement,
859 did text not null,
860 rkey text,
861
862 -- content
863 subject_did text not null,
864 repo_at text not null,
865
866 -- meta
867 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
868
869 -- constraints
870 foreign key (repo_at) references repos(at_uri) on delete cascade
871 )
872 `)
873 if err != nil {
874 return err
875 }
876
877 // copy data
878 _, err = tx.Exec(`
879 insert into collaborators_new (id, did, rkey, subject_did, repo_at)
880 select
881 c.id,
882 r.did,
883 '',
884 c.did,
885 r.at_uri
886 from collaborators c
887 join repos r on c.repo = r.id
888 `)
889 if err != nil {
890 return err
891 }
892
893 // drop old table
894 _, err = tx.Exec(`drop table collaborators`)
895 if err != nil {
896 return err
897 }
898
899 // rename new table
900 _, err = tx.Exec(`alter table collaborators_new rename to collaborators`)
901 return err
902 })
903
904 orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
905 _, err := tx.Exec(`
906 alter table issues add column rkey text not null default '';
907
908 -- get last url section from issue_at and save to rkey column
909 update issues
910 set rkey = replace(issue_at, rtrim(issue_at, replace(issue_at, '/', '')), '');
911 `)
912 return err
913 })
914
915 // repurpose the read-only column to "needs-upgrade"
916 orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
917 _, err := tx.Exec(`
918 alter table registrations rename column read_only to needs_upgrade;
919 `)
920 return err
921 })
922
923 // require all knots to upgrade after the release of total xrpc
924 orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
925 _, err := tx.Exec(`
926 update registrations set needs_upgrade = 1;
927 `)
928 return err
929 })
930
931 // require all knots to upgrade after the release of total xrpc
932 orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
933 _, err := tx.Exec(`
934 alter table spindles add column needs_upgrade integer not null default 0;
935 `)
936 return err
937 })
938
939 // remove issue_at from issues and replace with generated column
940 //
941 // this requires a full table recreation because stored columns
942 // cannot be added via alter
943 //
944 // couple other changes:
945 // - columns renamed to be more consistent
946 // - adds edited and deleted fields
947 //
948 // disable foreign-keys for the next migration
949 conn.ExecContext(ctx, "pragma foreign_keys = off;")
950 orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
951 _, err := tx.Exec(`
952 create table if not exists issues_new (
953 -- identifiers
954 id integer primary key autoincrement,
955 did text not null,
956 rkey text not null,
957 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) stored,
958
959 -- at identifiers
960 repo_at text not null,
961
962 -- content
963 issue_id integer not null,
964 title text not null,
965 body text not null,
966 open integer not null default 1,
967 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
968 edited text, -- timestamp
969 deleted text, -- timestamp
970
971 unique(did, rkey),
972 unique(repo_at, issue_id),
973 unique(at_uri),
974 foreign key (repo_at) references repos(at_uri) on delete cascade
975 );
976 `)
977 if err != nil {
978 return err
979 }
980
981 // transfer data
982 _, err = tx.Exec(`
983 insert into issues_new (id, did, rkey, repo_at, issue_id, title, body, open, created)
984 select
985 i.id,
986 i.owner_did,
987 i.rkey,
988 i.repo_at,
989 i.issue_id,
990 i.title,
991 i.body,
992 i.open,
993 i.created
994 from issues i;
995 `)
996 if err != nil {
997 return err
998 }
999
1000 // drop old table
1001 _, err = tx.Exec(`drop table issues`)
1002 if err != nil {
1003 return err
1004 }
1005
1006 // rename new table
1007 _, err = tx.Exec(`alter table issues_new rename to issues`)
1008 return err
1009 })
1010 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1011
1012 // - renames the comments table to 'issue_comments'
1013 // - rework issue comments to update constraints:
1014 // * unique(did, rkey)
1015 // * remove comment-id and just use the global ID
1016 // * foreign key (repo_at, issue_id)
1017 // - new columns
1018 // * column "reply_to" which can be any other comment
1019 // * column "at-uri" which is a generated column
1020 orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
1021 _, err := tx.Exec(`
1022 create table if not exists issue_comments (
1023 -- identifiers
1024 id integer primary key autoincrement,
1025 did text not null,
1026 rkey text,
1027 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue.comment' || '/' || rkey) stored,
1028
1029 -- at identifiers
1030 issue_at text not null,
1031 reply_to text, -- at_uri of parent comment
1032
1033 -- content
1034 body text not null,
1035 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1036 edited text,
1037 deleted text,
1038
1039 -- constraints
1040 unique(did, rkey),
1041 unique(at_uri),
1042 foreign key (issue_at) references issues(at_uri) on delete cascade
1043 );
1044 `)
1045 if err != nil {
1046 return err
1047 }
1048
1049 // transfer data
1050 _, err = tx.Exec(`
1051 insert into issue_comments (id, did, rkey, issue_at, body, created, edited, deleted)
1052 select
1053 c.id,
1054 c.owner_did,
1055 c.rkey,
1056 i.at_uri, -- get at_uri from issues table
1057 c.body,
1058 c.created,
1059 c.edited,
1060 c.deleted
1061 from comments c
1062 join issues i on c.repo_at = i.repo_at and c.issue_id = i.issue_id;
1063 `)
1064 if err != nil {
1065 return err
1066 }
1067
1068 // drop old table
1069 _, err = tx.Exec(`drop table comments`)
1070 return err
1071 })
1072
1073 // add generated at_uri column to pulls table
1074 //
1075 // this requires a full table recreation because stored columns
1076 // cannot be added via alter
1077 //
1078 // disable foreign-keys for the next migration
1079 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1080 orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
1081 _, err := tx.Exec(`
1082 create table if not exists pulls_new (
1083 -- identifiers
1084 id integer primary key autoincrement,
1085 pull_id integer not null,
1086 at_uri text generated always as ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) stored,
1087
1088 -- at identifiers
1089 repo_at text not null,
1090 owner_did text not null,
1091 rkey text not null,
1092
1093 -- content
1094 title text not null,
1095 body text not null,
1096 target_branch text not null,
1097 state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted
1098
1099 -- source info
1100 source_branch text,
1101 source_repo_at text,
1102
1103 -- stacking
1104 stack_id text,
1105 change_id text,
1106 parent_change_id text,
1107
1108 -- meta
1109 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1110
1111 -- constraints
1112 unique(repo_at, pull_id),
1113 unique(at_uri),
1114 foreign key (repo_at) references repos(at_uri) on delete cascade
1115 );
1116 `)
1117 if err != nil {
1118 return err
1119 }
1120
1121 // transfer data
1122 _, err = tx.Exec(`
1123 insert into pulls_new (
1124 id, pull_id, repo_at, owner_did, rkey,
1125 title, body, target_branch, state,
1126 source_branch, source_repo_at,
1127 stack_id, change_id, parent_change_id,
1128 created
1129 )
1130 select
1131 id, pull_id, repo_at, owner_did, rkey,
1132 title, body, target_branch, state,
1133 source_branch, source_repo_at,
1134 stack_id, change_id, parent_change_id,
1135 created
1136 from pulls;
1137 `)
1138 if err != nil {
1139 return err
1140 }
1141
1142 // drop old table
1143 _, err = tx.Exec(`drop table pulls`)
1144 if err != nil {
1145 return err
1146 }
1147
1148 // rename new table
1149 _, err = tx.Exec(`alter table pulls_new rename to pulls`)
1150 return err
1151 })
1152 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1153
1154 // remove repo_at and pull_id from pull_submissions and replace with pull_at
1155 //
1156 // this requires a full table recreation because stored columns
1157 // cannot be added via alter
1158 //
1159 // disable foreign-keys for the next migration
1160 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1161 orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1162 _, err := tx.Exec(`
1163 create table if not exists pull_submissions_new (
1164 -- identifiers
1165 id integer primary key autoincrement,
1166 pull_at text not null,
1167
1168 -- content, these are immutable, and require a resubmission to update
1169 round_number integer not null default 0,
1170 patch text,
1171 source_rev text,
1172
1173 -- meta
1174 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1175
1176 -- constraints
1177 unique(pull_at, round_number),
1178 foreign key (pull_at) references pulls(at_uri) on delete cascade
1179 );
1180 `)
1181 if err != nil {
1182 return err
1183 }
1184
1185 // transfer data, constructing pull_at from pulls table
1186 _, err = tx.Exec(`
1187 insert into pull_submissions_new (id, pull_at, round_number, patch, created)
1188 select
1189 ps.id,
1190 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey,
1191 ps.round_number,
1192 ps.patch,
1193 ps.created
1194 from pull_submissions ps
1195 join pulls p on ps.repo_at = p.repo_at and ps.pull_id = p.pull_id;
1196 `)
1197 if err != nil {
1198 return err
1199 }
1200
1201 // drop old table
1202 _, err = tx.Exec(`drop table pull_submissions`)
1203 if err != nil {
1204 return err
1205 }
1206
1207 // rename new table
1208 _, err = tx.Exec(`alter table pull_submissions_new rename to pull_submissions`)
1209 return err
1210 })
1211 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1212
1213 // knots may report the combined patch for a comparison, we can store that on the appview side
1214 // (but not on the pds record), because calculating the combined patch requires a git index
1215 orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1216 _, err := tx.Exec(`
1217 alter table pull_submissions add column combined text;
1218 `)
1219 return err
1220 })
1221
1222 orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1223 _, err := tx.Exec(`
1224 alter table profile add column pronouns text;
1225 `)
1226 return err
1227 })
1228
1229 orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1230 _, err := tx.Exec(`
1231 alter table repos add column website text;
1232 alter table repos add column topics text;
1233 `)
1234 return err
1235 })
1236
1237 orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1238 _, err := tx.Exec(`
1239 alter table notification_preferences add column user_mentioned integer not null default 1;
1240 `)
1241 return err
1242 })
1243
1244 // remove the foreign key constraints from stars.
1245 orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
1246 _, err := tx.Exec(`
1247 create table stars_new (
1248 id integer primary key autoincrement,
1249 did text not null,
1250 rkey text not null,
1251
1252 subject_at text not null,
1253
1254 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1255 unique(did, rkey),
1256 unique(did, subject_at)
1257 );
1258
1259 insert into stars_new (
1260 id,
1261 did,
1262 rkey,
1263 subject_at,
1264 created
1265 )
1266 select
1267 id,
1268 starred_by_did,
1269 rkey,
1270 repo_at,
1271 created
1272 from stars;
1273
1274 drop table stars;
1275 alter table stars_new rename to stars;
1276
1277 create index if not exists idx_stars_created on stars(created);
1278 create index if not exists idx_stars_subject_at_created on stars(subject_at, created);
1279 `)
1280 return err
1281 })
1282
1283 orm.RunMigration(conn, logger, "add-avatar-to-profile", func(tx *sql.Tx) error {
1284 _, err := tx.Exec(`
1285 alter table profile add column avatar text;
1286 `)
1287 return err
1288 })
1289
1290 orm.RunMigration(conn, logger, "remove-profile-stats-column-constraint", func(tx *sql.Tx) error {
1291 _, err := tx.Exec(`
1292 -- create new table without the check constraint
1293 create table profile_stats_new (
1294 id integer primary key autoincrement,
1295 did text not null,
1296 kind text not null, -- no constraint this time
1297 foreign key (did) references profile(did) on delete cascade
1298 );
1299
1300 -- copy data from old table
1301 insert into profile_stats_new (id, did, kind)
1302 select id, did, kind
1303 from profile_stats;
1304
1305 -- drop old table
1306 drop table profile_stats;
1307
1308 -- rename new table
1309 alter table profile_stats_new rename to profile_stats;
1310 `)
1311 return err
1312 })
1313
1314 orm.RunMigration(conn, logger, "add-preferred-handle-profile", func(tx *sql.Tx) error {
1315 _, err := tx.Exec(`
1316 alter table profile add column preferred_handle text;
1317 `)
1318 return err
1319 })
1320
1321 orm.RunMigration(conn, logger, "add-repo-did-column", func(tx *sql.Tx) error {
1322 _, err := tx.Exec(`
1323 alter table repos add column repo_did text;
1324 create unique index if not exists idx_repos_repo_did on repos(repo_did);
1325 `)
1326 return err
1327 })
1328
1329 orm.RunMigration(conn, logger, "add-pds-rewrite-status", func(tx *sql.Tx) error {
1330 _, err := tx.Exec(`
1331 create table if not exists pds_rewrite_status (
1332 id integer primary key autoincrement,
1333 user_did text not null,
1334 repo_did text not null,
1335 record_nsid text not null,
1336 record_rkey text not null,
1337 old_repo_at text not null,
1338 status text not null default 'pending',
1339 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1340 unique(user_did, record_nsid, record_rkey)
1341 );
1342 create index if not exists idx_pds_rewrite_user on pds_rewrite_status(user_did, status);
1343 `)
1344 return err
1345 })
1346
1347 orm.RunMigration(conn, logger, "add-pipelines-repo-did", func(tx *sql.Tx) error {
1348 _, err := tx.Exec(`
1349 alter table pipelines add column repo_did text;
1350 create index if not exists idx_pipelines_repo_did on pipelines(repo_did);
1351 `)
1352 return err
1353 })
1354
1355 orm.RunMigration(conn, logger, "migrate-knots-to-repo-dids", func(tx *sql.Tx) error {
1356 _, err := tx.Exec(`update registrations set needs_upgrade = 1`)
1357 return err
1358 })
1359
1360 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1361 orm.RunMigration(conn, logger, "drop-pinned-repos-at-uri-fk", func(tx *sql.Tx) error {
1362 _, err := tx.Exec(`
1363 create table if not exists profile_pinned_repositories_new (
1364 id integer primary key autoincrement,
1365 did text not null,
1366 pin text not null,
1367
1368 unique(did, pin),
1369 foreign key (did) references profile(did) on delete cascade
1370 );
1371
1372 insert into profile_pinned_repositories_new (id, did, pin)
1373 select id, did, at_uri from profile_pinned_repositories;
1374
1375 drop table profile_pinned_repositories;
1376
1377 alter table profile_pinned_repositories_new rename to profile_pinned_repositories;
1378 `)
1379 return err
1380 })
1381 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1382
1383 orm.RunMigration(conn, logger, "reset-profile-pin-rewrites", func(tx *sql.Tx) error {
1384 _, err := tx.Exec(`
1385 update pds_rewrite_status
1386 set status = 'pending',
1387 updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
1388 where record_nsid = 'sh.tangled.actor.profile'
1389 and status = 'done'
1390 `)
1391 return err
1392 })
1393
1394 orm.RunMigration(conn, logger, "add-blob-data-to-pull-submissions", func(tx *sql.Tx) error {
1395 _, err := tx.Exec(`
1396 alter table pull_submissions add column patch_blob_ref text;
1397 alter table pull_submissions add column patch_blob_mime text;
1398 alter table pull_submissions add column patch_blob_size integer;
1399 `)
1400 return err
1401 })
1402
1403 orm.RunMigration(conn, logger, "replace-parent-change-id-with-aturi", func(tx *sql.Tx) error {
1404 // add new column
1405 _, err := tx.Exec(`
1406 alter table pulls add column dependent_on text;
1407 `)
1408 if err != nil {
1409 return err
1410 }
1411
1412 // populate dependent_on with at_uri of the parent
1413 _, err = tx.Exec(`
1414 update pulls
1415 set dependent_on = (
1416 select at_uri
1417 from pulls as parent
1418 where parent.stack_id = pulls.stack_id
1419 and parent.change_id = pulls.parent_change_id
1420 )
1421 where parent_change_id is not null;
1422 `)
1423 if err != nil {
1424 return err
1425 }
1426
1427 // drop old columns
1428 _, err = tx.Exec(`
1429 alter table pulls drop column parent_change_id;
1430 alter table pulls drop column stack_id;
1431 `)
1432
1433 return err
1434 })
1435
1436 orm.RunMigration(conn, logger, "add-pds-migration", func(tx *sql.Tx) error {
1437 _, err := tx.Exec(`
1438 create table if not exists pds_migration (
1439 name text not null,
1440
1441 -- record at_uri
1442 did text not null,
1443 collection text not null,
1444 rkey text not null,
1445
1446 status text not null default 'pending',
1447 error_msg text,
1448 retry_count integer not null default 0,
1449 retry_after integer not null default 0,
1450 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1451
1452 unique(name, did, collection, rkey)
1453 );
1454 `)
1455 return err
1456 })
1457
1458 orm.RunMigration(conn, logger, "unify-pds-record-migration-table", func(tx *sql.Tx) error {
1459 _, err := tx.Exec(`
1460 insert into pds_migration (
1461 name,
1462 did,
1463 collection,
1464 rkey,
1465 status,
1466 updated_at
1467 )
1468 select
1469 'add-repo-did',
1470 user_did,
1471 record_nsid,
1472 record_rkey,
1473 status,
1474 updated_at
1475 from pds_rewrite_status;
1476
1477 drop table pds_rewrite_status;
1478 `)
1479 return err
1480 })
1481
1482 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1483 orm.RunMigration(conn, logger, "add-id-to-vouches", func(tx *sql.Tx) error {
1484 _, err := tx.Exec(`
1485 create table vouches_new (
1486 id integer primary key autoincrement,
1487 did text not null,
1488 subject_did text not null,
1489 cid text not null,
1490 kind text not null default 'vouch',
1491 reason text,
1492 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1493 unique(did, subject_did),
1494 check (did <> subject_did),
1495 check (kind in ('vouch', 'denounce'))
1496 );
1497
1498 insert into vouches_new (did, subject_did, cid, kind, reason, created_at)
1499 select did, subject_did, cid, kind, reason, created_at
1500 from vouches;
1501
1502 drop table vouches;
1503 alter table vouches_new rename to vouches;
1504 `)
1505 return err
1506 })
1507 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1508
1509 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1510 orm.RunMigration(conn, logger, "drop-pipeline-statuses-pipeline-fk", func(tx *sql.Tx) error {
1511 _, err := tx.Exec(`
1512 create table if not exists pipeline_statuses_new (
1513 id integer primary key autoincrement,
1514 spindle text not null,
1515 rkey text not null,
1516
1517 pipeline_knot text not null,
1518 pipeline_rkey text not null,
1519
1520 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1521 workflow text not null,
1522 status text not null,
1523 error text,
1524 exit_code integer not null default 0,
1525
1526 unique (spindle, rkey)
1527 );
1528
1529 insert into pipeline_statuses_new
1530 select * from pipeline_statuses;
1531
1532 drop table pipeline_statuses;
1533 alter table pipeline_statuses_new rename to pipeline_statuses;
1534 `)
1535 return err
1536 })
1537 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1538
1539 orm.RunMigration(conn, logger, "add-repo-renames", func(tx *sql.Tx) error {
1540 res, err := tx.Exec(`
1541 update repos
1542 set name = name || '-renamed-' || id || '-' || lower(hex(randomblob(4)))
1543 where id in (
1544 select id from (
1545 select id, row_number() over (
1546 partition by did, knot, name
1547 order by created desc, id desc
1548 ) as rn
1549 from repos
1550 ) where rn > 1
1551 );
1552 `)
1553 if err != nil {
1554 return err
1555 }
1556 if n, _ := res.RowsAffected(); n > 0 {
1557 logger.Warn("suffixed legacy duplicate repo names before adding unique index", "rows", n)
1558 }
1559
1560 var remaining int
1561 if err := tx.QueryRow(`
1562 select count(*) from (
1563 select 1 from repos group by did, knot, name having count(*) > 1
1564 )
1565 `).Scan(&remaining); err != nil {
1566 return fmt.Errorf("checking for residual duplicate (did, knot, name) groups: %w", err)
1567 }
1568 if remaining > 0 {
1569 return fmt.Errorf("add-repo-renames: %d duplicate (did, knot, name) groups remain after suffix pass; manual cleanup required before unique index can be created", remaining)
1570 }
1571
1572 _, err = tx.Exec(`
1573 create table if not exists repo_renames (
1574 owner_did text not null,
1575 old_rkey text not null,
1576 repo_did text not null,
1577 renamed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1578 primary key (owner_did, old_rkey)
1579 );
1580 create unique index if not exists idx_repos_owner_knot_name
1581 on repos(did, knot, name);
1582 `)
1583 return err
1584 })
1585
1586 orm.RunMigration(conn, logger, "repos-canonical-rkey-uniqueness", func(tx *sql.Tx) error {
1587 _, err := tx.Exec(`
1588 drop index if exists idx_repos_owner_knot_name;
1589 create unique index if not exists idx_repos_did_rkey
1590 on repos(did, rkey);
1591 `)
1592 return err
1593 })
1594
1595 orm.RunMigration(conn, logger, "repo-did-references", func(tx *sql.Tx) error {
1596 tables := []struct{ table, oldCol, newCol string }{
1597 {"issues", "repo_at", "repo_did"},
1598 {"pulls", "repo_at", "repo_did"},
1599 {"pull_comments", "repo_at", "repo_did"},
1600 {"stars", "subject_at", "subject_did"},
1601 {"artifacts", "repo_at", "repo_did"},
1602 {"webhooks", "repo_at", "repo_did"},
1603 {"repo_sites", "repo_at", "repo_did"},
1604 {"site_deploys", "repo_at", "repo_did"},
1605 {"collaborators", "repo_at", "repo_did"},
1606 {"repo_issue_seqs", "repo_at", "repo_did"},
1607 {"repo_pull_seqs", "repo_at", "repo_did"},
1608 {"repo_languages", "repo_at", "repo_did"},
1609 {"repo_labels", "repo_at", "repo_did"},
1610 }
1611
1612 stmts := ""
1613 for _, t := range tables {
1614 stmts += fmt.Sprintf(
1615 `ALTER TABLE %s ADD COLUMN %s TEXT;
1616 UPDATE %s SET %s = (SELECT repos.repo_did FROM repos WHERE repos.at_uri = %s.%s);
1617 CREATE INDEX IF NOT EXISTS idx_%s_%s ON %s(%s);
1618 `, t.table, t.newCol, t.table, t.newCol, t.table, t.oldCol, t.table, t.newCol, t.table, t.newCol)
1619 }
1620
1621 stmts += `ALTER TABLE pulls ADD COLUMN source_repo_did TEXT;
1622 UPDATE pulls SET source_repo_did = (SELECT repos.repo_did FROM repos WHERE repos.at_uri = pulls.source_repo_at);
1623
1624 UPDATE profile_pinned_repositories SET pin = (
1625 SELECT repos.repo_did FROM repos WHERE repos.at_uri = profile_pinned_repositories.pin
1626 ) WHERE pin LIKE 'at://%'
1627 AND EXISTS (SELECT 1 FROM repos WHERE repos.at_uri = profile_pinned_repositories.pin AND repos.repo_did IS NOT NULL AND repos.repo_did != '');
1628 `
1629
1630 _, err := tx.Exec(stmts)
1631 return err
1632 })
1633
1634 orm.RunMigration(conn, logger, "backfill-pds-rewrites-star-issue-pull-collab", func(tx *sql.Tx) error {
1635 type source struct {
1636 userDidCol string
1637 table string
1638 nsid string
1639 fkCol string
1640 }
1641 sources := []source{
1642 {"did", "stars", "sh.tangled.feed.star", "subject_at"},
1643 {"did", "issues", "sh.tangled.repo.issue", "repo_at"},
1644 {"owner_did", "pulls", "sh.tangled.repo.pull", "repo_at"},
1645 {"did", "collaborators", "sh.tangled.repo.collaborator", "repo_at"},
1646 }
1647
1648 for _, src := range sources {
1649 _, err := tx.Exec(fmt.Sprintf(`
1650 INSERT INTO pds_migration (name, did, collection, rkey, status)
1651 SELECT 'add-repo-did', t.%s, '%s', t.rkey, 'pending'
1652 FROM %s t
1653 JOIN repos r ON r.at_uri = t.%s
1654 WHERE r.repo_did IS NOT NULL AND r.repo_did != ''
1655 ON CONFLICT(name, did, collection, rkey) DO NOTHING
1656 `, src.userDidCol, src.nsid, src.table, src.fkCol))
1657 if err != nil {
1658 return fmt.Errorf("backfill pds rewrites for %s: %w", src.table, err)
1659 }
1660 }
1661
1662 return nil
1663 })
1664
1665 orm.RunMigration(conn, logger, "backfill-pds-rewrites-profiles", func(tx *sql.Tx) error {
1666 _, err := tx.Exec(`
1667 INSERT INTO pds_migration (name, did, collection, rkey, status)
1668 SELECT DISTINCT 'add-repo-did', pp.did, 'sh.tangled.actor.profile', 'self', 'pending'
1669 FROM profile_pinned_repositories pp
1670 JOIN repos r ON r.at_uri = pp.pin
1671 WHERE pp.pin LIKE 'at://%'
1672 AND r.repo_did IS NOT NULL AND r.repo_did != ''
1673 ON CONFLICT(name, did, collection, rkey) DO NOTHING
1674 `)
1675 if err != nil {
1676 return fmt.Errorf("backfill pds rewrites for profiles: %w", err)
1677 }
1678 return nil
1679 })
1680
1681 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1682 orm.RunMigration(conn, logger, "drop-old-at-uri-columns", func(tx *sql.Tx) error {
1683 _, err := tx.Exec(`
1684 CREATE TABLE repos_new (
1685 id INTEGER PRIMARY KEY AUTOINCREMENT,
1686 did TEXT NOT NULL,
1687 name TEXT NOT NULL,
1688 knot TEXT NOT NULL,
1689 rkey TEXT NOT NULL,
1690 at_uri TEXT NOT NULL UNIQUE,
1691 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1692 description TEXT CHECK (length(description) <= 200),
1693 source TEXT,
1694 spindle TEXT,
1695 website TEXT,
1696 topics TEXT,
1697 repo_did TEXT,
1698 UNIQUE(did, rkey)
1699 );
1700 INSERT INTO repos_new (id, did, name, knot, rkey, at_uri, created, description, source, spindle, website, topics, repo_did)
1701 SELECT id, did, name, knot, rkey, at_uri, created, description, source, spindle, website, topics, repo_did
1702 FROM repos;
1703 DROP TABLE repos;
1704 ALTER TABLE repos_new RENAME TO repos;
1705 CREATE UNIQUE INDEX idx_repos_repo_did ON repos(repo_did);
1706 CREATE UNIQUE INDEX idx_repos_did_rkey ON repos(did, rkey);
1707
1708 CREATE TABLE issues_new (
1709 id INTEGER PRIMARY KEY AUTOINCREMENT,
1710 did TEXT NOT NULL,
1711 rkey TEXT NOT NULL,
1712 at_uri TEXT GENERATED ALWAYS AS ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) STORED,
1713 repo_did TEXT NOT NULL,
1714 issue_id INTEGER NOT NULL,
1715 title TEXT NOT NULL,
1716 body TEXT NOT NULL,
1717 open INTEGER NOT NULL DEFAULT 1,
1718 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1719 edited TEXT,
1720 deleted TEXT,
1721 UNIQUE(did, rkey),
1722 UNIQUE(repo_did, issue_id),
1723 UNIQUE(at_uri),
1724 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1725 );
1726 INSERT INTO issues_new (id, did, rkey, repo_did, issue_id, title, body, open, created, edited, deleted)
1727 SELECT id, did, rkey, repo_did, issue_id, title, body, open, created, edited, deleted
1728 FROM issues WHERE repo_did IS NOT NULL AND repo_did != '';
1729 DROP TABLE issues;
1730 ALTER TABLE issues_new RENAME TO issues;
1731 CREATE INDEX idx_issues_repo_did ON issues(repo_did);
1732
1733 CREATE TABLE pulls_new (
1734 id INTEGER PRIMARY KEY AUTOINCREMENT,
1735 pull_id INTEGER NOT NULL,
1736 at_uri TEXT GENERATED ALWAYS AS ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) STORED,
1737 repo_did TEXT NOT NULL,
1738 owner_did TEXT NOT NULL,
1739 rkey TEXT NOT NULL,
1740 title TEXT NOT NULL,
1741 body TEXT NOT NULL,
1742 target_branch TEXT NOT NULL,
1743 state INTEGER NOT NULL DEFAULT 0 CHECK (state IN (0, 1, 2, 3)),
1744 source_branch TEXT,
1745 source_repo_did TEXT,
1746 change_id TEXT,
1747 dependent_on TEXT,
1748 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1749 UNIQUE(repo_did, pull_id),
1750 UNIQUE(at_uri),
1751 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1752 );
1753 INSERT INTO pulls_new (id, pull_id, repo_did, owner_did, rkey, title, body, target_branch, state, source_branch, source_repo_did, change_id, dependent_on, created)
1754 SELECT id, pull_id, repo_did, owner_did, rkey, title, body, target_branch, state, source_branch, source_repo_did, change_id, dependent_on, created
1755 FROM pulls WHERE repo_did IS NOT NULL AND repo_did != '';
1756 DROP TABLE pulls;
1757 ALTER TABLE pulls_new RENAME TO pulls;
1758 CREATE INDEX idx_pulls_repo_did ON pulls(repo_did);
1759 CREATE INDEX idx_pulls_source_repo_did ON pulls(source_repo_did);
1760
1761 CREATE TABLE pull_comments_new (
1762 id INTEGER PRIMARY KEY AUTOINCREMENT,
1763 pull_id INTEGER NOT NULL,
1764 submission_id INTEGER NOT NULL,
1765 repo_did TEXT NOT NULL,
1766 owner_did TEXT NOT NULL,
1767 comment_at TEXT NOT NULL,
1768 body TEXT NOT NULL,
1769 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1770 FOREIGN KEY (repo_did, pull_id) REFERENCES pulls(repo_did, pull_id) ON DELETE CASCADE,
1771 FOREIGN KEY (submission_id) REFERENCES pull_submissions(id) ON DELETE CASCADE
1772 );
1773 INSERT INTO pull_comments_new (id, pull_id, submission_id, repo_did, owner_did, comment_at, body, created)
1774 SELECT id, pull_id, submission_id, repo_did, owner_did, comment_at, body, created
1775 FROM pull_comments WHERE repo_did IS NOT NULL AND repo_did != '';
1776 DROP TABLE pull_comments;
1777 ALTER TABLE pull_comments_new RENAME TO pull_comments;
1778 CREATE INDEX idx_pull_comments_repo_did ON pull_comments(repo_did);
1779
1780 CREATE TABLE stars_new (
1781 id INTEGER PRIMARY KEY AUTOINCREMENT,
1782 did TEXT NOT NULL,
1783 rkey TEXT NOT NULL,
1784 subject_type TEXT NOT NULL CHECK (subject_type IN ('repo', 'string')),
1785 subject TEXT NOT NULL,
1786 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1787 UNIQUE(did, rkey),
1788 UNIQUE(did, subject)
1789 );
1790 INSERT INTO stars_new (id, did, rkey, subject_type, subject, created)
1791 SELECT id, did, rkey, 'repo', subject_did, created
1792 FROM stars
1793 WHERE subject_did IS NOT NULL AND subject_did != '';
1794 INSERT OR IGNORE INTO stars_new (id, did, rkey, subject_type, subject, created)
1795 SELECT id, did, rkey, 'string', subject_at, created
1796 FROM stars
1797 WHERE (subject_did IS NULL OR subject_did = '')
1798 AND subject_at LIKE 'at://%/sh.tangled.string/%';
1799 DROP TABLE stars;
1800 ALTER TABLE stars_new RENAME TO stars;
1801 CREATE INDEX idx_stars_subject ON stars(subject);
1802 CREATE INDEX idx_stars_subject_type ON stars(subject_type);
1803 CREATE INDEX idx_stars_created ON stars(created);
1804
1805 CREATE TABLE collaborators_new (
1806 id INTEGER PRIMARY KEY AUTOINCREMENT,
1807 did TEXT NOT NULL,
1808 rkey TEXT,
1809 subject_did TEXT NOT NULL,
1810 repo_did TEXT NOT NULL,
1811 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1812 UNIQUE(did, rkey),
1813 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1814 );
1815 INSERT INTO collaborators_new (id, did, rkey, subject_did, repo_did, created)
1816 SELECT id, did, NULLIF(rkey, ''), subject_did, repo_did, created
1817 FROM collaborators WHERE repo_did IS NOT NULL AND repo_did != '';
1818 DROP TABLE collaborators;
1819 ALTER TABLE collaborators_new RENAME TO collaborators;
1820 CREATE INDEX idx_collaborators_repo_did ON collaborators(repo_did);
1821
1822 CREATE TABLE artifacts_new (
1823 id INTEGER PRIMARY KEY AUTOINCREMENT,
1824 did TEXT NOT NULL,
1825 rkey TEXT NOT NULL,
1826 repo_did TEXT NOT NULL,
1827 tag BINARY(20) NOT NULL,
1828 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1829 blob_cid TEXT NOT NULL,
1830 name TEXT NOT NULL,
1831 size INTEGER NOT NULL DEFAULT 0,
1832 mimetype TEXT NOT NULL DEFAULT '*/*',
1833 UNIQUE(did, rkey),
1834 UNIQUE(repo_did, tag, name),
1835 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1836 );
1837 INSERT INTO artifacts_new (id, did, rkey, repo_did, tag, created, blob_cid, name, size, mimetype)
1838 SELECT id, did, rkey, repo_did, tag, created, blob_cid, name, size, mimetype
1839 FROM artifacts WHERE repo_did IS NOT NULL AND repo_did != '';
1840 DROP TABLE artifacts;
1841 ALTER TABLE artifacts_new RENAME TO artifacts;
1842 CREATE INDEX idx_artifacts_repo_did ON artifacts(repo_did);
1843
1844 CREATE TABLE webhooks_new (
1845 id INTEGER PRIMARY KEY AUTOINCREMENT,
1846 repo_did TEXT NOT NULL,
1847 url TEXT NOT NULL,
1848 secret TEXT,
1849 active INTEGER NOT NULL DEFAULT 1,
1850 events TEXT NOT NULL,
1851 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1852 updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1853 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1854 );
1855 INSERT INTO webhooks_new (id, repo_did, url, secret, active, events, created_at, updated_at)
1856 SELECT id, repo_did, url, secret, active, events, created_at, updated_at
1857 FROM webhooks WHERE repo_did IS NOT NULL AND repo_did != '';
1858 DROP TABLE webhooks;
1859 ALTER TABLE webhooks_new RENAME TO webhooks;
1860 CREATE INDEX idx_webhooks_repo_did ON webhooks(repo_did);
1861
1862 CREATE TABLE repo_sites_new (
1863 id INTEGER PRIMARY KEY AUTOINCREMENT,
1864 repo_did TEXT NOT NULL UNIQUE,
1865 branch TEXT NOT NULL,
1866 dir TEXT NOT NULL DEFAULT '/',
1867 is_index INTEGER NOT NULL DEFAULT 0,
1868 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1869 updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1870 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1871 );
1872 INSERT INTO repo_sites_new (id, repo_did, branch, dir, is_index, created, updated)
1873 SELECT id, repo_did, branch, dir, is_index, created, updated
1874 FROM repo_sites WHERE repo_did IS NOT NULL AND repo_did != '';
1875 DROP TABLE repo_sites;
1876 ALTER TABLE repo_sites_new RENAME TO repo_sites;
1877
1878 CREATE TABLE site_deploys_new (
1879 id INTEGER PRIMARY KEY AUTOINCREMENT,
1880 repo_did TEXT NOT NULL,
1881 branch TEXT NOT NULL,
1882 dir TEXT NOT NULL DEFAULT '/',
1883 commit_sha TEXT NOT NULL DEFAULT '',
1884 status TEXT NOT NULL CHECK (status IN ('success', 'failure')),
1885 trigger TEXT NOT NULL CHECK (trigger IN ('config_change', 'push')),
1886 error TEXT NOT NULL DEFAULT '',
1887 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1888 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1889 );
1890 INSERT INTO site_deploys_new (id, repo_did, branch, dir, commit_sha, status, trigger, error, created_at)
1891 SELECT id, repo_did, branch, dir, commit_sha, status, trigger, error, created_at
1892 FROM site_deploys WHERE repo_did IS NOT NULL AND repo_did != '';
1893 DROP TABLE site_deploys;
1894 ALTER TABLE site_deploys_new RENAME TO site_deploys;
1895 CREATE INDEX idx_site_deploys_repo_did ON site_deploys(repo_did);
1896
1897 CREATE TABLE repo_issue_seqs_new (
1898 repo_did TEXT PRIMARY KEY,
1899 next_issue_id INTEGER NOT NULL DEFAULT 1,
1900 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1901 );
1902 INSERT INTO repo_issue_seqs_new (repo_did, next_issue_id)
1903 SELECT repo_did, next_issue_id
1904 FROM repo_issue_seqs WHERE repo_did IS NOT NULL AND repo_did != '';
1905 DROP TABLE repo_issue_seqs;
1906 ALTER TABLE repo_issue_seqs_new RENAME TO repo_issue_seqs;
1907
1908 CREATE TABLE repo_pull_seqs_new (
1909 repo_did TEXT PRIMARY KEY,
1910 next_pull_id INTEGER NOT NULL DEFAULT 1,
1911 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1912 );
1913 INSERT INTO repo_pull_seqs_new (repo_did, next_pull_id)
1914 SELECT repo_did, next_pull_id
1915 FROM repo_pull_seqs WHERE repo_did IS NOT NULL AND repo_did != '';
1916 DROP TABLE repo_pull_seqs;
1917 ALTER TABLE repo_pull_seqs_new RENAME TO repo_pull_seqs;
1918
1919 CREATE TABLE repo_languages_new (
1920 id INTEGER PRIMARY KEY AUTOINCREMENT,
1921 repo_did TEXT NOT NULL,
1922 ref TEXT NOT NULL,
1923 is_default_ref INTEGER NOT NULL DEFAULT 0,
1924 language TEXT NOT NULL,
1925 bytes INTEGER NOT NULL CHECK (bytes >= 0),
1926 UNIQUE(repo_did, ref, language),
1927 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1928 );
1929 INSERT INTO repo_languages_new (id, repo_did, ref, is_default_ref, language, bytes)
1930 SELECT id, repo_did, ref, is_default_ref, language, bytes
1931 FROM repo_languages WHERE repo_did IS NOT NULL AND repo_did != '';
1932 DROP TABLE repo_languages;
1933 ALTER TABLE repo_languages_new RENAME TO repo_languages;
1934
1935 CREATE TABLE repo_labels_new (
1936 id INTEGER PRIMARY KEY AUTOINCREMENT,
1937 repo_did TEXT NOT NULL,
1938 label_at TEXT NOT NULL,
1939 UNIQUE(repo_did, label_at),
1940 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1941 );
1942 INSERT INTO repo_labels_new (id, repo_did, label_at)
1943 SELECT id, repo_did, label_at
1944 FROM repo_labels WHERE repo_did IS NOT NULL AND repo_did != '';
1945 DROP TABLE repo_labels;
1946 ALTER TABLE repo_labels_new RENAME TO repo_labels;
1947 `)
1948 return err
1949 })
1950 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1951
1952 orm.RunMigration(conn, logger, "migrate-knots-to-repo-did-rename", func(tx *sql.Tx) error {
1953 _, err := tx.Exec(`
1954 update registrations set needs_upgrade = 1;
1955 `)
1956 return err
1957 })
1958
1959 orm.RunMigration(conn, logger, "drop-ghost-comments-table", func(tx *sql.Tx) error {
1960 _, err := tx.Exec(`DROP TABLE IF EXISTS comments`)
1961 return err
1962 })
1963
1964 return &DB{
1965 db,
1966 logger,
1967 }, nil
1968}
1969
1970func (d *DB) Close() error {
1971 return d.DB.Close()
1972}