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