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