Monorepo for Tangled
tangled.org
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 comments (
121 id integer primary key autoincrement,
122 owner_did text not null,
123 issue_id integer not null,
124 repo_at text not null,
125 comment_id integer not null,
126 comment_at text not null,
127 body text not null,
128 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
129 unique(issue_id, comment_id),
130 foreign key (repo_at, issue_id) references issues(repo_at, issue_id) on delete cascade
131 );
132 create table if not exists pulls (
133 -- identifiers
134 id integer primary key autoincrement,
135 pull_id integer not null,
136
137 -- at identifiers
138 repo_at text not null,
139 owner_did text not null,
140 rkey text not null,
141 pull_at text,
142
143 -- content
144 title text not null,
145 body text not null,
146 target_branch text not null,
147 state integer not null default 0 check (state in (0, 1, 2)), -- open, merged, closed
148
149 -- meta
150 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
151
152 -- constraints
153 unique(repo_at, pull_id),
154 foreign key (repo_at) references repos(at_uri) on delete cascade
155 );
156
157 -- every pull must have atleast 1 submission: the initial submission
158 create table if not exists pull_submissions (
159 -- identifiers
160 id integer primary key autoincrement,
161 pull_id integer not null,
162
163 -- at identifiers
164 repo_at text not null,
165
166 -- content, these are immutable, and require a resubmission to update
167 round_number integer not null default 0,
168 patch text,
169
170 -- meta
171 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
172
173 -- constraints
174 unique(repo_at, pull_id, round_number),
175 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade
176 );
177
178 create table if not exists pull_comments (
179 -- identifiers
180 id integer primary key autoincrement,
181 pull_id integer not null,
182 submission_id integer not null,
183
184 -- at identifiers
185 repo_at text not null,
186 owner_did text not null,
187 comment_at text not null,
188
189 -- content
190 body text not null,
191
192 -- meta
193 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
194
195 -- constraints
196 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade,
197 foreign key (submission_id) references pull_submissions(id) on delete cascade
198 );
199
200 create table if not exists _jetstream (
201 id integer primary key autoincrement,
202 last_time_us integer not null
203 );
204
205 create table if not exists repo_issue_seqs (
206 repo_at text primary key,
207 next_issue_id integer not null default 1
208 );
209
210 create table if not exists repo_pull_seqs (
211 repo_at text primary key,
212 next_pull_id integer not null default 1
213 );
214
215 create table if not exists stars (
216 id integer primary key autoincrement,
217 starred_by_did text not null,
218 repo_at text not null,
219 rkey text not null,
220 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
221 foreign key (repo_at) references repos(at_uri) on delete cascade,
222 unique(starred_by_did, repo_at)
223 );
224
225 create table if not exists reactions (
226 id integer primary key autoincrement,
227 reacted_by_did text not null,
228 thread_at text not null,
229 kind text not null,
230 rkey text not null,
231 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
232 unique(reacted_by_did, thread_at, kind)
233 );
234
235 create table if not exists emails (
236 id integer primary key autoincrement,
237 did text not null,
238 email text not null,
239 verified integer not null default 0,
240 verification_code text not null,
241 last_sent text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
242 is_primary integer not null default 0,
243 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
244 unique(did, email)
245 );
246
247 create table if not exists artifacts (
248 -- id
249 id integer primary key autoincrement,
250 did text not null,
251 rkey text not null,
252
253 -- meta
254 repo_at text not null,
255 tag binary(20) not null,
256 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
257
258 -- data
259 blob_cid text not null,
260 name text not null,
261 size integer not null default 0,
262 mimetype string not null default "*/*",
263
264 -- constraints
265 unique(did, rkey), -- record must be unique
266 unique(repo_at, tag, name), -- for a given tag object, each file must be unique
267 foreign key (repo_at) references repos(at_uri) on delete cascade
268 );
269
270 create table if not exists profile (
271 -- id
272 id integer primary key autoincrement,
273 did text not null,
274
275 -- data
276 description text not null,
277 include_bluesky integer not null default 0,
278 location text,
279
280 -- constraints
281 unique(did)
282 );
283 create table if not exists profile_links (
284 -- id
285 id integer primary key autoincrement,
286 did text not null,
287
288 -- data
289 link text not null,
290
291 -- constraints
292 foreign key (did) references profile(did) on delete cascade
293 );
294 create table if not exists profile_stats (
295 -- id
296 id integer primary key autoincrement,
297 did text not null,
298
299 -- data
300 kind text not null check (kind in (
301 "merged-pull-request-count",
302 "closed-pull-request-count",
303 "open-pull-request-count",
304 "open-issue-count",
305 "closed-issue-count",
306 "repository-count"
307 )),
308
309 -- constraints
310 foreign key (did) references profile(did) on delete cascade
311 );
312 create table if not exists profile_pinned_repositories (
313 -- id
314 id integer primary key autoincrement,
315 did text not null,
316
317 -- data
318 at_uri text not null,
319
320 -- constraints
321 unique(did, at_uri),
322 foreign key (did) references profile(did) on delete cascade,
323 foreign key (at_uri) references repos(at_uri) on delete cascade
324 );
325
326 create table if not exists oauth_requests (
327 id integer primary key autoincrement,
328 auth_server_iss text not null,
329 state text not null,
330 did text not null,
331 handle text not null,
332 pds_url text not null,
333 pkce_verifier text not null,
334 dpop_auth_server_nonce text not null,
335 dpop_private_jwk text not null
336 );
337
338 create table if not exists oauth_sessions (
339 id integer primary key autoincrement,
340 did text not null,
341 handle text not null,
342 pds_url text not null,
343 auth_server_iss text not null,
344 access_jwt text not null,
345 refresh_jwt text not null,
346 dpop_pds_nonce text,
347 dpop_auth_server_nonce text not null,
348 dpop_private_jwk text not null,
349 expiry text not null
350 );
351
352 create table if not exists punchcard (
353 did text not null,
354 date text not null, -- yyyy-mm-dd
355 count integer,
356 primary key (did, date)
357 );
358
359 create table if not exists spindles (
360 id integer primary key autoincrement,
361 owner text not null,
362 instance text not null,
363 verified text, -- time of verification
364 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
365
366 unique(owner, instance)
367 );
368
369 create table if not exists spindle_members (
370 -- identifiers for the record
371 id integer primary key autoincrement,
372 did text not null,
373 rkey text not null,
374
375 -- data
376 instance text not null,
377 subject text not null,
378 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
379
380 -- constraints
381 unique (did, instance, subject)
382 );
383
384 create table if not exists pipelines (
385 -- identifiers
386 id integer primary key autoincrement,
387 knot text not null,
388 rkey text not null,
389
390 repo_owner text not null,
391 repo_name text not null,
392
393 -- every pipeline must be associated with exactly one commit
394 sha text not null check (length(sha) = 40),
395 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
396
397 -- trigger data
398 trigger_id integer not null,
399
400 unique(knot, rkey),
401 foreign key (trigger_id) references triggers(id) on delete cascade
402 );
403
404 create table if not exists triggers (
405 -- primary key
406 id integer primary key autoincrement,
407
408 -- top-level fields
409 kind text not null,
410
411 -- pushTriggerData fields
412 push_ref text,
413 push_new_sha text check (length(push_new_sha) = 40),
414 push_old_sha text check (length(push_old_sha) = 40),
415
416 -- pullRequestTriggerData fields
417 pr_source_branch text,
418 pr_target_branch text,
419 pr_source_sha text check (length(pr_source_sha) = 40),
420 pr_action text
421 );
422
423 create table if not exists pipeline_statuses (
424 -- identifiers
425 id integer primary key autoincrement,
426 spindle text not null,
427 rkey text not null,
428
429 -- referenced pipeline. these form the (did, rkey) pair
430 pipeline_knot text not null,
431 pipeline_rkey text not null,
432
433 -- content
434 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
435 workflow text not null,
436 status text not null,
437 error text,
438 exit_code integer not null default 0,
439
440 unique (spindle, rkey),
441 foreign key (pipeline_knot, pipeline_rkey)
442 references pipelines (knot, rkey)
443 on delete cascade
444 );
445
446 create table if not exists repo_languages (
447 -- identifiers
448 id integer primary key autoincrement,
449
450 -- repo identifiers
451 repo_at text not null,
452 ref text not null,
453 is_default_ref integer not null default 0,
454
455 -- language breakdown
456 language text not null,
457 bytes integer not null check (bytes >= 0),
458
459 unique(repo_at, ref, language)
460 );
461
462 create table if not exists signups_inflight (
463 id integer primary key autoincrement,
464 email text not null unique,
465 invite_code text not null,
466 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
467 );
468
469 create table if not exists strings (
470 -- identifiers
471 did text not null,
472 rkey text not null,
473
474 -- content
475 filename text not null,
476 description text,
477 content text not null,
478 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
479 edited text,
480
481 primary key (did, rkey)
482 );
483
484 create table if not exists label_definitions (
485 -- identifiers
486 id integer primary key autoincrement,
487 did text not null,
488 rkey text not null,
489 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.label.definition' || '/' || rkey) stored,
490
491 -- content
492 name text not null,
493 value_type text not null check (value_type in (
494 "null",
495 "boolean",
496 "integer",
497 "string"
498 )),
499 value_format text not null default "any",
500 value_enum text, -- comma separated list
501 scope text not null, -- comma separated list of nsid
502 color text,
503 multiple integer not null default 0,
504 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
505
506 -- constraints
507 unique (did, rkey)
508 unique (at_uri)
509 );
510
511 -- ops are flattened, a record may contain several additions and deletions, but the table will include one row per add/del
512 create table if not exists label_ops (
513 -- identifiers
514 id integer primary key autoincrement,
515 did text not null,
516 rkey text not null,
517 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.label.op' || '/' || rkey) stored,
518
519 -- content
520 subject text not null,
521 operation text not null check (operation in ("add", "del")),
522 operand_key text not null,
523 operand_value text not null,
524 -- we need two time values: performed is declared by the user, indexed is calculated by the av
525 performed text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
526 indexed text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
527
528 -- constraints
529 -- traditionally (did, rkey) pair should be unique, but not in this case
530 -- operand_key should reference a label definition
531 foreign key (operand_key) references label_definitions (at_uri) on delete cascade,
532 unique (did, rkey, subject, operand_key, operand_value)
533 );
534
535 create table if not exists repo_labels (
536 -- identifiers
537 id integer primary key autoincrement,
538
539 -- repo identifiers
540 repo_at text not null,
541
542 -- label to subscribe to
543 label_at text not null,
544
545 unique (repo_at, label_at)
546 );
547
548 create table if not exists notifications (
549 id integer primary key autoincrement,
550 recipient_did text not null,
551 actor_did text not null,
552 type text not null,
553 entity_type text not null,
554 entity_id text not null,
555 read integer not null default 0,
556 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
557 repo_id integer references repos(id),
558 issue_id integer references issues(id),
559 pull_id integer references pulls(id)
560 );
561
562 create table if not exists recent_links (
563 id integer primary key autoincrement,
564 user_did text not null,
565 link_type text not null,
566 target text not null,
567 visited text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
568 unique(user_did, target)
569 );
570
571 create table if not exists notification_preferences (
572 id integer primary key autoincrement,
573 user_did text not null unique,
574 repo_starred integer not null default 1,
575 issue_created integer not null default 1,
576 issue_commented integer not null default 1,
577 pull_created integer not null default 1,
578 pull_commented integer not null default 1,
579 followed integer not null default 1,
580 pull_merged integer not null default 1,
581 issue_closed integer not null default 1,
582 email_notifications integer not null default 0
583 );
584
585 create table if not exists reference_links (
586 id integer primary key autoincrement,
587 from_at text not null,
588 to_at text not null,
589 unique (from_at, to_at)
590 );
591
592 create table if not exists webhooks (
593 id integer primary key autoincrement,
594 repo_at text not null,
595 url text not null,
596 secret text,
597 active integer not null default 1,
598 events text not null, -- comma-separated list of events
599 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
600 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
601
602 foreign key (repo_at) references repos(at_uri) on delete cascade
603 );
604
605 create table if not exists webhook_deliveries (
606 id integer primary key autoincrement,
607 webhook_id integer not null,
608 event text not null,
609 delivery_id text not null,
610 url text not null,
611 request_body text not null,
612 response_code integer,
613 response_body text,
614 success integer not null default 0,
615 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
616
617 foreign key (webhook_id) references webhooks(id) on delete cascade
618 );
619
620 create table if not exists bluesky_posts (
621 rkey text primary key,
622 text text not null,
623 created_at text not null,
624 langs text,
625 facets text,
626 embed text,
627 like_count integer not null default 0,
628 reply_count integer not null default 0,
629 repost_count integer not null default 0,
630 quote_count integer not null default 0
631 );
632
633 create table if not exists domain_claims (
634 id integer primary key autoincrement,
635 did text not null unique,
636 domain text not null unique,
637 deleted text -- timestamp when the domain was released/unclaimed; null means actively claimed
638 );
639
640 create table if not exists repo_sites (
641 id integer primary key autoincrement,
642 repo_at text not null unique,
643 branch text not null,
644 dir text not null default '/',
645 is_index integer not null default 0,
646 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
647 updated text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
648 foreign key (repo_at) references repos(at_uri) on delete cascade
649 );
650
651 create table if not exists site_deploys (
652 id integer primary key autoincrement,
653 repo_at text not null,
654 branch text not null,
655 dir text not null default '/',
656 commit_sha text not null default '',
657 status text not null check (status in ('success', 'failure')),
658 trigger text not null check (trigger in ('config_change', 'push')),
659 error text not null default '',
660 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
661 foreign key (repo_at) references repos(at_uri) on delete cascade
662 );
663
664 create table if not exists punchcard_preferences (
665 id integer primary key autoincrement,
666 user_did text not null unique,
667 hide_mine integer default 0,
668 hide_others integer default 0
669 );
670
671 create table if not exists newsletter_preferences (
672 id integer primary key autoincrement,
673 user_did text not null unique,
674 status text not null check (status in ('subscribed', 'dismissed')),
675 email text,
676 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
677 );
678
679 create table if not exists vouch_evidences (
680 id integer primary key autoincrement,
681 vouch_id integer not null,
682 at_uri text not null,
683 unique(vouch_id, at_uri),
684 foreign key (vouch_id) references vouches(id) on delete cascade
685 );
686
687 create table if not exists vouch_skips (
688 did text not null,
689 subject_did text not null,
690 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
691 primary key (did, subject_did),
692 check (did <> subject_did)
693 );
694
695
696 create table if not exists migrations (
697 id integer primary key autoincrement,
698 name text unique
699 );
700
701 -- indexes for better performance
702 create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc);
703 create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read);
704 create index if not exists idx_references_from_at on reference_links(from_at);
705 create index if not exists idx_references_to_at on reference_links(to_at);
706 create index if not exists idx_webhook_deliveries_webhook_id on webhook_deliveries(webhook_id);
707 create index if not exists idx_newsletter_prefs_user_did on newsletter_preferences(user_did);
708 `)
709 if err != nil {
710 return nil, err
711 }
712
713 // run migrations
714 orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
715 tx.Exec(`
716 alter table repos add column description text check (length(description) <= 200);
717 `)
718 return nil
719 })
720
721 orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
722 // add unconstrained column
723 _, err := tx.Exec(`
724 alter table public_keys
725 add column rkey text;
726 `)
727 if err != nil {
728 return err
729 }
730
731 // backfill
732 _, err = tx.Exec(`
733 update public_keys
734 set rkey = ''
735 where rkey is null;
736 `)
737 if err != nil {
738 return err
739 }
740
741 return nil
742 })
743
744 orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
745 _, err := tx.Exec(`
746 alter table comments drop column comment_at;
747 alter table comments add column rkey text;
748 `)
749 return err
750 })
751
752 orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
753 _, err := tx.Exec(`
754 alter table comments add column deleted text; -- timestamp
755 alter table comments add column edited text; -- timestamp
756 `)
757 return err
758 })
759
760 orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
761 _, err := tx.Exec(`
762 alter table pulls add column source_branch text;
763 alter table pulls add column source_repo_at text;
764 alter table pull_submissions add column source_rev text;
765 `)
766 return err
767 })
768
769 orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
770 _, err := tx.Exec(`
771 alter table repos add column source text;
772 `)
773 return err
774 })
775
776 // disable foreign-keys for the next migration
777 // NOTE: this cannot be done in a transaction, so it is run outside [0]
778 //
779 // [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
780 conn.ExecContext(ctx, "pragma foreign_keys = off;")
781 orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
782 _, err := tx.Exec(`
783 create table pulls_new (
784 -- identifiers
785 id integer primary key autoincrement,
786 pull_id integer not null,
787
788 -- at identifiers
789 repo_at text not null,
790 owner_did text not null,
791 rkey text not null,
792
793 -- content
794 title text not null,
795 body text not null,
796 target_branch text not null,
797 state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted
798
799 -- source info
800 source_branch text,
801 source_repo_at text,
802
803 -- stacking
804 stack_id text,
805 change_id text,
806 parent_change_id text,
807
808 -- meta
809 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
810
811 -- constraints
812 unique(repo_at, pull_id),
813 foreign key (repo_at) references repos(at_uri) on delete cascade
814 );
815
816 insert into pulls_new (
817 id, pull_id,
818 repo_at, owner_did, rkey,
819 title, body, target_branch, state,
820 source_branch, source_repo_at,
821 created
822 )
823 select
824 id, pull_id,
825 repo_at, owner_did, rkey,
826 title, body, target_branch, state,
827 source_branch, source_repo_at,
828 created
829 FROM pulls;
830
831 drop table pulls;
832 alter table pulls_new rename to pulls;
833 `)
834 return err
835 })
836 conn.ExecContext(ctx, "pragma foreign_keys = on;")
837
838 orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
839 tx.Exec(`
840 alter table repos add column spindle text;
841 `)
842 return nil
843 })
844
845 // drop all knot secrets, add unique constraint to knots
846 //
847 // knots will henceforth use service auth for signed requests
848 orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
849 _, err := tx.Exec(`
850 create table registrations_new (
851 id integer primary key autoincrement,
852 domain text not null,
853 did text not null,
854 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
855 registered text,
856 read_only integer not null default 0,
857 unique(domain, did)
858 );
859
860 insert into registrations_new (id, domain, did, created, registered, read_only)
861 select id, domain, did, created, registered, 1 from registrations
862 where registered is not null;
863
864 drop table registrations;
865 alter table registrations_new rename to registrations;
866 `)
867 return err
868 })
869
870 // recreate and add rkey + created columns with default constraint
871 orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
872 // create new table
873 // - repo_at instead of repo integer
874 // - rkey field
875 // - created field
876 _, err := tx.Exec(`
877 create table collaborators_new (
878 -- identifiers for the record
879 id integer primary key autoincrement,
880 did text not null,
881 rkey text,
882
883 -- content
884 subject_did text not null,
885 repo_at text not null,
886
887 -- meta
888 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
889
890 -- constraints
891 foreign key (repo_at) references repos(at_uri) on delete cascade
892 )
893 `)
894 if err != nil {
895 return err
896 }
897
898 // copy data
899 _, err = tx.Exec(`
900 insert into collaborators_new (id, did, rkey, subject_did, repo_at)
901 select
902 c.id,
903 r.did,
904 '',
905 c.did,
906 r.at_uri
907 from collaborators c
908 join repos r on c.repo = r.id
909 `)
910 if err != nil {
911 return err
912 }
913
914 // drop old table
915 _, err = tx.Exec(`drop table collaborators`)
916 if err != nil {
917 return err
918 }
919
920 // rename new table
921 _, err = tx.Exec(`alter table collaborators_new rename to collaborators`)
922 return err
923 })
924
925 orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
926 _, err := tx.Exec(`
927 alter table issues add column rkey text not null default '';
928
929 -- get last url section from issue_at and save to rkey column
930 update issues
931 set rkey = replace(issue_at, rtrim(issue_at, replace(issue_at, '/', '')), '');
932 `)
933 return err
934 })
935
936 // repurpose the read-only column to "needs-upgrade"
937 orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
938 _, err := tx.Exec(`
939 alter table registrations rename column read_only to needs_upgrade;
940 `)
941 return err
942 })
943
944 // require all knots to upgrade after the release of total xrpc
945 orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
946 _, err := tx.Exec(`
947 update registrations set needs_upgrade = 1;
948 `)
949 return err
950 })
951
952 // require all knots to upgrade after the release of total xrpc
953 orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
954 _, err := tx.Exec(`
955 alter table spindles add column needs_upgrade integer not null default 0;
956 `)
957 return err
958 })
959
960 // remove issue_at from issues and replace with generated column
961 //
962 // this requires a full table recreation because stored columns
963 // cannot be added via alter
964 //
965 // couple other changes:
966 // - columns renamed to be more consistent
967 // - adds edited and deleted fields
968 //
969 // disable foreign-keys for the next migration
970 conn.ExecContext(ctx, "pragma foreign_keys = off;")
971 orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
972 _, err := tx.Exec(`
973 create table if not exists issues_new (
974 -- identifiers
975 id integer primary key autoincrement,
976 did text not null,
977 rkey text not null,
978 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) stored,
979
980 -- at identifiers
981 repo_at text not null,
982
983 -- content
984 issue_id integer not null,
985 title text not null,
986 body text not null,
987 open integer not null default 1,
988 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
989 edited text, -- timestamp
990 deleted text, -- timestamp
991
992 unique(did, rkey),
993 unique(repo_at, issue_id),
994 unique(at_uri),
995 foreign key (repo_at) references repos(at_uri) on delete cascade
996 );
997 `)
998 if err != nil {
999 return err
1000 }
1001
1002 // transfer data
1003 _, err = tx.Exec(`
1004 insert into issues_new (id, did, rkey, repo_at, issue_id, title, body, open, created)
1005 select
1006 i.id,
1007 i.owner_did,
1008 i.rkey,
1009 i.repo_at,
1010 i.issue_id,
1011 i.title,
1012 i.body,
1013 i.open,
1014 i.created
1015 from issues i;
1016 `)
1017 if err != nil {
1018 return err
1019 }
1020
1021 // drop old table
1022 _, err = tx.Exec(`drop table issues`)
1023 if err != nil {
1024 return err
1025 }
1026
1027 // rename new table
1028 _, err = tx.Exec(`alter table issues_new rename to issues`)
1029 return err
1030 })
1031 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1032
1033 // - renames the comments table to 'issue_comments'
1034 // - rework issue comments to update constraints:
1035 // * unique(did, rkey)
1036 // * remove comment-id and just use the global ID
1037 // * foreign key (repo_at, issue_id)
1038 // - new columns
1039 // * column "reply_to" which can be any other comment
1040 // * column "at-uri" which is a generated column
1041 orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
1042 _, err := tx.Exec(`
1043 create table if not exists issue_comments (
1044 -- identifiers
1045 id integer primary key autoincrement,
1046 did text not null,
1047 rkey text,
1048 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue.comment' || '/' || rkey) stored,
1049
1050 -- at identifiers
1051 issue_at text not null,
1052 reply_to text, -- at_uri of parent comment
1053
1054 -- content
1055 body text not null,
1056 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1057 edited text,
1058 deleted text,
1059
1060 -- constraints
1061 unique(did, rkey),
1062 unique(at_uri),
1063 foreign key (issue_at) references issues(at_uri) on delete cascade
1064 );
1065 `)
1066 if err != nil {
1067 return err
1068 }
1069
1070 // transfer data
1071 _, err = tx.Exec(`
1072 insert into issue_comments (id, did, rkey, issue_at, body, created, edited, deleted)
1073 select
1074 c.id,
1075 c.owner_did,
1076 c.rkey,
1077 i.at_uri, -- get at_uri from issues table
1078 c.body,
1079 c.created,
1080 c.edited,
1081 c.deleted
1082 from comments c
1083 join issues i on c.repo_at = i.repo_at and c.issue_id = i.issue_id;
1084 `)
1085 if err != nil {
1086 return err
1087 }
1088
1089 // drop old table
1090 _, err = tx.Exec(`drop table comments`)
1091 return err
1092 })
1093
1094 // add generated at_uri column to pulls table
1095 //
1096 // this requires a full table recreation because stored columns
1097 // cannot be added via alter
1098 //
1099 // disable foreign-keys for the next migration
1100 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1101 orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
1102 _, err := tx.Exec(`
1103 create table if not exists pulls_new (
1104 -- identifiers
1105 id integer primary key autoincrement,
1106 pull_id integer not null,
1107 at_uri text generated always as ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) stored,
1108
1109 -- at identifiers
1110 repo_at text not null,
1111 owner_did text not null,
1112 rkey text not null,
1113
1114 -- content
1115 title text not null,
1116 body text not null,
1117 target_branch text not null,
1118 state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted
1119
1120 -- source info
1121 source_branch text,
1122 source_repo_at text,
1123
1124 -- stacking
1125 stack_id text,
1126 change_id text,
1127 parent_change_id text,
1128
1129 -- meta
1130 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1131
1132 -- constraints
1133 unique(repo_at, pull_id),
1134 unique(at_uri),
1135 foreign key (repo_at) references repos(at_uri) on delete cascade
1136 );
1137 `)
1138 if err != nil {
1139 return err
1140 }
1141
1142 // transfer data
1143 _, err = tx.Exec(`
1144 insert into pulls_new (
1145 id, pull_id, repo_at, owner_did, rkey,
1146 title, body, target_branch, state,
1147 source_branch, source_repo_at,
1148 stack_id, change_id, parent_change_id,
1149 created
1150 )
1151 select
1152 id, pull_id, repo_at, owner_did, rkey,
1153 title, body, target_branch, state,
1154 source_branch, source_repo_at,
1155 stack_id, change_id, parent_change_id,
1156 created
1157 from pulls;
1158 `)
1159 if err != nil {
1160 return err
1161 }
1162
1163 // drop old table
1164 _, err = tx.Exec(`drop table pulls`)
1165 if err != nil {
1166 return err
1167 }
1168
1169 // rename new table
1170 _, err = tx.Exec(`alter table pulls_new rename to pulls`)
1171 return err
1172 })
1173 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1174
1175 // remove repo_at and pull_id from pull_submissions and replace with pull_at
1176 //
1177 // this requires a full table recreation because stored columns
1178 // cannot be added via alter
1179 //
1180 // disable foreign-keys for the next migration
1181 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1182 orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1183 _, err := tx.Exec(`
1184 create table if not exists pull_submissions_new (
1185 -- identifiers
1186 id integer primary key autoincrement,
1187 pull_at text not null,
1188
1189 -- content, these are immutable, and require a resubmission to update
1190 round_number integer not null default 0,
1191 patch text,
1192 source_rev text,
1193
1194 -- meta
1195 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1196
1197 -- constraints
1198 unique(pull_at, round_number),
1199 foreign key (pull_at) references pulls(at_uri) on delete cascade
1200 );
1201 `)
1202 if err != nil {
1203 return err
1204 }
1205
1206 // transfer data, constructing pull_at from pulls table
1207 _, err = tx.Exec(`
1208 insert into pull_submissions_new (id, pull_at, round_number, patch, created)
1209 select
1210 ps.id,
1211 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey,
1212 ps.round_number,
1213 ps.patch,
1214 ps.created
1215 from pull_submissions ps
1216 join pulls p on ps.repo_at = p.repo_at and ps.pull_id = p.pull_id;
1217 `)
1218 if err != nil {
1219 return err
1220 }
1221
1222 // drop old table
1223 _, err = tx.Exec(`drop table pull_submissions`)
1224 if err != nil {
1225 return err
1226 }
1227
1228 // rename new table
1229 _, err = tx.Exec(`alter table pull_submissions_new rename to pull_submissions`)
1230 return err
1231 })
1232 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1233
1234 // knots may report the combined patch for a comparison, we can store that on the appview side
1235 // (but not on the pds record), because calculating the combined patch requires a git index
1236 orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1237 _, err := tx.Exec(`
1238 alter table pull_submissions add column combined text;
1239 `)
1240 return err
1241 })
1242
1243 orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1244 _, err := tx.Exec(`
1245 alter table profile add column pronouns text;
1246 `)
1247 return err
1248 })
1249
1250 orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1251 _, err := tx.Exec(`
1252 alter table repos add column website text;
1253 alter table repos add column topics text;
1254 `)
1255 return err
1256 })
1257
1258 orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1259 _, err := tx.Exec(`
1260 alter table notification_preferences add column user_mentioned integer not null default 1;
1261 `)
1262 return err
1263 })
1264
1265 // remove the foreign key constraints from stars.
1266 orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
1267 _, err := tx.Exec(`
1268 create table stars_new (
1269 id integer primary key autoincrement,
1270 did text not null,
1271 rkey text not null,
1272
1273 subject_at text not null,
1274
1275 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1276 unique(did, rkey),
1277 unique(did, subject_at)
1278 );
1279
1280 insert into stars_new (
1281 id,
1282 did,
1283 rkey,
1284 subject_at,
1285 created
1286 )
1287 select
1288 id,
1289 starred_by_did,
1290 rkey,
1291 repo_at,
1292 created
1293 from stars;
1294
1295 drop table stars;
1296 alter table stars_new rename to stars;
1297
1298 create index if not exists idx_stars_created on stars(created);
1299 create index if not exists idx_stars_subject_at_created on stars(subject_at, created);
1300 `)
1301 return err
1302 })
1303
1304 orm.RunMigration(conn, logger, "add-avatar-to-profile", func(tx *sql.Tx) error {
1305 _, err := tx.Exec(`
1306 alter table profile add column avatar text;
1307 `)
1308 return err
1309 })
1310
1311 orm.RunMigration(conn, logger, "remove-profile-stats-column-constraint", func(tx *sql.Tx) error {
1312 _, err := tx.Exec(`
1313 -- create new table without the check constraint
1314 create table profile_stats_new (
1315 id integer primary key autoincrement,
1316 did text not null,
1317 kind text not null, -- no constraint this time
1318 foreign key (did) references profile(did) on delete cascade
1319 );
1320
1321 -- copy data from old table
1322 insert into profile_stats_new (id, did, kind)
1323 select id, did, kind
1324 from profile_stats;
1325
1326 -- drop old table
1327 drop table profile_stats;
1328
1329 -- rename new table
1330 alter table profile_stats_new rename to profile_stats;
1331 `)
1332 return err
1333 })
1334
1335 orm.RunMigration(conn, logger, "add-preferred-handle-profile", func(tx *sql.Tx) error {
1336 _, err := tx.Exec(`
1337 alter table profile add column preferred_handle text;
1338 `)
1339 return err
1340 })
1341
1342 orm.RunMigration(conn, logger, "add-repo-did-column", func(tx *sql.Tx) error {
1343 _, err := tx.Exec(`
1344 alter table repos add column repo_did text;
1345 create unique index if not exists idx_repos_repo_did on repos(repo_did);
1346 `)
1347 return err
1348 })
1349
1350 orm.RunMigration(conn, logger, "add-pds-rewrite-status", func(tx *sql.Tx) error {
1351 _, err := tx.Exec(`
1352 create table if not exists pds_rewrite_status (
1353 id integer primary key autoincrement,
1354 user_did text not null,
1355 repo_did text not null,
1356 record_nsid text not null,
1357 record_rkey text not null,
1358 old_repo_at text not null,
1359 status text not null default 'pending',
1360 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1361 unique(user_did, record_nsid, record_rkey)
1362 );
1363 create index if not exists idx_pds_rewrite_user on pds_rewrite_status(user_did, status);
1364 `)
1365 return err
1366 })
1367
1368 orm.RunMigration(conn, logger, "add-pipelines-repo-did", func(tx *sql.Tx) error {
1369 _, err := tx.Exec(`
1370 alter table pipelines add column repo_did text;
1371 create index if not exists idx_pipelines_repo_did on pipelines(repo_did);
1372 `)
1373 return err
1374 })
1375
1376 orm.RunMigration(conn, logger, "migrate-knots-to-repo-dids", func(tx *sql.Tx) error {
1377 _, err := tx.Exec(`update registrations set needs_upgrade = 1`)
1378 return err
1379 })
1380
1381 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1382 orm.RunMigration(conn, logger, "drop-pinned-repos-at-uri-fk", func(tx *sql.Tx) error {
1383 _, err := tx.Exec(`
1384 create table if not exists profile_pinned_repositories_new (
1385 id integer primary key autoincrement,
1386 did text not null,
1387 pin text not null,
1388
1389 unique(did, pin),
1390 foreign key (did) references profile(did) on delete cascade
1391 );
1392
1393 insert into profile_pinned_repositories_new (id, did, pin)
1394 select id, did, at_uri from profile_pinned_repositories;
1395
1396 drop table profile_pinned_repositories;
1397
1398 alter table profile_pinned_repositories_new rename to profile_pinned_repositories;
1399 `)
1400 return err
1401 })
1402 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1403
1404 orm.RunMigration(conn, logger, "reset-profile-pin-rewrites", func(tx *sql.Tx) error {
1405 _, err := tx.Exec(`
1406 update pds_rewrite_status
1407 set status = 'pending',
1408 updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
1409 where record_nsid = 'sh.tangled.actor.profile'
1410 and status = 'done'
1411 `)
1412 return err
1413 })
1414
1415 orm.RunMigration(conn, logger, "add-blob-data-to-pull-submissions", func(tx *sql.Tx) error {
1416 _, err := tx.Exec(`
1417 alter table pull_submissions add column patch_blob_ref text;
1418 alter table pull_submissions add column patch_blob_mime text;
1419 alter table pull_submissions add column patch_blob_size integer;
1420 `)
1421 return err
1422 })
1423
1424 orm.RunMigration(conn, logger, "replace-parent-change-id-with-aturi", func(tx *sql.Tx) error {
1425 // add new column
1426 _, err := tx.Exec(`
1427 alter table pulls add column dependent_on text;
1428 `)
1429 if err != nil {
1430 return err
1431 }
1432
1433 // populate dependent_on with at_uri of the parent
1434 _, err = tx.Exec(`
1435 update pulls
1436 set dependent_on = (
1437 select at_uri
1438 from pulls as parent
1439 where parent.stack_id = pulls.stack_id
1440 and parent.change_id = pulls.parent_change_id
1441 )
1442 where parent_change_id is not null;
1443 `)
1444 if err != nil {
1445 return err
1446 }
1447
1448 // drop old columns
1449 _, err = tx.Exec(`
1450 alter table pulls drop column parent_change_id;
1451 alter table pulls drop column stack_id;
1452 `)
1453
1454 return err
1455 })
1456
1457 orm.RunMigration(conn, logger, "add-pds-migration", func(tx *sql.Tx) error {
1458 _, err := tx.Exec(`
1459 create table if not exists pds_migration (
1460 name text not null,
1461
1462 -- record at_uri
1463 did text not null,
1464 collection text not null,
1465 rkey text not null,
1466
1467 status text not null default 'pending',
1468 error_msg text,
1469 retry_count integer not null default 0,
1470 retry_after integer not null default 0,
1471 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1472
1473 unique(name, did, collection, rkey)
1474 );
1475 `)
1476 return err
1477 })
1478
1479 orm.RunMigration(conn, logger, "unify-pds-record-migration-table", func(tx *sql.Tx) error {
1480 _, err := tx.Exec(`
1481 insert into pds_migration (
1482 name,
1483 did,
1484 collection,
1485 rkey,
1486 status,
1487 updated_at
1488 )
1489 select
1490 'add-repo-did',
1491 user_did,
1492 record_nsid,
1493 record_rkey,
1494 status,
1495 updated_at
1496 from pds_rewrite_status;
1497
1498 drop table pds_rewrite_status;
1499 `)
1500 return err
1501 })
1502
1503 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1504 orm.RunMigration(conn, logger, "add-id-to-vouches", func(tx *sql.Tx) error {
1505 _, err := tx.Exec(`
1506 create table vouches_new (
1507 id integer primary key autoincrement,
1508 did text not null,
1509 subject_did text not null,
1510 cid text not null,
1511 kind text not null default 'vouch',
1512 reason text,
1513 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1514 unique(did, subject_did),
1515 check (did <> subject_did),
1516 check (kind in ('vouch', 'denounce'))
1517 );
1518
1519 insert into vouches_new (did, subject_did, cid, kind, reason, created_at)
1520 select did, subject_did, cid, kind, reason, created_at
1521 from vouches;
1522
1523 drop table vouches;
1524 alter table vouches_new rename to vouches;
1525 `)
1526 return err
1527 })
1528 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1529
1530 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1531 orm.RunMigration(conn, logger, "drop-pipeline-statuses-pipeline-fk", func(tx *sql.Tx) error {
1532 _, err := tx.Exec(`
1533 create table if not exists pipeline_statuses_new (
1534 id integer primary key autoincrement,
1535 spindle text not null,
1536 rkey text not null,
1537
1538 pipeline_knot text not null,
1539 pipeline_rkey text not null,
1540
1541 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1542 workflow text not null,
1543 status text not null,
1544 error text,
1545 exit_code integer not null default 0,
1546
1547 unique (spindle, rkey)
1548 );
1549
1550 insert into pipeline_statuses_new
1551 select * from pipeline_statuses;
1552
1553 drop table pipeline_statuses;
1554 alter table pipeline_statuses_new rename to pipeline_statuses;
1555 `)
1556 return err
1557 })
1558 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1559
1560 orm.RunMigration(conn, logger, "add-repo-renames", func(tx *sql.Tx) error {
1561 res, err := tx.Exec(`
1562 update repos
1563 set name = name || '-renamed-' || id || '-' || lower(hex(randomblob(4)))
1564 where id in (
1565 select id from (
1566 select id, row_number() over (
1567 partition by did, knot, name
1568 order by created desc, id desc
1569 ) as rn
1570 from repos
1571 ) where rn > 1
1572 );
1573 `)
1574 if err != nil {
1575 return err
1576 }
1577 if n, _ := res.RowsAffected(); n > 0 {
1578 logger.Warn("suffixed legacy duplicate repo names before adding unique index", "rows", n)
1579 }
1580
1581 var remaining int
1582 if err := tx.QueryRow(`
1583 select count(*) from (
1584 select 1 from repos group by did, knot, name having count(*) > 1
1585 )
1586 `).Scan(&remaining); err != nil {
1587 return fmt.Errorf("checking for residual duplicate (did, knot, name) groups: %w", err)
1588 }
1589 if remaining > 0 {
1590 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)
1591 }
1592
1593 _, err = tx.Exec(`
1594 create table if not exists repo_renames (
1595 owner_did text not null,
1596 old_rkey text not null,
1597 repo_did text not null,
1598 renamed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1599 primary key (owner_did, old_rkey)
1600 );
1601 create unique index if not exists idx_repos_owner_knot_name
1602 on repos(did, knot, name);
1603 `)
1604 return err
1605 })
1606
1607 orm.RunMigration(conn, logger, "repos-canonical-rkey-uniqueness", func(tx *sql.Tx) error {
1608 _, err := tx.Exec(`
1609 drop index if exists idx_repos_owner_knot_name;
1610 create unique index if not exists idx_repos_did_rkey
1611 on repos(did, rkey);
1612 `)
1613 return err
1614 })
1615
1616 orm.RunMigration(conn, logger, "repo-did-references", func(tx *sql.Tx) error {
1617 tables := []struct{ table, oldCol, newCol string }{
1618 {"issues", "repo_at", "repo_did"},
1619 {"pulls", "repo_at", "repo_did"},
1620 {"pull_comments", "repo_at", "repo_did"},
1621 {"stars", "subject_at", "subject_did"},
1622 {"artifacts", "repo_at", "repo_did"},
1623 {"webhooks", "repo_at", "repo_did"},
1624 {"repo_sites", "repo_at", "repo_did"},
1625 {"site_deploys", "repo_at", "repo_did"},
1626 {"collaborators", "repo_at", "repo_did"},
1627 {"repo_issue_seqs", "repo_at", "repo_did"},
1628 {"repo_pull_seqs", "repo_at", "repo_did"},
1629 {"repo_languages", "repo_at", "repo_did"},
1630 {"repo_labels", "repo_at", "repo_did"},
1631 }
1632
1633 stmts := ""
1634 for _, t := range tables {
1635 stmts += fmt.Sprintf(
1636 `ALTER TABLE %s ADD COLUMN %s TEXT;
1637 UPDATE %s SET %s = (SELECT repos.repo_did FROM repos WHERE repos.at_uri = %s.%s);
1638 CREATE INDEX IF NOT EXISTS idx_%s_%s ON %s(%s);
1639 `, t.table, t.newCol, t.table, t.newCol, t.table, t.oldCol, t.table, t.newCol, t.table, t.newCol)
1640 }
1641
1642 stmts += `ALTER TABLE pulls ADD COLUMN source_repo_did TEXT;
1643 UPDATE pulls SET source_repo_did = (SELECT repos.repo_did FROM repos WHERE repos.at_uri = pulls.source_repo_at);
1644
1645 UPDATE profile_pinned_repositories SET pin = (
1646 SELECT repos.repo_did FROM repos WHERE repos.at_uri = profile_pinned_repositories.pin
1647 ) WHERE pin LIKE 'at://%'
1648 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 != '');
1649 `
1650
1651 _, err := tx.Exec(stmts)
1652 return err
1653 })
1654
1655 orm.RunMigration(conn, logger, "backfill-pds-rewrites-star-issue-pull-collab", func(tx *sql.Tx) error {
1656 type source struct {
1657 userDidCol string
1658 table string
1659 nsid string
1660 fkCol string
1661 }
1662 sources := []source{
1663 {"did", "stars", "sh.tangled.feed.star", "subject_at"},
1664 {"did", "issues", "sh.tangled.repo.issue", "repo_at"},
1665 {"owner_did", "pulls", "sh.tangled.repo.pull", "repo_at"},
1666 {"did", "collaborators", "sh.tangled.repo.collaborator", "repo_at"},
1667 }
1668
1669 for _, src := range sources {
1670 _, err := tx.Exec(fmt.Sprintf(`
1671 INSERT INTO pds_migration (name, did, collection, rkey, status)
1672 SELECT 'add-repo-did', t.%s, '%s', t.rkey, 'pending'
1673 FROM %s t
1674 JOIN repos r ON r.at_uri = t.%s
1675 WHERE r.repo_did IS NOT NULL AND r.repo_did != ''
1676 ON CONFLICT(name, did, collection, rkey) DO NOTHING
1677 `, src.userDidCol, src.nsid, src.table, src.fkCol))
1678 if err != nil {
1679 return fmt.Errorf("backfill pds rewrites for %s: %w", src.table, err)
1680 }
1681 }
1682
1683 return nil
1684 })
1685
1686 orm.RunMigration(conn, logger, "backfill-pds-rewrites-profiles", func(tx *sql.Tx) error {
1687 _, err := tx.Exec(`
1688 INSERT INTO pds_migration (name, did, collection, rkey, status)
1689 SELECT DISTINCT 'add-repo-did', pp.did, 'sh.tangled.actor.profile', 'self', 'pending'
1690 FROM profile_pinned_repositories pp
1691 JOIN repos r ON r.at_uri = pp.pin
1692 WHERE pp.pin LIKE 'at://%'
1693 AND r.repo_did IS NOT NULL AND r.repo_did != ''
1694 ON CONFLICT(name, did, collection, rkey) DO NOTHING
1695 `)
1696 if err != nil {
1697 return fmt.Errorf("backfill pds rewrites for profiles: %w", err)
1698 }
1699 return nil
1700 })
1701
1702 conn.ExecContext(ctx, "pragma foreign_keys = off;")
1703 orm.RunMigration(conn, logger, "drop-old-at-uri-columns", func(tx *sql.Tx) error {
1704 _, err := tx.Exec(`
1705 CREATE TABLE repos_new (
1706 id INTEGER PRIMARY KEY AUTOINCREMENT,
1707 did TEXT NOT NULL,
1708 name TEXT NOT NULL,
1709 knot TEXT NOT NULL,
1710 rkey TEXT NOT NULL,
1711 at_uri TEXT NOT NULL UNIQUE,
1712 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1713 description TEXT CHECK (length(description) <= 200),
1714 source TEXT,
1715 spindle TEXT,
1716 website TEXT,
1717 topics TEXT,
1718 repo_did TEXT,
1719 UNIQUE(did, rkey)
1720 );
1721 INSERT INTO repos_new (id, did, name, knot, rkey, at_uri, created, description, source, spindle, website, topics, repo_did)
1722 SELECT id, did, name, knot, rkey, at_uri, created, description, source, spindle, website, topics, repo_did
1723 FROM repos;
1724 DROP TABLE repos;
1725 ALTER TABLE repos_new RENAME TO repos;
1726 CREATE UNIQUE INDEX idx_repos_repo_did ON repos(repo_did);
1727 CREATE UNIQUE INDEX idx_repos_did_rkey ON repos(did, rkey);
1728
1729 CREATE TABLE issues_new (
1730 id INTEGER PRIMARY KEY AUTOINCREMENT,
1731 did TEXT NOT NULL,
1732 rkey TEXT NOT NULL,
1733 at_uri TEXT GENERATED ALWAYS AS ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) STORED,
1734 repo_did TEXT NOT NULL,
1735 issue_id INTEGER NOT NULL,
1736 title TEXT NOT NULL,
1737 body TEXT NOT NULL,
1738 open INTEGER NOT NULL DEFAULT 1,
1739 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1740 edited TEXT,
1741 deleted TEXT,
1742 UNIQUE(did, rkey),
1743 UNIQUE(repo_did, issue_id),
1744 UNIQUE(at_uri),
1745 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1746 );
1747 INSERT INTO issues_new (id, did, rkey, repo_did, issue_id, title, body, open, created, edited, deleted)
1748 SELECT id, did, rkey, repo_did, issue_id, title, body, open, created, edited, deleted
1749 FROM issues WHERE repo_did IS NOT NULL AND repo_did != '';
1750 DROP TABLE issues;
1751 ALTER TABLE issues_new RENAME TO issues;
1752 CREATE INDEX idx_issues_repo_did ON issues(repo_did);
1753
1754 CREATE TABLE pulls_new (
1755 id INTEGER PRIMARY KEY AUTOINCREMENT,
1756 pull_id INTEGER NOT NULL,
1757 at_uri TEXT GENERATED ALWAYS AS ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) STORED,
1758 repo_did TEXT NOT NULL,
1759 owner_did TEXT NOT NULL,
1760 rkey TEXT NOT NULL,
1761 title TEXT NOT NULL,
1762 body TEXT NOT NULL,
1763 target_branch TEXT NOT NULL,
1764 state INTEGER NOT NULL DEFAULT 0 CHECK (state IN (0, 1, 2, 3)),
1765 source_branch TEXT,
1766 source_repo_did TEXT,
1767 change_id TEXT,
1768 dependent_on TEXT,
1769 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1770 UNIQUE(repo_did, pull_id),
1771 UNIQUE(at_uri),
1772 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1773 );
1774 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)
1775 SELECT id, pull_id, repo_did, owner_did, rkey, title, body, target_branch, state, source_branch, source_repo_did, change_id, dependent_on, created
1776 FROM pulls WHERE repo_did IS NOT NULL AND repo_did != '';
1777 DROP TABLE pulls;
1778 ALTER TABLE pulls_new RENAME TO pulls;
1779 CREATE INDEX idx_pulls_repo_did ON pulls(repo_did);
1780 CREATE INDEX idx_pulls_source_repo_did ON pulls(source_repo_did);
1781
1782 CREATE TABLE pull_comments_new (
1783 id INTEGER PRIMARY KEY AUTOINCREMENT,
1784 pull_id INTEGER NOT NULL,
1785 submission_id INTEGER NOT NULL,
1786 repo_did TEXT NOT NULL,
1787 owner_did TEXT NOT NULL,
1788 comment_at TEXT NOT NULL,
1789 body TEXT NOT NULL,
1790 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1791 FOREIGN KEY (repo_did, pull_id) REFERENCES pulls(repo_did, pull_id) ON DELETE CASCADE,
1792 FOREIGN KEY (submission_id) REFERENCES pull_submissions(id) ON DELETE CASCADE
1793 );
1794 INSERT INTO pull_comments_new (id, pull_id, submission_id, repo_did, owner_did, comment_at, body, created)
1795 SELECT id, pull_id, submission_id, repo_did, owner_did, comment_at, body, created
1796 FROM pull_comments WHERE repo_did IS NOT NULL AND repo_did != '';
1797 DROP TABLE pull_comments;
1798 ALTER TABLE pull_comments_new RENAME TO pull_comments;
1799 CREATE INDEX idx_pull_comments_repo_did ON pull_comments(repo_did);
1800
1801 CREATE TABLE stars_new (
1802 id INTEGER PRIMARY KEY AUTOINCREMENT,
1803 did TEXT NOT NULL,
1804 rkey TEXT NOT NULL,
1805 subject_type TEXT NOT NULL CHECK (subject_type IN ('repo', 'string')),
1806 subject TEXT NOT NULL,
1807 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1808 UNIQUE(did, rkey),
1809 UNIQUE(did, subject)
1810 );
1811 INSERT INTO stars_new (id, did, rkey, subject_type, subject, created)
1812 SELECT id, did, rkey, 'repo', subject_did, created
1813 FROM stars
1814 WHERE subject_did IS NOT NULL AND subject_did != '';
1815 INSERT OR IGNORE INTO stars_new (id, did, rkey, subject_type, subject, created)
1816 SELECT id, did, rkey, 'string', subject_at, created
1817 FROM stars
1818 WHERE (subject_did IS NULL OR subject_did = '')
1819 AND subject_at LIKE 'at://%/sh.tangled.string/%';
1820 DROP TABLE stars;
1821 ALTER TABLE stars_new RENAME TO stars;
1822 CREATE INDEX idx_stars_subject ON stars(subject);
1823 CREATE INDEX idx_stars_subject_type ON stars(subject_type);
1824 CREATE INDEX idx_stars_created ON stars(created);
1825
1826 CREATE TABLE collaborators_new (
1827 id INTEGER PRIMARY KEY AUTOINCREMENT,
1828 did TEXT NOT NULL,
1829 rkey TEXT,
1830 subject_did TEXT NOT NULL,
1831 repo_did TEXT NOT NULL,
1832 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1833 UNIQUE(did, rkey),
1834 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1835 );
1836 INSERT INTO collaborators_new (id, did, rkey, subject_did, repo_did, created)
1837 SELECT id, did, NULLIF(rkey, ''), subject_did, repo_did, created
1838 FROM collaborators WHERE repo_did IS NOT NULL AND repo_did != '';
1839 DROP TABLE collaborators;
1840 ALTER TABLE collaborators_new RENAME TO collaborators;
1841 CREATE INDEX idx_collaborators_repo_did ON collaborators(repo_did);
1842
1843 CREATE TABLE artifacts_new (
1844 id INTEGER PRIMARY KEY AUTOINCREMENT,
1845 did TEXT NOT NULL,
1846 rkey TEXT NOT NULL,
1847 repo_did TEXT NOT NULL,
1848 tag BINARY(20) NOT NULL,
1849 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1850 blob_cid TEXT NOT NULL,
1851 name TEXT NOT NULL,
1852 size INTEGER NOT NULL DEFAULT 0,
1853 mimetype TEXT NOT NULL DEFAULT '*/*',
1854 UNIQUE(did, rkey),
1855 UNIQUE(repo_did, tag, name),
1856 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1857 );
1858 INSERT INTO artifacts_new (id, did, rkey, repo_did, tag, created, blob_cid, name, size, mimetype)
1859 SELECT id, did, rkey, repo_did, tag, created, blob_cid, name, size, mimetype
1860 FROM artifacts WHERE repo_did IS NOT NULL AND repo_did != '';
1861 DROP TABLE artifacts;
1862 ALTER TABLE artifacts_new RENAME TO artifacts;
1863 CREATE INDEX idx_artifacts_repo_did ON artifacts(repo_did);
1864
1865 CREATE TABLE webhooks_new (
1866 id INTEGER PRIMARY KEY AUTOINCREMENT,
1867 repo_did TEXT NOT NULL,
1868 url TEXT NOT NULL,
1869 secret TEXT,
1870 active INTEGER NOT NULL DEFAULT 1,
1871 events TEXT NOT NULL,
1872 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1873 updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1874 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1875 );
1876 INSERT INTO webhooks_new (id, repo_did, url, secret, active, events, created_at, updated_at)
1877 SELECT id, repo_did, url, secret, active, events, created_at, updated_at
1878 FROM webhooks WHERE repo_did IS NOT NULL AND repo_did != '';
1879 DROP TABLE webhooks;
1880 ALTER TABLE webhooks_new RENAME TO webhooks;
1881 CREATE INDEX idx_webhooks_repo_did ON webhooks(repo_did);
1882
1883 CREATE TABLE repo_sites_new (
1884 id INTEGER PRIMARY KEY AUTOINCREMENT,
1885 repo_did TEXT NOT NULL UNIQUE,
1886 branch TEXT NOT NULL,
1887 dir TEXT NOT NULL DEFAULT '/',
1888 is_index INTEGER NOT NULL DEFAULT 0,
1889 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1890 updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1891 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1892 );
1893 INSERT INTO repo_sites_new (id, repo_did, branch, dir, is_index, created, updated)
1894 SELECT id, repo_did, branch, dir, is_index, created, updated
1895 FROM repo_sites WHERE repo_did IS NOT NULL AND repo_did != '';
1896 DROP TABLE repo_sites;
1897 ALTER TABLE repo_sites_new RENAME TO repo_sites;
1898
1899 CREATE TABLE site_deploys_new (
1900 id INTEGER PRIMARY KEY AUTOINCREMENT,
1901 repo_did TEXT NOT NULL,
1902 branch TEXT NOT NULL,
1903 dir TEXT NOT NULL DEFAULT '/',
1904 commit_sha TEXT NOT NULL DEFAULT '',
1905 status TEXT NOT NULL CHECK (status IN ('success', 'failure')),
1906 trigger TEXT NOT NULL CHECK (trigger IN ('config_change', 'push')),
1907 error TEXT NOT NULL DEFAULT '',
1908 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1909 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1910 );
1911 INSERT INTO site_deploys_new (id, repo_did, branch, dir, commit_sha, status, trigger, error, created_at)
1912 SELECT id, repo_did, branch, dir, commit_sha, status, trigger, error, created_at
1913 FROM site_deploys WHERE repo_did IS NOT NULL AND repo_did != '';
1914 DROP TABLE site_deploys;
1915 ALTER TABLE site_deploys_new RENAME TO site_deploys;
1916 CREATE INDEX idx_site_deploys_repo_did ON site_deploys(repo_did);
1917
1918 CREATE TABLE repo_issue_seqs_new (
1919 repo_did TEXT PRIMARY KEY,
1920 next_issue_id INTEGER NOT NULL DEFAULT 1,
1921 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1922 );
1923 INSERT INTO repo_issue_seqs_new (repo_did, next_issue_id)
1924 SELECT repo_did, next_issue_id
1925 FROM repo_issue_seqs WHERE repo_did IS NOT NULL AND repo_did != '';
1926 DROP TABLE repo_issue_seqs;
1927 ALTER TABLE repo_issue_seqs_new RENAME TO repo_issue_seqs;
1928
1929 CREATE TABLE repo_pull_seqs_new (
1930 repo_did TEXT PRIMARY KEY,
1931 next_pull_id INTEGER NOT NULL DEFAULT 1,
1932 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1933 );
1934 INSERT INTO repo_pull_seqs_new (repo_did, next_pull_id)
1935 SELECT repo_did, next_pull_id
1936 FROM repo_pull_seqs WHERE repo_did IS NOT NULL AND repo_did != '';
1937 DROP TABLE repo_pull_seqs;
1938 ALTER TABLE repo_pull_seqs_new RENAME TO repo_pull_seqs;
1939
1940 CREATE TABLE repo_languages_new (
1941 id INTEGER PRIMARY KEY AUTOINCREMENT,
1942 repo_did TEXT NOT NULL,
1943 ref TEXT NOT NULL,
1944 is_default_ref INTEGER NOT NULL DEFAULT 0,
1945 language TEXT NOT NULL,
1946 bytes INTEGER NOT NULL CHECK (bytes >= 0),
1947 UNIQUE(repo_did, ref, language),
1948 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1949 );
1950 INSERT INTO repo_languages_new (id, repo_did, ref, is_default_ref, language, bytes)
1951 SELECT id, repo_did, ref, is_default_ref, language, bytes
1952 FROM repo_languages WHERE repo_did IS NOT NULL AND repo_did != '';
1953 DROP TABLE repo_languages;
1954 ALTER TABLE repo_languages_new RENAME TO repo_languages;
1955
1956 CREATE TABLE repo_labels_new (
1957 id INTEGER PRIMARY KEY AUTOINCREMENT,
1958 repo_did TEXT NOT NULL,
1959 label_at TEXT NOT NULL,
1960 UNIQUE(repo_did, label_at),
1961 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
1962 );
1963 INSERT INTO repo_labels_new (id, repo_did, label_at)
1964 SELECT id, repo_did, label_at
1965 FROM repo_labels WHERE repo_did IS NOT NULL AND repo_did != '';
1966 DROP TABLE repo_labels;
1967 ALTER TABLE repo_labels_new RENAME TO repo_labels;
1968 `)
1969 return err
1970 })
1971 conn.ExecContext(ctx, "pragma foreign_keys = on;")
1972
1973 orm.RunMigration(conn, logger, "migrate-knots-to-repo-did-rename", func(tx *sql.Tx) error {
1974 _, err := tx.Exec(`
1975 update registrations set needs_upgrade = 1;
1976 `)
1977 return err
1978 })
1979
1980 orm.RunMigration(conn, logger, "drop-ghost-comments-table", func(tx *sql.Tx) error {
1981 _, err := tx.Exec(`DROP TABLE IF EXISTS comments`)
1982 return err
1983 })
1984
1985 orm.RunMigration(conn, logger, "add-knot-members-table", func(tx *sql.Tx) error {
1986 _, err := tx.Exec(`
1987 create table if not exists knot_members (
1988 id integer primary key autoincrement,
1989 did text not null,
1990 rkey text not null,
1991 domain text not null,
1992 subject text not null,
1993 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1994 unique (did, domain, subject)
1995 );
1996 create index if not exists idx_knot_members_did_rkey on knot_members(did, rkey);
1997 `)
1998 return err
1999 })
2000
2001 orm.RunMigration(conn, logger, "add-comments-table", func(tx *sql.Tx) error {
2002 _, err := tx.Exec(`
2003 drop table if exists comments;
2004
2005 create table comments (
2006 -- identifiers
2007 id integer primary key autoincrement,
2008
2009 did text not null,
2010 collection text not null default 'sh.tangled.feed.comment',
2011 rkey text not null,
2012 at_uri text generated always as ('at://' || did || '/' || collection || '/' || rkey) stored,
2013 cid text,
2014
2015 -- content
2016 subject_uri text not null, -- at_uri of subject (issue, pr, string)
2017 subject_cid text not null, -- cid of subject
2018
2019 body_text text not null,
2020 body_original text,
2021 body_blobs text, -- json
2022
2023 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
2024
2025 reply_to_uri text, -- at_uri of parent comment
2026 reply_to_cid text, -- cid of parent comment
2027
2028 pull_round_idx integer, -- pull round index. required when subject is sh.tangled.repo.pull
2029
2030 -- appview-local information
2031 edited text,
2032 deleted text,
2033
2034 unique(did, collection, rkey)
2035 );
2036
2037 insert into comments (
2038 did,
2039 collection,
2040 rkey,
2041 subject_uri,
2042 subject_cid, -- we need to know cid
2043 body_text,
2044 created,
2045 reply_to_uri,
2046 reply_to_cid, -- we need to know cid
2047 edited,
2048 deleted
2049 )
2050 select
2051 did,
2052 'sh.tangled.repo.issue.comment',
2053 rkey,
2054 issue_at,
2055 '',
2056 body,
2057 created,
2058 reply_to,
2059 '',
2060 edited,
2061 deleted
2062 from issue_comments
2063 where rkey is not null;
2064
2065 insert into comments (
2066 did,
2067 collection,
2068 rkey,
2069 subject_uri,
2070 subject_cid, -- we need to know cid
2071 body_text,
2072 created,
2073 pull_round_idx
2074 )
2075 select
2076 c.owner_did,
2077 'sh.tangled.repo.pull.comment',
2078 substr(
2079 substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey
2080 instr(
2081 substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey
2082 '/'
2083 ) + 1
2084 ), -- rkey
2085 p.at_uri,
2086 '',
2087 c.body,
2088 c.created,
2089 s.round_number
2090 from pull_comments c
2091 join pulls p on c.repo_did = p.repo_did and c.pull_id = p.pull_id
2092 join pull_submissions s on s.id = c.submission_id;
2093 `)
2094 return err
2095 })
2096
2097 orm.RunMigration(conn, logger, "migrate-legacy-comments", func(tx *sql.Tx) error {
2098 _, err := tx.Exec(`
2099 insert into pds_migration (name, did, collection, rkey)
2100 select
2101 'use-feed-comment',
2102 did,
2103 collection,
2104 rkey
2105 from comments
2106 where collection <> 'sh.tangled.feed.comment';
2107 `)
2108 return err
2109 })
2110
2111 conn.ExecContext(ctx, "pragma foreign_keys = off;")
2112 orm.RunMigration(conn, logger, "cascade-notification-entity-fks", func(tx *sql.Tx) error {
2113 _, err := tx.Exec(`
2114 CREATE TABLE notifications_new (
2115 id INTEGER PRIMARY KEY AUTOINCREMENT,
2116 recipient_did TEXT NOT NULL,
2117 actor_did TEXT NOT NULL,
2118 type TEXT NOT NULL,
2119 entity_type TEXT NOT NULL,
2120 entity_id TEXT NOT NULL,
2121 read INTEGER NOT NULL DEFAULT 0,
2122 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
2123 repo_id INTEGER REFERENCES repos(id) ON DELETE CASCADE,
2124 issue_id INTEGER REFERENCES issues(id) ON DELETE CASCADE,
2125 pull_id INTEGER REFERENCES pulls(id) ON DELETE CASCADE
2126 );
2127 INSERT INTO notifications_new (id, recipient_did, actor_did, type, entity_type, entity_id, read, created, repo_id, issue_id, pull_id)
2128 SELECT id, recipient_did, actor_did, type, entity_type, entity_id, read, created, repo_id, issue_id, pull_id
2129 FROM notifications;
2130 DROP TABLE notifications;
2131 ALTER TABLE notifications_new RENAME TO notifications;
2132 CREATE INDEX idx_notifications_recipient_created ON notifications(recipient_did, created DESC);
2133 CREATE INDEX idx_notifications_recipient_read ON notifications(recipient_did, read);
2134 `)
2135 return err
2136 })
2137 conn.ExecContext(ctx, "pragma foreign_keys = on;")
2138
2139 orm.RunMigration(conn, logger, "collaborators-unique-on-repo-subject", func(tx *sql.Tx) error {
2140 _, err := tx.Exec(`
2141 CREATE TABLE collaborators_new (
2142 id INTEGER PRIMARY KEY AUTOINCREMENT,
2143 did TEXT NOT NULL,
2144 rkey TEXT,
2145 subject_did TEXT NOT NULL,
2146 repo_did TEXT NOT NULL,
2147 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
2148 UNIQUE(repo_did, subject_did),
2149 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE
2150 );
2151 INSERT INTO collaborators_new (id, did, rkey, subject_did, repo_did, created)
2152 SELECT id, did, rkey, subject_did, repo_did, created
2153 FROM (
2154 SELECT
2155 id, did, rkey, subject_did, repo_did, created,
2156 ROW_NUMBER() OVER (
2157 PARTITION BY repo_did, subject_did
2158 ORDER BY created DESC, id DESC
2159 ) AS rn
2160 FROM collaborators
2161 )
2162 WHERE rn = 1;
2163 DROP TABLE collaborators;
2164 ALTER TABLE collaborators_new RENAME TO collaborators;
2165 CREATE INDEX idx_collaborators_repo_did ON collaborators(repo_did);
2166 CREATE INDEX idx_collaborators_subject_did ON collaborators(subject_did);
2167 `)
2168 return err
2169 })
2170
2171 orm.RunMigration(conn, logger, "add-knot-acl-native", func(tx *sql.Tx) error {
2172 _, err := tx.Exec(`
2173 create table if not exists knot_acl_native (
2174 domain text primary key,
2175 since text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
2176 );
2177 `)
2178 return err
2179 })
2180
2181 orm.RunMigration(conn, logger, "delete-unused-pipeline-statuses", func(tx *sql.Tx) error {
2182 _, err := tx.Exec(`
2183 delete from pipeline_statuses as p
2184 where p.status = 'pending'
2185 and exists (
2186 select 1 from pipeline_statuses as q
2187 where q.pipeline_knot = p.pipeline_knot
2188 and q.pipeline_rkey = p.pipeline_rkey
2189 and q.workflow = p.workflow
2190 and q.status = 'pending'
2191 and q.created < p.created
2192 );
2193 `)
2194 return err
2195 })
2196
2197 orm.RunMigration(conn, logger, "timeline-query-indexes", func(tx *sql.Tx) error {
2198 _, err := tx.Exec(`
2199 -- following timeline: stars by a set of users, newest first
2200 create index if not exists idx_stars_did_type_created on stars(did, subject_type, created);
2201 -- follower counts and reverse lookups (no index on subject_did before)
2202 create index if not exists idx_follows_subject_did on follows(subject_did);
2203 -- global timeline: newest follows without a full sort
2204 create index if not exists idx_follows_followed_at on follows(followed_at);
2205 -- global timeline: newest repos without a full sort
2206 create index if not exists idx_repos_created on repos(created);
2207 `)
2208 return err
2209 })
2210
2211 orm.RunMigration(conn, logger, "add-focusing-table", func(tx *sql.Tx) error {
2212 _, err := tx.Exec(`
2213 create table if not exists focusing (
2214 did text primary key,
2215 started text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
2216 );
2217 `)
2218 return err
2219 })
2220
2221 return &DB{
2222 db,
2223 logger,
2224 }, nil
2225}
2226
2227func (d *DB) Close() error {
2228 return d.DB.Close()
2229}