This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / db / db.go
76 kB 2459 lines
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 create table if not exists onboarding ( 696 did text primary key, 697 step integer not null default 0, 698 status text not null default 'in_progress' 699 check (status in ('in_progress','completed','skipped')), 700 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 701 updated text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 702 ); 703 704 create table if not exists migrations ( 705 id integer primary key autoincrement, 706 name text unique 707 ); 708 709 -- indexes for better performance 710 create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc); 711 create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read); 712 create index if not exists idx_references_from_at on reference_links(from_at); 713 create index if not exists idx_references_to_at on reference_links(to_at); 714 create index if not exists idx_webhook_deliveries_webhook_id on webhook_deliveries(webhook_id); 715 create index if not exists idx_newsletter_prefs_user_did on newsletter_preferences(user_did); 716 `) 717 if err != nil { 718 return nil, err 719 } 720 721 // run migrations 722 orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error { 723 tx.Exec(` 724 alter table repos add column description text check (length(description) <= 200); 725 `) 726 return nil 727 }) 728 729 orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 730 // add unconstrained column 731 _, err := tx.Exec(` 732 alter table public_keys 733 add column rkey text; 734 `) 735 if err != nil { 736 return err 737 } 738 739 // backfill 740 _, err = tx.Exec(` 741 update public_keys 742 set rkey = '' 743 where rkey is null; 744 `) 745 if err != nil { 746 return err 747 } 748 749 return nil 750 }) 751 752 orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error { 753 _, err := tx.Exec(` 754 alter table comments drop column comment_at; 755 alter table comments add column rkey text; 756 `) 757 return err 758 }) 759 760 orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 761 _, err := tx.Exec(` 762 alter table comments add column deleted text; -- timestamp 763 alter table comments add column edited text; -- timestamp 764 `) 765 return err 766 }) 767 768 orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 769 _, err := tx.Exec(` 770 alter table pulls add column source_branch text; 771 alter table pulls add column source_repo_at text; 772 alter table pull_submissions add column source_rev text; 773 `) 774 return err 775 }) 776 777 orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error { 778 _, err := tx.Exec(` 779 alter table repos add column source text; 780 `) 781 return err 782 }) 783 784 // disable foreign-keys for the next migration 785 // NOTE: this cannot be done in a transaction, so it is run outside [0] 786 // 787 // [0]: https://sqlite.org/pragma.html#pragma_foreign_keys 788 conn.ExecContext(ctx, "pragma foreign_keys = off;") 789 orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error { 790 _, err := tx.Exec(` 791 create table pulls_new ( 792 -- identifiers 793 id integer primary key autoincrement, 794 pull_id integer not null, 795 796 -- at identifiers 797 repo_at text not null, 798 owner_did text not null, 799 rkey text not null, 800 801 -- content 802 title text not null, 803 body text not null, 804 target_branch text not null, 805 state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted 806 807 -- source info 808 source_branch text, 809 source_repo_at text, 810 811 -- stacking 812 stack_id text, 813 change_id text, 814 parent_change_id text, 815 816 -- meta 817 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 818 819 -- constraints 820 unique(repo_at, pull_id), 821 foreign key (repo_at) references repos(at_uri) on delete cascade 822 ); 823 824 insert into pulls_new ( 825 id, pull_id, 826 repo_at, owner_did, rkey, 827 title, body, target_branch, state, 828 source_branch, source_repo_at, 829 created 830 ) 831 select 832 id, pull_id, 833 repo_at, owner_did, rkey, 834 title, body, target_branch, state, 835 source_branch, source_repo_at, 836 created 837 FROM pulls; 838 839 drop table pulls; 840 alter table pulls_new rename to pulls; 841 `) 842 return err 843 }) 844 conn.ExecContext(ctx, "pragma foreign_keys = on;") 845 846 orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error { 847 tx.Exec(` 848 alter table repos add column spindle text; 849 `) 850 return nil 851 }) 852 853 // drop all knot secrets, add unique constraint to knots 854 // 855 // knots will henceforth use service auth for signed requests 856 orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error { 857 _, err := tx.Exec(` 858 create table registrations_new ( 859 id integer primary key autoincrement, 860 domain text not null, 861 did text not null, 862 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 863 registered text, 864 read_only integer not null default 0, 865 unique(domain, did) 866 ); 867 868 insert into registrations_new (id, domain, did, created, registered, read_only) 869 select id, domain, did, created, registered, 1 from registrations 870 where registered is not null; 871 872 drop table registrations; 873 alter table registrations_new rename to registrations; 874 `) 875 return err 876 }) 877 878 // recreate and add rkey + created columns with default constraint 879 orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error { 880 // create new table 881 // - repo_at instead of repo integer 882 // - rkey field 883 // - created field 884 _, err := tx.Exec(` 885 create table collaborators_new ( 886 -- identifiers for the record 887 id integer primary key autoincrement, 888 did text not null, 889 rkey text, 890 891 -- content 892 subject_did text not null, 893 repo_at text not null, 894 895 -- meta 896 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 897 898 -- constraints 899 foreign key (repo_at) references repos(at_uri) on delete cascade 900 ) 901 `) 902 if err != nil { 903 return err 904 } 905 906 // copy data 907 _, err = tx.Exec(` 908 insert into collaborators_new (id, did, rkey, subject_did, repo_at) 909 select 910 c.id, 911 r.did, 912 '', 913 c.did, 914 r.at_uri 915 from collaborators c 916 join repos r on c.repo = r.id 917 `) 918 if err != nil { 919 return err 920 } 921 922 // drop old table 923 _, err = tx.Exec(`drop table collaborators`) 924 if err != nil { 925 return err 926 } 927 928 // rename new table 929 _, err = tx.Exec(`alter table collaborators_new rename to collaborators`) 930 return err 931 }) 932 933 orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error { 934 _, err := tx.Exec(` 935 alter table issues add column rkey text not null default ''; 936 937 -- get last url section from issue_at and save to rkey column 938 update issues 939 set rkey = replace(issue_at, rtrim(issue_at, replace(issue_at, '/', '')), ''); 940 `) 941 return err 942 }) 943 944 // repurpose the read-only column to "needs-upgrade" 945 orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error { 946 _, err := tx.Exec(` 947 alter table registrations rename column read_only to needs_upgrade; 948 `) 949 return err 950 }) 951 952 // require all knots to upgrade after the release of total xrpc 953 orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error { 954 _, err := tx.Exec(` 955 update registrations set needs_upgrade = 1; 956 `) 957 return err 958 }) 959 960 // require all knots to upgrade after the release of total xrpc 961 orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error { 962 _, err := tx.Exec(` 963 alter table spindles add column needs_upgrade integer not null default 0; 964 `) 965 return err 966 }) 967 968 // remove issue_at from issues and replace with generated column 969 // 970 // this requires a full table recreation because stored columns 971 // cannot be added via alter 972 // 973 // couple other changes: 974 // - columns renamed to be more consistent 975 // - adds edited and deleted fields 976 // 977 // disable foreign-keys for the next migration 978 conn.ExecContext(ctx, "pragma foreign_keys = off;") 979 orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error { 980 _, err := tx.Exec(` 981 create table if not exists issues_new ( 982 -- identifiers 983 id integer primary key autoincrement, 984 did text not null, 985 rkey text not null, 986 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) stored, 987 988 -- at identifiers 989 repo_at text not null, 990 991 -- content 992 issue_id integer not null, 993 title text not null, 994 body text not null, 995 open integer not null default 1, 996 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 997 edited text, -- timestamp 998 deleted text, -- timestamp 999 1000 unique(did, rkey), 1001 unique(repo_at, issue_id), 1002 unique(at_uri), 1003 foreign key (repo_at) references repos(at_uri) on delete cascade 1004 ); 1005 `) 1006 if err != nil { 1007 return err 1008 } 1009 1010 // transfer data 1011 _, err = tx.Exec(` 1012 insert into issues_new (id, did, rkey, repo_at, issue_id, title, body, open, created) 1013 select 1014 i.id, 1015 i.owner_did, 1016 i.rkey, 1017 i.repo_at, 1018 i.issue_id, 1019 i.title, 1020 i.body, 1021 i.open, 1022 i.created 1023 from issues i; 1024 `) 1025 if err != nil { 1026 return err 1027 } 1028 1029 // drop old table 1030 _, err = tx.Exec(`drop table issues`) 1031 if err != nil { 1032 return err 1033 } 1034 1035 // rename new table 1036 _, err = tx.Exec(`alter table issues_new rename to issues`) 1037 return err 1038 }) 1039 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1040 1041 // - renames the comments table to 'issue_comments' 1042 // - rework issue comments to update constraints: 1043 // * unique(did, rkey) 1044 // * remove comment-id and just use the global ID 1045 // * foreign key (repo_at, issue_id) 1046 // - new columns 1047 // * column "reply_to" which can be any other comment 1048 // * column "at-uri" which is a generated column 1049 orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error { 1050 _, err := tx.Exec(` 1051 create table if not exists issue_comments ( 1052 -- identifiers 1053 id integer primary key autoincrement, 1054 did text not null, 1055 rkey text, 1056 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue.comment' || '/' || rkey) stored, 1057 1058 -- at identifiers 1059 issue_at text not null, 1060 reply_to text, -- at_uri of parent comment 1061 1062 -- content 1063 body text not null, 1064 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1065 edited text, 1066 deleted text, 1067 1068 -- constraints 1069 unique(did, rkey), 1070 unique(at_uri), 1071 foreign key (issue_at) references issues(at_uri) on delete cascade 1072 ); 1073 `) 1074 if err != nil { 1075 return err 1076 } 1077 1078 // transfer data 1079 _, err = tx.Exec(` 1080 insert into issue_comments (id, did, rkey, issue_at, body, created, edited, deleted) 1081 select 1082 c.id, 1083 c.owner_did, 1084 c.rkey, 1085 i.at_uri, -- get at_uri from issues table 1086 c.body, 1087 c.created, 1088 c.edited, 1089 c.deleted 1090 from comments c 1091 join issues i on c.repo_at = i.repo_at and c.issue_id = i.issue_id; 1092 `) 1093 if err != nil { 1094 return err 1095 } 1096 1097 // drop old table 1098 _, err = tx.Exec(`drop table comments`) 1099 return err 1100 }) 1101 1102 // add generated at_uri column to pulls table 1103 // 1104 // this requires a full table recreation because stored columns 1105 // cannot be added via alter 1106 // 1107 // disable foreign-keys for the next migration 1108 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1109 orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error { 1110 _, err := tx.Exec(` 1111 create table if not exists pulls_new ( 1112 -- identifiers 1113 id integer primary key autoincrement, 1114 pull_id integer not null, 1115 at_uri text generated always as ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) stored, 1116 1117 -- at identifiers 1118 repo_at text not null, 1119 owner_did text not null, 1120 rkey text not null, 1121 1122 -- content 1123 title text not null, 1124 body text not null, 1125 target_branch text not null, 1126 state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted 1127 1128 -- source info 1129 source_branch text, 1130 source_repo_at text, 1131 1132 -- stacking 1133 stack_id text, 1134 change_id text, 1135 parent_change_id text, 1136 1137 -- meta 1138 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1139 1140 -- constraints 1141 unique(repo_at, pull_id), 1142 unique(at_uri), 1143 foreign key (repo_at) references repos(at_uri) on delete cascade 1144 ); 1145 `) 1146 if err != nil { 1147 return err 1148 } 1149 1150 // transfer data 1151 _, err = tx.Exec(` 1152 insert into pulls_new ( 1153 id, pull_id, repo_at, owner_did, rkey, 1154 title, body, target_branch, state, 1155 source_branch, source_repo_at, 1156 stack_id, change_id, parent_change_id, 1157 created 1158 ) 1159 select 1160 id, pull_id, repo_at, owner_did, rkey, 1161 title, body, target_branch, state, 1162 source_branch, source_repo_at, 1163 stack_id, change_id, parent_change_id, 1164 created 1165 from pulls; 1166 `) 1167 if err != nil { 1168 return err 1169 } 1170 1171 // drop old table 1172 _, err = tx.Exec(`drop table pulls`) 1173 if err != nil { 1174 return err 1175 } 1176 1177 // rename new table 1178 _, err = tx.Exec(`alter table pulls_new rename to pulls`) 1179 return err 1180 }) 1181 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1182 1183 // remove repo_at and pull_id from pull_submissions and replace with pull_at 1184 // 1185 // this requires a full table recreation because stored columns 1186 // cannot be added via alter 1187 // 1188 // disable foreign-keys for the next migration 1189 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1190 orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error { 1191 _, err := tx.Exec(` 1192 create table if not exists pull_submissions_new ( 1193 -- identifiers 1194 id integer primary key autoincrement, 1195 pull_at text not null, 1196 1197 -- content, these are immutable, and require a resubmission to update 1198 round_number integer not null default 0, 1199 patch text, 1200 source_rev text, 1201 1202 -- meta 1203 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1204 1205 -- constraints 1206 unique(pull_at, round_number), 1207 foreign key (pull_at) references pulls(at_uri) on delete cascade 1208 ); 1209 `) 1210 if err != nil { 1211 return err 1212 } 1213 1214 // transfer data, constructing pull_at from pulls table 1215 _, err = tx.Exec(` 1216 insert into pull_submissions_new (id, pull_at, round_number, patch, created) 1217 select 1218 ps.id, 1219 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey, 1220 ps.round_number, 1221 ps.patch, 1222 ps.created 1223 from pull_submissions ps 1224 join pulls p on ps.repo_at = p.repo_at and ps.pull_id = p.pull_id; 1225 `) 1226 if err != nil { 1227 return err 1228 } 1229 1230 // drop old table 1231 _, err = tx.Exec(`drop table pull_submissions`) 1232 if err != nil { 1233 return err 1234 } 1235 1236 // rename new table 1237 _, err = tx.Exec(`alter table pull_submissions_new rename to pull_submissions`) 1238 return err 1239 }) 1240 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1241 1242 // knots may report the combined patch for a comparison, we can store that on the appview side 1243 // (but not on the pds record), because calculating the combined patch requires a git index 1244 orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error { 1245 _, err := tx.Exec(` 1246 alter table pull_submissions add column combined text; 1247 `) 1248 return err 1249 }) 1250 1251 orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error { 1252 _, err := tx.Exec(` 1253 alter table profile add column pronouns text; 1254 `) 1255 return err 1256 }) 1257 1258 orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error { 1259 _, err := tx.Exec(` 1260 alter table repos add column website text; 1261 alter table repos add column topics text; 1262 `) 1263 return err 1264 }) 1265 1266 orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error { 1267 _, err := tx.Exec(` 1268 alter table notification_preferences add column user_mentioned integer not null default 1; 1269 `) 1270 return err 1271 }) 1272 1273 // remove the foreign key constraints from stars. 1274 orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error { 1275 _, err := tx.Exec(` 1276 create table stars_new ( 1277 id integer primary key autoincrement, 1278 did text not null, 1279 rkey text not null, 1280 1281 subject_at text not null, 1282 1283 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1284 unique(did, rkey), 1285 unique(did, subject_at) 1286 ); 1287 1288 insert into stars_new ( 1289 id, 1290 did, 1291 rkey, 1292 subject_at, 1293 created 1294 ) 1295 select 1296 id, 1297 starred_by_did, 1298 rkey, 1299 repo_at, 1300 created 1301 from stars; 1302 1303 drop table stars; 1304 alter table stars_new rename to stars; 1305 1306 create index if not exists idx_stars_created on stars(created); 1307 create index if not exists idx_stars_subject_at_created on stars(subject_at, created); 1308 `) 1309 return err 1310 }) 1311 1312 orm.RunMigration(conn, logger, "add-avatar-to-profile", func(tx *sql.Tx) error { 1313 _, err := tx.Exec(` 1314 alter table profile add column avatar text; 1315 `) 1316 return err 1317 }) 1318 1319 orm.RunMigration(conn, logger, "remove-profile-stats-column-constraint", func(tx *sql.Tx) error { 1320 _, err := tx.Exec(` 1321 -- create new table without the check constraint 1322 create table profile_stats_new ( 1323 id integer primary key autoincrement, 1324 did text not null, 1325 kind text not null, -- no constraint this time 1326 foreign key (did) references profile(did) on delete cascade 1327 ); 1328 1329 -- copy data from old table 1330 insert into profile_stats_new (id, did, kind) 1331 select id, did, kind 1332 from profile_stats; 1333 1334 -- drop old table 1335 drop table profile_stats; 1336 1337 -- rename new table 1338 alter table profile_stats_new rename to profile_stats; 1339 `) 1340 return err 1341 }) 1342 1343 orm.RunMigration(conn, logger, "add-preferred-handle-profile", func(tx *sql.Tx) error { 1344 _, err := tx.Exec(` 1345 alter table profile add column preferred_handle text; 1346 `) 1347 return err 1348 }) 1349 1350 orm.RunMigration(conn, logger, "add-repo-did-column", func(tx *sql.Tx) error { 1351 _, err := tx.Exec(` 1352 alter table repos add column repo_did text; 1353 create unique index if not exists idx_repos_repo_did on repos(repo_did); 1354 `) 1355 return err 1356 }) 1357 1358 orm.RunMigration(conn, logger, "add-pds-rewrite-status", func(tx *sql.Tx) error { 1359 _, err := tx.Exec(` 1360 create table if not exists pds_rewrite_status ( 1361 id integer primary key autoincrement, 1362 user_did text not null, 1363 repo_did text not null, 1364 record_nsid text not null, 1365 record_rkey text not null, 1366 old_repo_at text not null, 1367 status text not null default 'pending', 1368 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1369 unique(user_did, record_nsid, record_rkey) 1370 ); 1371 create index if not exists idx_pds_rewrite_user on pds_rewrite_status(user_did, status); 1372 `) 1373 return err 1374 }) 1375 1376 orm.RunMigration(conn, logger, "add-pipelines-repo-did", func(tx *sql.Tx) error { 1377 _, err := tx.Exec(` 1378 alter table pipelines add column repo_did text; 1379 create index if not exists idx_pipelines_repo_did on pipelines(repo_did); 1380 `) 1381 return err 1382 }) 1383 1384 orm.RunMigration(conn, logger, "migrate-knots-to-repo-dids", func(tx *sql.Tx) error { 1385 _, err := tx.Exec(`update registrations set needs_upgrade = 1`) 1386 return err 1387 }) 1388 1389 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1390 orm.RunMigration(conn, logger, "drop-pinned-repos-at-uri-fk", func(tx *sql.Tx) error { 1391 _, err := tx.Exec(` 1392 create table if not exists profile_pinned_repositories_new ( 1393 id integer primary key autoincrement, 1394 did text not null, 1395 pin text not null, 1396 1397 unique(did, pin), 1398 foreign key (did) references profile(did) on delete cascade 1399 ); 1400 1401 insert into profile_pinned_repositories_new (id, did, pin) 1402 select id, did, at_uri from profile_pinned_repositories; 1403 1404 drop table profile_pinned_repositories; 1405 1406 alter table profile_pinned_repositories_new rename to profile_pinned_repositories; 1407 `) 1408 return err 1409 }) 1410 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1411 1412 orm.RunMigration(conn, logger, "reset-profile-pin-rewrites", func(tx *sql.Tx) error { 1413 _, err := tx.Exec(` 1414 update pds_rewrite_status 1415 set status = 'pending', 1416 updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 1417 where record_nsid = 'sh.tangled.actor.profile' 1418 and status = 'done' 1419 `) 1420 return err 1421 }) 1422 1423 orm.RunMigration(conn, logger, "add-blob-data-to-pull-submissions", func(tx *sql.Tx) error { 1424 _, err := tx.Exec(` 1425 alter table pull_submissions add column patch_blob_ref text; 1426 alter table pull_submissions add column patch_blob_mime text; 1427 alter table pull_submissions add column patch_blob_size integer; 1428 `) 1429 return err 1430 }) 1431 1432 orm.RunMigration(conn, logger, "replace-parent-change-id-with-aturi", func(tx *sql.Tx) error { 1433 // add new column 1434 _, err := tx.Exec(` 1435 alter table pulls add column dependent_on text; 1436 `) 1437 if err != nil { 1438 return err 1439 } 1440 1441 // populate dependent_on with at_uri of the parent 1442 _, err = tx.Exec(` 1443 update pulls 1444 set dependent_on = ( 1445 select at_uri 1446 from pulls as parent 1447 where parent.stack_id = pulls.stack_id 1448 and parent.change_id = pulls.parent_change_id 1449 ) 1450 where parent_change_id is not null; 1451 `) 1452 if err != nil { 1453 return err 1454 } 1455 1456 // drop old columns 1457 _, err = tx.Exec(` 1458 alter table pulls drop column parent_change_id; 1459 alter table pulls drop column stack_id; 1460 `) 1461 1462 return err 1463 }) 1464 1465 orm.RunMigration(conn, logger, "add-pds-migration", func(tx *sql.Tx) error { 1466 _, err := tx.Exec(` 1467 create table if not exists pds_migration ( 1468 name text not null, 1469 1470 -- record at_uri 1471 did text not null, 1472 collection text not null, 1473 rkey text not null, 1474 1475 status text not null default 'pending', 1476 error_msg text, 1477 retry_count integer not null default 0, 1478 retry_after integer not null default 0, 1479 updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1480 1481 unique(name, did, collection, rkey) 1482 ); 1483 `) 1484 return err 1485 }) 1486 1487 orm.RunMigration(conn, logger, "unify-pds-record-migration-table", func(tx *sql.Tx) error { 1488 _, err := tx.Exec(` 1489 insert into pds_migration ( 1490 name, 1491 did, 1492 collection, 1493 rkey, 1494 status, 1495 updated_at 1496 ) 1497 select 1498 'add-repo-did', 1499 user_did, 1500 record_nsid, 1501 record_rkey, 1502 status, 1503 updated_at 1504 from pds_rewrite_status; 1505 1506 drop table pds_rewrite_status; 1507 `) 1508 return err 1509 }) 1510 1511 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1512 orm.RunMigration(conn, logger, "add-id-to-vouches", func(tx *sql.Tx) error { 1513 _, err := tx.Exec(` 1514 create table vouches_new ( 1515 id integer primary key autoincrement, 1516 did text not null, 1517 subject_did text not null, 1518 cid text not null, 1519 kind text not null default 'vouch', 1520 reason text, 1521 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1522 unique(did, subject_did), 1523 check (did <> subject_did), 1524 check (kind in ('vouch', 'denounce')) 1525 ); 1526 1527 insert into vouches_new (did, subject_did, cid, kind, reason, created_at) 1528 select did, subject_did, cid, kind, reason, created_at 1529 from vouches; 1530 1531 drop table vouches; 1532 alter table vouches_new rename to vouches; 1533 `) 1534 return err 1535 }) 1536 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1537 1538 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1539 orm.RunMigration(conn, logger, "drop-pipeline-statuses-pipeline-fk", func(tx *sql.Tx) error { 1540 _, err := tx.Exec(` 1541 create table if not exists pipeline_statuses_new ( 1542 id integer primary key autoincrement, 1543 spindle text not null, 1544 rkey text not null, 1545 1546 pipeline_knot text not null, 1547 pipeline_rkey text not null, 1548 1549 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1550 workflow text not null, 1551 status text not null, 1552 error text, 1553 exit_code integer not null default 0, 1554 1555 unique (spindle, rkey) 1556 ); 1557 1558 insert into pipeline_statuses_new 1559 select * from pipeline_statuses; 1560 1561 drop table pipeline_statuses; 1562 alter table pipeline_statuses_new rename to pipeline_statuses; 1563 `) 1564 return err 1565 }) 1566 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1567 1568 orm.RunMigration(conn, logger, "add-repo-renames", func(tx *sql.Tx) error { 1569 res, err := tx.Exec(` 1570 update repos 1571 set name = name || '-renamed-' || id || '-' || lower(hex(randomblob(4))) 1572 where id in ( 1573 select id from ( 1574 select id, row_number() over ( 1575 partition by did, knot, name 1576 order by created desc, id desc 1577 ) as rn 1578 from repos 1579 ) where rn > 1 1580 ); 1581 `) 1582 if err != nil { 1583 return err 1584 } 1585 if n, _ := res.RowsAffected(); n > 0 { 1586 logger.Warn("suffixed legacy duplicate repo names before adding unique index", "rows", n) 1587 } 1588 1589 var remaining int 1590 if err := tx.QueryRow(` 1591 select count(*) from ( 1592 select 1 from repos group by did, knot, name having count(*) > 1 1593 ) 1594 `).Scan(&remaining); err != nil { 1595 return fmt.Errorf("checking for residual duplicate (did, knot, name) groups: %w", err) 1596 } 1597 if remaining > 0 { 1598 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) 1599 } 1600 1601 _, err = tx.Exec(` 1602 create table if not exists repo_renames ( 1603 owner_did text not null, 1604 old_rkey text not null, 1605 repo_did text not null, 1606 renamed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1607 primary key (owner_did, old_rkey) 1608 ); 1609 create unique index if not exists idx_repos_owner_knot_name 1610 on repos(did, knot, name); 1611 `) 1612 return err 1613 }) 1614 1615 orm.RunMigration(conn, logger, "repos-canonical-rkey-uniqueness", func(tx *sql.Tx) error { 1616 _, err := tx.Exec(` 1617 drop index if exists idx_repos_owner_knot_name; 1618 create unique index if not exists idx_repos_did_rkey 1619 on repos(did, rkey); 1620 `) 1621 return err 1622 }) 1623 1624 orm.RunMigration(conn, logger, "repo-did-references", func(tx *sql.Tx) error { 1625 tables := []struct{ table, oldCol, newCol string }{ 1626 {"issues", "repo_at", "repo_did"}, 1627 {"pulls", "repo_at", "repo_did"}, 1628 {"pull_comments", "repo_at", "repo_did"}, 1629 {"stars", "subject_at", "subject_did"}, 1630 {"artifacts", "repo_at", "repo_did"}, 1631 {"webhooks", "repo_at", "repo_did"}, 1632 {"repo_sites", "repo_at", "repo_did"}, 1633 {"site_deploys", "repo_at", "repo_did"}, 1634 {"collaborators", "repo_at", "repo_did"}, 1635 {"repo_issue_seqs", "repo_at", "repo_did"}, 1636 {"repo_pull_seqs", "repo_at", "repo_did"}, 1637 {"repo_languages", "repo_at", "repo_did"}, 1638 {"repo_labels", "repo_at", "repo_did"}, 1639 } 1640 1641 stmts := "" 1642 for _, t := range tables { 1643 stmts += fmt.Sprintf( 1644 `ALTER TABLE %s ADD COLUMN %s TEXT; 1645 UPDATE %s SET %s = (SELECT repos.repo_did FROM repos WHERE repos.at_uri = %s.%s); 1646 CREATE INDEX IF NOT EXISTS idx_%s_%s ON %s(%s); 1647 `, t.table, t.newCol, t.table, t.newCol, t.table, t.oldCol, t.table, t.newCol, t.table, t.newCol) 1648 } 1649 1650 stmts += `ALTER TABLE pulls ADD COLUMN source_repo_did TEXT; 1651 UPDATE pulls SET source_repo_did = (SELECT repos.repo_did FROM repos WHERE repos.at_uri = pulls.source_repo_at); 1652 1653 UPDATE profile_pinned_repositories SET pin = ( 1654 SELECT repos.repo_did FROM repos WHERE repos.at_uri = profile_pinned_repositories.pin 1655 ) WHERE pin LIKE 'at://%' 1656 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 != ''); 1657 ` 1658 1659 _, err := tx.Exec(stmts) 1660 return err 1661 }) 1662 1663 orm.RunMigration(conn, logger, "backfill-pds-rewrites-star-issue-pull-collab", func(tx *sql.Tx) error { 1664 type source struct { 1665 userDidCol string 1666 table string 1667 nsid string 1668 fkCol string 1669 } 1670 sources := []source{ 1671 {"did", "stars", "sh.tangled.feed.star", "subject_at"}, 1672 {"did", "issues", "sh.tangled.repo.issue", "repo_at"}, 1673 {"owner_did", "pulls", "sh.tangled.repo.pull", "repo_at"}, 1674 {"did", "collaborators", "sh.tangled.repo.collaborator", "repo_at"}, 1675 } 1676 1677 for _, src := range sources { 1678 _, err := tx.Exec(fmt.Sprintf(` 1679 INSERT INTO pds_migration (name, did, collection, rkey, status) 1680 SELECT 'add-repo-did', t.%s, '%s', t.rkey, 'pending' 1681 FROM %s t 1682 JOIN repos r ON r.at_uri = t.%s 1683 WHERE r.repo_did IS NOT NULL AND r.repo_did != '' 1684 ON CONFLICT(name, did, collection, rkey) DO NOTHING 1685 `, src.userDidCol, src.nsid, src.table, src.fkCol)) 1686 if err != nil { 1687 return fmt.Errorf("backfill pds rewrites for %s: %w", src.table, err) 1688 } 1689 } 1690 1691 return nil 1692 }) 1693 1694 orm.RunMigration(conn, logger, "backfill-pds-rewrites-profiles", func(tx *sql.Tx) error { 1695 _, err := tx.Exec(` 1696 INSERT INTO pds_migration (name, did, collection, rkey, status) 1697 SELECT DISTINCT 'add-repo-did', pp.did, 'sh.tangled.actor.profile', 'self', 'pending' 1698 FROM profile_pinned_repositories pp 1699 JOIN repos r ON r.at_uri = pp.pin 1700 WHERE pp.pin LIKE 'at://%' 1701 AND r.repo_did IS NOT NULL AND r.repo_did != '' 1702 ON CONFLICT(name, did, collection, rkey) DO NOTHING 1703 `) 1704 if err != nil { 1705 return fmt.Errorf("backfill pds rewrites for profiles: %w", err) 1706 } 1707 return nil 1708 }) 1709 1710 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1711 orm.RunMigration(conn, logger, "drop-old-at-uri-columns", func(tx *sql.Tx) error { 1712 _, err := tx.Exec(` 1713 CREATE TABLE repos_new ( 1714 id INTEGER PRIMARY KEY AUTOINCREMENT, 1715 did TEXT NOT NULL, 1716 name TEXT NOT NULL, 1717 knot TEXT NOT NULL, 1718 rkey TEXT NOT NULL, 1719 at_uri TEXT NOT NULL UNIQUE, 1720 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1721 description TEXT CHECK (length(description) <= 200), 1722 source TEXT, 1723 spindle TEXT, 1724 website TEXT, 1725 topics TEXT, 1726 repo_did TEXT, 1727 UNIQUE(did, rkey) 1728 ); 1729 INSERT INTO repos_new (id, did, name, knot, rkey, at_uri, created, description, source, spindle, website, topics, repo_did) 1730 SELECT id, did, name, knot, rkey, at_uri, created, description, source, spindle, website, topics, repo_did 1731 FROM repos; 1732 DROP TABLE repos; 1733 ALTER TABLE repos_new RENAME TO repos; 1734 CREATE UNIQUE INDEX idx_repos_repo_did ON repos(repo_did); 1735 CREATE UNIQUE INDEX idx_repos_did_rkey ON repos(did, rkey); 1736 1737 CREATE TABLE issues_new ( 1738 id INTEGER PRIMARY KEY AUTOINCREMENT, 1739 did TEXT NOT NULL, 1740 rkey TEXT NOT NULL, 1741 at_uri TEXT GENERATED ALWAYS AS ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) STORED, 1742 repo_did TEXT NOT NULL, 1743 issue_id INTEGER NOT NULL, 1744 title TEXT NOT NULL, 1745 body TEXT NOT NULL, 1746 open INTEGER NOT NULL DEFAULT 1, 1747 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1748 edited TEXT, 1749 deleted TEXT, 1750 UNIQUE(did, rkey), 1751 UNIQUE(repo_did, issue_id), 1752 UNIQUE(at_uri), 1753 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1754 ); 1755 INSERT INTO issues_new (id, did, rkey, repo_did, issue_id, title, body, open, created, edited, deleted) 1756 SELECT id, did, rkey, repo_did, issue_id, title, body, open, created, edited, deleted 1757 FROM issues WHERE repo_did IS NOT NULL AND repo_did != ''; 1758 DROP TABLE issues; 1759 ALTER TABLE issues_new RENAME TO issues; 1760 CREATE INDEX idx_issues_repo_did ON issues(repo_did); 1761 1762 CREATE TABLE pulls_new ( 1763 id INTEGER PRIMARY KEY AUTOINCREMENT, 1764 pull_id INTEGER NOT NULL, 1765 at_uri TEXT GENERATED ALWAYS AS ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) STORED, 1766 repo_did TEXT NOT NULL, 1767 owner_did TEXT NOT NULL, 1768 rkey TEXT NOT NULL, 1769 title TEXT NOT NULL, 1770 body TEXT NOT NULL, 1771 target_branch TEXT NOT NULL, 1772 state INTEGER NOT NULL DEFAULT 0 CHECK (state IN (0, 1, 2, 3)), 1773 source_branch TEXT, 1774 source_repo_did TEXT, 1775 change_id TEXT, 1776 dependent_on TEXT, 1777 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1778 UNIQUE(repo_did, pull_id), 1779 UNIQUE(at_uri), 1780 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1781 ); 1782 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) 1783 SELECT id, pull_id, repo_did, owner_did, rkey, title, body, target_branch, state, source_branch, source_repo_did, change_id, dependent_on, created 1784 FROM pulls WHERE repo_did IS NOT NULL AND repo_did != ''; 1785 DROP TABLE pulls; 1786 ALTER TABLE pulls_new RENAME TO pulls; 1787 CREATE INDEX idx_pulls_repo_did ON pulls(repo_did); 1788 CREATE INDEX idx_pulls_source_repo_did ON pulls(source_repo_did); 1789 1790 CREATE TABLE pull_comments_new ( 1791 id INTEGER PRIMARY KEY AUTOINCREMENT, 1792 pull_id INTEGER NOT NULL, 1793 submission_id INTEGER NOT NULL, 1794 repo_did TEXT NOT NULL, 1795 owner_did TEXT NOT NULL, 1796 comment_at TEXT NOT NULL, 1797 body TEXT NOT NULL, 1798 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1799 FOREIGN KEY (repo_did, pull_id) REFERENCES pulls(repo_did, pull_id) ON DELETE CASCADE, 1800 FOREIGN KEY (submission_id) REFERENCES pull_submissions(id) ON DELETE CASCADE 1801 ); 1802 INSERT INTO pull_comments_new (id, pull_id, submission_id, repo_did, owner_did, comment_at, body, created) 1803 SELECT id, pull_id, submission_id, repo_did, owner_did, comment_at, body, created 1804 FROM pull_comments WHERE repo_did IS NOT NULL AND repo_did != ''; 1805 DROP TABLE pull_comments; 1806 ALTER TABLE pull_comments_new RENAME TO pull_comments; 1807 CREATE INDEX idx_pull_comments_repo_did ON pull_comments(repo_did); 1808 1809 CREATE TABLE stars_new ( 1810 id INTEGER PRIMARY KEY AUTOINCREMENT, 1811 did TEXT NOT NULL, 1812 rkey TEXT NOT NULL, 1813 subject_type TEXT NOT NULL CHECK (subject_type IN ('repo', 'string')), 1814 subject TEXT NOT NULL, 1815 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1816 UNIQUE(did, rkey), 1817 UNIQUE(did, subject) 1818 ); 1819 INSERT INTO stars_new (id, did, rkey, subject_type, subject, created) 1820 SELECT id, did, rkey, 'repo', subject_did, created 1821 FROM stars 1822 WHERE subject_did IS NOT NULL AND subject_did != ''; 1823 INSERT OR IGNORE INTO stars_new (id, did, rkey, subject_type, subject, created) 1824 SELECT id, did, rkey, 'string', subject_at, created 1825 FROM stars 1826 WHERE (subject_did IS NULL OR subject_did = '') 1827 AND subject_at LIKE 'at://%/sh.tangled.string/%'; 1828 DROP TABLE stars; 1829 ALTER TABLE stars_new RENAME TO stars; 1830 CREATE INDEX idx_stars_subject ON stars(subject); 1831 CREATE INDEX idx_stars_subject_type ON stars(subject_type); 1832 CREATE INDEX idx_stars_created ON stars(created); 1833 1834 CREATE TABLE collaborators_new ( 1835 id INTEGER PRIMARY KEY AUTOINCREMENT, 1836 did TEXT NOT NULL, 1837 rkey TEXT, 1838 subject_did TEXT NOT NULL, 1839 repo_did TEXT NOT NULL, 1840 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1841 UNIQUE(did, rkey), 1842 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1843 ); 1844 INSERT INTO collaborators_new (id, did, rkey, subject_did, repo_did, created) 1845 SELECT id, did, NULLIF(rkey, ''), subject_did, repo_did, created 1846 FROM collaborators WHERE repo_did IS NOT NULL AND repo_did != ''; 1847 DROP TABLE collaborators; 1848 ALTER TABLE collaborators_new RENAME TO collaborators; 1849 CREATE INDEX idx_collaborators_repo_did ON collaborators(repo_did); 1850 1851 CREATE TABLE artifacts_new ( 1852 id INTEGER PRIMARY KEY AUTOINCREMENT, 1853 did TEXT NOT NULL, 1854 rkey TEXT NOT NULL, 1855 repo_did TEXT NOT NULL, 1856 tag BINARY(20) NOT NULL, 1857 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1858 blob_cid TEXT NOT NULL, 1859 name TEXT NOT NULL, 1860 size INTEGER NOT NULL DEFAULT 0, 1861 mimetype TEXT NOT NULL DEFAULT '*/*', 1862 UNIQUE(did, rkey), 1863 UNIQUE(repo_did, tag, name), 1864 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1865 ); 1866 INSERT INTO artifacts_new (id, did, rkey, repo_did, tag, created, blob_cid, name, size, mimetype) 1867 SELECT id, did, rkey, repo_did, tag, created, blob_cid, name, size, mimetype 1868 FROM artifacts WHERE repo_did IS NOT NULL AND repo_did != ''; 1869 DROP TABLE artifacts; 1870 ALTER TABLE artifacts_new RENAME TO artifacts; 1871 CREATE INDEX idx_artifacts_repo_did ON artifacts(repo_did); 1872 1873 CREATE TABLE webhooks_new ( 1874 id INTEGER PRIMARY KEY AUTOINCREMENT, 1875 repo_did TEXT NOT NULL, 1876 url TEXT NOT NULL, 1877 secret TEXT, 1878 active INTEGER NOT NULL DEFAULT 1, 1879 events TEXT NOT NULL, 1880 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1881 updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1882 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1883 ); 1884 INSERT INTO webhooks_new (id, repo_did, url, secret, active, events, created_at, updated_at) 1885 SELECT id, repo_did, url, secret, active, events, created_at, updated_at 1886 FROM webhooks WHERE repo_did IS NOT NULL AND repo_did != ''; 1887 DROP TABLE webhooks; 1888 ALTER TABLE webhooks_new RENAME TO webhooks; 1889 CREATE INDEX idx_webhooks_repo_did ON webhooks(repo_did); 1890 1891 CREATE TABLE repo_sites_new ( 1892 id INTEGER PRIMARY KEY AUTOINCREMENT, 1893 repo_did TEXT NOT NULL UNIQUE, 1894 branch TEXT NOT NULL, 1895 dir TEXT NOT NULL DEFAULT '/', 1896 is_index INTEGER NOT NULL DEFAULT 0, 1897 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1898 updated TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1899 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1900 ); 1901 INSERT INTO repo_sites_new (id, repo_did, branch, dir, is_index, created, updated) 1902 SELECT id, repo_did, branch, dir, is_index, created, updated 1903 FROM repo_sites WHERE repo_did IS NOT NULL AND repo_did != ''; 1904 DROP TABLE repo_sites; 1905 ALTER TABLE repo_sites_new RENAME TO repo_sites; 1906 1907 CREATE TABLE site_deploys_new ( 1908 id INTEGER PRIMARY KEY AUTOINCREMENT, 1909 repo_did TEXT NOT NULL, 1910 branch TEXT NOT NULL, 1911 dir TEXT NOT NULL DEFAULT '/', 1912 commit_sha TEXT NOT NULL DEFAULT '', 1913 status TEXT NOT NULL CHECK (status IN ('success', 'failure')), 1914 trigger TEXT NOT NULL CHECK (trigger IN ('config_change', 'push')), 1915 error TEXT NOT NULL DEFAULT '', 1916 created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1917 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1918 ); 1919 INSERT INTO site_deploys_new (id, repo_did, branch, dir, commit_sha, status, trigger, error, created_at) 1920 SELECT id, repo_did, branch, dir, commit_sha, status, trigger, error, created_at 1921 FROM site_deploys WHERE repo_did IS NOT NULL AND repo_did != ''; 1922 DROP TABLE site_deploys; 1923 ALTER TABLE site_deploys_new RENAME TO site_deploys; 1924 CREATE INDEX idx_site_deploys_repo_did ON site_deploys(repo_did); 1925 1926 CREATE TABLE repo_issue_seqs_new ( 1927 repo_did TEXT PRIMARY KEY, 1928 next_issue_id INTEGER NOT NULL DEFAULT 1, 1929 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1930 ); 1931 INSERT INTO repo_issue_seqs_new (repo_did, next_issue_id) 1932 SELECT repo_did, next_issue_id 1933 FROM repo_issue_seqs WHERE repo_did IS NOT NULL AND repo_did != ''; 1934 DROP TABLE repo_issue_seqs; 1935 ALTER TABLE repo_issue_seqs_new RENAME TO repo_issue_seqs; 1936 1937 CREATE TABLE repo_pull_seqs_new ( 1938 repo_did TEXT PRIMARY KEY, 1939 next_pull_id INTEGER NOT NULL DEFAULT 1, 1940 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1941 ); 1942 INSERT INTO repo_pull_seqs_new (repo_did, next_pull_id) 1943 SELECT repo_did, next_pull_id 1944 FROM repo_pull_seqs WHERE repo_did IS NOT NULL AND repo_did != ''; 1945 DROP TABLE repo_pull_seqs; 1946 ALTER TABLE repo_pull_seqs_new RENAME TO repo_pull_seqs; 1947 1948 CREATE TABLE repo_languages_new ( 1949 id INTEGER PRIMARY KEY AUTOINCREMENT, 1950 repo_did TEXT NOT NULL, 1951 ref TEXT NOT NULL, 1952 is_default_ref INTEGER NOT NULL DEFAULT 0, 1953 language TEXT NOT NULL, 1954 bytes INTEGER NOT NULL CHECK (bytes >= 0), 1955 UNIQUE(repo_did, ref, language), 1956 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1957 ); 1958 INSERT INTO repo_languages_new (id, repo_did, ref, is_default_ref, language, bytes) 1959 SELECT id, repo_did, ref, is_default_ref, language, bytes 1960 FROM repo_languages WHERE repo_did IS NOT NULL AND repo_did != ''; 1961 DROP TABLE repo_languages; 1962 ALTER TABLE repo_languages_new RENAME TO repo_languages; 1963 1964 CREATE TABLE repo_labels_new ( 1965 id INTEGER PRIMARY KEY AUTOINCREMENT, 1966 repo_did TEXT NOT NULL, 1967 label_at TEXT NOT NULL, 1968 UNIQUE(repo_did, label_at), 1969 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 1970 ); 1971 INSERT INTO repo_labels_new (id, repo_did, label_at) 1972 SELECT id, repo_did, label_at 1973 FROM repo_labels WHERE repo_did IS NOT NULL AND repo_did != ''; 1974 DROP TABLE repo_labels; 1975 ALTER TABLE repo_labels_new RENAME TO repo_labels; 1976 `) 1977 return err 1978 }) 1979 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1980 1981 orm.RunMigration(conn, logger, "migrate-knots-to-repo-did-rename", func(tx *sql.Tx) error { 1982 _, err := tx.Exec(` 1983 update registrations set needs_upgrade = 1; 1984 `) 1985 return err 1986 }) 1987 1988 orm.RunMigration(conn, logger, "drop-ghost-comments-table", func(tx *sql.Tx) error { 1989 _, err := tx.Exec(`DROP TABLE IF EXISTS comments`) 1990 return err 1991 }) 1992 1993 orm.RunMigration(conn, logger, "add-knot-members-table", func(tx *sql.Tx) error { 1994 _, err := tx.Exec(` 1995 create table if not exists knot_members ( 1996 id integer primary key autoincrement, 1997 did text not null, 1998 rkey text not null, 1999 domain text not null, 2000 subject text not null, 2001 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 2002 unique (did, domain, subject) 2003 ); 2004 create index if not exists idx_knot_members_did_rkey on knot_members(did, rkey); 2005 `) 2006 return err 2007 }) 2008 2009 orm.RunMigration(conn, logger, "add-comments-table", func(tx *sql.Tx) error { 2010 _, err := tx.Exec(` 2011 drop table if exists comments; 2012 2013 create table comments ( 2014 -- identifiers 2015 id integer primary key autoincrement, 2016 2017 did text not null, 2018 collection text not null default 'sh.tangled.feed.comment', 2019 rkey text not null, 2020 at_uri text generated always as ('at://' || did || '/' || collection || '/' || rkey) stored, 2021 cid text, 2022 2023 -- content 2024 subject_uri text not null, -- at_uri of subject (issue, pr, string) 2025 subject_cid text not null, -- cid of subject 2026 2027 body_text text not null, 2028 body_original text, 2029 body_blobs text, -- json 2030 2031 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 2032 2033 reply_to_uri text, -- at_uri of parent comment 2034 reply_to_cid text, -- cid of parent comment 2035 2036 pull_round_idx integer, -- pull round index. required when subject is sh.tangled.repo.pull 2037 2038 -- appview-local information 2039 edited text, 2040 deleted text, 2041 2042 unique(did, collection, rkey) 2043 ); 2044 2045 insert into comments ( 2046 did, 2047 collection, 2048 rkey, 2049 subject_uri, 2050 subject_cid, -- we need to know cid 2051 body_text, 2052 created, 2053 reply_to_uri, 2054 reply_to_cid, -- we need to know cid 2055 edited, 2056 deleted 2057 ) 2058 select 2059 did, 2060 'sh.tangled.repo.issue.comment', 2061 rkey, 2062 issue_at, 2063 '', 2064 body, 2065 created, 2066 reply_to, 2067 '', 2068 edited, 2069 deleted 2070 from issue_comments 2071 where rkey is not null; 2072 2073 insert into comments ( 2074 did, 2075 collection, 2076 rkey, 2077 subject_uri, 2078 subject_cid, -- we need to know cid 2079 body_text, 2080 created, 2081 pull_round_idx 2082 ) 2083 select 2084 c.owner_did, 2085 'sh.tangled.repo.pull.comment', 2086 substr( 2087 substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey 2088 instr( 2089 substr(c.comment_at, 6 + instr(substr(c.comment_at, 6), '/')), -- nsid/rkey 2090 '/' 2091 ) + 1 2092 ), -- rkey 2093 p.at_uri, 2094 '', 2095 c.body, 2096 c.created, 2097 s.round_number 2098 from pull_comments c 2099 join pulls p on c.repo_did = p.repo_did and c.pull_id = p.pull_id 2100 join pull_submissions s on s.id = c.submission_id; 2101 `) 2102 return err 2103 }) 2104 2105 orm.RunMigration(conn, logger, "migrate-legacy-comments", func(tx *sql.Tx) error { 2106 _, err := tx.Exec(` 2107 insert into pds_migration (name, did, collection, rkey) 2108 select 2109 'use-feed-comment', 2110 did, 2111 collection, 2112 rkey 2113 from comments 2114 where collection <> 'sh.tangled.feed.comment'; 2115 `) 2116 return err 2117 }) 2118 2119 conn.ExecContext(ctx, "pragma foreign_keys = off;") 2120 orm.RunMigration(conn, logger, "cascade-notification-entity-fks", func(tx *sql.Tx) error { 2121 _, err := tx.Exec(` 2122 CREATE TABLE notifications_new ( 2123 id INTEGER PRIMARY KEY AUTOINCREMENT, 2124 recipient_did TEXT NOT NULL, 2125 actor_did TEXT NOT NULL, 2126 type TEXT NOT NULL, 2127 entity_type TEXT NOT NULL, 2128 entity_id TEXT NOT NULL, 2129 read INTEGER NOT NULL DEFAULT 0, 2130 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 2131 repo_id INTEGER REFERENCES repos(id) ON DELETE CASCADE, 2132 issue_id INTEGER REFERENCES issues(id) ON DELETE CASCADE, 2133 pull_id INTEGER REFERENCES pulls(id) ON DELETE CASCADE 2134 ); 2135 INSERT INTO notifications_new (id, recipient_did, actor_did, type, entity_type, entity_id, read, created, repo_id, issue_id, pull_id) 2136 SELECT id, recipient_did, actor_did, type, entity_type, entity_id, read, created, repo_id, issue_id, pull_id 2137 FROM notifications; 2138 DROP TABLE notifications; 2139 ALTER TABLE notifications_new RENAME TO notifications; 2140 CREATE INDEX idx_notifications_recipient_created ON notifications(recipient_did, created DESC); 2141 CREATE INDEX idx_notifications_recipient_read ON notifications(recipient_did, read); 2142 `) 2143 return err 2144 }) 2145 conn.ExecContext(ctx, "pragma foreign_keys = on;") 2146 2147 orm.RunMigration(conn, logger, "collaborators-unique-on-repo-subject", func(tx *sql.Tx) error { 2148 _, err := tx.Exec(` 2149 CREATE TABLE collaborators_new ( 2150 id INTEGER PRIMARY KEY AUTOINCREMENT, 2151 did TEXT NOT NULL, 2152 rkey TEXT, 2153 subject_did TEXT NOT NULL, 2154 repo_did TEXT NOT NULL, 2155 created TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 2156 UNIQUE(repo_did, subject_did), 2157 FOREIGN KEY (repo_did) REFERENCES repos(repo_did) ON DELETE CASCADE 2158 ); 2159 INSERT INTO collaborators_new (id, did, rkey, subject_did, repo_did, created) 2160 SELECT id, did, rkey, subject_did, repo_did, created 2161 FROM ( 2162 SELECT 2163 id, did, rkey, subject_did, repo_did, created, 2164 ROW_NUMBER() OVER ( 2165 PARTITION BY repo_did, subject_did 2166 ORDER BY created DESC, id DESC 2167 ) AS rn 2168 FROM collaborators 2169 ) 2170 WHERE rn = 1; 2171 DROP TABLE collaborators; 2172 ALTER TABLE collaborators_new RENAME TO collaborators; 2173 CREATE INDEX idx_collaborators_repo_did ON collaborators(repo_did); 2174 CREATE INDEX idx_collaborators_subject_did ON collaborators(subject_did); 2175 `) 2176 return err 2177 }) 2178 2179 orm.RunMigration(conn, logger, "add-knot-acl-native", func(tx *sql.Tx) error { 2180 _, err := tx.Exec(` 2181 create table if not exists knot_acl_native ( 2182 domain text primary key, 2183 since text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 2184 ); 2185 `) 2186 return err 2187 }) 2188 2189 orm.RunMigration(conn, logger, "delete-unused-pipeline-statuses", func(tx *sql.Tx) error { 2190 _, err := tx.Exec(` 2191 delete from pipeline_statuses as p 2192 where p.status = 'pending' 2193 and exists ( 2194 select 1 from pipeline_statuses as q 2195 where q.pipeline_knot = p.pipeline_knot 2196 and q.pipeline_rkey = p.pipeline_rkey 2197 and q.workflow = p.workflow 2198 and q.status = 'pending' 2199 and q.created < p.created 2200 ); 2201 `) 2202 return err 2203 }) 2204 2205 orm.RunMigration(conn, logger, "timeline-query-indexes", func(tx *sql.Tx) error { 2206 _, err := tx.Exec(` 2207 -- following timeline: stars by a set of users, newest first 2208 create index if not exists idx_stars_did_type_created on stars(did, subject_type, created); 2209 -- follower counts and reverse lookups (no index on subject_did before) 2210 create index if not exists idx_follows_subject_did on follows(subject_did); 2211 -- global timeline: newest follows without a full sort 2212 create index if not exists idx_follows_followed_at on follows(followed_at); 2213 -- global timeline: newest repos without a full sort 2214 create index if not exists idx_repos_created on repos(created); 2215 `) 2216 return err 2217 }) 2218 2219 orm.RunMigration(conn, logger, "add-focusing-table", func(tx *sql.Tx) error { 2220 _, err := tx.Exec(` 2221 create table if not exists focusing ( 2222 did text primary key, 2223 started text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 2224 ); 2225 `) 2226 return err 2227 }) 2228 2229 orm.RunMigration(conn, logger, "add-knotacl-sync-table", func(tx *sql.Tx) error { 2230 _, err := tx.Exec(` 2231 create table if not exists knotacl_sync ( 2232 scope_key text primary key, 2233 synced_at text not null 2234 ); 2235 `) 2236 return err 2237 }) 2238 2239 orm.RunMigration(conn, logger, "add-knotacl-delta-cursor-table", func(tx *sql.Tx) error { 2240 _, err := tx.Exec(` 2241 create table if not exists knotacl_delta_cursor ( 2242 scope_key text not null, 2243 subject text not null, 2244 cursor integer not null, 2245 primary key (scope_key, subject) 2246 ); 2247 `) 2248 return err 2249 }) 2250 2251 orm.RunMigration(conn, logger, "migrate-knots-to-knot-owned-acl", func(tx *sql.Tx) error { 2252 _, err := tx.Exec(` 2253 update registrations set needs_upgrade = 1; 2254 `) 2255 return err 2256 }) 2257 2258 // several changes here 2259 // 1. remove autoincrement id for these tables 2260 // 2. remove unique constraints other than (did, rkey) to handle non-unique atproto records 2261 // 3. add generated at_uri field 2262 // 2263 // see comments below and commit message for details 2264 orm.RunMigration(conn, logger, "flexible-stars-reactions-follows-public_keys", func(tx *sql.Tx) error { 2265 // - add at_uri 2266 // - remove autoincrement id and the (did, subject) unique constraint 2267 if _, err := tx.Exec(` 2268 create table stars_new ( 2269 did text not null, 2270 rkey text not null, 2271 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.feed.star' || '/' || rkey) stored, 2272 2273 subject_type text not null, 2274 subject text not null, 2275 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 2276 2277 unique(did, rkey) 2278 ); 2279 2280 insert into stars_new (did, rkey, subject_type, subject, created) 2281 select did, rkey, subject_type, subject, created from stars; 2282 2283 drop table stars; 2284 alter table stars_new rename to stars; 2285 2286 create index if not exists idx_stars_subject on stars(subject); 2287 create index if not exists idx_stars_subject_type on stars(subject_type); 2288 create index if not exists idx_stars_created on stars(created); 2289 create index if not exists idx_stars_did_type_created on stars(did, subject_type, created); 2290 `); err != nil { 2291 return fmt.Errorf("migrating stars: %w", err) 2292 } 2293 2294 // - add at_uri 2295 // - reacted_by_did -> did 2296 // - thread_at -> subject_at 2297 // - remove unique constraint 2298 if _, err := tx.Exec(` 2299 create table reactions_new ( 2300 did text not null, 2301 rkey text not null, 2302 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.feed.reaction' || '/' || rkey) stored, 2303 2304 subject_at text not null, 2305 kind text not null, 2306 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 2307 2308 unique(did, rkey) 2309 ); 2310 2311 insert into reactions_new (did, rkey, subject_at, kind, created) 2312 select reacted_by_did, rkey, thread_at, kind, created from reactions; 2313 2314 drop table reactions; 2315 alter table reactions_new rename to reactions; 2316 `); err != nil { 2317 return fmt.Errorf("migrating reactions: %w", err) 2318 } 2319 2320 // - add at_uri column 2321 // - user_did -> did 2322 // - followed_at -> created 2323 // - remove unique constraint 2324 // - remove check constraint 2325 if _, err := tx.Exec(` 2326 create table follows_new ( 2327 did text not null, 2328 rkey text not null, 2329 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.graph.follow' || '/' || rkey) stored, 2330 2331 subject_did text not null, 2332 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 2333 2334 unique(did, rkey) 2335 ); 2336 2337 insert into follows_new (did, rkey, subject_did, created) 2338 select user_did, rkey, subject_did, followed_at from follows; 2339 2340 drop table follows; 2341 alter table follows_new rename to follows; 2342 2343 create index if not exists idx_follows_subject_did on follows(subject_did); 2344 create index if not exists idx_follows_created on follows(created); 2345 `); err != nil { 2346 return fmt.Errorf("migrating follows: %w", err) 2347 } 2348 2349 // - add at_uri column 2350 // - remove foreign key relationship from repos 2351 if _, err := tx.Exec(` 2352 create table public_keys_new ( 2353 did text not null, 2354 rkey text not null, 2355 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.publicKey' || '/' || rkey) stored, 2356 2357 name text not null, 2358 key text not null, 2359 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 2360 2361 unique(did, rkey) 2362 ); 2363 2364 insert or ignore into public_keys_new (did, rkey, name, key, created) 2365 select did, rkey, name, key, created from public_keys; 2366 2367 drop table public_keys; 2368 alter table public_keys_new rename to public_keys; 2369 `); err != nil { 2370 return fmt.Errorf("migrating public_keys: %w", err) 2371 } 2372 2373 return nil 2374 }) 2375 2376 orm.RunMigration(conn, logger, "add-author-to-bluesky-posts", func(tx *sql.Tx) error { 2377 _, err := tx.Exec(` 2378 alter table bluesky_posts add column author_did text not null default ''; 2379 `) 2380 return err 2381 }) 2382 2383 orm.RunMigration(conn, logger, "add-issue-pull-state-tables", func(tx *sql.Tx) error { 2384 _, err := tx.Exec(` 2385 create table if not exists issue_states ( 2386 id integer primary key autoincrement, 2387 did text not null, 2388 rkey text not null, 2389 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue.state' || '/' || rkey) stored, 2390 2391 subject text not null, 2392 state text not null check (state in ('open', 'closed')), 2393 created_micros integer not null, 2394 2395 unique(did, rkey), 2396 foreign key (subject) references issues(at_uri) on delete cascade 2397 ); 2398 create index if not exists idx_issue_states_subject on issue_states(subject); 2399 2400 create table if not exists pull_states ( 2401 id integer primary key autoincrement, 2402 did text not null, 2403 rkey text not null, 2404 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.pull.status' || '/' || rkey) stored, 2405 2406 subject text not null, 2407 status text not null check (status in ('open', 'closed', 'merged')), 2408 created_micros integer not null, 2409 2410 unique(did, rkey), 2411 foreign key (subject) references pulls(at_uri) on delete cascade 2412 ); 2413 create index if not exists idx_pull_states_subject on pull_states(subject); 2414 2415 create table if not exists pending_state_records ( 2416 id integer primary key autoincrement, 2417 did text not null, 2418 rkey text not null, 2419 nsid text not null, 2420 2421 subject text not null, 2422 record blob not null, 2423 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 2424 2425 unique(did, rkey, nsid) 2426 ); 2427 create index if not exists idx_pending_state_subject on pending_state_records(subject); 2428 `) 2429 return err 2430 }) 2431 2432 orm.RunMigration(conn, logger, "drop-label-ops-indexed", func(tx *sql.Tx) error { 2433 _, err := tx.Exec(`alter table label_ops drop column indexed`) 2434 return err 2435 }) 2436 2437 orm.RunMigration(conn, logger, "spindle-pipeline-ownership-migration", func(tx *sql.Tx) error { 2438 _, err := tx.Exec(` 2439 update spindles set needs_upgrade = 1; 2440 `) 2441 return err 2442 }) 2443 2444 orm.RunMigration(conn, logger, "add-pull-submissions-merge-base", func(tx *sql.Tx) error { 2445 _, err := tx.Exec(` 2446 alter table pull_submissions add column merge_base text; 2447 `) 2448 return err 2449 }) 2450 2451 return &DB{ 2452 db, 2453 logger, 2454 }, nil 2455} 2456 2457func (d *DB) Close() error { 2458 return d.DB.Close() 2459}