Monorepo for Tangled tangled.org

appview/db: refactor GetPulls

instead of using a massive left join, it now uses a few FilterIns. we
can also get rid of the GetPull helper, it is a specialization of
GetPulls that returns a single pull request.

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 494ace72 4805af2f

verified
Changed files
+252 -240
appview
db
models
+140
appview/db/db.go
··· 954 954 return err 955 955 }) 956 956 957 + // add generated at_uri column to pulls table 958 + // 959 + // this requires a full table recreation because stored columns 960 + // cannot be added via alter 961 + // 962 + // disable foreign-keys for the next migration 963 + conn.ExecContext(ctx, "pragma foreign_keys = off;") 964 + runMigration(conn, "add-at-uri-to-pulls", func(tx *sql.Tx) error { 965 + _, err := tx.Exec(` 966 + create table if not exists pulls_new ( 967 + -- identifiers 968 + id integer primary key autoincrement, 969 + pull_id integer not null, 970 + at_uri text generated always as ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) stored, 971 + 972 + -- at identifiers 973 + repo_at text not null, 974 + owner_did text not null, 975 + rkey text not null, 976 + 977 + -- content 978 + title text not null, 979 + body text not null, 980 + target_branch text not null, 981 + state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted 982 + 983 + -- source info 984 + source_branch text, 985 + source_repo_at text, 986 + 987 + -- stacking 988 + stack_id text, 989 + change_id text, 990 + parent_change_id text, 991 + 992 + -- meta 993 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 994 + 995 + -- constraints 996 + unique(repo_at, pull_id), 997 + unique(at_uri), 998 + foreign key (repo_at) references repos(at_uri) on delete cascade 999 + ); 1000 + `) 1001 + if err != nil { 1002 + return err 1003 + } 1004 + 1005 + // transfer data 1006 + _, err = tx.Exec(` 1007 + insert into pulls_new ( 1008 + id, pull_id, repo_at, owner_did, rkey, 1009 + title, body, target_branch, state, 1010 + source_branch, source_repo_at, 1011 + stack_id, change_id, parent_change_id, 1012 + created 1013 + ) 1014 + select 1015 + id, pull_id, repo_at, owner_did, rkey, 1016 + title, body, target_branch, state, 1017 + source_branch, source_repo_at, 1018 + stack_id, change_id, parent_change_id, 1019 + created 1020 + from pulls; 1021 + `) 1022 + if err != nil { 1023 + return err 1024 + } 1025 + 1026 + // drop old table 1027 + _, err = tx.Exec(`drop table pulls`) 1028 + if err != nil { 1029 + return err 1030 + } 1031 + 1032 + // rename new table 1033 + _, err = tx.Exec(`alter table pulls_new rename to pulls`) 1034 + return err 1035 + }) 1036 + conn.ExecContext(ctx, "pragma foreign_keys = on;") 1037 + 1038 + // remove repo_at and pull_id from pull_submissions and replace with pull_at 1039 + // 1040 + // this requires a full table recreation because stored columns 1041 + // cannot be added via alter 1042 + // 1043 + // disable foreign-keys for the next migration 1044 + conn.ExecContext(ctx, "pragma foreign_keys = off;") 1045 + runMigration(conn, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error { 1046 + _, err := tx.Exec(` 1047 + create table if not exists pull_submissions_new ( 1048 + -- identifiers 1049 + id integer primary key autoincrement, 1050 + pull_at text not null, 1051 + 1052 + -- content, these are immutable, and require a resubmission to update 1053 + round_number integer not null default 0, 1054 + patch text, 1055 + source_rev text, 1056 + 1057 + -- meta 1058 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1059 + 1060 + -- constraints 1061 + unique(pull_at, round_number), 1062 + foreign key (pull_at) references pulls(at_uri) on delete cascade 1063 + ); 1064 + `) 1065 + if err != nil { 1066 + return err 1067 + } 1068 + 1069 + // transfer data, constructing pull_at from pulls table 1070 + _, err = tx.Exec(` 1071 + insert into pull_submissions_new (id, pull_at, round_number, patch, created) 1072 + select 1073 + ps.id, 1074 + 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey, 1075 + ps.round_number, 1076 + ps.patch, 1077 + ps.created 1078 + from pull_submissions ps 1079 + join pulls p on ps.repo_at = p.repo_at and ps.pull_id = p.pull_id; 1080 + `) 1081 + if err != nil { 1082 + return err 1083 + } 1084 + 1085 + // drop old table 1086 + _, err = tx.Exec(`drop table pull_submissions`) 1087 + if err != nil { 1088 + return err 1089 + } 1090 + 1091 + // rename new table 1092 + _, err = tx.Exec(`alter table pull_submissions_new rename to pull_submissions`) 1093 + return err 1094 + }) 1095 + conn.ExecContext(ctx, "pragma foreign_keys = on;") 1096 + 957 1097 return &DB{db}, nil 958 1098 } 959 1099
+110 -237
appview/db/pulls.go
··· 3 3 import ( 4 4 "database/sql" 5 5 "fmt" 6 - "log" 6 + "maps" 7 + "slices" 7 8 "sort" 8 9 "strings" 9 10 "time" ··· 87 88 pull.ID = int(id) 88 89 89 90 _, err = tx.Exec(` 90 - insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 91 - values (?, ?, ?, ?, ?) 92 - `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev) 91 + insert into pull_submissions (pull_at, round_number, patch, source_rev) 92 + values (?, ?, ?, ?) 93 + `, pull.PullAt(), 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev) 93 94 return err 94 95 } 95 96 ··· 108 109 } 109 110 110 111 func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*models.Pull, error) { 111 - pulls := make(map[int]*models.Pull) 112 + pulls := make(map[syntax.ATURI]*models.Pull) 112 113 113 114 var conditions []string 114 115 var args []any ··· 211 212 pull.ParentChangeId = parentChangeId.String 212 213 } 213 214 214 - pulls[pull.PullId] = &pull 215 - } 216 - 217 - // get latest round no. for each pull 218 - inClause := strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ") 219 - submissionsQuery := fmt.Sprintf(` 220 - select 221 - id, pull_id, round_number, patch, created, source_rev 222 - from 223 - pull_submissions 224 - where 225 - repo_at in (%s) and pull_id in (%s) 226 - `, inClause, inClause) 227 - 228 - args = make([]any, len(pulls)*2) 229 - idx := 0 230 - for _, p := range pulls { 231 - args[idx] = p.RepoAt 232 - idx += 1 233 - } 234 - for _, p := range pulls { 235 - args[idx] = p.PullId 236 - idx += 1 237 - } 238 - submissionsRows, err := e.Query(submissionsQuery, args...) 239 - if err != nil { 240 - return nil, err 215 + pulls[pull.PullAt()] = &pull 241 216 } 242 - defer submissionsRows.Close() 243 217 244 - for submissionsRows.Next() { 245 - var s models.PullSubmission 246 - var sourceRev sql.NullString 247 - var createdAt string 248 - err := submissionsRows.Scan( 249 - &s.ID, 250 - &s.PullId, 251 - &s.RoundNumber, 252 - &s.Patch, 253 - &createdAt, 254 - &sourceRev, 255 - ) 256 - if err != nil { 257 - return nil, err 258 - } 259 - 260 - createdTime, err := time.Parse(time.RFC3339, createdAt) 261 - if err != nil { 262 - return nil, err 263 - } 264 - s.Created = createdTime 265 - 266 - if sourceRev.Valid { 267 - s.SourceRev = sourceRev.String 268 - } 269 - 270 - if p, ok := pulls[s.PullId]; ok { 271 - p.Submissions = make([]*models.PullSubmission, s.RoundNumber+1) 272 - p.Submissions[s.RoundNumber] = &s 273 - } 274 - } 275 - if err := rows.Err(); err != nil { 276 - return nil, err 277 - } 278 - 279 - // get comment count on latest submission on each pull 280 - inClause = strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ") 281 - commentsQuery := fmt.Sprintf(` 282 - select 283 - count(id), pull_id 284 - from 285 - pull_comments 286 - where 287 - submission_id in (%s) 288 - group by 289 - submission_id 290 - `, inClause) 291 - 292 - args = []any{} 218 + var pullAts []syntax.ATURI 293 219 for _, p := range pulls { 294 - args = append(args, p.Submissions[p.LastRoundNumber()].ID) 220 + pullAts = append(pullAts, p.PullAt()) 295 221 } 296 - commentsRows, err := e.Query(commentsQuery, args...) 222 + submissionsMap, err := GetPullSubmissions(e, FilterIn("pull_at", pullAts)) 297 223 if err != nil { 298 - return nil, err 224 + return nil, fmt.Errorf("failed to get submissions: %w", err) 299 225 } 300 - defer commentsRows.Close() 301 226 302 - for commentsRows.Next() { 303 - var commentCount, pullId int 304 - err := commentsRows.Scan( 305 - &commentCount, 306 - &pullId, 307 - ) 308 - if err != nil { 309 - return nil, err 310 - } 311 - if p, ok := pulls[pullId]; ok { 312 - p.Submissions[p.LastRoundNumber()].Comments = make([]models.PullComment, commentCount) 227 + for pullAt, submissions := range submissionsMap { 228 + if p, ok := pulls[pullAt]; ok { 229 + p.Submissions = submissions 313 230 } 314 - } 315 - if err := rows.Err(); err != nil { 316 - return nil, err 317 231 } 318 232 319 233 orderedByPullId := []*models.Pull{} ··· 332 246 } 333 247 334 248 func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) { 335 - query := ` 336 - select 337 - id, 338 - owner_did, 339 - pull_id, 340 - created, 341 - title, 342 - state, 343 - target_branch, 344 - repo_at, 345 - body, 346 - rkey, 347 - source_branch, 348 - source_repo_at, 349 - stack_id, 350 - change_id, 351 - parent_change_id 352 - from 353 - pulls 354 - where 355 - repo_at = ? and pull_id = ? 356 - ` 357 - row := e.QueryRow(query, repoAt, pullId) 358 - 359 - var pull models.Pull 360 - var createdAt string 361 - var sourceBranch, sourceRepoAt, stackId, changeId, parentChangeId sql.NullString 362 - err := row.Scan( 363 - &pull.ID, 364 - &pull.OwnerDid, 365 - &pull.PullId, 366 - &createdAt, 367 - &pull.Title, 368 - &pull.State, 369 - &pull.TargetBranch, 370 - &pull.RepoAt, 371 - &pull.Body, 372 - &pull.Rkey, 373 - &sourceBranch, 374 - &sourceRepoAt, 375 - &stackId, 376 - &changeId, 377 - &parentChangeId, 378 - ) 249 + pulls, err := GetPullsWithLimit(e, 1, FilterEq("repo_at", repoAt), FilterEq("pull_id", pullId)) 379 250 if err != nil { 380 251 return nil, err 381 252 } 253 + if pulls == nil { 254 + return nil, sql.ErrNoRows 255 + } 382 256 383 - createdTime, err := time.Parse(time.RFC3339, createdAt) 384 - if err != nil { 385 - return nil, err 386 - } 387 - pull.Created = createdTime 257 + return pulls[0], nil 258 + } 388 259 389 - // populate source 390 - if sourceBranch.Valid { 391 - pull.PullSource = &models.PullSource{ 392 - Branch: sourceBranch.String, 393 - } 394 - if sourceRepoAt.Valid { 395 - sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String) 396 - if err != nil { 397 - return nil, err 398 - } 399 - pull.PullSource.RepoAt = &sourceRepoAtParsed 400 - } 260 + // mapping from pull -> pull submissions 261 + func GetPullSubmissions(e Execer, filters ...filter) (map[syntax.ATURI][]*models.PullSubmission, error) { 262 + var conditions []string 263 + var args []any 264 + for _, filter := range filters { 265 + conditions = append(conditions, filter.Condition()) 266 + args = append(args, filter.Arg()...) 401 267 } 402 268 403 - if stackId.Valid { 404 - pull.StackId = stackId.String 405 - } 406 - if changeId.Valid { 407 - pull.ChangeId = changeId.String 408 - } 409 - if parentChangeId.Valid { 410 - pull.ParentChangeId = parentChangeId.String 269 + whereClause := "" 270 + if conditions != nil { 271 + whereClause = " where " + strings.Join(conditions, " and ") 411 272 } 412 273 413 - submissionsQuery := ` 274 + query := fmt.Sprintf(` 414 275 select 415 - id, pull_id, repo_at, round_number, patch, created, source_rev 276 + id, 277 + pull_at, 278 + round_number, 279 + patch, 280 + created, 281 + source_rev 416 282 from 417 283 pull_submissions 418 - where 419 - repo_at = ? and pull_id = ? 420 - ` 421 - submissionsRows, err := e.Query(submissionsQuery, repoAt, pullId) 284 + %s 285 + order by 286 + round_number asc 287 + `, whereClause) 288 + 289 + rows, err := e.Query(query, args...) 422 290 if err != nil { 423 291 return nil, err 424 292 } 425 - defer submissionsRows.Close() 293 + defer rows.Close() 426 294 427 - submissionsMap := make(map[int]*models.PullSubmission) 295 + submissionMap := make(map[int]*models.PullSubmission) 428 296 429 - for submissionsRows.Next() { 297 + for rows.Next() { 430 298 var submission models.PullSubmission 431 - var submissionCreatedStr string 432 - var submissionSourceRev sql.NullString 433 - err := submissionsRows.Scan( 299 + var createdAt string 300 + var sourceRev sql.NullString 301 + err := rows.Scan( 434 302 &submission.ID, 435 - &submission.PullId, 436 - &submission.RepoAt, 303 + &submission.PullAt, 437 304 &submission.RoundNumber, 438 305 &submission.Patch, 439 - &submissionCreatedStr, 440 - &submissionSourceRev, 306 + &createdAt, 307 + &sourceRev, 441 308 ) 442 309 if err != nil { 443 310 return nil, err 444 311 } 445 312 446 - submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr) 313 + createdTime, err := time.Parse(time.RFC3339, createdAt) 447 314 if err != nil { 448 315 return nil, err 449 316 } 450 - submission.Created = submissionCreatedTime 317 + submission.Created = createdTime 451 318 452 - if submissionSourceRev.Valid { 453 - submission.SourceRev = submissionSourceRev.String 319 + if sourceRev.Valid { 320 + submission.SourceRev = sourceRev.String 454 321 } 455 322 456 - submissionsMap[submission.ID] = &submission 323 + submissionMap[submission.ID] = &submission 324 + } 325 + 326 + if err := rows.Err(); err != nil { 327 + return nil, err 457 328 } 458 - if err = submissionsRows.Close(); err != nil { 329 + 330 + // Get comments for all submissions using GetPullComments 331 + submissionIds := slices.Collect(maps.Keys(submissionMap)) 332 + comments, err := GetPullComments(e, FilterIn("submission_id", submissionIds)) 333 + if err != nil { 459 334 return nil, err 460 335 } 461 - if len(submissionsMap) == 0 { 462 - return &pull, nil 336 + for _, comment := range comments { 337 + if submission, ok := submissionMap[comment.SubmissionId]; ok { 338 + submission.Comments = append(submission.Comments, comment) 339 + } 463 340 } 464 341 342 + // order the submissions by pull_at 343 + m := make(map[syntax.ATURI][]*models.PullSubmission) 344 + for _, s := range submissionMap { 345 + m[s.PullAt] = append(m[s.PullAt], s) 346 + } 347 + 348 + return m, nil 349 + } 350 + 351 + func GetPullComments(e Execer, filters ...filter) ([]models.PullComment, error) { 352 + var conditions []string 465 353 var args []any 466 - for k := range submissionsMap { 467 - args = append(args, k) 354 + for _, filter := range filters { 355 + conditions = append(conditions, filter.Condition()) 356 + args = append(args, filter.Arg()...) 468 357 } 469 - inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ") 470 - commentsQuery := fmt.Sprintf(` 358 + 359 + whereClause := "" 360 + if conditions != nil { 361 + whereClause = " where " + strings.Join(conditions, " and ") 362 + } 363 + 364 + query := fmt.Sprintf(` 471 365 select 472 366 id, 473 367 pull_id, ··· 479 373 created 480 374 from 481 375 pull_comments 482 - where 483 - submission_id IN (%s) 376 + %s 484 377 order by 485 378 created asc 486 - `, inClause) 487 - commentsRows, err := e.Query(commentsQuery, args...) 379 + `, whereClause) 380 + 381 + rows, err := e.Query(query, args...) 488 382 if err != nil { 489 383 return nil, err 490 384 } 491 - defer commentsRows.Close() 385 + defer rows.Close() 492 386 493 - for commentsRows.Next() { 387 + var comments []models.PullComment 388 + for rows.Next() { 494 389 var comment models.PullComment 495 - var commentCreatedStr string 496 - err := commentsRows.Scan( 390 + var createdAt string 391 + err := rows.Scan( 497 392 &comment.ID, 498 393 &comment.PullId, 499 394 &comment.SubmissionId, ··· 501 396 &comment.OwnerDid, 502 397 &comment.CommentAt, 503 398 &comment.Body, 504 - &commentCreatedStr, 399 + &createdAt, 505 400 ) 506 401 if err != nil { 507 402 return nil, err 508 403 } 509 404 510 - commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr) 511 - if err != nil { 512 - return nil, err 513 - } 514 - comment.Created = commentCreatedTime 515 - 516 - // Add the comment to its submission 517 - if submission, ok := submissionsMap[comment.SubmissionId]; ok { 518 - submission.Comments = append(submission.Comments, comment) 405 + if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 406 + comment.Created = t 519 407 } 520 408 521 - } 522 - if err = commentsRows.Err(); err != nil { 523 - return nil, err 524 - } 525 - 526 - var pullSourceRepo *models.Repo 527 - if pull.PullSource != nil { 528 - if pull.PullSource.RepoAt != nil { 529 - pullSourceRepo, err = GetRepoByAtUri(e, pull.PullSource.RepoAt.String()) 530 - if err != nil { 531 - log.Printf("failed to get repo by at uri: %v", err) 532 - } else { 533 - pull.PullSource.Repo = pullSourceRepo 534 - } 535 - } 409 + comments = append(comments, comment) 536 410 } 537 411 538 - pull.Submissions = make([]*models.PullSubmission, len(submissionsMap)) 539 - for _, submission := range submissionsMap { 540 - pull.Submissions[submission.RoundNumber] = submission 412 + if err := rows.Err(); err != nil { 413 + return nil, err 541 414 } 542 415 543 - return &pull, nil 416 + return comments, nil 544 417 } 545 418 546 419 // timeframe here is directly passed into the sql query filter, and any ··· 677 550 func ResubmitPull(e Execer, pull *models.Pull, newPatch, sourceRev string) error { 678 551 newRoundNumber := len(pull.Submissions) 679 552 _, err := e.Exec(` 680 - insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 681 - values (?, ?, ?, ?, ?) 682 - `, pull.PullId, pull.RepoAt, newRoundNumber, newPatch, sourceRev) 553 + insert into pull_submissions (pull_at, round_number, patch, source_rev) 554 + values (?, ?, ?, ?) 555 + `, pull.PullAt(), newRoundNumber, newPatch, sourceRev) 683 556 684 557 return err 685 558 }
+2 -3
appview/models/pull.go
··· 125 125 126 126 type PullSubmission struct { 127 127 // ids 128 - ID int 129 - PullId int 128 + ID int 130 129 131 130 // at ids 132 - RepoAt syntax.ATURI 131 + PullAt syntax.ATURI 133 132 134 133 // content 135 134 RoundNumber int