+140
appview/db/db.go
+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
+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
}