tenseleyflow/shithub / 54bdb31

Browse files

S23: pr_reviews migration 0024 + queries + sqlc regen

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
54bdb31cf78c9511ebdcf5a0368989a34180eb9a
Parents
8aaaf88
Tree
e0553f1

14 changed files

StatusFile+-
M internal/auth/policy/sqlc/models.go 146 12
M internal/issues/sqlc/models.go 146 12
M internal/meta/sqlc/models.go 146 12
A internal/migrationsfs/migrations/0024_pr_reviews.sql 134 0
A internal/pulls/queries/reviews.sql 170 0
M internal/pulls/sqlc/models.go 146 12
M internal/pulls/sqlc/querier.go 48 0
A internal/pulls/sqlc/reviews.sql.go 743 0
M internal/repos/queries/branch_protection.sql 13 2
M internal/repos/sqlc/branch_protection.sql.go 37 2
M internal/repos/sqlc/models.go 146 12
M internal/repos/sqlc/querier.go 3 0
M internal/users/sqlc/models.go 146 12
M internal/worker/sqlc/models.go 146 12
internal/auth/policy/sqlc/models.gomodified
@@ -403,6 +403,91 @@ func (ns NullPrMergeableState) Value() (driver.Value, error) {
403403
 	return string(ns.PrMergeableState), nil
404404
 }
405405
 
406
+type PrReviewSide string
407
+
408
+const (
409
+	PrReviewSideLeft  PrReviewSide = "left"
410
+	PrReviewSideRight PrReviewSide = "right"
411
+)
412
+
413
+func (e *PrReviewSide) Scan(src interface{}) error {
414
+	switch s := src.(type) {
415
+	case []byte:
416
+		*e = PrReviewSide(s)
417
+	case string:
418
+		*e = PrReviewSide(s)
419
+	default:
420
+		return fmt.Errorf("unsupported scan type for PrReviewSide: %T", src)
421
+	}
422
+	return nil
423
+}
424
+
425
+type NullPrReviewSide struct {
426
+	PrReviewSide PrReviewSide
427
+	Valid        bool // Valid is true if PrReviewSide is not NULL
428
+}
429
+
430
+// Scan implements the Scanner interface.
431
+func (ns *NullPrReviewSide) Scan(value interface{}) error {
432
+	if value == nil {
433
+		ns.PrReviewSide, ns.Valid = "", false
434
+		return nil
435
+	}
436
+	ns.Valid = true
437
+	return ns.PrReviewSide.Scan(value)
438
+}
439
+
440
+// Value implements the driver Valuer interface.
441
+func (ns NullPrReviewSide) Value() (driver.Value, error) {
442
+	if !ns.Valid {
443
+		return nil, nil
444
+	}
445
+	return string(ns.PrReviewSide), nil
446
+}
447
+
448
+type PrReviewState string
449
+
450
+const (
451
+	PrReviewStateComment        PrReviewState = "comment"
452
+	PrReviewStateApprove        PrReviewState = "approve"
453
+	PrReviewStateRequestChanges PrReviewState = "request_changes"
454
+)
455
+
456
+func (e *PrReviewState) Scan(src interface{}) error {
457
+	switch s := src.(type) {
458
+	case []byte:
459
+		*e = PrReviewState(s)
460
+	case string:
461
+		*e = PrReviewState(s)
462
+	default:
463
+		return fmt.Errorf("unsupported scan type for PrReviewState: %T", src)
464
+	}
465
+	return nil
466
+}
467
+
468
+type NullPrReviewState struct {
469
+	PrReviewState PrReviewState
470
+	Valid         bool // Valid is true if PrReviewState is not NULL
471
+}
472
+
473
+// Scan implements the Scanner interface.
474
+func (ns *NullPrReviewState) Scan(value interface{}) error {
475
+	if value == nil {
476
+		ns.PrReviewState, ns.Valid = "", false
477
+		return nil
478
+	}
479
+	ns.Valid = true
480
+	return ns.PrReviewState.Scan(value)
481
+}
482
+
483
+// Value implements the driver Valuer interface.
484
+func (ns NullPrReviewState) Value() (driver.Value, error) {
485
+	if !ns.Valid {
486
+		return nil, nil
487
+	}
488
+	return string(ns.PrReviewState), nil
489
+}
490
+
406491
 type RepoVisibility string
407492
 
408493
 const (
@@ -551,18 +636,21 @@ type AuthThrottle struct {
551636
 }
552637
 
553638
 type BranchProtectionRule struct {
554
-	ID                   int64
555
-	RepoID               int64
556
-	Pattern              string
557
-	PreventForcePush     bool
558
-	PreventDeletion      bool
559
-	RequirePrForPush     bool
560
-	AllowedPusherUserIds []int64
561
-	RequireSignedCommits bool
562
-	StatusChecksRequired []string
563
-	CreatedAt            pgtype.Timestamptz
564
-	UpdatedAt            pgtype.Timestamptz
565
-	CreatedByUserID      pgtype.Int8
639
+	ID                        int64
640
+	RepoID                    int64
641
+	Pattern                   string
642
+	PreventForcePush          bool
643
+	PreventDeletion           bool
644
+	RequirePrForPush          bool
645
+	AllowedPusherUserIds      []int64
646
+	RequireSignedCommits      bool
647
+	StatusChecksRequired      []string
648
+	CreatedAt                 pgtype.Timestamptz
649
+	UpdatedAt                 pgtype.Timestamptz
650
+	CreatedByUserID           pgtype.Int8
651
+	RequiredReviewCount       int32
652
+	DismissStaleReviewsOnPush bool
653
+	RequireCodeOwnerReview    bool
566654
 }
567655
 
568656
 type EmailVerification struct {
@@ -691,6 +779,52 @@ type PasswordReset struct {
691779
 	CreatedAt pgtype.Timestamptz
692780
 }
693781
 
782
+type PrReview struct {
783
+	ID                int64
784
+	PrIssueID         int64
785
+	AuthorUserID      pgtype.Int8
786
+	State             PrReviewState
787
+	Body              string
788
+	BodyHtmlCached    pgtype.Text
789
+	SubmittedAt       pgtype.Timestamptz
790
+	DismissedAt       pgtype.Timestamptz
791
+	DismissedByUserID pgtype.Int8
792
+	DismissalReason   string
793
+}
794
+
795
+type PrReviewComment struct {
796
+	ID                int64
797
+	PrIssueID         int64
798
+	ReviewID          pgtype.Int8
799
+	AuthorUserID      pgtype.Int8
800
+	FilePath          string
801
+	Side              PrReviewSide
802
+	OriginalCommitSha string
803
+	OriginalLine      int32
804
+	OriginalPosition  int32
805
+	CurrentPosition   pgtype.Int4
806
+	Body              string
807
+	BodyHtmlCached    pgtype.Text
808
+	InReplyToID       pgtype.Int8
809
+	Pending           bool
810
+	ResolvedAt        pgtype.Timestamptz
811
+	ResolvedByUserID  pgtype.Int8
812
+	CreatedAt         pgtype.Timestamptz
813
+	UpdatedAt         pgtype.Timestamptz
814
+	EditedAt          pgtype.Timestamptz
815
+}
816
+
817
+type PrReviewRequest struct {
818
+	ID                  int64
819
+	PrIssueID           int64
820
+	RequestedUserID     pgtype.Int8
821
+	RequestedTeamID     pgtype.Int8
822
+	RequestedByUserID   pgtype.Int8
823
+	RequestedAt         pgtype.Timestamptz
824
+	DismissedAt         pgtype.Timestamptz
825
+	SatisfiedByReviewID pgtype.Int8
826
+}
827
+
694828
 type PullRequest struct {
695829
 	IssueID            int64
696830
 	BaseRef            string
internal/issues/sqlc/models.gomodified
@@ -403,6 +403,91 @@ func (ns NullPrMergeableState) Value() (driver.Value, error) {
403403
 	return string(ns.PrMergeableState), nil
404404
 }
405405
 
406
+type PrReviewSide string
407
+
408
+const (
409
+	PrReviewSideLeft  PrReviewSide = "left"
410
+	PrReviewSideRight PrReviewSide = "right"
411
+)
412
+
413
+func (e *PrReviewSide) Scan(src interface{}) error {
414
+	switch s := src.(type) {
415
+	case []byte:
416
+		*e = PrReviewSide(s)
417
+	case string:
418
+		*e = PrReviewSide(s)
419
+	default:
420
+		return fmt.Errorf("unsupported scan type for PrReviewSide: %T", src)
421
+	}
422
+	return nil
423
+}
424
+
425
+type NullPrReviewSide struct {
426
+	PrReviewSide PrReviewSide
427
+	Valid        bool // Valid is true if PrReviewSide is not NULL
428
+}
429
+
430
+// Scan implements the Scanner interface.
431
+func (ns *NullPrReviewSide) Scan(value interface{}) error {
432
+	if value == nil {
433
+		ns.PrReviewSide, ns.Valid = "", false
434
+		return nil
435
+	}
436
+	ns.Valid = true
437
+	return ns.PrReviewSide.Scan(value)
438
+}
439
+
440
+// Value implements the driver Valuer interface.
441
+func (ns NullPrReviewSide) Value() (driver.Value, error) {
442
+	if !ns.Valid {
443
+		return nil, nil
444
+	}
445
+	return string(ns.PrReviewSide), nil
446
+}
447
+
448
+type PrReviewState string
449
+
450
+const (
451
+	PrReviewStateComment        PrReviewState = "comment"
452
+	PrReviewStateApprove        PrReviewState = "approve"
453
+	PrReviewStateRequestChanges PrReviewState = "request_changes"
454
+)
455
+
456
+func (e *PrReviewState) Scan(src interface{}) error {
457
+	switch s := src.(type) {
458
+	case []byte:
459
+		*e = PrReviewState(s)
460
+	case string:
461
+		*e = PrReviewState(s)
462
+	default:
463
+		return fmt.Errorf("unsupported scan type for PrReviewState: %T", src)
464
+	}
465
+	return nil
466
+}
467
+
468
+type NullPrReviewState struct {
469
+	PrReviewState PrReviewState
470
+	Valid         bool // Valid is true if PrReviewState is not NULL
471
+}
472
+
473
+// Scan implements the Scanner interface.
474
+func (ns *NullPrReviewState) Scan(value interface{}) error {
475
+	if value == nil {
476
+		ns.PrReviewState, ns.Valid = "", false
477
+		return nil
478
+	}
479
+	ns.Valid = true
480
+	return ns.PrReviewState.Scan(value)
481
+}
482
+
483
+// Value implements the driver Valuer interface.
484
+func (ns NullPrReviewState) Value() (driver.Value, error) {
485
+	if !ns.Valid {
486
+		return nil, nil
487
+	}
488
+	return string(ns.PrReviewState), nil
489
+}
490
+
406491
 type RepoVisibility string
407492
 
408493
 const (
@@ -551,18 +636,21 @@ type AuthThrottle struct {
551636
 }
552637
 
553638
 type BranchProtectionRule struct {
554
-	ID                   int64
555
-	RepoID               int64
556
-	Pattern              string
557
-	PreventForcePush     bool
558
-	PreventDeletion      bool
559
-	RequirePrForPush     bool
560
-	AllowedPusherUserIds []int64
561
-	RequireSignedCommits bool
562
-	StatusChecksRequired []string
563
-	CreatedAt            pgtype.Timestamptz
564
-	UpdatedAt            pgtype.Timestamptz
565
-	CreatedByUserID      pgtype.Int8
639
+	ID                        int64
640
+	RepoID                    int64
641
+	Pattern                   string
642
+	PreventForcePush          bool
643
+	PreventDeletion           bool
644
+	RequirePrForPush          bool
645
+	AllowedPusherUserIds      []int64
646
+	RequireSignedCommits      bool
647
+	StatusChecksRequired      []string
648
+	CreatedAt                 pgtype.Timestamptz
649
+	UpdatedAt                 pgtype.Timestamptz
650
+	CreatedByUserID           pgtype.Int8
651
+	RequiredReviewCount       int32
652
+	DismissStaleReviewsOnPush bool
653
+	RequireCodeOwnerReview    bool
566654
 }
567655
 
568656
 type EmailVerification struct {
@@ -691,6 +779,52 @@ type PasswordReset struct {
691779
 	CreatedAt pgtype.Timestamptz
692780
 }
693781
 
782
+type PrReview struct {
783
+	ID                int64
784
+	PrIssueID         int64
785
+	AuthorUserID      pgtype.Int8
786
+	State             PrReviewState
787
+	Body              string
788
+	BodyHtmlCached    pgtype.Text
789
+	SubmittedAt       pgtype.Timestamptz
790
+	DismissedAt       pgtype.Timestamptz
791
+	DismissedByUserID pgtype.Int8
792
+	DismissalReason   string
793
+}
794
+
795
+type PrReviewComment struct {
796
+	ID                int64
797
+	PrIssueID         int64
798
+	ReviewID          pgtype.Int8
799
+	AuthorUserID      pgtype.Int8
800
+	FilePath          string
801
+	Side              PrReviewSide
802
+	OriginalCommitSha string
803
+	OriginalLine      int32
804
+	OriginalPosition  int32
805
+	CurrentPosition   pgtype.Int4
806
+	Body              string
807
+	BodyHtmlCached    pgtype.Text
808
+	InReplyToID       pgtype.Int8
809
+	Pending           bool
810
+	ResolvedAt        pgtype.Timestamptz
811
+	ResolvedByUserID  pgtype.Int8
812
+	CreatedAt         pgtype.Timestamptz
813
+	UpdatedAt         pgtype.Timestamptz
814
+	EditedAt          pgtype.Timestamptz
815
+}
816
+
817
+type PrReviewRequest struct {
818
+	ID                  int64
819
+	PrIssueID           int64
820
+	RequestedUserID     pgtype.Int8
821
+	RequestedTeamID     pgtype.Int8
822
+	RequestedByUserID   pgtype.Int8
823
+	RequestedAt         pgtype.Timestamptz
824
+	DismissedAt         pgtype.Timestamptz
825
+	SatisfiedByReviewID pgtype.Int8
826
+}
827
+
694828
 type PullRequest struct {
695829
 	IssueID            int64
696830
 	BaseRef            string
internal/meta/sqlc/models.gomodified
@@ -403,6 +403,91 @@ func (ns NullPrMergeableState) Value() (driver.Value, error) {
403403
 	return string(ns.PrMergeableState), nil
404404
 }
405405
 
406
+type PrReviewSide string
407
+
408
+const (
409
+	PrReviewSideLeft  PrReviewSide = "left"
410
+	PrReviewSideRight PrReviewSide = "right"
411
+)
412
+
413
+func (e *PrReviewSide) Scan(src interface{}) error {
414
+	switch s := src.(type) {
415
+	case []byte:
416
+		*e = PrReviewSide(s)
417
+	case string:
418
+		*e = PrReviewSide(s)
419
+	default:
420
+		return fmt.Errorf("unsupported scan type for PrReviewSide: %T", src)
421
+	}
422
+	return nil
423
+}
424
+
425
+type NullPrReviewSide struct {
426
+	PrReviewSide PrReviewSide
427
+	Valid        bool // Valid is true if PrReviewSide is not NULL
428
+}
429
+
430
+// Scan implements the Scanner interface.
431
+func (ns *NullPrReviewSide) Scan(value interface{}) error {
432
+	if value == nil {
433
+		ns.PrReviewSide, ns.Valid = "", false
434
+		return nil
435
+	}
436
+	ns.Valid = true
437
+	return ns.PrReviewSide.Scan(value)
438
+}
439
+
440
+// Value implements the driver Valuer interface.
441
+func (ns NullPrReviewSide) Value() (driver.Value, error) {
442
+	if !ns.Valid {
443
+		return nil, nil
444
+	}
445
+	return string(ns.PrReviewSide), nil
446
+}
447
+
448
+type PrReviewState string
449
+
450
+const (
451
+	PrReviewStateComment        PrReviewState = "comment"
452
+	PrReviewStateApprove        PrReviewState = "approve"
453
+	PrReviewStateRequestChanges PrReviewState = "request_changes"
454
+)
455
+
456
+func (e *PrReviewState) Scan(src interface{}) error {
457
+	switch s := src.(type) {
458
+	case []byte:
459
+		*e = PrReviewState(s)
460
+	case string:
461
+		*e = PrReviewState(s)
462
+	default:
463
+		return fmt.Errorf("unsupported scan type for PrReviewState: %T", src)
464
+	}
465
+	return nil
466
+}
467
+
468
+type NullPrReviewState struct {
469
+	PrReviewState PrReviewState
470
+	Valid         bool // Valid is true if PrReviewState is not NULL
471
+}
472
+
473
+// Scan implements the Scanner interface.
474
+func (ns *NullPrReviewState) Scan(value interface{}) error {
475
+	if value == nil {
476
+		ns.PrReviewState, ns.Valid = "", false
477
+		return nil
478
+	}
479
+	ns.Valid = true
480
+	return ns.PrReviewState.Scan(value)
481
+}
482
+
483
+// Value implements the driver Valuer interface.
484
+func (ns NullPrReviewState) Value() (driver.Value, error) {
485
+	if !ns.Valid {
486
+		return nil, nil
487
+	}
488
+	return string(ns.PrReviewState), nil
489
+}
490
+
406491
 type RepoVisibility string
407492
 
408493
 const (
@@ -551,18 +636,21 @@ type AuthThrottle struct {
551636
 }
552637
 
553638
 type BranchProtectionRule struct {
554
-	ID                   int64
555
-	RepoID               int64
556
-	Pattern              string
557
-	PreventForcePush     bool
558
-	PreventDeletion      bool
559
-	RequirePrForPush     bool
560
-	AllowedPusherUserIds []int64
561
-	RequireSignedCommits bool
562
-	StatusChecksRequired []string
563
-	CreatedAt            pgtype.Timestamptz
564
-	UpdatedAt            pgtype.Timestamptz
565
-	CreatedByUserID      pgtype.Int8
639
+	ID                        int64
640
+	RepoID                    int64
641
+	Pattern                   string
642
+	PreventForcePush          bool
643
+	PreventDeletion           bool
644
+	RequirePrForPush          bool
645
+	AllowedPusherUserIds      []int64
646
+	RequireSignedCommits      bool
647
+	StatusChecksRequired      []string
648
+	CreatedAt                 pgtype.Timestamptz
649
+	UpdatedAt                 pgtype.Timestamptz
650
+	CreatedByUserID           pgtype.Int8
651
+	RequiredReviewCount       int32
652
+	DismissStaleReviewsOnPush bool
653
+	RequireCodeOwnerReview    bool
566654
 }
567655
 
568656
 type EmailVerification struct {
@@ -691,6 +779,52 @@ type PasswordReset struct {
691779
 	CreatedAt pgtype.Timestamptz
692780
 }
693781
 
782
+type PrReview struct {
783
+	ID                int64
784
+	PrIssueID         int64
785
+	AuthorUserID      pgtype.Int8
786
+	State             PrReviewState
787
+	Body              string
788
+	BodyHtmlCached    pgtype.Text
789
+	SubmittedAt       pgtype.Timestamptz
790
+	DismissedAt       pgtype.Timestamptz
791
+	DismissedByUserID pgtype.Int8
792
+	DismissalReason   string
793
+}
794
+
795
+type PrReviewComment struct {
796
+	ID                int64
797
+	PrIssueID         int64
798
+	ReviewID          pgtype.Int8
799
+	AuthorUserID      pgtype.Int8
800
+	FilePath          string
801
+	Side              PrReviewSide
802
+	OriginalCommitSha string
803
+	OriginalLine      int32
804
+	OriginalPosition  int32
805
+	CurrentPosition   pgtype.Int4
806
+	Body              string
807
+	BodyHtmlCached    pgtype.Text
808
+	InReplyToID       pgtype.Int8
809
+	Pending           bool
810
+	ResolvedAt        pgtype.Timestamptz
811
+	ResolvedByUserID  pgtype.Int8
812
+	CreatedAt         pgtype.Timestamptz
813
+	UpdatedAt         pgtype.Timestamptz
814
+	EditedAt          pgtype.Timestamptz
815
+}
816
+
817
+type PrReviewRequest struct {
818
+	ID                  int64
819
+	PrIssueID           int64
820
+	RequestedUserID     pgtype.Int8
821
+	RequestedTeamID     pgtype.Int8
822
+	RequestedByUserID   pgtype.Int8
823
+	RequestedAt         pgtype.Timestamptz
824
+	DismissedAt         pgtype.Timestamptz
825
+	SatisfiedByReviewID pgtype.Int8
826
+}
827
+
694828
 type PullRequest struct {
695829
 	IssueID            int64
696830
 	BaseRef            string
internal/migrationsfs/migrations/0024_pr_reviews.sqladded
@@ -0,0 +1,134 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+--
3
+-- S23 PR-review subsystem.
4
+--
5
+--   pr_reviews          — one row per submitted review (comment / approve /
6
+--                         request_changes); also dismissable with reason.
7
+--   pr_review_comments  — file-level inline comments anchored on
8
+--                         (file_path, side, original_commit_sha,
9
+--                         original_line, original_position). Each is
10
+--                         either part of a submitted review (review_id
11
+--                         non-NULL) or a single inline comment outside a
12
+--                         review (review_id IS NULL, pending=false), or a
13
+--                         server-side draft that hasn't been submitted yet
14
+--                         (review_id IS NULL, pending=true).
15
+--   pr_review_requests  — pending review requests; satisfied when the
16
+--                         requested user submits an approve/request_changes.
17
+--
18
+-- Branch protection extensions (the actual gate evaluation lives in
19
+-- internal/pulls/review/required.go):
20
+--
21
+--   required_review_count           int  DEFAULT 0
22
+--   dismiss_stale_reviews_on_push   bool DEFAULT false
23
+--   require_code_owner_review       bool DEFAULT false  (placeholder; CODEOWNERS post-MVP)
24
+
25
+-- +goose Up
26
+
27
+CREATE TYPE pr_review_state AS ENUM ('comment', 'approve', 'request_changes');
28
+CREATE TYPE pr_review_side  AS ENUM ('left', 'right');
29
+
30
+CREATE TABLE pr_reviews (
31
+    id                    bigserial         PRIMARY KEY,
32
+    pr_issue_id           bigint            NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
33
+    author_user_id        bigint            REFERENCES users(id) ON DELETE SET NULL,
34
+    state                 pr_review_state   NOT NULL,
35
+    body                  text              NOT NULL DEFAULT '',
36
+    body_html_cached      text,
37
+    submitted_at          timestamptz       NOT NULL DEFAULT now(),
38
+    dismissed_at          timestamptz,
39
+    dismissed_by_user_id  bigint            REFERENCES users(id) ON DELETE SET NULL,
40
+    dismissal_reason      text              NOT NULL DEFAULT ''
41
+);
42
+
43
+CREATE INDEX pr_reviews_pr_idx
44
+    ON pr_reviews (pr_issue_id, submitted_at DESC);
45
+CREATE INDEX pr_reviews_state_idx
46
+    ON pr_reviews (pr_issue_id, state) WHERE dismissed_at IS NULL;
47
+
48
+
49
+CREATE TABLE pr_review_comments (
50
+    id                    bigserial         PRIMARY KEY,
51
+    pr_issue_id           bigint            NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
52
+    review_id             bigint            REFERENCES pr_reviews(id) ON DELETE SET NULL,
53
+    author_user_id        bigint            REFERENCES users(id) ON DELETE SET NULL,
54
+    file_path             text              NOT NULL,
55
+    side                  pr_review_side    NOT NULL DEFAULT 'right',
56
+    original_commit_sha   text              NOT NULL,
57
+    original_line         int               NOT NULL,
58
+    original_position     int               NOT NULL,
59
+    -- NULL means the comment has gone outdated (no equivalent line on the
60
+    -- current head). Comments still display in the timeline + a "Show
61
+    -- outdated" toggle in the Files tab.
62
+    current_position      int,
63
+    body                  text              NOT NULL,
64
+    body_html_cached      text,
65
+    in_reply_to_id        bigint            REFERENCES pr_review_comments(id) ON DELETE SET NULL,
66
+    -- Server-side draft state: pending=true rows belong to a draft review
67
+    -- the author hasn't submitted yet. They flip to pending=false +
68
+    -- review_id=N when the review is submitted.
69
+    pending               boolean           NOT NULL DEFAULT false,
70
+    resolved_at           timestamptz,
71
+    resolved_by_user_id   bigint            REFERENCES users(id) ON DELETE SET NULL,
72
+    created_at            timestamptz       NOT NULL DEFAULT now(),
73
+    updated_at            timestamptz       NOT NULL DEFAULT now(),
74
+    edited_at             timestamptz,
75
+
76
+    CONSTRAINT pr_review_comments_body_length CHECK (char_length(body) BETWEEN 1 AND 65535)
77
+);
78
+
79
+CREATE INDEX pr_review_comments_pr_idx
80
+    ON pr_review_comments (pr_issue_id, created_at);
81
+CREATE INDEX pr_review_comments_review_idx
82
+    ON pr_review_comments (review_id) WHERE review_id IS NOT NULL;
83
+CREATE INDEX pr_review_comments_drafts_idx
84
+    ON pr_review_comments (pr_issue_id, author_user_id) WHERE pending = true;
85
+CREATE INDEX pr_review_comments_threads_idx
86
+    ON pr_review_comments (in_reply_to_id);
87
+
88
+CREATE TRIGGER set_updated_at BEFORE UPDATE ON pr_review_comments
89
+    FOR EACH ROW EXECUTE FUNCTION tg_set_updated_at();
90
+
91
+
92
+CREATE TABLE pr_review_requests (
93
+    id                       bigserial    PRIMARY KEY,
94
+    pr_issue_id              bigint       NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
95
+    requested_user_id        bigint       REFERENCES users(id) ON DELETE CASCADE,
96
+    -- Teams arrive in S31; column is in from day one so the migration
97
+    -- doesn't need to touch this table when teams ship.
98
+    requested_team_id        bigint,
99
+    requested_by_user_id     bigint       REFERENCES users(id) ON DELETE SET NULL,
100
+    requested_at             timestamptz  NOT NULL DEFAULT now(),
101
+    dismissed_at             timestamptz,
102
+    satisfied_by_review_id   bigint       REFERENCES pr_reviews(id) ON DELETE SET NULL,
103
+
104
+    CONSTRAINT pr_review_requests_target_xor CHECK (
105
+        (requested_user_id IS NOT NULL) <> (requested_team_id IS NOT NULL)
106
+    )
107
+);
108
+
109
+CREATE INDEX pr_review_requests_user_pending_idx
110
+    ON pr_review_requests (requested_user_id)
111
+    WHERE dismissed_at IS NULL AND satisfied_by_review_id IS NULL;
112
+CREATE INDEX pr_review_requests_pr_idx
113
+    ON pr_review_requests (pr_issue_id);
114
+
115
+
116
+-- Branch-protection knobs.
117
+ALTER TABLE branch_protection_rules
118
+    ADD COLUMN required_review_count          int  NOT NULL DEFAULT 0,
119
+    ADD COLUMN dismiss_stale_reviews_on_push  bool NOT NULL DEFAULT false,
120
+    ADD COLUMN require_code_owner_review      bool NOT NULL DEFAULT false,
121
+    ADD CONSTRAINT branch_protection_rules_required_review_nonneg CHECK (required_review_count >= 0);
122
+
123
+
124
+-- +goose Down
125
+ALTER TABLE branch_protection_rules
126
+    DROP CONSTRAINT IF EXISTS branch_protection_rules_required_review_nonneg,
127
+    DROP COLUMN IF EXISTS require_code_owner_review,
128
+    DROP COLUMN IF EXISTS dismiss_stale_reviews_on_push,
129
+    DROP COLUMN IF EXISTS required_review_count;
130
+DROP TABLE IF EXISTS pr_review_requests;
131
+DROP TABLE IF EXISTS pr_review_comments;
132
+DROP TABLE IF EXISTS pr_reviews;
133
+DROP TYPE IF EXISTS pr_review_side;
134
+DROP TYPE IF EXISTS pr_review_state;
internal/pulls/queries/reviews.sqladded
@@ -0,0 +1,170 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+-- ─── pr_reviews ──────────────────────────────────────────────────────
4
+
5
+-- name: CreatePRReview :one
6
+INSERT INTO pr_reviews (
7
+    pr_issue_id, author_user_id, state, body, body_html_cached
8
+) VALUES (
9
+    $1, sqlc.narg(author_user_id)::bigint, $2, $3, sqlc.narg(body_html_cached)::text
10
+)
11
+RETURNING *;
12
+
13
+-- name: GetPRReviewByID :one
14
+SELECT * FROM pr_reviews WHERE id = $1;
15
+
16
+-- name: ListPRReviews :many
17
+SELECT * FROM pr_reviews
18
+WHERE pr_issue_id = $1
19
+ORDER BY submitted_at;
20
+
21
+-- name: DismissPRReview :exec
22
+UPDATE pr_reviews
23
+SET dismissed_at = now(),
24
+    dismissed_by_user_id = sqlc.narg(dismissed_by_user_id)::bigint,
25
+    dismissal_reason = $2
26
+WHERE id = $1;
27
+
28
+-- name: CountPRReviewsForGate :one
29
+-- Returns the counts the merge gate cares about. `approves` excludes
30
+-- dismissed reviews; `request_changes` includes only undismissed and
31
+-- unsuperseded-by-same-author reviews. The "unsuperseded" semantics
32
+-- (a later review by the same author wins) is computed in Go on the
33
+-- caller side; this query only returns the raw rows it needs.
34
+SELECT
35
+    count(*) FILTER (WHERE state = 'approve' AND dismissed_at IS NULL)::int AS approves,
36
+    count(*) FILTER (WHERE state = 'request_changes' AND dismissed_at IS NULL)::int AS request_changes
37
+FROM pr_reviews
38
+WHERE pr_issue_id = $1;
39
+
40
+-- name: ListUndismissedReviewsForGate :many
41
+-- Used by the merge gate to compute "latest review per author" semantics
42
+-- in Go. Ordered author + submitted_at so the caller can pick the last
43
+-- per author cheaply.
44
+SELECT id, pr_issue_id, author_user_id, state, submitted_at
45
+FROM pr_reviews
46
+WHERE pr_issue_id = $1
47
+  AND dismissed_at IS NULL
48
+ORDER BY author_user_id, submitted_at;
49
+
50
+
51
+-- ─── pr_review_comments ──────────────────────────────────────────────
52
+
53
+-- name: CreatePRReviewComment :one
54
+INSERT INTO pr_review_comments (
55
+    pr_issue_id, review_id, author_user_id, file_path, side,
56
+    original_commit_sha, original_line, original_position, current_position,
57
+    body, body_html_cached, in_reply_to_id, pending
58
+) VALUES (
59
+    $1, sqlc.narg(review_id)::bigint, sqlc.narg(author_user_id)::bigint, $2, $3,
60
+    $4, $5, $6, $7,
61
+    $8, sqlc.narg(body_html_cached)::text, sqlc.narg(in_reply_to_id)::bigint, $9
62
+)
63
+RETURNING *;
64
+
65
+-- name: GetPRReviewComment :one
66
+SELECT * FROM pr_review_comments WHERE id = $1;
67
+
68
+-- name: ListPRReviewComments :many
69
+SELECT * FROM pr_review_comments
70
+WHERE pr_issue_id = $1
71
+ORDER BY created_at;
72
+
73
+-- name: ListPRReviewCommentsForFile :many
74
+-- Files-tab fetch: comments anchored to a single file path, oldest first.
75
+SELECT * FROM pr_review_comments
76
+WHERE pr_issue_id = $1
77
+  AND file_path = $2
78
+  AND pending = false
79
+ORDER BY created_at;
80
+
81
+-- name: ListPendingDraftCommentsForUser :many
82
+-- Server-side draft listing: rows with pending=true belonging to one
83
+-- user. Ordered by creation so the submit step processes them in order.
84
+SELECT * FROM pr_review_comments
85
+WHERE pr_issue_id = $1
86
+  AND author_user_id = $2
87
+  AND pending = true
88
+ORDER BY created_at;
89
+
90
+-- name: AttachPendingCommentsToReview :exec
91
+-- One-shot UPDATE that flips a user's pending draft comments on a PR
92
+-- into the just-submitted review. Runs inside the submit-review tx.
93
+UPDATE pr_review_comments
94
+SET review_id = $3, pending = false, updated_at = now()
95
+WHERE pr_issue_id = $1
96
+  AND author_user_id = $2
97
+  AND pending = true;
98
+
99
+-- name: UpdatePRReviewCommentBody :exec
100
+UPDATE pr_review_comments
101
+SET body = $2, body_html_cached = sqlc.narg(body_html_cached)::text,
102
+    edited_at = now(), updated_at = now()
103
+WHERE id = $1;
104
+
105
+-- name: SetPRReviewCommentResolved :exec
106
+UPDATE pr_review_comments
107
+SET resolved_at = sqlc.narg(resolved_at)::timestamptz,
108
+    resolved_by_user_id = sqlc.narg(resolved_by_user_id)::bigint,
109
+    updated_at = now()
110
+WHERE id = $1;
111
+
112
+-- name: SetPRReviewCommentCurrentPosition :exec
113
+-- Position-mapping update emitted by pulls.Synchronize. NULL means
114
+-- the comment has gone outdated.
115
+UPDATE pr_review_comments
116
+SET current_position = sqlc.narg(current_position)::int,
117
+    updated_at = now()
118
+WHERE id = $1;
119
+
120
+-- name: ListNonDraftCommentsForPositionMap :many
121
+-- Position-mapping reads only submitted comments (drafts re-anchor
122
+-- when the user resumes the diff view).
123
+SELECT id, file_path, side, original_commit_sha, original_line, original_position
124
+FROM pr_review_comments
125
+WHERE pr_issue_id = $1
126
+  AND pending = false;
127
+
128
+
129
+-- ─── pr_review_requests ──────────────────────────────────────────────
130
+
131
+-- name: CreatePRReviewRequest :one
132
+INSERT INTO pr_review_requests (
133
+    pr_issue_id, requested_user_id, requested_team_id, requested_by_user_id
134
+) VALUES (
135
+    $1, sqlc.narg(requested_user_id)::bigint, sqlc.narg(requested_team_id)::bigint, sqlc.narg(requested_by_user_id)::bigint
136
+)
137
+RETURNING *;
138
+
139
+-- name: ListPRReviewRequests :many
140
+SELECT * FROM pr_review_requests
141
+WHERE pr_issue_id = $1
142
+ORDER BY requested_at;
143
+
144
+-- name: ListPendingReviewRequestsForUser :many
145
+-- Reviewer's inbox feed. Excludes dismissed + satisfied requests.
146
+SELECT pr.* FROM pr_review_requests pr
147
+WHERE requested_user_id = $1
148
+  AND dismissed_at IS NULL
149
+  AND satisfied_by_review_id IS NULL
150
+ORDER BY requested_at DESC;
151
+
152
+-- name: SatisfyPRReviewRequest :exec
153
+UPDATE pr_review_requests
154
+SET satisfied_by_review_id = $2
155
+WHERE pr_issue_id = $1
156
+  AND requested_user_id = $3
157
+  AND dismissed_at IS NULL
158
+  AND satisfied_by_review_id IS NULL;
159
+
160
+-- name: DismissPRReviewRequest :exec
161
+UPDATE pr_review_requests
162
+SET dismissed_at = now()
163
+WHERE id = $1;
164
+
165
+-- name: CountActivePRReviewRequests :one
166
+-- Used by the rate-limit gate (max 20 reviewers per PR per the spec
167
+-- pitfall section).
168
+SELECT count(*)::int FROM pr_review_requests
169
+WHERE pr_issue_id = $1
170
+  AND dismissed_at IS NULL;
internal/pulls/sqlc/models.gomodified
@@ -403,6 +403,91 @@ func (ns NullPrMergeableState) Value() (driver.Value, error) {
403403
 	return string(ns.PrMergeableState), nil
404404
 }
405405
 
406
+type PrReviewSide string
407
+
408
+const (
409
+	PrReviewSideLeft  PrReviewSide = "left"
410
+	PrReviewSideRight PrReviewSide = "right"
411
+)
412
+
413
+func (e *PrReviewSide) Scan(src interface{}) error {
414
+	switch s := src.(type) {
415
+	case []byte:
416
+		*e = PrReviewSide(s)
417
+	case string:
418
+		*e = PrReviewSide(s)
419
+	default:
420
+		return fmt.Errorf("unsupported scan type for PrReviewSide: %T", src)
421
+	}
422
+	return nil
423
+}
424
+
425
+type NullPrReviewSide struct {
426
+	PrReviewSide PrReviewSide
427
+	Valid        bool // Valid is true if PrReviewSide is not NULL
428
+}
429
+
430
+// Scan implements the Scanner interface.
431
+func (ns *NullPrReviewSide) Scan(value interface{}) error {
432
+	if value == nil {
433
+		ns.PrReviewSide, ns.Valid = "", false
434
+		return nil
435
+	}
436
+	ns.Valid = true
437
+	return ns.PrReviewSide.Scan(value)
438
+}
439
+
440
+// Value implements the driver Valuer interface.
441
+func (ns NullPrReviewSide) Value() (driver.Value, error) {
442
+	if !ns.Valid {
443
+		return nil, nil
444
+	}
445
+	return string(ns.PrReviewSide), nil
446
+}
447
+
448
+type PrReviewState string
449
+
450
+const (
451
+	PrReviewStateComment        PrReviewState = "comment"
452
+	PrReviewStateApprove        PrReviewState = "approve"
453
+	PrReviewStateRequestChanges PrReviewState = "request_changes"
454
+)
455
+
456
+func (e *PrReviewState) Scan(src interface{}) error {
457
+	switch s := src.(type) {
458
+	case []byte:
459
+		*e = PrReviewState(s)
460
+	case string:
461
+		*e = PrReviewState(s)
462
+	default:
463
+		return fmt.Errorf("unsupported scan type for PrReviewState: %T", src)
464
+	}
465
+	return nil
466
+}
467
+
468
+type NullPrReviewState struct {
469
+	PrReviewState PrReviewState
470
+	Valid         bool // Valid is true if PrReviewState is not NULL
471
+}
472
+
473
+// Scan implements the Scanner interface.
474
+func (ns *NullPrReviewState) Scan(value interface{}) error {
475
+	if value == nil {
476
+		ns.PrReviewState, ns.Valid = "", false
477
+		return nil
478
+	}
479
+	ns.Valid = true
480
+	return ns.PrReviewState.Scan(value)
481
+}
482
+
483
+// Value implements the driver Valuer interface.
484
+func (ns NullPrReviewState) Value() (driver.Value, error) {
485
+	if !ns.Valid {
486
+		return nil, nil
487
+	}
488
+	return string(ns.PrReviewState), nil
489
+}
490
+
406491
 type RepoVisibility string
407492
 
408493
 const (
@@ -551,18 +636,21 @@ type AuthThrottle struct {
551636
 }
552637
 
553638
 type BranchProtectionRule struct {
554
-	ID                   int64
555
-	RepoID               int64
556
-	Pattern              string
557
-	PreventForcePush     bool
558
-	PreventDeletion      bool
559
-	RequirePrForPush     bool
560
-	AllowedPusherUserIds []int64
561
-	RequireSignedCommits bool
562
-	StatusChecksRequired []string
563
-	CreatedAt            pgtype.Timestamptz
564
-	UpdatedAt            pgtype.Timestamptz
565
-	CreatedByUserID      pgtype.Int8
639
+	ID                        int64
640
+	RepoID                    int64
641
+	Pattern                   string
642
+	PreventForcePush          bool
643
+	PreventDeletion           bool
644
+	RequirePrForPush          bool
645
+	AllowedPusherUserIds      []int64
646
+	RequireSignedCommits      bool
647
+	StatusChecksRequired      []string
648
+	CreatedAt                 pgtype.Timestamptz
649
+	UpdatedAt                 pgtype.Timestamptz
650
+	CreatedByUserID           pgtype.Int8
651
+	RequiredReviewCount       int32
652
+	DismissStaleReviewsOnPush bool
653
+	RequireCodeOwnerReview    bool
566654
 }
567655
 
568656
 type EmailVerification struct {
@@ -691,6 +779,52 @@ type PasswordReset struct {
691779
 	CreatedAt pgtype.Timestamptz
692780
 }
693781
 
782
+type PrReview struct {
783
+	ID                int64
784
+	PrIssueID         int64
785
+	AuthorUserID      pgtype.Int8
786
+	State             PrReviewState
787
+	Body              string
788
+	BodyHtmlCached    pgtype.Text
789
+	SubmittedAt       pgtype.Timestamptz
790
+	DismissedAt       pgtype.Timestamptz
791
+	DismissedByUserID pgtype.Int8
792
+	DismissalReason   string
793
+}
794
+
795
+type PrReviewComment struct {
796
+	ID                int64
797
+	PrIssueID         int64
798
+	ReviewID          pgtype.Int8
799
+	AuthorUserID      pgtype.Int8
800
+	FilePath          string
801
+	Side              PrReviewSide
802
+	OriginalCommitSha string
803
+	OriginalLine      int32
804
+	OriginalPosition  int32
805
+	CurrentPosition   pgtype.Int4
806
+	Body              string
807
+	BodyHtmlCached    pgtype.Text
808
+	InReplyToID       pgtype.Int8
809
+	Pending           bool
810
+	ResolvedAt        pgtype.Timestamptz
811
+	ResolvedByUserID  pgtype.Int8
812
+	CreatedAt         pgtype.Timestamptz
813
+	UpdatedAt         pgtype.Timestamptz
814
+	EditedAt          pgtype.Timestamptz
815
+}
816
+
817
+type PrReviewRequest struct {
818
+	ID                  int64
819
+	PrIssueID           int64
820
+	RequestedUserID     pgtype.Int8
821
+	RequestedTeamID     pgtype.Int8
822
+	RequestedByUserID   pgtype.Int8
823
+	RequestedAt         pgtype.Timestamptz
824
+	DismissedAt         pgtype.Timestamptz
825
+	SatisfiedByReviewID pgtype.Int8
826
+}
827
+
694828
 type PullRequest struct {
695829
 	IssueID            int64
696830
 	BaseRef            string
internal/pulls/sqlc/querier.gomodified
@@ -6,44 +6,92 @@ package pullsdb
66
 
77
 import (
88
 	"context"
9
+
10
+	"github.com/jackc/pgx/v5/pgtype"
911
 )
1012
 
1113
 type Querier interface {
14
+	// One-shot UPDATE that flips a user's pending draft comments on a PR
15
+	// into the just-submitted review. Runs inside the submit-review tx.
16
+	AttachPendingCommentsToReview(ctx context.Context, db DBTX, arg AttachPendingCommentsToReviewParams) error
1217
 	// ─── commits + files (refreshed on synchronize) ──────────────────────
1318
 	ClearPullRequestCommits(ctx context.Context, db DBTX, prID int64) error
1419
 	ClearPullRequestFiles(ctx context.Context, db DBTX, prID int64) error
20
+	// Used by the rate-limit gate (max 20 reviewers per PR per the spec
21
+	// pitfall section).
22
+	CountActivePRReviewRequests(ctx context.Context, db DBTX, prIssueID int64) (int32, error)
23
+	// Returns the counts the merge gate cares about. `approves` excludes
24
+	// dismissed reviews; `request_changes` includes only undismissed and
25
+	// unsuperseded-by-same-author reviews. The "unsuperseded" semantics
26
+	// (a later review by the same author wins) is computed in Go on the
27
+	// caller side; this query only returns the raw rows it needs.
28
+	CountPRReviewsForGate(ctx context.Context, db DBTX, prIssueID int64) (CountPRReviewsForGateRow, error)
1529
 	CountPullRequestsByRepo(ctx context.Context, db DBTX, arg CountPullRequestsByRepoParams) (int64, error)
1630
 	// SPDX-License-Identifier: AGPL-3.0-or-later
31
+	// ─── pr_reviews ──────────────────────────────────────────────────────
32
+	CreatePRReview(ctx context.Context, db DBTX, arg CreatePRReviewParams) (PrReview, error)
33
+	// ─── pr_review_comments ──────────────────────────────────────────────
34
+	CreatePRReviewComment(ctx context.Context, db DBTX, arg CreatePRReviewCommentParams) (PrReviewComment, error)
35
+	// ─── pr_review_requests ──────────────────────────────────────────────
36
+	CreatePRReviewRequest(ctx context.Context, db DBTX, arg CreatePRReviewRequestParams) (PrReviewRequest, error)
37
+	// SPDX-License-Identifier: AGPL-3.0-or-later
1738
 	// ─── pull_requests core ──────────────────────────────────────────────
1839
 	// Creates the PR-side row keyed on issue_id (the caller already
1940
 	// inserted the issues row with kind='pr'). Defaults give a freshly-
2041
 	// opened PR `mergeable_state='unknown'` until the mergeability job
2142
 	// ticks.
2243
 	CreatePullRequest(ctx context.Context, db DBTX, arg CreatePullRequestParams) (PullRequest, error)
44
+	DismissPRReview(ctx context.Context, db DBTX, arg DismissPRReviewParams) error
45
+	DismissPRReviewRequest(ctx context.Context, db DBTX, id int64) error
46
+	GetPRReviewByID(ctx context.Context, db DBTX, id int64) (PrReview, error)
47
+	GetPRReviewComment(ctx context.Context, db DBTX, id int64) (PrReviewComment, error)
2348
 	GetPullRequestByIssueID(ctx context.Context, db DBTX, issueID int64) (PullRequest, error)
2449
 	// Joins issues + pull_requests so handlers can resolve via the URL
2550
 	// {owner}/{repo}/pulls/{number} in one round-trip.
2651
 	GetPullRequestByRepoAndNumber(ctx context.Context, db DBTX, arg GetPullRequestByRepoAndNumberParams) (GetPullRequestByRepoAndNumberRow, error)
2752
 	InsertPullRequestCommit(ctx context.Context, db DBTX, arg InsertPullRequestCommitParams) error
2853
 	InsertPullRequestFile(ctx context.Context, db DBTX, arg InsertPullRequestFileParams) error
54
+	// Position-mapping reads only submitted comments (drafts re-anchor
55
+	// when the user resumes the diff view).
56
+	ListNonDraftCommentsForPositionMap(ctx context.Context, db DBTX, prIssueID int64) ([]ListNonDraftCommentsForPositionMapRow, error)
2957
 	// Returns the issue_ids of every still-open PR whose head_repo_id +
3058
 	// head_ref match the pushed ref. push:process uses this to fan-out
3159
 	// pr:synchronize jobs after a head-side push.
3260
 	ListOpenPRsForHeadRef(ctx context.Context, db DBTX, arg ListOpenPRsForHeadRefParams) ([]int64, error)
61
+	ListPRReviewComments(ctx context.Context, db DBTX, prIssueID int64) ([]PrReviewComment, error)
62
+	// Files-tab fetch: comments anchored to a single file path, oldest first.
63
+	ListPRReviewCommentsForFile(ctx context.Context, db DBTX, arg ListPRReviewCommentsForFileParams) ([]PrReviewComment, error)
64
+	ListPRReviewRequests(ctx context.Context, db DBTX, prIssueID int64) ([]PrReviewRequest, error)
65
+	ListPRReviews(ctx context.Context, db DBTX, prIssueID int64) ([]PrReview, error)
66
+	// Server-side draft listing: rows with pending=true belonging to one
67
+	// user. Ordered by creation so the submit step processes them in order.
68
+	ListPendingDraftCommentsForUser(ctx context.Context, db DBTX, arg ListPendingDraftCommentsForUserParams) ([]PrReviewComment, error)
69
+	// Reviewer's inbox feed. Excludes dismissed + satisfied requests.
70
+	ListPendingReviewRequestsForUser(ctx context.Context, db DBTX, requestedUserID pgtype.Int8) ([]PrReviewRequest, error)
3371
 	ListPullRequestCommits(ctx context.Context, db DBTX, prID int64) ([]PullRequestCommit, error)
3472
 	ListPullRequestFiles(ctx context.Context, db DBTX, prID int64) ([]PullRequestFile, error)
3573
 	// Mirrors the issues list query: state filter via narg, pagination,
3674
 	// ordered by recent activity.
3775
 	ListPullRequestsByRepo(ctx context.Context, db DBTX, arg ListPullRequestsByRepoParams) ([]ListPullRequestsByRepoRow, error)
76
+	// Used by the merge gate to compute "latest review per author" semantics
77
+	// in Go. Ordered author + submitted_at so the caller can pick the last
78
+	// per author cheaply.
79
+	ListUndismissedReviewsForGate(ctx context.Context, db DBTX, prIssueID int64) ([]ListUndismissedReviewsForGateRow, error)
3880
 	// FOR UPDATE row lock + return current shape so the merge job can
3981
 	// decide whether to proceed (e.g. someone else just merged it).
4082
 	LockPullRequestForMerge(ctx context.Context, db DBTX, issueID int64) (PullRequest, error)
83
+	SatisfyPRReviewRequest(ctx context.Context, db DBTX, arg SatisfyPRReviewRequestParams) error
84
+	// Position-mapping update emitted by pulls.Synchronize. NULL means
85
+	// the comment has gone outdated.
86
+	SetPRReviewCommentCurrentPosition(ctx context.Context, db DBTX, arg SetPRReviewCommentCurrentPositionParams) error
87
+	SetPRReviewCommentResolved(ctx context.Context, db DBTX, arg SetPRReviewCommentResolvedParams) error
4188
 	SetPullRequestDraft(ctx context.Context, db DBTX, arg SetPullRequestDraftParams) error
4289
 	SetPullRequestMergeability(ctx context.Context, db DBTX, arg SetPullRequestMergeabilityParams) error
4390
 	SetPullRequestMerged(ctx context.Context, db DBTX, arg SetPullRequestMergedParams) error
4491
 	// Updates base_oid + head_oid + last_synchronized_at after a
4592
 	// synchronize tick.
4693
 	SetPullRequestSnapshot(ctx context.Context, db DBTX, arg SetPullRequestSnapshotParams) error
94
+	UpdatePRReviewCommentBody(ctx context.Context, db DBTX, arg UpdatePRReviewCommentBodyParams) error
4795
 }
4896
 
4997
 var _ Querier = (*Queries)(nil)
internal/pulls/sqlc/reviews.sql.goadded
@@ -0,0 +1,743 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+// source: reviews.sql
5
+
6
+package pullsdb
7
+
8
+import (
9
+	"context"
10
+
11
+	"github.com/jackc/pgx/v5/pgtype"
12
+)
13
+
14
+const attachPendingCommentsToReview = `-- name: AttachPendingCommentsToReview :exec
15
+UPDATE pr_review_comments
16
+SET review_id = $3, pending = false, updated_at = now()
17
+WHERE pr_issue_id = $1
18
+  AND author_user_id = $2
19
+  AND pending = true
20
+`
21
+
22
+type AttachPendingCommentsToReviewParams struct {
23
+	PrIssueID    int64
24
+	AuthorUserID pgtype.Int8
25
+	ReviewID     pgtype.Int8
26
+}
27
+
28
+// One-shot UPDATE that flips a user's pending draft comments on a PR
29
+// into the just-submitted review. Runs inside the submit-review tx.
30
+func (q *Queries) AttachPendingCommentsToReview(ctx context.Context, db DBTX, arg AttachPendingCommentsToReviewParams) error {
31
+	_, err := db.Exec(ctx, attachPendingCommentsToReview, arg.PrIssueID, arg.AuthorUserID, arg.ReviewID)
32
+	return err
33
+}
34
+
35
+const countActivePRReviewRequests = `-- name: CountActivePRReviewRequests :one
36
+SELECT count(*)::int FROM pr_review_requests
37
+WHERE pr_issue_id = $1
38
+  AND dismissed_at IS NULL
39
+`
40
+
41
+// Used by the rate-limit gate (max 20 reviewers per PR per the spec
42
+// pitfall section).
43
+func (q *Queries) CountActivePRReviewRequests(ctx context.Context, db DBTX, prIssueID int64) (int32, error) {
44
+	row := db.QueryRow(ctx, countActivePRReviewRequests, prIssueID)
45
+	var column_1 int32
46
+	err := row.Scan(&column_1)
47
+	return column_1, err
48
+}
49
+
50
+const countPRReviewsForGate = `-- name: CountPRReviewsForGate :one
51
+SELECT
52
+    count(*) FILTER (WHERE state = 'approve' AND dismissed_at IS NULL)::int AS approves,
53
+    count(*) FILTER (WHERE state = 'request_changes' AND dismissed_at IS NULL)::int AS request_changes
54
+FROM pr_reviews
55
+WHERE pr_issue_id = $1
56
+`
57
+
58
+type CountPRReviewsForGateRow struct {
59
+	Approves       int32
60
+	RequestChanges int32
61
+}
62
+
63
+// Returns the counts the merge gate cares about. `approves` excludes
64
+// dismissed reviews; `request_changes` includes only undismissed and
65
+// unsuperseded-by-same-author reviews. The "unsuperseded" semantics
66
+// (a later review by the same author wins) is computed in Go on the
67
+// caller side; this query only returns the raw rows it needs.
68
+func (q *Queries) CountPRReviewsForGate(ctx context.Context, db DBTX, prIssueID int64) (CountPRReviewsForGateRow, error) {
69
+	row := db.QueryRow(ctx, countPRReviewsForGate, prIssueID)
70
+	var i CountPRReviewsForGateRow
71
+	err := row.Scan(&i.Approves, &i.RequestChanges)
72
+	return i, err
73
+}
74
+
75
+const createPRReview = `-- name: CreatePRReview :one
76
+
77
+
78
+INSERT INTO pr_reviews (
79
+    pr_issue_id, author_user_id, state, body, body_html_cached
80
+) VALUES (
81
+    $1, $4::bigint, $2, $3, $5::text
82
+)
83
+RETURNING id, pr_issue_id, author_user_id, state, body, body_html_cached, submitted_at, dismissed_at, dismissed_by_user_id, dismissal_reason
84
+`
85
+
86
+type CreatePRReviewParams struct {
87
+	PrIssueID      int64
88
+	State          PrReviewState
89
+	Body           string
90
+	AuthorUserID   pgtype.Int8
91
+	BodyHtmlCached pgtype.Text
92
+}
93
+
94
+// SPDX-License-Identifier: AGPL-3.0-or-later
95
+// ─── pr_reviews ──────────────────────────────────────────────────────
96
+func (q *Queries) CreatePRReview(ctx context.Context, db DBTX, arg CreatePRReviewParams) (PrReview, error) {
97
+	row := db.QueryRow(ctx, createPRReview,
98
+		arg.PrIssueID,
99
+		arg.State,
100
+		arg.Body,
101
+		arg.AuthorUserID,
102
+		arg.BodyHtmlCached,
103
+	)
104
+	var i PrReview
105
+	err := row.Scan(
106
+		&i.ID,
107
+		&i.PrIssueID,
108
+		&i.AuthorUserID,
109
+		&i.State,
110
+		&i.Body,
111
+		&i.BodyHtmlCached,
112
+		&i.SubmittedAt,
113
+		&i.DismissedAt,
114
+		&i.DismissedByUserID,
115
+		&i.DismissalReason,
116
+	)
117
+	return i, err
118
+}
119
+
120
+const createPRReviewComment = `-- name: CreatePRReviewComment :one
121
+
122
+INSERT INTO pr_review_comments (
123
+    pr_issue_id, review_id, author_user_id, file_path, side,
124
+    original_commit_sha, original_line, original_position, current_position,
125
+    body, body_html_cached, in_reply_to_id, pending
126
+) VALUES (
127
+    $1, $10::bigint, $11::bigint, $2, $3,
128
+    $4, $5, $6, $7,
129
+    $8, $12::text, $13::bigint, $9
130
+)
131
+RETURNING id, pr_issue_id, review_id, author_user_id, file_path, side, original_commit_sha, original_line, original_position, current_position, body, body_html_cached, in_reply_to_id, pending, resolved_at, resolved_by_user_id, created_at, updated_at, edited_at
132
+`
133
+
134
+type CreatePRReviewCommentParams struct {
135
+	PrIssueID         int64
136
+	FilePath          string
137
+	Side              PrReviewSide
138
+	OriginalCommitSha string
139
+	OriginalLine      int32
140
+	OriginalPosition  int32
141
+	CurrentPosition   pgtype.Int4
142
+	Body              string
143
+	Pending           bool
144
+	ReviewID          pgtype.Int8
145
+	AuthorUserID      pgtype.Int8
146
+	BodyHtmlCached    pgtype.Text
147
+	InReplyToID       pgtype.Int8
148
+}
149
+
150
+// ─── pr_review_comments ──────────────────────────────────────────────
151
+func (q *Queries) CreatePRReviewComment(ctx context.Context, db DBTX, arg CreatePRReviewCommentParams) (PrReviewComment, error) {
152
+	row := db.QueryRow(ctx, createPRReviewComment,
153
+		arg.PrIssueID,
154
+		arg.FilePath,
155
+		arg.Side,
156
+		arg.OriginalCommitSha,
157
+		arg.OriginalLine,
158
+		arg.OriginalPosition,
159
+		arg.CurrentPosition,
160
+		arg.Body,
161
+		arg.Pending,
162
+		arg.ReviewID,
163
+		arg.AuthorUserID,
164
+		arg.BodyHtmlCached,
165
+		arg.InReplyToID,
166
+	)
167
+	var i PrReviewComment
168
+	err := row.Scan(
169
+		&i.ID,
170
+		&i.PrIssueID,
171
+		&i.ReviewID,
172
+		&i.AuthorUserID,
173
+		&i.FilePath,
174
+		&i.Side,
175
+		&i.OriginalCommitSha,
176
+		&i.OriginalLine,
177
+		&i.OriginalPosition,
178
+		&i.CurrentPosition,
179
+		&i.Body,
180
+		&i.BodyHtmlCached,
181
+		&i.InReplyToID,
182
+		&i.Pending,
183
+		&i.ResolvedAt,
184
+		&i.ResolvedByUserID,
185
+		&i.CreatedAt,
186
+		&i.UpdatedAt,
187
+		&i.EditedAt,
188
+	)
189
+	return i, err
190
+}
191
+
192
+const createPRReviewRequest = `-- name: CreatePRReviewRequest :one
193
+
194
+INSERT INTO pr_review_requests (
195
+    pr_issue_id, requested_user_id, requested_team_id, requested_by_user_id
196
+) VALUES (
197
+    $1, $2::bigint, $3::bigint, $4::bigint
198
+)
199
+RETURNING id, pr_issue_id, requested_user_id, requested_team_id, requested_by_user_id, requested_at, dismissed_at, satisfied_by_review_id
200
+`
201
+
202
+type CreatePRReviewRequestParams struct {
203
+	PrIssueID         int64
204
+	RequestedUserID   pgtype.Int8
205
+	RequestedTeamID   pgtype.Int8
206
+	RequestedByUserID pgtype.Int8
207
+}
208
+
209
+// ─── pr_review_requests ──────────────────────────────────────────────
210
+func (q *Queries) CreatePRReviewRequest(ctx context.Context, db DBTX, arg CreatePRReviewRequestParams) (PrReviewRequest, error) {
211
+	row := db.QueryRow(ctx, createPRReviewRequest,
212
+		arg.PrIssueID,
213
+		arg.RequestedUserID,
214
+		arg.RequestedTeamID,
215
+		arg.RequestedByUserID,
216
+	)
217
+	var i PrReviewRequest
218
+	err := row.Scan(
219
+		&i.ID,
220
+		&i.PrIssueID,
221
+		&i.RequestedUserID,
222
+		&i.RequestedTeamID,
223
+		&i.RequestedByUserID,
224
+		&i.RequestedAt,
225
+		&i.DismissedAt,
226
+		&i.SatisfiedByReviewID,
227
+	)
228
+	return i, err
229
+}
230
+
231
+const dismissPRReview = `-- name: DismissPRReview :exec
232
+UPDATE pr_reviews
233
+SET dismissed_at = now(),
234
+    dismissed_by_user_id = $3::bigint,
235
+    dismissal_reason = $2
236
+WHERE id = $1
237
+`
238
+
239
+type DismissPRReviewParams struct {
240
+	ID                int64
241
+	DismissalReason   string
242
+	DismissedByUserID pgtype.Int8
243
+}
244
+
245
+func (q *Queries) DismissPRReview(ctx context.Context, db DBTX, arg DismissPRReviewParams) error {
246
+	_, err := db.Exec(ctx, dismissPRReview, arg.ID, arg.DismissalReason, arg.DismissedByUserID)
247
+	return err
248
+}
249
+
250
+const dismissPRReviewRequest = `-- name: DismissPRReviewRequest :exec
251
+UPDATE pr_review_requests
252
+SET dismissed_at = now()
253
+WHERE id = $1
254
+`
255
+
256
+func (q *Queries) DismissPRReviewRequest(ctx context.Context, db DBTX, id int64) error {
257
+	_, err := db.Exec(ctx, dismissPRReviewRequest, id)
258
+	return err
259
+}
260
+
261
+const getPRReviewByID = `-- name: GetPRReviewByID :one
262
+SELECT id, pr_issue_id, author_user_id, state, body, body_html_cached, submitted_at, dismissed_at, dismissed_by_user_id, dismissal_reason FROM pr_reviews WHERE id = $1
263
+`
264
+
265
+func (q *Queries) GetPRReviewByID(ctx context.Context, db DBTX, id int64) (PrReview, error) {
266
+	row := db.QueryRow(ctx, getPRReviewByID, id)
267
+	var i PrReview
268
+	err := row.Scan(
269
+		&i.ID,
270
+		&i.PrIssueID,
271
+		&i.AuthorUserID,
272
+		&i.State,
273
+		&i.Body,
274
+		&i.BodyHtmlCached,
275
+		&i.SubmittedAt,
276
+		&i.DismissedAt,
277
+		&i.DismissedByUserID,
278
+		&i.DismissalReason,
279
+	)
280
+	return i, err
281
+}
282
+
283
+const getPRReviewComment = `-- name: GetPRReviewComment :one
284
+SELECT id, pr_issue_id, review_id, author_user_id, file_path, side, original_commit_sha, original_line, original_position, current_position, body, body_html_cached, in_reply_to_id, pending, resolved_at, resolved_by_user_id, created_at, updated_at, edited_at FROM pr_review_comments WHERE id = $1
285
+`
286
+
287
+func (q *Queries) GetPRReviewComment(ctx context.Context, db DBTX, id int64) (PrReviewComment, error) {
288
+	row := db.QueryRow(ctx, getPRReviewComment, id)
289
+	var i PrReviewComment
290
+	err := row.Scan(
291
+		&i.ID,
292
+		&i.PrIssueID,
293
+		&i.ReviewID,
294
+		&i.AuthorUserID,
295
+		&i.FilePath,
296
+		&i.Side,
297
+		&i.OriginalCommitSha,
298
+		&i.OriginalLine,
299
+		&i.OriginalPosition,
300
+		&i.CurrentPosition,
301
+		&i.Body,
302
+		&i.BodyHtmlCached,
303
+		&i.InReplyToID,
304
+		&i.Pending,
305
+		&i.ResolvedAt,
306
+		&i.ResolvedByUserID,
307
+		&i.CreatedAt,
308
+		&i.UpdatedAt,
309
+		&i.EditedAt,
310
+	)
311
+	return i, err
312
+}
313
+
314
+const listNonDraftCommentsForPositionMap = `-- name: ListNonDraftCommentsForPositionMap :many
315
+SELECT id, file_path, side, original_commit_sha, original_line, original_position
316
+FROM pr_review_comments
317
+WHERE pr_issue_id = $1
318
+  AND pending = false
319
+`
320
+
321
+type ListNonDraftCommentsForPositionMapRow struct {
322
+	ID                int64
323
+	FilePath          string
324
+	Side              PrReviewSide
325
+	OriginalCommitSha string
326
+	OriginalLine      int32
327
+	OriginalPosition  int32
328
+}
329
+
330
+// Position-mapping reads only submitted comments (drafts re-anchor
331
+// when the user resumes the diff view).
332
+func (q *Queries) ListNonDraftCommentsForPositionMap(ctx context.Context, db DBTX, prIssueID int64) ([]ListNonDraftCommentsForPositionMapRow, error) {
333
+	rows, err := db.Query(ctx, listNonDraftCommentsForPositionMap, prIssueID)
334
+	if err != nil {
335
+		return nil, err
336
+	}
337
+	defer rows.Close()
338
+	items := []ListNonDraftCommentsForPositionMapRow{}
339
+	for rows.Next() {
340
+		var i ListNonDraftCommentsForPositionMapRow
341
+		if err := rows.Scan(
342
+			&i.ID,
343
+			&i.FilePath,
344
+			&i.Side,
345
+			&i.OriginalCommitSha,
346
+			&i.OriginalLine,
347
+			&i.OriginalPosition,
348
+		); err != nil {
349
+			return nil, err
350
+		}
351
+		items = append(items, i)
352
+	}
353
+	if err := rows.Err(); err != nil {
354
+		return nil, err
355
+	}
356
+	return items, nil
357
+}
358
+
359
+const listPRReviewComments = `-- name: ListPRReviewComments :many
360
+SELECT id, pr_issue_id, review_id, author_user_id, file_path, side, original_commit_sha, original_line, original_position, current_position, body, body_html_cached, in_reply_to_id, pending, resolved_at, resolved_by_user_id, created_at, updated_at, edited_at FROM pr_review_comments
361
+WHERE pr_issue_id = $1
362
+ORDER BY created_at
363
+`
364
+
365
+func (q *Queries) ListPRReviewComments(ctx context.Context, db DBTX, prIssueID int64) ([]PrReviewComment, error) {
366
+	rows, err := db.Query(ctx, listPRReviewComments, prIssueID)
367
+	if err != nil {
368
+		return nil, err
369
+	}
370
+	defer rows.Close()
371
+	items := []PrReviewComment{}
372
+	for rows.Next() {
373
+		var i PrReviewComment
374
+		if err := rows.Scan(
375
+			&i.ID,
376
+			&i.PrIssueID,
377
+			&i.ReviewID,
378
+			&i.AuthorUserID,
379
+			&i.FilePath,
380
+			&i.Side,
381
+			&i.OriginalCommitSha,
382
+			&i.OriginalLine,
383
+			&i.OriginalPosition,
384
+			&i.CurrentPosition,
385
+			&i.Body,
386
+			&i.BodyHtmlCached,
387
+			&i.InReplyToID,
388
+			&i.Pending,
389
+			&i.ResolvedAt,
390
+			&i.ResolvedByUserID,
391
+			&i.CreatedAt,
392
+			&i.UpdatedAt,
393
+			&i.EditedAt,
394
+		); err != nil {
395
+			return nil, err
396
+		}
397
+		items = append(items, i)
398
+	}
399
+	if err := rows.Err(); err != nil {
400
+		return nil, err
401
+	}
402
+	return items, nil
403
+}
404
+
405
+const listPRReviewCommentsForFile = `-- name: ListPRReviewCommentsForFile :many
406
+SELECT id, pr_issue_id, review_id, author_user_id, file_path, side, original_commit_sha, original_line, original_position, current_position, body, body_html_cached, in_reply_to_id, pending, resolved_at, resolved_by_user_id, created_at, updated_at, edited_at FROM pr_review_comments
407
+WHERE pr_issue_id = $1
408
+  AND file_path = $2
409
+  AND pending = false
410
+ORDER BY created_at
411
+`
412
+
413
+type ListPRReviewCommentsForFileParams struct {
414
+	PrIssueID int64
415
+	FilePath  string
416
+}
417
+
418
+// Files-tab fetch: comments anchored to a single file path, oldest first.
419
+func (q *Queries) ListPRReviewCommentsForFile(ctx context.Context, db DBTX, arg ListPRReviewCommentsForFileParams) ([]PrReviewComment, error) {
420
+	rows, err := db.Query(ctx, listPRReviewCommentsForFile, arg.PrIssueID, arg.FilePath)
421
+	if err != nil {
422
+		return nil, err
423
+	}
424
+	defer rows.Close()
425
+	items := []PrReviewComment{}
426
+	for rows.Next() {
427
+		var i PrReviewComment
428
+		if err := rows.Scan(
429
+			&i.ID,
430
+			&i.PrIssueID,
431
+			&i.ReviewID,
432
+			&i.AuthorUserID,
433
+			&i.FilePath,
434
+			&i.Side,
435
+			&i.OriginalCommitSha,
436
+			&i.OriginalLine,
437
+			&i.OriginalPosition,
438
+			&i.CurrentPosition,
439
+			&i.Body,
440
+			&i.BodyHtmlCached,
441
+			&i.InReplyToID,
442
+			&i.Pending,
443
+			&i.ResolvedAt,
444
+			&i.ResolvedByUserID,
445
+			&i.CreatedAt,
446
+			&i.UpdatedAt,
447
+			&i.EditedAt,
448
+		); err != nil {
449
+			return nil, err
450
+		}
451
+		items = append(items, i)
452
+	}
453
+	if err := rows.Err(); err != nil {
454
+		return nil, err
455
+	}
456
+	return items, nil
457
+}
458
+
459
+const listPRReviewRequests = `-- name: ListPRReviewRequests :many
460
+SELECT id, pr_issue_id, requested_user_id, requested_team_id, requested_by_user_id, requested_at, dismissed_at, satisfied_by_review_id FROM pr_review_requests
461
+WHERE pr_issue_id = $1
462
+ORDER BY requested_at
463
+`
464
+
465
+func (q *Queries) ListPRReviewRequests(ctx context.Context, db DBTX, prIssueID int64) ([]PrReviewRequest, error) {
466
+	rows, err := db.Query(ctx, listPRReviewRequests, prIssueID)
467
+	if err != nil {
468
+		return nil, err
469
+	}
470
+	defer rows.Close()
471
+	items := []PrReviewRequest{}
472
+	for rows.Next() {
473
+		var i PrReviewRequest
474
+		if err := rows.Scan(
475
+			&i.ID,
476
+			&i.PrIssueID,
477
+			&i.RequestedUserID,
478
+			&i.RequestedTeamID,
479
+			&i.RequestedByUserID,
480
+			&i.RequestedAt,
481
+			&i.DismissedAt,
482
+			&i.SatisfiedByReviewID,
483
+		); err != nil {
484
+			return nil, err
485
+		}
486
+		items = append(items, i)
487
+	}
488
+	if err := rows.Err(); err != nil {
489
+		return nil, err
490
+	}
491
+	return items, nil
492
+}
493
+
494
+const listPRReviews = `-- name: ListPRReviews :many
495
+SELECT id, pr_issue_id, author_user_id, state, body, body_html_cached, submitted_at, dismissed_at, dismissed_by_user_id, dismissal_reason FROM pr_reviews
496
+WHERE pr_issue_id = $1
497
+ORDER BY submitted_at
498
+`
499
+
500
+func (q *Queries) ListPRReviews(ctx context.Context, db DBTX, prIssueID int64) ([]PrReview, error) {
501
+	rows, err := db.Query(ctx, listPRReviews, prIssueID)
502
+	if err != nil {
503
+		return nil, err
504
+	}
505
+	defer rows.Close()
506
+	items := []PrReview{}
507
+	for rows.Next() {
508
+		var i PrReview
509
+		if err := rows.Scan(
510
+			&i.ID,
511
+			&i.PrIssueID,
512
+			&i.AuthorUserID,
513
+			&i.State,
514
+			&i.Body,
515
+			&i.BodyHtmlCached,
516
+			&i.SubmittedAt,
517
+			&i.DismissedAt,
518
+			&i.DismissedByUserID,
519
+			&i.DismissalReason,
520
+		); err != nil {
521
+			return nil, err
522
+		}
523
+		items = append(items, i)
524
+	}
525
+	if err := rows.Err(); err != nil {
526
+		return nil, err
527
+	}
528
+	return items, nil
529
+}
530
+
531
+const listPendingDraftCommentsForUser = `-- name: ListPendingDraftCommentsForUser :many
532
+SELECT id, pr_issue_id, review_id, author_user_id, file_path, side, original_commit_sha, original_line, original_position, current_position, body, body_html_cached, in_reply_to_id, pending, resolved_at, resolved_by_user_id, created_at, updated_at, edited_at FROM pr_review_comments
533
+WHERE pr_issue_id = $1
534
+  AND author_user_id = $2
535
+  AND pending = true
536
+ORDER BY created_at
537
+`
538
+
539
+type ListPendingDraftCommentsForUserParams struct {
540
+	PrIssueID    int64
541
+	AuthorUserID pgtype.Int8
542
+}
543
+
544
+// Server-side draft listing: rows with pending=true belonging to one
545
+// user. Ordered by creation so the submit step processes them in order.
546
+func (q *Queries) ListPendingDraftCommentsForUser(ctx context.Context, db DBTX, arg ListPendingDraftCommentsForUserParams) ([]PrReviewComment, error) {
547
+	rows, err := db.Query(ctx, listPendingDraftCommentsForUser, arg.PrIssueID, arg.AuthorUserID)
548
+	if err != nil {
549
+		return nil, err
550
+	}
551
+	defer rows.Close()
552
+	items := []PrReviewComment{}
553
+	for rows.Next() {
554
+		var i PrReviewComment
555
+		if err := rows.Scan(
556
+			&i.ID,
557
+			&i.PrIssueID,
558
+			&i.ReviewID,
559
+			&i.AuthorUserID,
560
+			&i.FilePath,
561
+			&i.Side,
562
+			&i.OriginalCommitSha,
563
+			&i.OriginalLine,
564
+			&i.OriginalPosition,
565
+			&i.CurrentPosition,
566
+			&i.Body,
567
+			&i.BodyHtmlCached,
568
+			&i.InReplyToID,
569
+			&i.Pending,
570
+			&i.ResolvedAt,
571
+			&i.ResolvedByUserID,
572
+			&i.CreatedAt,
573
+			&i.UpdatedAt,
574
+			&i.EditedAt,
575
+		); err != nil {
576
+			return nil, err
577
+		}
578
+		items = append(items, i)
579
+	}
580
+	if err := rows.Err(); err != nil {
581
+		return nil, err
582
+	}
583
+	return items, nil
584
+}
585
+
586
+const listPendingReviewRequestsForUser = `-- name: ListPendingReviewRequestsForUser :many
587
+SELECT pr.id, pr.pr_issue_id, pr.requested_user_id, pr.requested_team_id, pr.requested_by_user_id, pr.requested_at, pr.dismissed_at, pr.satisfied_by_review_id FROM pr_review_requests pr
588
+WHERE requested_user_id = $1
589
+  AND dismissed_at IS NULL
590
+  AND satisfied_by_review_id IS NULL
591
+ORDER BY requested_at DESC
592
+`
593
+
594
+// Reviewer's inbox feed. Excludes dismissed + satisfied requests.
595
+func (q *Queries) ListPendingReviewRequestsForUser(ctx context.Context, db DBTX, requestedUserID pgtype.Int8) ([]PrReviewRequest, error) {
596
+	rows, err := db.Query(ctx, listPendingReviewRequestsForUser, requestedUserID)
597
+	if err != nil {
598
+		return nil, err
599
+	}
600
+	defer rows.Close()
601
+	items := []PrReviewRequest{}
602
+	for rows.Next() {
603
+		var i PrReviewRequest
604
+		if err := rows.Scan(
605
+			&i.ID,
606
+			&i.PrIssueID,
607
+			&i.RequestedUserID,
608
+			&i.RequestedTeamID,
609
+			&i.RequestedByUserID,
610
+			&i.RequestedAt,
611
+			&i.DismissedAt,
612
+			&i.SatisfiedByReviewID,
613
+		); err != nil {
614
+			return nil, err
615
+		}
616
+		items = append(items, i)
617
+	}
618
+	if err := rows.Err(); err != nil {
619
+		return nil, err
620
+	}
621
+	return items, nil
622
+}
623
+
624
+const listUndismissedReviewsForGate = `-- name: ListUndismissedReviewsForGate :many
625
+SELECT id, pr_issue_id, author_user_id, state, submitted_at
626
+FROM pr_reviews
627
+WHERE pr_issue_id = $1
628
+  AND dismissed_at IS NULL
629
+ORDER BY author_user_id, submitted_at
630
+`
631
+
632
+type ListUndismissedReviewsForGateRow struct {
633
+	ID           int64
634
+	PrIssueID    int64
635
+	AuthorUserID pgtype.Int8
636
+	State        PrReviewState
637
+	SubmittedAt  pgtype.Timestamptz
638
+}
639
+
640
+// Used by the merge gate to compute "latest review per author" semantics
641
+// in Go. Ordered author + submitted_at so the caller can pick the last
642
+// per author cheaply.
643
+func (q *Queries) ListUndismissedReviewsForGate(ctx context.Context, db DBTX, prIssueID int64) ([]ListUndismissedReviewsForGateRow, error) {
644
+	rows, err := db.Query(ctx, listUndismissedReviewsForGate, prIssueID)
645
+	if err != nil {
646
+		return nil, err
647
+	}
648
+	defer rows.Close()
649
+	items := []ListUndismissedReviewsForGateRow{}
650
+	for rows.Next() {
651
+		var i ListUndismissedReviewsForGateRow
652
+		if err := rows.Scan(
653
+			&i.ID,
654
+			&i.PrIssueID,
655
+			&i.AuthorUserID,
656
+			&i.State,
657
+			&i.SubmittedAt,
658
+		); err != nil {
659
+			return nil, err
660
+		}
661
+		items = append(items, i)
662
+	}
663
+	if err := rows.Err(); err != nil {
664
+		return nil, err
665
+	}
666
+	return items, nil
667
+}
668
+
669
+const satisfyPRReviewRequest = `-- name: SatisfyPRReviewRequest :exec
670
+UPDATE pr_review_requests
671
+SET satisfied_by_review_id = $2
672
+WHERE pr_issue_id = $1
673
+  AND requested_user_id = $3
674
+  AND dismissed_at IS NULL
675
+  AND satisfied_by_review_id IS NULL
676
+`
677
+
678
+type SatisfyPRReviewRequestParams struct {
679
+	PrIssueID           int64
680
+	SatisfiedByReviewID pgtype.Int8
681
+	RequestedUserID     pgtype.Int8
682
+}
683
+
684
+func (q *Queries) SatisfyPRReviewRequest(ctx context.Context, db DBTX, arg SatisfyPRReviewRequestParams) error {
685
+	_, err := db.Exec(ctx, satisfyPRReviewRequest, arg.PrIssueID, arg.SatisfiedByReviewID, arg.RequestedUserID)
686
+	return err
687
+}
688
+
689
+const setPRReviewCommentCurrentPosition = `-- name: SetPRReviewCommentCurrentPosition :exec
690
+UPDATE pr_review_comments
691
+SET current_position = $2::int,
692
+    updated_at = now()
693
+WHERE id = $1
694
+`
695
+
696
+type SetPRReviewCommentCurrentPositionParams struct {
697
+	ID              int64
698
+	CurrentPosition pgtype.Int4
699
+}
700
+
701
+// Position-mapping update emitted by pulls.Synchronize. NULL means
702
+// the comment has gone outdated.
703
+func (q *Queries) SetPRReviewCommentCurrentPosition(ctx context.Context, db DBTX, arg SetPRReviewCommentCurrentPositionParams) error {
704
+	_, err := db.Exec(ctx, setPRReviewCommentCurrentPosition, arg.ID, arg.CurrentPosition)
705
+	return err
706
+}
707
+
708
+const setPRReviewCommentResolved = `-- name: SetPRReviewCommentResolved :exec
709
+UPDATE pr_review_comments
710
+SET resolved_at = $2::timestamptz,
711
+    resolved_by_user_id = $3::bigint,
712
+    updated_at = now()
713
+WHERE id = $1
714
+`
715
+
716
+type SetPRReviewCommentResolvedParams struct {
717
+	ID               int64
718
+	ResolvedAt       pgtype.Timestamptz
719
+	ResolvedByUserID pgtype.Int8
720
+}
721
+
722
+func (q *Queries) SetPRReviewCommentResolved(ctx context.Context, db DBTX, arg SetPRReviewCommentResolvedParams) error {
723
+	_, err := db.Exec(ctx, setPRReviewCommentResolved, arg.ID, arg.ResolvedAt, arg.ResolvedByUserID)
724
+	return err
725
+}
726
+
727
+const updatePRReviewCommentBody = `-- name: UpdatePRReviewCommentBody :exec
728
+UPDATE pr_review_comments
729
+SET body = $2, body_html_cached = $3::text,
730
+    edited_at = now(), updated_at = now()
731
+WHERE id = $1
732
+`
733
+
734
+type UpdatePRReviewCommentBodyParams struct {
735
+	ID             int64
736
+	Body           string
737
+	BodyHtmlCached pgtype.Text
738
+}
739
+
740
+func (q *Queries) UpdatePRReviewCommentBody(ctx context.Context, db DBTX, arg UpdatePRReviewCommentBodyParams) error {
741
+	_, err := db.Exec(ctx, updatePRReviewCommentBody, arg.ID, arg.Body, arg.BodyHtmlCached)
742
+	return err
743
+}
internal/repos/queries/branch_protection.sqlmodified
@@ -5,7 +5,8 @@ SELECT id, repo_id, pattern,
55
        prevent_force_push, prevent_deletion, require_pr_for_push,
66
        allowed_pusher_user_ids,
77
        require_signed_commits, status_checks_required,
8
-       created_at, updated_at, created_by_user_id
8
+       created_at, updated_at, created_by_user_id,
9
+       required_review_count, dismiss_stale_reviews_on_push, require_code_owner_review
910
 FROM branch_protection_rules
1011
 WHERE repo_id = $1
1112
 ORDER BY pattern;
@@ -15,7 +16,8 @@ SELECT id, repo_id, pattern,
1516
        prevent_force_push, prevent_deletion, require_pr_for_push,
1617
        allowed_pusher_user_ids,
1718
        require_signed_commits, status_checks_required,
18
-       created_at, updated_at, created_by_user_id
19
+       created_at, updated_at, created_by_user_id,
20
+       required_review_count, dismiss_stale_reviews_on_push, require_code_owner_review
1921
 FROM branch_protection_rules
2022
 WHERE id = $1;
2123
 
@@ -38,6 +40,15 @@ SET pattern = $2,
3840
     allowed_pusher_user_ids = $6
3941
 WHERE id = $1;
4042
 
43
+-- name: UpdateBranchProtectionReviewSettings :exec
44
+-- S23 surface for the review-related knobs. Branch-protection edit
45
+-- handler calls this alongside UpdateBranchProtectionRule.
46
+UPDATE branch_protection_rules
47
+SET required_review_count = $2,
48
+    dismiss_stale_reviews_on_push = $3,
49
+    require_code_owner_review = $4
50
+WHERE id = $1;
51
+
4152
 -- name: DeleteBranchProtectionRule :exec
4253
 DELETE FROM branch_protection_rules WHERE id = $1;
4354
 
internal/repos/sqlc/branch_protection.sql.gomodified
@@ -25,7 +25,8 @@ SELECT id, repo_id, pattern,
2525
        prevent_force_push, prevent_deletion, require_pr_for_push,
2626
        allowed_pusher_user_ids,
2727
        require_signed_commits, status_checks_required,
28
-       created_at, updated_at, created_by_user_id
28
+       created_at, updated_at, created_by_user_id,
29
+       required_review_count, dismiss_stale_reviews_on_push, require_code_owner_review
2930
 FROM branch_protection_rules
3031
 WHERE id = $1
3132
 `
@@ -46,6 +47,9 @@ func (q *Queries) GetBranchProtectionRule(ctx context.Context, db DBTX, id int64
4647
 		&i.CreatedAt,
4748
 		&i.UpdatedAt,
4849
 		&i.CreatedByUserID,
50
+		&i.RequiredReviewCount,
51
+		&i.DismissStaleReviewsOnPush,
52
+		&i.RequireCodeOwnerReview,
4953
 	)
5054
 	return i, err
5155
 }
@@ -56,7 +60,8 @@ SELECT id, repo_id, pattern,
5660
        prevent_force_push, prevent_deletion, require_pr_for_push,
5761
        allowed_pusher_user_ids,
5862
        require_signed_commits, status_checks_required,
59
-       created_at, updated_at, created_by_user_id
63
+       created_at, updated_at, created_by_user_id,
64
+       required_review_count, dismiss_stale_reviews_on_push, require_code_owner_review
6065
 FROM branch_protection_rules
6166
 WHERE repo_id = $1
6267
 ORDER BY pattern
@@ -85,6 +90,9 @@ func (q *Queries) ListBranchProtectionRules(ctx context.Context, db DBTX, repoID
8590
 			&i.CreatedAt,
8691
 			&i.UpdatedAt,
8792
 			&i.CreatedByUserID,
93
+			&i.RequiredReviewCount,
94
+			&i.DismissStaleReviewsOnPush,
95
+			&i.RequireCodeOwnerReview,
8896
 		); err != nil {
8997
 			return nil, err
9098
 		}
@@ -96,6 +104,33 @@ func (q *Queries) ListBranchProtectionRules(ctx context.Context, db DBTX, repoID
96104
 	return items, nil
97105
 }
98106
 
107
+const updateBranchProtectionReviewSettings = `-- name: UpdateBranchProtectionReviewSettings :exec
108
+UPDATE branch_protection_rules
109
+SET required_review_count = $2,
110
+    dismiss_stale_reviews_on_push = $3,
111
+    require_code_owner_review = $4
112
+WHERE id = $1
113
+`
114
+
115
+type UpdateBranchProtectionReviewSettingsParams struct {
116
+	ID                        int64
117
+	RequiredReviewCount       int32
118
+	DismissStaleReviewsOnPush bool
119
+	RequireCodeOwnerReview    bool
120
+}
121
+
122
+// S23 surface for the review-related knobs. Branch-protection edit
123
+// handler calls this alongside UpdateBranchProtectionRule.
124
+func (q *Queries) UpdateBranchProtectionReviewSettings(ctx context.Context, db DBTX, arg UpdateBranchProtectionReviewSettingsParams) error {
125
+	_, err := db.Exec(ctx, updateBranchProtectionReviewSettings,
126
+		arg.ID,
127
+		arg.RequiredReviewCount,
128
+		arg.DismissStaleReviewsOnPush,
129
+		arg.RequireCodeOwnerReview,
130
+	)
131
+	return err
132
+}
133
+
99134
 const updateBranchProtectionRule = `-- name: UpdateBranchProtectionRule :exec
100135
 UPDATE branch_protection_rules
101136
 SET pattern = $2,
internal/repos/sqlc/models.gomodified
@@ -403,6 +403,91 @@ func (ns NullPrMergeableState) Value() (driver.Value, error) {
403403
 	return string(ns.PrMergeableState), nil
404404
 }
405405
 
406
+type PrReviewSide string
407
+
408
+const (
409
+	PrReviewSideLeft  PrReviewSide = "left"
410
+	PrReviewSideRight PrReviewSide = "right"
411
+)
412
+
413
+func (e *PrReviewSide) Scan(src interface{}) error {
414
+	switch s := src.(type) {
415
+	case []byte:
416
+		*e = PrReviewSide(s)
417
+	case string:
418
+		*e = PrReviewSide(s)
419
+	default:
420
+		return fmt.Errorf("unsupported scan type for PrReviewSide: %T", src)
421
+	}
422
+	return nil
423
+}
424
+
425
+type NullPrReviewSide struct {
426
+	PrReviewSide PrReviewSide
427
+	Valid        bool // Valid is true if PrReviewSide is not NULL
428
+}
429
+
430
+// Scan implements the Scanner interface.
431
+func (ns *NullPrReviewSide) Scan(value interface{}) error {
432
+	if value == nil {
433
+		ns.PrReviewSide, ns.Valid = "", false
434
+		return nil
435
+	}
436
+	ns.Valid = true
437
+	return ns.PrReviewSide.Scan(value)
438
+}
439
+
440
+// Value implements the driver Valuer interface.
441
+func (ns NullPrReviewSide) Value() (driver.Value, error) {
442
+	if !ns.Valid {
443
+		return nil, nil
444
+	}
445
+	return string(ns.PrReviewSide), nil
446
+}
447
+
448
+type PrReviewState string
449
+
450
+const (
451
+	PrReviewStateComment        PrReviewState = "comment"
452
+	PrReviewStateApprove        PrReviewState = "approve"
453
+	PrReviewStateRequestChanges PrReviewState = "request_changes"
454
+)
455
+
456
+func (e *PrReviewState) Scan(src interface{}) error {
457
+	switch s := src.(type) {
458
+	case []byte:
459
+		*e = PrReviewState(s)
460
+	case string:
461
+		*e = PrReviewState(s)
462
+	default:
463
+		return fmt.Errorf("unsupported scan type for PrReviewState: %T", src)
464
+	}
465
+	return nil
466
+}
467
+
468
+type NullPrReviewState struct {
469
+	PrReviewState PrReviewState
470
+	Valid         bool // Valid is true if PrReviewState is not NULL
471
+}
472
+
473
+// Scan implements the Scanner interface.
474
+func (ns *NullPrReviewState) Scan(value interface{}) error {
475
+	if value == nil {
476
+		ns.PrReviewState, ns.Valid = "", false
477
+		return nil
478
+	}
479
+	ns.Valid = true
480
+	return ns.PrReviewState.Scan(value)
481
+}
482
+
483
+// Value implements the driver Valuer interface.
484
+func (ns NullPrReviewState) Value() (driver.Value, error) {
485
+	if !ns.Valid {
486
+		return nil, nil
487
+	}
488
+	return string(ns.PrReviewState), nil
489
+}
490
+
406491
 type RepoVisibility string
407492
 
408493
 const (
@@ -551,18 +636,21 @@ type AuthThrottle struct {
551636
 }
552637
 
553638
 type BranchProtectionRule struct {
554
-	ID                   int64
555
-	RepoID               int64
556
-	Pattern              string
557
-	PreventForcePush     bool
558
-	PreventDeletion      bool
559
-	RequirePrForPush     bool
560
-	AllowedPusherUserIds []int64
561
-	RequireSignedCommits bool
562
-	StatusChecksRequired []string
563
-	CreatedAt            pgtype.Timestamptz
564
-	UpdatedAt            pgtype.Timestamptz
565
-	CreatedByUserID      pgtype.Int8
639
+	ID                        int64
640
+	RepoID                    int64
641
+	Pattern                   string
642
+	PreventForcePush          bool
643
+	PreventDeletion           bool
644
+	RequirePrForPush          bool
645
+	AllowedPusherUserIds      []int64
646
+	RequireSignedCommits      bool
647
+	StatusChecksRequired      []string
648
+	CreatedAt                 pgtype.Timestamptz
649
+	UpdatedAt                 pgtype.Timestamptz
650
+	CreatedByUserID           pgtype.Int8
651
+	RequiredReviewCount       int32
652
+	DismissStaleReviewsOnPush bool
653
+	RequireCodeOwnerReview    bool
566654
 }
567655
 
568656
 type EmailVerification struct {
@@ -691,6 +779,52 @@ type PasswordReset struct {
691779
 	CreatedAt pgtype.Timestamptz
692780
 }
693781
 
782
+type PrReview struct {
783
+	ID                int64
784
+	PrIssueID         int64
785
+	AuthorUserID      pgtype.Int8
786
+	State             PrReviewState
787
+	Body              string
788
+	BodyHtmlCached    pgtype.Text
789
+	SubmittedAt       pgtype.Timestamptz
790
+	DismissedAt       pgtype.Timestamptz
791
+	DismissedByUserID pgtype.Int8
792
+	DismissalReason   string
793
+}
794
+
795
+type PrReviewComment struct {
796
+	ID                int64
797
+	PrIssueID         int64
798
+	ReviewID          pgtype.Int8
799
+	AuthorUserID      pgtype.Int8
800
+	FilePath          string
801
+	Side              PrReviewSide
802
+	OriginalCommitSha string
803
+	OriginalLine      int32
804
+	OriginalPosition  int32
805
+	CurrentPosition   pgtype.Int4
806
+	Body              string
807
+	BodyHtmlCached    pgtype.Text
808
+	InReplyToID       pgtype.Int8
809
+	Pending           bool
810
+	ResolvedAt        pgtype.Timestamptz
811
+	ResolvedByUserID  pgtype.Int8
812
+	CreatedAt         pgtype.Timestamptz
813
+	UpdatedAt         pgtype.Timestamptz
814
+	EditedAt          pgtype.Timestamptz
815
+}
816
+
817
+type PrReviewRequest struct {
818
+	ID                  int64
819
+	PrIssueID           int64
820
+	RequestedUserID     pgtype.Int8
821
+	RequestedTeamID     pgtype.Int8
822
+	RequestedByUserID   pgtype.Int8
823
+	RequestedAt         pgtype.Timestamptz
824
+	DismissedAt         pgtype.Timestamptz
825
+	SatisfiedByReviewID pgtype.Int8
826
+}
827
+
694828
 type PullRequest struct {
695829
 	IssueID            int64
696830
 	BaseRef            string
internal/repos/sqlc/querier.gomodified
@@ -90,6 +90,9 @@ type Querier interface {
9090
 	// so a user→org transfer flips both columns atomically.
9191
 	TransferRepoOwner(ctx context.Context, db DBTX, arg TransferRepoOwnerParams) error
9292
 	UnarchiveRepo(ctx context.Context, db DBTX, id int64) error
93
+	// S23 surface for the review-related knobs. Branch-protection edit
94
+	// handler calls this alongside UpdateBranchProtectionRule.
95
+	UpdateBranchProtectionReviewSettings(ctx context.Context, db DBTX, arg UpdateBranchProtectionReviewSettingsParams) error
9396
 	UpdateBranchProtectionRule(ctx context.Context, db DBTX, arg UpdateBranchProtectionRuleParams) error
9497
 	// Used by the default-branch settings handler. The on-disk HEAD update
9598
 	// is a separate step done via `git symbolic-ref` from the orchestrator.
internal/users/sqlc/models.gomodified
@@ -403,6 +403,91 @@ func (ns NullPrMergeableState) Value() (driver.Value, error) {
403403
 	return string(ns.PrMergeableState), nil
404404
 }
405405
 
406
+type PrReviewSide string
407
+
408
+const (
409
+	PrReviewSideLeft  PrReviewSide = "left"
410
+	PrReviewSideRight PrReviewSide = "right"
411
+)
412
+
413
+func (e *PrReviewSide) Scan(src interface{}) error {
414
+	switch s := src.(type) {
415
+	case []byte:
416
+		*e = PrReviewSide(s)
417
+	case string:
418
+		*e = PrReviewSide(s)
419
+	default:
420
+		return fmt.Errorf("unsupported scan type for PrReviewSide: %T", src)
421
+	}
422
+	return nil
423
+}
424
+
425
+type NullPrReviewSide struct {
426
+	PrReviewSide PrReviewSide
427
+	Valid        bool // Valid is true if PrReviewSide is not NULL
428
+}
429
+
430
+// Scan implements the Scanner interface.
431
+func (ns *NullPrReviewSide) Scan(value interface{}) error {
432
+	if value == nil {
433
+		ns.PrReviewSide, ns.Valid = "", false
434
+		return nil
435
+	}
436
+	ns.Valid = true
437
+	return ns.PrReviewSide.Scan(value)
438
+}
439
+
440
+// Value implements the driver Valuer interface.
441
+func (ns NullPrReviewSide) Value() (driver.Value, error) {
442
+	if !ns.Valid {
443
+		return nil, nil
444
+	}
445
+	return string(ns.PrReviewSide), nil
446
+}
447
+
448
+type PrReviewState string
449
+
450
+const (
451
+	PrReviewStateComment        PrReviewState = "comment"
452
+	PrReviewStateApprove        PrReviewState = "approve"
453
+	PrReviewStateRequestChanges PrReviewState = "request_changes"
454
+)
455
+
456
+func (e *PrReviewState) Scan(src interface{}) error {
457
+	switch s := src.(type) {
458
+	case []byte:
459
+		*e = PrReviewState(s)
460
+	case string:
461
+		*e = PrReviewState(s)
462
+	default:
463
+		return fmt.Errorf("unsupported scan type for PrReviewState: %T", src)
464
+	}
465
+	return nil
466
+}
467
+
468
+type NullPrReviewState struct {
469
+	PrReviewState PrReviewState
470
+	Valid         bool // Valid is true if PrReviewState is not NULL
471
+}
472
+
473
+// Scan implements the Scanner interface.
474
+func (ns *NullPrReviewState) Scan(value interface{}) error {
475
+	if value == nil {
476
+		ns.PrReviewState, ns.Valid = "", false
477
+		return nil
478
+	}
479
+	ns.Valid = true
480
+	return ns.PrReviewState.Scan(value)
481
+}
482
+
483
+// Value implements the driver Valuer interface.
484
+func (ns NullPrReviewState) Value() (driver.Value, error) {
485
+	if !ns.Valid {
486
+		return nil, nil
487
+	}
488
+	return string(ns.PrReviewState), nil
489
+}
490
+
406491
 type RepoVisibility string
407492
 
408493
 const (
@@ -551,18 +636,21 @@ type AuthThrottle struct {
551636
 }
552637
 
553638
 type BranchProtectionRule struct {
554
-	ID                   int64
555
-	RepoID               int64
556
-	Pattern              string
557
-	PreventForcePush     bool
558
-	PreventDeletion      bool
559
-	RequirePrForPush     bool
560
-	AllowedPusherUserIds []int64
561
-	RequireSignedCommits bool
562
-	StatusChecksRequired []string
563
-	CreatedAt            pgtype.Timestamptz
564
-	UpdatedAt            pgtype.Timestamptz
565
-	CreatedByUserID      pgtype.Int8
639
+	ID                        int64
640
+	RepoID                    int64
641
+	Pattern                   string
642
+	PreventForcePush          bool
643
+	PreventDeletion           bool
644
+	RequirePrForPush          bool
645
+	AllowedPusherUserIds      []int64
646
+	RequireSignedCommits      bool
647
+	StatusChecksRequired      []string
648
+	CreatedAt                 pgtype.Timestamptz
649
+	UpdatedAt                 pgtype.Timestamptz
650
+	CreatedByUserID           pgtype.Int8
651
+	RequiredReviewCount       int32
652
+	DismissStaleReviewsOnPush bool
653
+	RequireCodeOwnerReview    bool
566654
 }
567655
 
568656
 type EmailVerification struct {
@@ -691,6 +779,52 @@ type PasswordReset struct {
691779
 	CreatedAt pgtype.Timestamptz
692780
 }
693781
 
782
+type PrReview struct {
783
+	ID                int64
784
+	PrIssueID         int64
785
+	AuthorUserID      pgtype.Int8
786
+	State             PrReviewState
787
+	Body              string
788
+	BodyHtmlCached    pgtype.Text
789
+	SubmittedAt       pgtype.Timestamptz
790
+	DismissedAt       pgtype.Timestamptz
791
+	DismissedByUserID pgtype.Int8
792
+	DismissalReason   string
793
+}
794
+
795
+type PrReviewComment struct {
796
+	ID                int64
797
+	PrIssueID         int64
798
+	ReviewID          pgtype.Int8
799
+	AuthorUserID      pgtype.Int8
800
+	FilePath          string
801
+	Side              PrReviewSide
802
+	OriginalCommitSha string
803
+	OriginalLine      int32
804
+	OriginalPosition  int32
805
+	CurrentPosition   pgtype.Int4
806
+	Body              string
807
+	BodyHtmlCached    pgtype.Text
808
+	InReplyToID       pgtype.Int8
809
+	Pending           bool
810
+	ResolvedAt        pgtype.Timestamptz
811
+	ResolvedByUserID  pgtype.Int8
812
+	CreatedAt         pgtype.Timestamptz
813
+	UpdatedAt         pgtype.Timestamptz
814
+	EditedAt          pgtype.Timestamptz
815
+}
816
+
817
+type PrReviewRequest struct {
818
+	ID                  int64
819
+	PrIssueID           int64
820
+	RequestedUserID     pgtype.Int8
821
+	RequestedTeamID     pgtype.Int8
822
+	RequestedByUserID   pgtype.Int8
823
+	RequestedAt         pgtype.Timestamptz
824
+	DismissedAt         pgtype.Timestamptz
825
+	SatisfiedByReviewID pgtype.Int8
826
+}
827
+
694828
 type PullRequest struct {
695829
 	IssueID            int64
696830
 	BaseRef            string
internal/worker/sqlc/models.gomodified
@@ -403,6 +403,91 @@ func (ns NullPrMergeableState) Value() (driver.Value, error) {
403403
 	return string(ns.PrMergeableState), nil
404404
 }
405405
 
406
+type PrReviewSide string
407
+
408
+const (
409
+	PrReviewSideLeft  PrReviewSide = "left"
410
+	PrReviewSideRight PrReviewSide = "right"
411
+)
412
+
413
+func (e *PrReviewSide) Scan(src interface{}) error {
414
+	switch s := src.(type) {
415
+	case []byte:
416
+		*e = PrReviewSide(s)
417
+	case string:
418
+		*e = PrReviewSide(s)
419
+	default:
420
+		return fmt.Errorf("unsupported scan type for PrReviewSide: %T", src)
421
+	}
422
+	return nil
423
+}
424
+
425
+type NullPrReviewSide struct {
426
+	PrReviewSide PrReviewSide
427
+	Valid        bool // Valid is true if PrReviewSide is not NULL
428
+}
429
+
430
+// Scan implements the Scanner interface.
431
+func (ns *NullPrReviewSide) Scan(value interface{}) error {
432
+	if value == nil {
433
+		ns.PrReviewSide, ns.Valid = "", false
434
+		return nil
435
+	}
436
+	ns.Valid = true
437
+	return ns.PrReviewSide.Scan(value)
438
+}
439
+
440
+// Value implements the driver Valuer interface.
441
+func (ns NullPrReviewSide) Value() (driver.Value, error) {
442
+	if !ns.Valid {
443
+		return nil, nil
444
+	}
445
+	return string(ns.PrReviewSide), nil
446
+}
447
+
448
+type PrReviewState string
449
+
450
+const (
451
+	PrReviewStateComment        PrReviewState = "comment"
452
+	PrReviewStateApprove        PrReviewState = "approve"
453
+	PrReviewStateRequestChanges PrReviewState = "request_changes"
454
+)
455
+
456
+func (e *PrReviewState) Scan(src interface{}) error {
457
+	switch s := src.(type) {
458
+	case []byte:
459
+		*e = PrReviewState(s)
460
+	case string:
461
+		*e = PrReviewState(s)
462
+	default:
463
+		return fmt.Errorf("unsupported scan type for PrReviewState: %T", src)
464
+	}
465
+	return nil
466
+}
467
+
468
+type NullPrReviewState struct {
469
+	PrReviewState PrReviewState
470
+	Valid         bool // Valid is true if PrReviewState is not NULL
471
+}
472
+
473
+// Scan implements the Scanner interface.
474
+func (ns *NullPrReviewState) Scan(value interface{}) error {
475
+	if value == nil {
476
+		ns.PrReviewState, ns.Valid = "", false
477
+		return nil
478
+	}
479
+	ns.Valid = true
480
+	return ns.PrReviewState.Scan(value)
481
+}
482
+
483
+// Value implements the driver Valuer interface.
484
+func (ns NullPrReviewState) Value() (driver.Value, error) {
485
+	if !ns.Valid {
486
+		return nil, nil
487
+	}
488
+	return string(ns.PrReviewState), nil
489
+}
490
+
406491
 type RepoVisibility string
407492
 
408493
 const (
@@ -551,18 +636,21 @@ type AuthThrottle struct {
551636
 }
552637
 
553638
 type BranchProtectionRule struct {
554
-	ID                   int64
555
-	RepoID               int64
556
-	Pattern              string
557
-	PreventForcePush     bool
558
-	PreventDeletion      bool
559
-	RequirePrForPush     bool
560
-	AllowedPusherUserIds []int64
561
-	RequireSignedCommits bool
562
-	StatusChecksRequired []string
563
-	CreatedAt            pgtype.Timestamptz
564
-	UpdatedAt            pgtype.Timestamptz
565
-	CreatedByUserID      pgtype.Int8
639
+	ID                        int64
640
+	RepoID                    int64
641
+	Pattern                   string
642
+	PreventForcePush          bool
643
+	PreventDeletion           bool
644
+	RequirePrForPush          bool
645
+	AllowedPusherUserIds      []int64
646
+	RequireSignedCommits      bool
647
+	StatusChecksRequired      []string
648
+	CreatedAt                 pgtype.Timestamptz
649
+	UpdatedAt                 pgtype.Timestamptz
650
+	CreatedByUserID           pgtype.Int8
651
+	RequiredReviewCount       int32
652
+	DismissStaleReviewsOnPush bool
653
+	RequireCodeOwnerReview    bool
566654
 }
567655
 
568656
 type EmailVerification struct {
@@ -691,6 +779,52 @@ type PasswordReset struct {
691779
 	CreatedAt pgtype.Timestamptz
692780
 }
693781
 
782
+type PrReview struct {
783
+	ID                int64
784
+	PrIssueID         int64
785
+	AuthorUserID      pgtype.Int8
786
+	State             PrReviewState
787
+	Body              string
788
+	BodyHtmlCached    pgtype.Text
789
+	SubmittedAt       pgtype.Timestamptz
790
+	DismissedAt       pgtype.Timestamptz
791
+	DismissedByUserID pgtype.Int8
792
+	DismissalReason   string
793
+}
794
+
795
+type PrReviewComment struct {
796
+	ID                int64
797
+	PrIssueID         int64
798
+	ReviewID          pgtype.Int8
799
+	AuthorUserID      pgtype.Int8
800
+	FilePath          string
801
+	Side              PrReviewSide
802
+	OriginalCommitSha string
803
+	OriginalLine      int32
804
+	OriginalPosition  int32
805
+	CurrentPosition   pgtype.Int4
806
+	Body              string
807
+	BodyHtmlCached    pgtype.Text
808
+	InReplyToID       pgtype.Int8
809
+	Pending           bool
810
+	ResolvedAt        pgtype.Timestamptz
811
+	ResolvedByUserID  pgtype.Int8
812
+	CreatedAt         pgtype.Timestamptz
813
+	UpdatedAt         pgtype.Timestamptz
814
+	EditedAt          pgtype.Timestamptz
815
+}
816
+
817
+type PrReviewRequest struct {
818
+	ID                  int64
819
+	PrIssueID           int64
820
+	RequestedUserID     pgtype.Int8
821
+	RequestedTeamID     pgtype.Int8
822
+	RequestedByUserID   pgtype.Int8
823
+	RequestedAt         pgtype.Timestamptz
824
+	DismissedAt         pgtype.Timestamptz
825
+	SatisfiedByReviewID pgtype.Int8
826
+}
827
+
694828
 type PullRequest struct {
695829
 	IssueID            int64
696830
 	BaseRef            string