forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 32 kB view raw
1package db 2 3import ( 4 "context" 5 "database/sql" 6 "log/slog" 7 "strings" 8 9 _ "github.com/mattn/go-sqlite3" 10 "tangled.org/core/log" 11 "tangled.org/core/orm" 12) 13 14type DB struct { 15 *sql.DB 16 logger *slog.Logger 17} 18 19type Execer interface { 20 Query(query string, args ...any) (*sql.Rows, error) 21 QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) 22 QueryRow(query string, args ...any) *sql.Row 23 QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row 24 Exec(query string, args ...any) (sql.Result, error) 25 ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) 26 Prepare(query string) (*sql.Stmt, error) 27 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 28} 29 30func Make(ctx context.Context, dbPath string) (*DB, error) { 31 // https://github.com/mattn/go-sqlite3#connection-string 32 opts := []string{ 33 "_foreign_keys=1", 34 "_journal_mode=WAL", 35 "_synchronous=NORMAL", 36 "_auto_vacuum=incremental", 37 } 38 39 logger := log.FromContext(ctx) 40 logger = log.SubLogger(logger, "db") 41 42 db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 43 if err != nil { 44 return nil, err 45 } 46 47 conn, err := db.Conn(ctx) 48 if err != nil { 49 return nil, err 50 } 51 defer conn.Close() 52 53 _, err = conn.ExecContext(ctx, ` 54 create table if not exists registrations ( 55 id integer primary key autoincrement, 56 domain text not null unique, 57 did text not null, 58 secret text not null, 59 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 60 registered text 61 ); 62 create table if not exists public_keys ( 63 id integer primary key autoincrement, 64 did text not null, 65 name text not null, 66 key text not null, 67 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 68 unique(did, name, key) 69 ); 70 create table if not exists repos ( 71 id integer primary key autoincrement, 72 did text not null, 73 name text not null, 74 knot text not null, 75 rkey text not null, 76 at_uri text not null unique, 77 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 78 unique(did, name, knot, rkey) 79 ); 80 create table if not exists collaborators ( 81 id integer primary key autoincrement, 82 did text not null, 83 repo integer not null, 84 foreign key (repo) references repos(id) on delete cascade 85 ); 86 create table if not exists follows ( 87 user_did text not null, 88 subject_did text not null, 89 rkey text not null, 90 followed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 91 primary key (user_did, subject_did), 92 check (user_did <> subject_did) 93 ); 94 create table if not exists issues ( 95 id integer primary key autoincrement, 96 owner_did text not null, 97 repo_at text not null, 98 issue_id integer not null, 99 title text not null, 100 body text not null, 101 open integer not null default 1, 102 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 103 issue_at text, 104 unique(repo_at, issue_id), 105 foreign key (repo_at) references repos(at_uri) on delete cascade 106 ); 107 create table if not exists comments ( 108 id integer primary key autoincrement, 109 owner_did text not null, 110 issue_id integer not null, 111 repo_at text not null, 112 comment_id integer not null, 113 comment_at text not null, 114 body text not null, 115 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 116 unique(issue_id, comment_id), 117 foreign key (repo_at, issue_id) references issues(repo_at, issue_id) on delete cascade 118 ); 119 create table if not exists pulls ( 120 -- identifiers 121 id integer primary key autoincrement, 122 pull_id integer not null, 123 124 -- at identifiers 125 repo_at text not null, 126 owner_did text not null, 127 rkey text not null, 128 pull_at text, 129 130 -- content 131 title text not null, 132 body text not null, 133 target_branch text not null, 134 state integer not null default 0 check (state in (0, 1, 2)), -- open, merged, closed 135 136 -- meta 137 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 138 139 -- constraints 140 unique(repo_at, pull_id), 141 foreign key (repo_at) references repos(at_uri) on delete cascade 142 ); 143 144 -- every pull must have atleast 1 submission: the initial submission 145 create table if not exists pull_submissions ( 146 -- identifiers 147 id integer primary key autoincrement, 148 pull_id integer not null, 149 150 -- at identifiers 151 repo_at text not null, 152 153 -- content, these are immutable, and require a resubmission to update 154 round_number integer not null default 0, 155 patch text, 156 157 -- meta 158 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 159 160 -- constraints 161 unique(repo_at, pull_id, round_number), 162 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade 163 ); 164 165 create table if not exists pull_comments ( 166 -- identifiers 167 id integer primary key autoincrement, 168 pull_id integer not null, 169 submission_id integer not null, 170 171 -- at identifiers 172 repo_at text not null, 173 owner_did text not null, 174 comment_at text not null, 175 176 -- content 177 body text not null, 178 179 -- meta 180 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 181 182 -- constraints 183 foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade, 184 foreign key (submission_id) references pull_submissions(id) on delete cascade 185 ); 186 187 create table if not exists _jetstream ( 188 id integer primary key autoincrement, 189 last_time_us integer not null 190 ); 191 192 create table if not exists repo_issue_seqs ( 193 repo_at text primary key, 194 next_issue_id integer not null default 1 195 ); 196 197 create table if not exists repo_pull_seqs ( 198 repo_at text primary key, 199 next_pull_id integer not null default 1 200 ); 201 202 create table if not exists stars ( 203 id integer primary key autoincrement, 204 starred_by_did text not null, 205 repo_at text not null, 206 rkey text not null, 207 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 208 foreign key (repo_at) references repos(at_uri) on delete cascade, 209 unique(starred_by_did, repo_at) 210 ); 211 212 create table if not exists reactions ( 213 id integer primary key autoincrement, 214 reacted_by_did text not null, 215 thread_at text not null, 216 kind text not null, 217 rkey text not null, 218 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 219 unique(reacted_by_did, thread_at, kind) 220 ); 221 222 create table if not exists emails ( 223 id integer primary key autoincrement, 224 did text not null, 225 email text not null, 226 verified integer not null default 0, 227 verification_code text not null, 228 last_sent text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 229 is_primary integer not null default 0, 230 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 231 unique(did, email) 232 ); 233 234 create table if not exists artifacts ( 235 -- id 236 id integer primary key autoincrement, 237 did text not null, 238 rkey text not null, 239 240 -- meta 241 repo_at text not null, 242 tag binary(20) not null, 243 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 244 245 -- data 246 blob_cid text not null, 247 name text not null, 248 size integer not null default 0, 249 mimetype string not null default "*/*", 250 251 -- constraints 252 unique(did, rkey), -- record must be unique 253 unique(repo_at, tag, name), -- for a given tag object, each file must be unique 254 foreign key (repo_at) references repos(at_uri) on delete cascade 255 ); 256 257 create table if not exists profile ( 258 -- id 259 id integer primary key autoincrement, 260 did text not null, 261 262 -- data 263 description text not null, 264 include_bluesky integer not null default 0, 265 location text, 266 267 -- constraints 268 unique(did) 269 ); 270 create table if not exists profile_links ( 271 -- id 272 id integer primary key autoincrement, 273 did text not null, 274 275 -- data 276 link text not null, 277 278 -- constraints 279 foreign key (did) references profile(did) on delete cascade 280 ); 281 create table if not exists profile_stats ( 282 -- id 283 id integer primary key autoincrement, 284 did text not null, 285 286 -- data 287 kind text not null check (kind in ( 288 "merged-pull-request-count", 289 "closed-pull-request-count", 290 "open-pull-request-count", 291 "open-issue-count", 292 "closed-issue-count", 293 "repository-count" 294 )), 295 296 -- constraints 297 foreign key (did) references profile(did) on delete cascade 298 ); 299 create table if not exists profile_pinned_repositories ( 300 -- id 301 id integer primary key autoincrement, 302 did text not null, 303 304 -- data 305 at_uri text not null, 306 307 -- constraints 308 unique(did, at_uri), 309 foreign key (did) references profile(did) on delete cascade, 310 foreign key (at_uri) references repos(at_uri) on delete cascade 311 ); 312 313 create table if not exists oauth_requests ( 314 id integer primary key autoincrement, 315 auth_server_iss text not null, 316 state text not null, 317 did text not null, 318 handle text not null, 319 pds_url text not null, 320 pkce_verifier text not null, 321 dpop_auth_server_nonce text not null, 322 dpop_private_jwk text not null 323 ); 324 325 create table if not exists oauth_sessions ( 326 id integer primary key autoincrement, 327 did text not null, 328 handle text not null, 329 pds_url text not null, 330 auth_server_iss text not null, 331 access_jwt text not null, 332 refresh_jwt text not null, 333 dpop_pds_nonce text, 334 dpop_auth_server_nonce text not null, 335 dpop_private_jwk text not null, 336 expiry text not null 337 ); 338 339 create table if not exists punchcard ( 340 did text not null, 341 date text not null, -- yyyy-mm-dd 342 count integer, 343 primary key (did, date) 344 ); 345 346 create table if not exists spindles ( 347 id integer primary key autoincrement, 348 owner text not null, 349 instance text not null, 350 verified text, -- time of verification 351 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 352 353 unique(owner, instance) 354 ); 355 356 create table if not exists spindle_members ( 357 -- identifiers for the record 358 id integer primary key autoincrement, 359 did text not null, 360 rkey text not null, 361 362 -- data 363 instance text not null, 364 subject text not null, 365 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 366 367 -- constraints 368 unique (did, instance, subject) 369 ); 370 371 create table if not exists pipelines ( 372 -- identifiers 373 id integer primary key autoincrement, 374 knot text not null, 375 rkey text not null, 376 377 repo_owner text not null, 378 repo_name text not null, 379 380 -- every pipeline must be associated with exactly one commit 381 sha text not null check (length(sha) = 40), 382 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 383 384 -- trigger data 385 trigger_id integer not null, 386 387 unique(knot, rkey), 388 foreign key (trigger_id) references triggers(id) on delete cascade 389 ); 390 391 create table if not exists triggers ( 392 -- primary key 393 id integer primary key autoincrement, 394 395 -- top-level fields 396 kind text not null, 397 398 -- pushTriggerData fields 399 push_ref text, 400 push_new_sha text check (length(push_new_sha) = 40), 401 push_old_sha text check (length(push_old_sha) = 40), 402 403 -- pullRequestTriggerData fields 404 pr_source_branch text, 405 pr_target_branch text, 406 pr_source_sha text check (length(pr_source_sha) = 40), 407 pr_action text 408 ); 409 410 create table if not exists pipeline_statuses ( 411 -- identifiers 412 id integer primary key autoincrement, 413 spindle text not null, 414 rkey text not null, 415 416 -- referenced pipeline. these form the (did, rkey) pair 417 pipeline_knot text not null, 418 pipeline_rkey text not null, 419 420 -- content 421 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 422 workflow text not null, 423 status text not null, 424 error text, 425 exit_code integer not null default 0, 426 427 unique (spindle, rkey), 428 foreign key (pipeline_knot, pipeline_rkey) 429 references pipelines (knot, rkey) 430 on delete cascade 431 ); 432 433 create table if not exists repo_languages ( 434 -- identifiers 435 id integer primary key autoincrement, 436 437 -- repo identifiers 438 repo_at text not null, 439 ref text not null, 440 is_default_ref integer not null default 0, 441 442 -- language breakdown 443 language text not null, 444 bytes integer not null check (bytes >= 0), 445 446 unique(repo_at, ref, language) 447 ); 448 449 create table if not exists signups_inflight ( 450 id integer primary key autoincrement, 451 email text not null unique, 452 invite_code text not null, 453 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 454 ); 455 456 create table if not exists strings ( 457 -- identifiers 458 did text not null, 459 rkey text not null, 460 461 -- content 462 filename text not null, 463 description text, 464 content text not null, 465 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 466 edited text, 467 468 primary key (did, rkey) 469 ); 470 471 create table if not exists label_definitions ( 472 -- identifiers 473 id integer primary key autoincrement, 474 did text not null, 475 rkey text not null, 476 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.label.definition' || '/' || rkey) stored, 477 478 -- content 479 name text not null, 480 value_type text not null check (value_type in ( 481 "null", 482 "boolean", 483 "integer", 484 "string" 485 )), 486 value_format text not null default "any", 487 value_enum text, -- comma separated list 488 scope text not null, -- comma separated list of nsid 489 color text, 490 multiple integer not null default 0, 491 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 492 493 -- constraints 494 unique (did, rkey) 495 unique (at_uri) 496 ); 497 498 -- ops are flattened, a record may contain several additions and deletions, but the table will include one row per add/del 499 create table if not exists label_ops ( 500 -- identifiers 501 id integer primary key autoincrement, 502 did text not null, 503 rkey text not null, 504 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.label.op' || '/' || rkey) stored, 505 506 -- content 507 subject text not null, 508 operation text not null check (operation in ("add", "del")), 509 operand_key text not null, 510 operand_value text not null, 511 -- we need two time values: performed is declared by the user, indexed is calculated by the av 512 performed text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 513 indexed text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 514 515 -- constraints 516 -- traditionally (did, rkey) pair should be unique, but not in this case 517 -- operand_key should reference a label definition 518 foreign key (operand_key) references label_definitions (at_uri) on delete cascade, 519 unique (did, rkey, subject, operand_key, operand_value) 520 ); 521 522 create table if not exists repo_labels ( 523 -- identifiers 524 id integer primary key autoincrement, 525 526 -- repo identifiers 527 repo_at text not null, 528 529 -- label to subscribe to 530 label_at text not null, 531 532 unique (repo_at, label_at) 533 ); 534 535 create table if not exists notifications ( 536 id integer primary key autoincrement, 537 recipient_did text not null, 538 actor_did text not null, 539 type text not null, 540 entity_type text not null, 541 entity_id text not null, 542 read integer not null default 0, 543 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 544 repo_id integer references repos(id), 545 issue_id integer references issues(id), 546 pull_id integer references pulls(id) 547 ); 548 549 create table if not exists notification_preferences ( 550 id integer primary key autoincrement, 551 user_did text not null unique, 552 repo_starred integer not null default 1, 553 issue_created integer not null default 1, 554 issue_commented integer not null default 1, 555 pull_created integer not null default 1, 556 pull_commented integer not null default 1, 557 followed integer not null default 1, 558 pull_merged integer not null default 1, 559 issue_closed integer not null default 1, 560 email_notifications integer not null default 0 561 ); 562 563 create table if not exists reference_links ( 564 id integer primary key autoincrement, 565 from_at text not null, 566 to_at text not null, 567 unique (from_at, to_at) 568 ); 569 570 create table if not exists migrations ( 571 id integer primary key autoincrement, 572 name text unique 573 ); 574 575 -- indexes for better performance 576 create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc); 577 create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read); 578 create index if not exists idx_references_from_at on reference_links(from_at); 579 create index if not exists idx_references_to_at on reference_links(to_at); 580 `) 581 if err != nil { 582 return nil, err 583 } 584 585 // run migrations 586 orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error { 587 tx.Exec(` 588 alter table repos add column description text check (length(description) <= 200); 589 `) 590 return nil 591 }) 592 593 orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 594 // add unconstrained column 595 _, err := tx.Exec(` 596 alter table public_keys 597 add column rkey text; 598 `) 599 if err != nil { 600 return err 601 } 602 603 // backfill 604 _, err = tx.Exec(` 605 update public_keys 606 set rkey = '' 607 where rkey is null; 608 `) 609 if err != nil { 610 return err 611 } 612 613 return nil 614 }) 615 616 orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error { 617 _, err := tx.Exec(` 618 alter table comments drop column comment_at; 619 alter table comments add column rkey text; 620 `) 621 return err 622 }) 623 624 orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 625 _, err := tx.Exec(` 626 alter table comments add column deleted text; -- timestamp 627 alter table comments add column edited text; -- timestamp 628 `) 629 return err 630 }) 631 632 orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 633 _, err := tx.Exec(` 634 alter table pulls add column source_branch text; 635 alter table pulls add column source_repo_at text; 636 alter table pull_submissions add column source_rev text; 637 `) 638 return err 639 }) 640 641 orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error { 642 _, err := tx.Exec(` 643 alter table repos add column source text; 644 `) 645 return err 646 }) 647 648 // disable foreign-keys for the next migration 649 // NOTE: this cannot be done in a transaction, so it is run outside [0] 650 // 651 // [0]: https://sqlite.org/pragma.html#pragma_foreign_keys 652 conn.ExecContext(ctx, "pragma foreign_keys = off;") 653 orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error { 654 _, err := tx.Exec(` 655 create table pulls_new ( 656 -- identifiers 657 id integer primary key autoincrement, 658 pull_id integer not null, 659 660 -- at identifiers 661 repo_at text not null, 662 owner_did text not null, 663 rkey text not null, 664 665 -- content 666 title text not null, 667 body text not null, 668 target_branch text not null, 669 state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted 670 671 -- source info 672 source_branch text, 673 source_repo_at text, 674 675 -- stacking 676 stack_id text, 677 change_id text, 678 parent_change_id text, 679 680 -- meta 681 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 682 683 -- constraints 684 unique(repo_at, pull_id), 685 foreign key (repo_at) references repos(at_uri) on delete cascade 686 ); 687 688 insert into pulls_new ( 689 id, pull_id, 690 repo_at, owner_did, rkey, 691 title, body, target_branch, state, 692 source_branch, source_repo_at, 693 created 694 ) 695 select 696 id, pull_id, 697 repo_at, owner_did, rkey, 698 title, body, target_branch, state, 699 source_branch, source_repo_at, 700 created 701 FROM pulls; 702 703 drop table pulls; 704 alter table pulls_new rename to pulls; 705 `) 706 return err 707 }) 708 conn.ExecContext(ctx, "pragma foreign_keys = on;") 709 710 orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error { 711 tx.Exec(` 712 alter table repos add column spindle text; 713 `) 714 return nil 715 }) 716 717 // drop all knot secrets, add unique constraint to knots 718 // 719 // knots will henceforth use service auth for signed requests 720 orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error { 721 _, err := tx.Exec(` 722 create table registrations_new ( 723 id integer primary key autoincrement, 724 domain text not null, 725 did text not null, 726 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 727 registered text, 728 read_only integer not null default 0, 729 unique(domain, did) 730 ); 731 732 insert into registrations_new (id, domain, did, created, registered, read_only) 733 select id, domain, did, created, registered, 1 from registrations 734 where registered is not null; 735 736 drop table registrations; 737 alter table registrations_new rename to registrations; 738 `) 739 return err 740 }) 741 742 // recreate and add rkey + created columns with default constraint 743 orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error { 744 // create new table 745 // - repo_at instead of repo integer 746 // - rkey field 747 // - created field 748 _, err := tx.Exec(` 749 create table collaborators_new ( 750 -- identifiers for the record 751 id integer primary key autoincrement, 752 did text not null, 753 rkey text, 754 755 -- content 756 subject_did text not null, 757 repo_at text not null, 758 759 -- meta 760 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 761 762 -- constraints 763 foreign key (repo_at) references repos(at_uri) on delete cascade 764 ) 765 `) 766 if err != nil { 767 return err 768 } 769 770 // copy data 771 _, err = tx.Exec(` 772 insert into collaborators_new (id, did, rkey, subject_did, repo_at) 773 select 774 c.id, 775 r.did, 776 '', 777 c.did, 778 r.at_uri 779 from collaborators c 780 join repos r on c.repo = r.id 781 `) 782 if err != nil { 783 return err 784 } 785 786 // drop old table 787 _, err = tx.Exec(`drop table collaborators`) 788 if err != nil { 789 return err 790 } 791 792 // rename new table 793 _, err = tx.Exec(`alter table collaborators_new rename to collaborators`) 794 return err 795 }) 796 797 orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error { 798 _, err := tx.Exec(` 799 alter table issues add column rkey text not null default ''; 800 801 -- get last url section from issue_at and save to rkey column 802 update issues 803 set rkey = replace(issue_at, rtrim(issue_at, replace(issue_at, '/', '')), ''); 804 `) 805 return err 806 }) 807 808 // repurpose the read-only column to "needs-upgrade" 809 orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error { 810 _, err := tx.Exec(` 811 alter table registrations rename column read_only to needs_upgrade; 812 `) 813 return err 814 }) 815 816 // require all knots to upgrade after the release of total xrpc 817 orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error { 818 _, err := tx.Exec(` 819 update registrations set needs_upgrade = 1; 820 `) 821 return err 822 }) 823 824 // require all knots to upgrade after the release of total xrpc 825 orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error { 826 _, err := tx.Exec(` 827 alter table spindles add column needs_upgrade integer not null default 0; 828 `) 829 return err 830 }) 831 832 // remove issue_at from issues and replace with generated column 833 // 834 // this requires a full table recreation because stored columns 835 // cannot be added via alter 836 // 837 // couple other changes: 838 // - columns renamed to be more consistent 839 // - adds edited and deleted fields 840 // 841 // disable foreign-keys for the next migration 842 conn.ExecContext(ctx, "pragma foreign_keys = off;") 843 orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error { 844 _, err := tx.Exec(` 845 create table if not exists issues_new ( 846 -- identifiers 847 id integer primary key autoincrement, 848 did text not null, 849 rkey text not null, 850 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) stored, 851 852 -- at identifiers 853 repo_at text not null, 854 855 -- content 856 issue_id integer not null, 857 title text not null, 858 body text not null, 859 open integer not null default 1, 860 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 861 edited text, -- timestamp 862 deleted text, -- timestamp 863 864 unique(did, rkey), 865 unique(repo_at, issue_id), 866 unique(at_uri), 867 foreign key (repo_at) references repos(at_uri) on delete cascade 868 ); 869 `) 870 if err != nil { 871 return err 872 } 873 874 // transfer data 875 _, err = tx.Exec(` 876 insert into issues_new (id, did, rkey, repo_at, issue_id, title, body, open, created) 877 select 878 i.id, 879 i.owner_did, 880 i.rkey, 881 i.repo_at, 882 i.issue_id, 883 i.title, 884 i.body, 885 i.open, 886 i.created 887 from issues i; 888 `) 889 if err != nil { 890 return err 891 } 892 893 // drop old table 894 _, err = tx.Exec(`drop table issues`) 895 if err != nil { 896 return err 897 } 898 899 // rename new table 900 _, err = tx.Exec(`alter table issues_new rename to issues`) 901 return err 902 }) 903 conn.ExecContext(ctx, "pragma foreign_keys = on;") 904 905 // - renames the comments table to 'issue_comments' 906 // - rework issue comments to update constraints: 907 // * unique(did, rkey) 908 // * remove comment-id and just use the global ID 909 // * foreign key (repo_at, issue_id) 910 // - new columns 911 // * column "reply_to" which can be any other comment 912 // * column "at-uri" which is a generated column 913 orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error { 914 _, err := tx.Exec(` 915 create table if not exists issue_comments ( 916 -- identifiers 917 id integer primary key autoincrement, 918 did text not null, 919 rkey text, 920 at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue.comment' || '/' || rkey) stored, 921 922 -- at identifiers 923 issue_at text not null, 924 reply_to text, -- at_uri of parent comment 925 926 -- content 927 body text not null, 928 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 929 edited text, 930 deleted text, 931 932 -- constraints 933 unique(did, rkey), 934 unique(at_uri), 935 foreign key (issue_at) references issues(at_uri) on delete cascade 936 ); 937 `) 938 if err != nil { 939 return err 940 } 941 942 // transfer data 943 _, err = tx.Exec(` 944 insert into issue_comments (id, did, rkey, issue_at, body, created, edited, deleted) 945 select 946 c.id, 947 c.owner_did, 948 c.rkey, 949 i.at_uri, -- get at_uri from issues table 950 c.body, 951 c.created, 952 c.edited, 953 c.deleted 954 from comments c 955 join issues i on c.repo_at = i.repo_at and c.issue_id = i.issue_id; 956 `) 957 if err != nil { 958 return err 959 } 960 961 // drop old table 962 _, err = tx.Exec(`drop table comments`) 963 return err 964 }) 965 966 // add generated at_uri column to pulls table 967 // 968 // this requires a full table recreation because stored columns 969 // cannot be added via alter 970 // 971 // disable foreign-keys for the next migration 972 conn.ExecContext(ctx, "pragma foreign_keys = off;") 973 orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error { 974 _, err := tx.Exec(` 975 create table if not exists pulls_new ( 976 -- identifiers 977 id integer primary key autoincrement, 978 pull_id integer not null, 979 at_uri text generated always as ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) stored, 980 981 -- at identifiers 982 repo_at text not null, 983 owner_did text not null, 984 rkey text not null, 985 986 -- content 987 title text not null, 988 body text not null, 989 target_branch text not null, 990 state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted 991 992 -- source info 993 source_branch text, 994 source_repo_at text, 995 996 -- stacking 997 stack_id text, 998 change_id text, 999 parent_change_id text, 1000 1001 -- meta 1002 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1003 1004 -- constraints 1005 unique(repo_at, pull_id), 1006 unique(at_uri), 1007 foreign key (repo_at) references repos(at_uri) on delete cascade 1008 ); 1009 `) 1010 if err != nil { 1011 return err 1012 } 1013 1014 // transfer data 1015 _, err = tx.Exec(` 1016 insert into pulls_new ( 1017 id, pull_id, repo_at, owner_did, rkey, 1018 title, body, target_branch, state, 1019 source_branch, source_repo_at, 1020 stack_id, change_id, parent_change_id, 1021 created 1022 ) 1023 select 1024 id, pull_id, repo_at, owner_did, rkey, 1025 title, body, target_branch, state, 1026 source_branch, source_repo_at, 1027 stack_id, change_id, parent_change_id, 1028 created 1029 from pulls; 1030 `) 1031 if err != nil { 1032 return err 1033 } 1034 1035 // drop old table 1036 _, err = tx.Exec(`drop table pulls`) 1037 if err != nil { 1038 return err 1039 } 1040 1041 // rename new table 1042 _, err = tx.Exec(`alter table pulls_new rename to pulls`) 1043 return err 1044 }) 1045 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1046 1047 // remove repo_at and pull_id from pull_submissions and replace with pull_at 1048 // 1049 // this requires a full table recreation because stored columns 1050 // cannot be added via alter 1051 // 1052 // disable foreign-keys for the next migration 1053 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1054 orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error { 1055 _, err := tx.Exec(` 1056 create table if not exists pull_submissions_new ( 1057 -- identifiers 1058 id integer primary key autoincrement, 1059 pull_at text not null, 1060 1061 -- content, these are immutable, and require a resubmission to update 1062 round_number integer not null default 0, 1063 patch text, 1064 source_rev text, 1065 1066 -- meta 1067 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1068 1069 -- constraints 1070 unique(pull_at, round_number), 1071 foreign key (pull_at) references pulls(at_uri) on delete cascade 1072 ); 1073 `) 1074 if err != nil { 1075 return err 1076 } 1077 1078 // transfer data, constructing pull_at from pulls table 1079 _, err = tx.Exec(` 1080 insert into pull_submissions_new (id, pull_at, round_number, patch, created) 1081 select 1082 ps.id, 1083 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey, 1084 ps.round_number, 1085 ps.patch, 1086 ps.created 1087 from pull_submissions ps 1088 join pulls p on ps.repo_at = p.repo_at and ps.pull_id = p.pull_id; 1089 `) 1090 if err != nil { 1091 return err 1092 } 1093 1094 // drop old table 1095 _, err = tx.Exec(`drop table pull_submissions`) 1096 if err != nil { 1097 return err 1098 } 1099 1100 // rename new table 1101 _, err = tx.Exec(`alter table pull_submissions_new rename to pull_submissions`) 1102 return err 1103 }) 1104 conn.ExecContext(ctx, "pragma foreign_keys = on;") 1105 1106 // knots may report the combined patch for a comparison, we can store that on the appview side 1107 // (but not on the pds record), because calculating the combined patch requires a git index 1108 orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error { 1109 _, err := tx.Exec(` 1110 alter table pull_submissions add column combined text; 1111 `) 1112 return err 1113 }) 1114 1115 orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error { 1116 _, err := tx.Exec(` 1117 alter table profile add column pronouns text; 1118 `) 1119 return err 1120 }) 1121 1122 orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error { 1123 _, err := tx.Exec(` 1124 alter table repos add column website text; 1125 alter table repos add column topics text; 1126 `) 1127 return err 1128 }) 1129 1130 orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error { 1131 _, err := tx.Exec(` 1132 alter table notification_preferences add column user_mentioned integer not null default 1; 1133 `) 1134 return err 1135 }) 1136 1137 // remove the foreign key constraints from stars. 1138 orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error { 1139 _, err := tx.Exec(` 1140 create table stars_new ( 1141 id integer primary key autoincrement, 1142 did text not null, 1143 rkey text not null, 1144 1145 subject_at text not null, 1146 1147 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1148 unique(did, rkey), 1149 unique(did, subject_at) 1150 ); 1151 1152 insert into stars_new ( 1153 id, 1154 did, 1155 rkey, 1156 subject_at, 1157 created 1158 ) 1159 select 1160 id, 1161 starred_by_did, 1162 rkey, 1163 repo_at, 1164 created 1165 from stars; 1166 1167 drop table stars; 1168 alter table stars_new rename to stars; 1169 1170 create index if not exists idx_stars_created on stars(created); 1171 create index if not exists idx_stars_subject_at_created on stars(subject_at, created); 1172 `) 1173 return err 1174 }) 1175 1176 return &DB{ 1177 db, 1178 logger, 1179 }, nil 1180} 1181 1182func (d *DB) Close() error { 1183 return d.DB.Close() 1184}