tenseleyflow/shithub / a396715

Browse files

S21: issues migration 0022 + queries + sqlc regen

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
a3967159c7bd68a6795ca5548b0af15f1d5ed5d1
Parents
abcf5b8
Tree
62f190d

12 changed files

StatusFile+-
M internal/auth/policy/sqlc/models.go 305 0
A internal/issues/queries/issues.sql 214 0
A internal/issues/sqlc/db.go 25 0
A internal/issues/sqlc/issues.sql.go 973 0
C internal/issues/sqlc/models.go 0 0
A internal/issues/sqlc/querier.go 74 0
M internal/meta/sqlc/models.go 305 0
A internal/migrationsfs/migrations/0022_issues.sql 192 0
M internal/repos/sqlc/models.go 305 0
M internal/users/sqlc/models.go 305 0
M internal/worker/sqlc/models.go 305 0
M sqlc.yaml 16 0
internal/auth/policy/sqlc/models.gomodified
@@ -57,6 +57,219 @@ func (ns NullCollabRole) Value() (driver.Value, error) {
5757
 	return string(ns.CollabRole), nil
5858
 }
5959
 
60
+type IssueKind string
61
+
62
+const (
63
+	IssueKindIssue IssueKind = "issue"
64
+	IssueKindPr    IssueKind = "pr"
65
+)
66
+
67
+func (e *IssueKind) Scan(src interface{}) error {
68
+	switch s := src.(type) {
69
+	case []byte:
70
+		*e = IssueKind(s)
71
+	case string:
72
+		*e = IssueKind(s)
73
+	default:
74
+		return fmt.Errorf("unsupported scan type for IssueKind: %T", src)
75
+	}
76
+	return nil
77
+}
78
+
79
+type NullIssueKind struct {
80
+	IssueKind IssueKind
81
+	Valid     bool // Valid is true if IssueKind is not NULL
82
+}
83
+
84
+// Scan implements the Scanner interface.
85
+func (ns *NullIssueKind) Scan(value interface{}) error {
86
+	if value == nil {
87
+		ns.IssueKind, ns.Valid = "", false
88
+		return nil
89
+	}
90
+	ns.Valid = true
91
+	return ns.IssueKind.Scan(value)
92
+}
93
+
94
+// Value implements the driver Valuer interface.
95
+func (ns NullIssueKind) Value() (driver.Value, error) {
96
+	if !ns.Valid {
97
+		return nil, nil
98
+	}
99
+	return string(ns.IssueKind), nil
100
+}
101
+
102
+type IssueRefSource string
103
+
104
+const (
105
+	IssueRefSourceCommentBody   IssueRefSource = "comment_body"
106
+	IssueRefSourceIssueBody     IssueRefSource = "issue_body"
107
+	IssueRefSourceCommitMessage IssueRefSource = "commit_message"
108
+)
109
+
110
+func (e *IssueRefSource) Scan(src interface{}) error {
111
+	switch s := src.(type) {
112
+	case []byte:
113
+		*e = IssueRefSource(s)
114
+	case string:
115
+		*e = IssueRefSource(s)
116
+	default:
117
+		return fmt.Errorf("unsupported scan type for IssueRefSource: %T", src)
118
+	}
119
+	return nil
120
+}
121
+
122
+type NullIssueRefSource struct {
123
+	IssueRefSource IssueRefSource
124
+	Valid          bool // Valid is true if IssueRefSource is not NULL
125
+}
126
+
127
+// Scan implements the Scanner interface.
128
+func (ns *NullIssueRefSource) Scan(value interface{}) error {
129
+	if value == nil {
130
+		ns.IssueRefSource, ns.Valid = "", false
131
+		return nil
132
+	}
133
+	ns.Valid = true
134
+	return ns.IssueRefSource.Scan(value)
135
+}
136
+
137
+// Value implements the driver Valuer interface.
138
+func (ns NullIssueRefSource) Value() (driver.Value, error) {
139
+	if !ns.Valid {
140
+		return nil, nil
141
+	}
142
+	return string(ns.IssueRefSource), nil
143
+}
144
+
145
+type IssueState string
146
+
147
+const (
148
+	IssueStateOpen   IssueState = "open"
149
+	IssueStateClosed IssueState = "closed"
150
+)
151
+
152
+func (e *IssueState) Scan(src interface{}) error {
153
+	switch s := src.(type) {
154
+	case []byte:
155
+		*e = IssueState(s)
156
+	case string:
157
+		*e = IssueState(s)
158
+	default:
159
+		return fmt.Errorf("unsupported scan type for IssueState: %T", src)
160
+	}
161
+	return nil
162
+}
163
+
164
+type NullIssueState struct {
165
+	IssueState IssueState
166
+	Valid      bool // Valid is true if IssueState is not NULL
167
+}
168
+
169
+// Scan implements the Scanner interface.
170
+func (ns *NullIssueState) Scan(value interface{}) error {
171
+	if value == nil {
172
+		ns.IssueState, ns.Valid = "", false
173
+		return nil
174
+	}
175
+	ns.Valid = true
176
+	return ns.IssueState.Scan(value)
177
+}
178
+
179
+// Value implements the driver Valuer interface.
180
+func (ns NullIssueState) Value() (driver.Value, error) {
181
+	if !ns.Valid {
182
+		return nil, nil
183
+	}
184
+	return string(ns.IssueState), nil
185
+}
186
+
187
+type IssueStateReason string
188
+
189
+const (
190
+	IssueStateReasonCompleted  IssueStateReason = "completed"
191
+	IssueStateReasonNotPlanned IssueStateReason = "not_planned"
192
+	IssueStateReasonReopened   IssueStateReason = "reopened"
193
+	IssueStateReasonDuplicate  IssueStateReason = "duplicate"
194
+)
195
+
196
+func (e *IssueStateReason) Scan(src interface{}) error {
197
+	switch s := src.(type) {
198
+	case []byte:
199
+		*e = IssueStateReason(s)
200
+	case string:
201
+		*e = IssueStateReason(s)
202
+	default:
203
+		return fmt.Errorf("unsupported scan type for IssueStateReason: %T", src)
204
+	}
205
+	return nil
206
+}
207
+
208
+type NullIssueStateReason struct {
209
+	IssueStateReason IssueStateReason
210
+	Valid            bool // Valid is true if IssueStateReason is not NULL
211
+}
212
+
213
+// Scan implements the Scanner interface.
214
+func (ns *NullIssueStateReason) Scan(value interface{}) error {
215
+	if value == nil {
216
+		ns.IssueStateReason, ns.Valid = "", false
217
+		return nil
218
+	}
219
+	ns.Valid = true
220
+	return ns.IssueStateReason.Scan(value)
221
+}
222
+
223
+// Value implements the driver Valuer interface.
224
+func (ns NullIssueStateReason) Value() (driver.Value, error) {
225
+	if !ns.Valid {
226
+		return nil, nil
227
+	}
228
+	return string(ns.IssueStateReason), nil
229
+}
230
+
231
+type MilestoneState string
232
+
233
+const (
234
+	MilestoneStateOpen   MilestoneState = "open"
235
+	MilestoneStateClosed MilestoneState = "closed"
236
+)
237
+
238
+func (e *MilestoneState) Scan(src interface{}) error {
239
+	switch s := src.(type) {
240
+	case []byte:
241
+		*e = MilestoneState(s)
242
+	case string:
243
+		*e = MilestoneState(s)
244
+	default:
245
+		return fmt.Errorf("unsupported scan type for MilestoneState: %T", src)
246
+	}
247
+	return nil
248
+}
249
+
250
+type NullMilestoneState struct {
251
+	MilestoneState MilestoneState
252
+	Valid          bool // Valid is true if MilestoneState is not NULL
253
+}
254
+
255
+// Scan implements the Scanner interface.
256
+func (ns *NullMilestoneState) Scan(value interface{}) error {
257
+	if value == nil {
258
+		ns.MilestoneState, ns.Valid = "", false
259
+		return nil
260
+	}
261
+	ns.Valid = true
262
+	return ns.MilestoneState.Scan(value)
263
+}
264
+
265
+// Value implements the driver Valuer interface.
266
+func (ns NullMilestoneState) Value() (driver.Value, error) {
267
+	if !ns.Valid {
268
+		return nil, nil
269
+	}
270
+	return string(ns.MilestoneState), nil
271
+}
272
+
60273
 type RepoVisibility string
61274
 
62275
 const (
@@ -228,6 +441,73 @@ type EmailVerification struct {
228441
 	CreatedAt   pgtype.Timestamptz
229442
 }
230443
 
444
+type Issue struct {
445
+	ID                int64
446
+	RepoID            int64
447
+	Number            int64
448
+	Kind              IssueKind
449
+	Title             string
450
+	Body              string
451
+	BodyHtmlCached    pgtype.Text
452
+	MdPipelineVersion int32
453
+	AuthorUserID      pgtype.Int8
454
+	State             IssueState
455
+	StateReason       NullIssueStateReason
456
+	Locked            bool
457
+	LockReason        pgtype.Text
458
+	MilestoneID       pgtype.Int8
459
+	CreatedAt         pgtype.Timestamptz
460
+	UpdatedAt         pgtype.Timestamptz
461
+	EditedAt          pgtype.Timestamptz
462
+	ClosedAt          pgtype.Timestamptz
463
+	ClosedByUserID    pgtype.Int8
464
+}
465
+
466
+type IssueAssignee struct {
467
+	IssueID          int64
468
+	UserID           int64
469
+	AssignedAt       pgtype.Timestamptz
470
+	AssignedByUserID pgtype.Int8
471
+}
472
+
473
+type IssueComment struct {
474
+	ID                int64
475
+	IssueID           int64
476
+	AuthorUserID      pgtype.Int8
477
+	Body              string
478
+	BodyHtmlCached    pgtype.Text
479
+	MdPipelineVersion int32
480
+	CreatedAt         pgtype.Timestamptz
481
+	UpdatedAt         pgtype.Timestamptz
482
+	EditedAt          pgtype.Timestamptz
483
+}
484
+
485
+type IssueEvent struct {
486
+	ID          int64
487
+	IssueID     int64
488
+	ActorUserID pgtype.Int8
489
+	Kind        string
490
+	Meta        []byte
491
+	RefTargetID pgtype.Int8
492
+	CreatedAt   pgtype.Timestamptz
493
+}
494
+
495
+type IssueLabel struct {
496
+	IssueID         int64
497
+	LabelID         int64
498
+	AppliedAt       pgtype.Timestamptz
499
+	AppliedByUserID pgtype.Int8
500
+}
501
+
502
+type IssueReference struct {
503
+	ID             int64
504
+	SourceIssueID  pgtype.Int8
505
+	TargetIssueID  int64
506
+	SourceKind     IssueRefSource
507
+	SourceObjectID pgtype.Int8
508
+	CreatedAt      pgtype.Timestamptz
509
+}
510
+
231511
 type Job struct {
232512
 	ID          int64
233513
 	Kind        string
@@ -243,12 +523,32 @@ type Job struct {
243523
 	CreatedAt   pgtype.Timestamptz
244524
 }
245525
 
526
+type Label struct {
527
+	ID          int64
528
+	RepoID      int64
529
+	Name        string
530
+	Color       string
531
+	Description string
532
+	CreatedAt   pgtype.Timestamptz
533
+}
534
+
246535
 type Meta struct {
247536
 	Key       string
248537
 	Value     []byte
249538
 	UpdatedAt pgtype.Timestamptz
250539
 }
251540
 
541
+type Milestone struct {
542
+	ID          int64
543
+	RepoID      int64
544
+	Title       string
545
+	Description string
546
+	State       MilestoneState
547
+	DueOn       pgtype.Timestamptz
548
+	CreatedAt   pgtype.Timestamptz
549
+	ClosedAt    pgtype.Timestamptz
550
+}
551
+
252552
 type PasswordReset struct {
253553
 	ID        int64
254554
 	UserID    int64
@@ -301,6 +601,11 @@ type RepoCollaborator struct {
301601
 	AddedByUserID pgtype.Int8
302602
 }
303603
 
604
+type RepoIssueCounter struct {
605
+	RepoID     int64
606
+	NextNumber int64
607
+}
608
+
304609
 type RepoRedirect struct {
305610
 	OldOwnerUserID pgtype.Int8
306611
 	OldOwnerOrgID  pgtype.Int8
internal/issues/queries/issues.sqladded
@@ -0,0 +1,214 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+-- ─── per-repo numbering ───────────────────────────────────────────────
4
+
5
+-- name: EnsureRepoIssueCounter :exec
6
+-- Lazy-initialize the counter row. Idempotent — invoked from repo
7
+-- create AND from the first issue insert (defensive in case someone
8
+-- migrates an old repo that predates S21).
9
+INSERT INTO repo_issue_counter (repo_id, next_number)
10
+VALUES ($1, 1)
11
+ON CONFLICT (repo_id) DO NOTHING;
12
+
13
+-- name: AllocateIssueNumber :one
14
+-- UPDATE … RETURNING is concurrency-safe: each row update is
15
+-- serialized by the row lock; concurrent transactions see different
16
+-- values. The caller wraps this in the same tx as the issue insert.
17
+UPDATE repo_issue_counter
18
+SET next_number = next_number + 1
19
+WHERE repo_id = $1
20
+RETURNING (next_number - 1)::bigint AS allocated;
21
+
22
+
23
+-- ─── issues ──────────────────────────────────────────────────────────
24
+
25
+-- name: CreateIssue :one
26
+INSERT INTO issues (
27
+    repo_id, number, kind, title, body, author_user_id
28
+) VALUES (
29
+    $1, $2, $3, $4, $5, sqlc.narg(author_user_id)::bigint
30
+)
31
+RETURNING *;
32
+
33
+-- name: GetIssueByNumber :one
34
+SELECT * FROM issues
35
+WHERE repo_id = $1 AND number = $2;
36
+
37
+-- name: GetIssueByID :one
38
+SELECT * FROM issues WHERE id = $1;
39
+
40
+-- name: ListIssues :many
41
+-- Filterable list. Caller passes a state filter (open/closed/all
42
+-- where 'all' is encoded as NULL); label/assignee/author/milestone
43
+-- filtering happens after this query in Go for v1 — see the
44
+-- internal/issues/list.go composer. Per-page hardcoded at 25 in the
45
+-- handler; offset is the (page-1)*25.
46
+SELECT * FROM issues
47
+WHERE repo_id = $1
48
+  AND (sqlc.narg(state_filter)::text IS NULL OR state::text = sqlc.narg(state_filter)::text)
49
+  AND kind = COALESCE(sqlc.narg(kind)::issue_kind, 'issue')
50
+ORDER BY updated_at DESC
51
+LIMIT $2 OFFSET $3;
52
+
53
+-- name: CountIssues :one
54
+SELECT count(*)::bigint FROM issues
55
+WHERE repo_id = $1
56
+  AND (sqlc.narg(state_filter)::text IS NULL OR state::text = sqlc.narg(state_filter)::text)
57
+  AND kind = COALESCE(sqlc.narg(kind)::issue_kind, 'issue');
58
+
59
+-- name: UpdateIssueTitleBody :exec
60
+UPDATE issues
61
+SET title = $2, body = $3, body_html_cached = $4, edited_at = now(), updated_at = now()
62
+WHERE id = $1;
63
+
64
+-- name: SetIssueState :exec
65
+UPDATE issues
66
+SET state = $2,
67
+    state_reason = sqlc.narg(state_reason)::issue_state_reason,
68
+    closed_at = CASE WHEN $2::issue_state = 'closed' THEN now() ELSE NULL END,
69
+    closed_by_user_id = sqlc.narg(closed_by_user_id)::bigint,
70
+    updated_at = now()
71
+WHERE id = $1;
72
+
73
+-- name: SetIssueLock :exec
74
+UPDATE issues
75
+SET locked = $2, lock_reason = sqlc.narg(lock_reason)::text, updated_at = now()
76
+WHERE id = $1;
77
+
78
+-- name: SetIssueMilestone :exec
79
+UPDATE issues
80
+SET milestone_id = sqlc.narg(milestone_id)::bigint, updated_at = now()
81
+WHERE id = $1;
82
+
83
+
84
+-- ─── comments ────────────────────────────────────────────────────────
85
+
86
+-- name: CreateIssueComment :one
87
+INSERT INTO issue_comments (issue_id, author_user_id, body, body_html_cached)
88
+VALUES ($1, sqlc.narg(author_user_id)::bigint, $2, $3)
89
+RETURNING *;
90
+
91
+-- name: ListIssueComments :many
92
+SELECT * FROM issue_comments
93
+WHERE issue_id = $1
94
+ORDER BY created_at ASC;
95
+
96
+-- name: GetIssueComment :one
97
+SELECT * FROM issue_comments WHERE id = $1;
98
+
99
+-- name: UpdateIssueCommentBody :exec
100
+UPDATE issue_comments
101
+SET body = $2, body_html_cached = $3, edited_at = now(), updated_at = now()
102
+WHERE id = $1;
103
+
104
+-- name: DeleteIssueComment :exec
105
+DELETE FROM issue_comments WHERE id = $1;
106
+
107
+
108
+-- ─── assignees ───────────────────────────────────────────────────────
109
+
110
+-- name: AssignUserToIssue :exec
111
+INSERT INTO issue_assignees (issue_id, user_id, assigned_by_user_id)
112
+VALUES ($1, $2, sqlc.narg(assigned_by_user_id)::bigint)
113
+ON CONFLICT (issue_id, user_id) DO NOTHING;
114
+
115
+-- name: UnassignUserFromIssue :exec
116
+DELETE FROM issue_assignees WHERE issue_id = $1 AND user_id = $2;
117
+
118
+-- name: ListIssueAssignees :many
119
+SELECT a.issue_id, a.user_id, a.assigned_at, u.username, u.display_name
120
+FROM issue_assignees a
121
+JOIN users u ON u.id = a.user_id
122
+WHERE a.issue_id = $1
123
+ORDER BY a.assigned_at;
124
+
125
+
126
+-- ─── labels ──────────────────────────────────────────────────────────
127
+
128
+-- name: CreateLabel :one
129
+INSERT INTO labels (repo_id, name, color, description)
130
+VALUES ($1, $2, $3, $4)
131
+RETURNING *;
132
+
133
+-- name: ListLabels :many
134
+SELECT * FROM labels WHERE repo_id = $1 ORDER BY name;
135
+
136
+-- name: GetLabelByName :one
137
+SELECT * FROM labels WHERE repo_id = $1 AND name = $2;
138
+
139
+-- name: UpdateLabel :exec
140
+UPDATE labels
141
+SET name = $2, color = $3, description = $4
142
+WHERE id = $1;
143
+
144
+-- name: DeleteLabel :exec
145
+DELETE FROM labels WHERE id = $1;
146
+
147
+
148
+-- ─── issue ↔ label ───────────────────────────────────────────────────
149
+
150
+-- name: AddIssueLabel :exec
151
+INSERT INTO issue_labels (issue_id, label_id, applied_by_user_id)
152
+VALUES ($1, $2, sqlc.narg(applied_by_user_id)::bigint)
153
+ON CONFLICT (issue_id, label_id) DO NOTHING;
154
+
155
+-- name: RemoveIssueLabel :exec
156
+DELETE FROM issue_labels WHERE issue_id = $1 AND label_id = $2;
157
+
158
+-- name: ListLabelsOnIssue :many
159
+SELECT l.id, l.repo_id, l.name, l.color, l.description, l.created_at
160
+FROM issue_labels il
161
+JOIN labels l ON l.id = il.label_id
162
+WHERE il.issue_id = $1
163
+ORDER BY l.name;
164
+
165
+
166
+-- ─── milestones ──────────────────────────────────────────────────────
167
+
168
+-- name: CreateMilestone :one
169
+INSERT INTO milestones (repo_id, title, description, due_on)
170
+VALUES ($1, $2, $3, sqlc.narg(due_on)::timestamptz)
171
+RETURNING *;
172
+
173
+-- name: ListMilestones :many
174
+SELECT * FROM milestones WHERE repo_id = $1 ORDER BY state, due_on NULLS LAST, title;
175
+
176
+-- name: GetMilestone :one
177
+SELECT * FROM milestones WHERE id = $1;
178
+
179
+-- name: UpdateMilestone :exec
180
+UPDATE milestones SET title = $2, description = $3, due_on = sqlc.narg(due_on)::timestamptz WHERE id = $1;
181
+
182
+-- name: SetMilestoneState :exec
183
+UPDATE milestones SET state = $2, closed_at = CASE WHEN $2::milestone_state = 'closed' THEN now() ELSE NULL END WHERE id = $1;
184
+
185
+-- name: DeleteMilestone :exec
186
+DELETE FROM milestones WHERE id = $1;
187
+
188
+-- name: MilestoneIssueCounts :one
189
+-- Open + closed counts for the milestone progress bar.
190
+SELECT
191
+    count(*) FILTER (WHERE state = 'open')::int   AS open_count,
192
+    count(*) FILTER (WHERE state = 'closed')::int AS closed_count
193
+FROM issues
194
+WHERE milestone_id = $1;
195
+
196
+
197
+-- ─── events + references ─────────────────────────────────────────────
198
+
199
+-- name: InsertIssueEvent :one
200
+INSERT INTO issue_events (issue_id, actor_user_id, kind, meta, ref_target_id)
201
+VALUES ($1, sqlc.narg(actor_user_id)::bigint, $2, $3, sqlc.narg(ref_target_id)::bigint)
202
+RETURNING *;
203
+
204
+-- name: ListIssueEvents :many
205
+SELECT * FROM issue_events
206
+WHERE issue_id = $1
207
+ORDER BY created_at ASC;
208
+
209
+-- name: InsertIssueReference :exec
210
+INSERT INTO issue_references (
211
+    source_issue_id, target_issue_id, source_kind, source_object_id
212
+) VALUES (
213
+    sqlc.narg(source_issue_id)::bigint, $1, $2, sqlc.narg(source_object_id)::bigint
214
+);
internal/issues/sqlc/db.goadded
@@ -0,0 +1,25 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+
5
+package issuesdb
6
+
7
+import (
8
+	"context"
9
+
10
+	"github.com/jackc/pgx/v5"
11
+	"github.com/jackc/pgx/v5/pgconn"
12
+)
13
+
14
+type DBTX interface {
15
+	Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
16
+	Query(context.Context, string, ...interface{}) (pgx.Rows, error)
17
+	QueryRow(context.Context, string, ...interface{}) pgx.Row
18
+}
19
+
20
+func New() *Queries {
21
+	return &Queries{}
22
+}
23
+
24
+type Queries struct {
25
+}
internal/issues/sqlc/issues.sql.goadded
@@ -0,0 +1,973 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+// source: issues.sql
5
+
6
+package issuesdb
7
+
8
+import (
9
+	"context"
10
+
11
+	"github.com/jackc/pgx/v5/pgtype"
12
+)
13
+
14
+const addIssueLabel = `-- name: AddIssueLabel :exec
15
+
16
+INSERT INTO issue_labels (issue_id, label_id, applied_by_user_id)
17
+VALUES ($1, $2, $3::bigint)
18
+ON CONFLICT (issue_id, label_id) DO NOTHING
19
+`
20
+
21
+type AddIssueLabelParams struct {
22
+	IssueID         int64
23
+	LabelID         int64
24
+	AppliedByUserID pgtype.Int8
25
+}
26
+
27
+// ─── issue ↔ label ───────────────────────────────────────────────────
28
+func (q *Queries) AddIssueLabel(ctx context.Context, db DBTX, arg AddIssueLabelParams) error {
29
+	_, err := db.Exec(ctx, addIssueLabel, arg.IssueID, arg.LabelID, arg.AppliedByUserID)
30
+	return err
31
+}
32
+
33
+const allocateIssueNumber = `-- name: AllocateIssueNumber :one
34
+UPDATE repo_issue_counter
35
+SET next_number = next_number + 1
36
+WHERE repo_id = $1
37
+RETURNING (next_number - 1)::bigint AS allocated
38
+`
39
+
40
+// UPDATE … RETURNING is concurrency-safe: each row update is
41
+// serialized by the row lock; concurrent transactions see different
42
+// values. The caller wraps this in the same tx as the issue insert.
43
+func (q *Queries) AllocateIssueNumber(ctx context.Context, db DBTX, repoID int64) (int64, error) {
44
+	row := db.QueryRow(ctx, allocateIssueNumber, repoID)
45
+	var allocated int64
46
+	err := row.Scan(&allocated)
47
+	return allocated, err
48
+}
49
+
50
+const assignUserToIssue = `-- name: AssignUserToIssue :exec
51
+
52
+INSERT INTO issue_assignees (issue_id, user_id, assigned_by_user_id)
53
+VALUES ($1, $2, $3::bigint)
54
+ON CONFLICT (issue_id, user_id) DO NOTHING
55
+`
56
+
57
+type AssignUserToIssueParams struct {
58
+	IssueID          int64
59
+	UserID           int64
60
+	AssignedByUserID pgtype.Int8
61
+}
62
+
63
+// ─── assignees ───────────────────────────────────────────────────────
64
+func (q *Queries) AssignUserToIssue(ctx context.Context, db DBTX, arg AssignUserToIssueParams) error {
65
+	_, err := db.Exec(ctx, assignUserToIssue, arg.IssueID, arg.UserID, arg.AssignedByUserID)
66
+	return err
67
+}
68
+
69
+const countIssues = `-- name: CountIssues :one
70
+SELECT count(*)::bigint FROM issues
71
+WHERE repo_id = $1
72
+  AND ($2::text IS NULL OR state::text = $2::text)
73
+  AND kind = COALESCE($3::issue_kind, 'issue')
74
+`
75
+
76
+type CountIssuesParams struct {
77
+	RepoID      int64
78
+	StateFilter pgtype.Text
79
+	Kind        NullIssueKind
80
+}
81
+
82
+func (q *Queries) CountIssues(ctx context.Context, db DBTX, arg CountIssuesParams) (int64, error) {
83
+	row := db.QueryRow(ctx, countIssues, arg.RepoID, arg.StateFilter, arg.Kind)
84
+	var column_1 int64
85
+	err := row.Scan(&column_1)
86
+	return column_1, err
87
+}
88
+
89
+const createIssue = `-- name: CreateIssue :one
90
+
91
+INSERT INTO issues (
92
+    repo_id, number, kind, title, body, author_user_id
93
+) VALUES (
94
+    $1, $2, $3, $4, $5, $6::bigint
95
+)
96
+RETURNING id, repo_id, number, kind, title, body, body_html_cached, md_pipeline_version, author_user_id, state, state_reason, locked, lock_reason, milestone_id, created_at, updated_at, edited_at, closed_at, closed_by_user_id
97
+`
98
+
99
+type CreateIssueParams struct {
100
+	RepoID       int64
101
+	Number       int64
102
+	Kind         IssueKind
103
+	Title        string
104
+	Body         string
105
+	AuthorUserID pgtype.Int8
106
+}
107
+
108
+// ─── issues ──────────────────────────────────────────────────────────
109
+func (q *Queries) CreateIssue(ctx context.Context, db DBTX, arg CreateIssueParams) (Issue, error) {
110
+	row := db.QueryRow(ctx, createIssue,
111
+		arg.RepoID,
112
+		arg.Number,
113
+		arg.Kind,
114
+		arg.Title,
115
+		arg.Body,
116
+		arg.AuthorUserID,
117
+	)
118
+	var i Issue
119
+	err := row.Scan(
120
+		&i.ID,
121
+		&i.RepoID,
122
+		&i.Number,
123
+		&i.Kind,
124
+		&i.Title,
125
+		&i.Body,
126
+		&i.BodyHtmlCached,
127
+		&i.MdPipelineVersion,
128
+		&i.AuthorUserID,
129
+		&i.State,
130
+		&i.StateReason,
131
+		&i.Locked,
132
+		&i.LockReason,
133
+		&i.MilestoneID,
134
+		&i.CreatedAt,
135
+		&i.UpdatedAt,
136
+		&i.EditedAt,
137
+		&i.ClosedAt,
138
+		&i.ClosedByUserID,
139
+	)
140
+	return i, err
141
+}
142
+
143
+const createIssueComment = `-- name: CreateIssueComment :one
144
+
145
+INSERT INTO issue_comments (issue_id, author_user_id, body, body_html_cached)
146
+VALUES ($1, $4::bigint, $2, $3)
147
+RETURNING id, issue_id, author_user_id, body, body_html_cached, md_pipeline_version, created_at, updated_at, edited_at
148
+`
149
+
150
+type CreateIssueCommentParams struct {
151
+	IssueID        int64
152
+	Body           string
153
+	BodyHtmlCached pgtype.Text
154
+	AuthorUserID   pgtype.Int8
155
+}
156
+
157
+// ─── comments ────────────────────────────────────────────────────────
158
+func (q *Queries) CreateIssueComment(ctx context.Context, db DBTX, arg CreateIssueCommentParams) (IssueComment, error) {
159
+	row := db.QueryRow(ctx, createIssueComment,
160
+		arg.IssueID,
161
+		arg.Body,
162
+		arg.BodyHtmlCached,
163
+		arg.AuthorUserID,
164
+	)
165
+	var i IssueComment
166
+	err := row.Scan(
167
+		&i.ID,
168
+		&i.IssueID,
169
+		&i.AuthorUserID,
170
+		&i.Body,
171
+		&i.BodyHtmlCached,
172
+		&i.MdPipelineVersion,
173
+		&i.CreatedAt,
174
+		&i.UpdatedAt,
175
+		&i.EditedAt,
176
+	)
177
+	return i, err
178
+}
179
+
180
+const createLabel = `-- name: CreateLabel :one
181
+
182
+INSERT INTO labels (repo_id, name, color, description)
183
+VALUES ($1, $2, $3, $4)
184
+RETURNING id, repo_id, name, color, description, created_at
185
+`
186
+
187
+type CreateLabelParams struct {
188
+	RepoID      int64
189
+	Name        string
190
+	Color       string
191
+	Description string
192
+}
193
+
194
+// ─── labels ──────────────────────────────────────────────────────────
195
+func (q *Queries) CreateLabel(ctx context.Context, db DBTX, arg CreateLabelParams) (Label, error) {
196
+	row := db.QueryRow(ctx, createLabel,
197
+		arg.RepoID,
198
+		arg.Name,
199
+		arg.Color,
200
+		arg.Description,
201
+	)
202
+	var i Label
203
+	err := row.Scan(
204
+		&i.ID,
205
+		&i.RepoID,
206
+		&i.Name,
207
+		&i.Color,
208
+		&i.Description,
209
+		&i.CreatedAt,
210
+	)
211
+	return i, err
212
+}
213
+
214
+const createMilestone = `-- name: CreateMilestone :one
215
+
216
+INSERT INTO milestones (repo_id, title, description, due_on)
217
+VALUES ($1, $2, $3, $4::timestamptz)
218
+RETURNING id, repo_id, title, description, state, due_on, created_at, closed_at
219
+`
220
+
221
+type CreateMilestoneParams struct {
222
+	RepoID      int64
223
+	Title       string
224
+	Description string
225
+	DueOn       pgtype.Timestamptz
226
+}
227
+
228
+// ─── milestones ──────────────────────────────────────────────────────
229
+func (q *Queries) CreateMilestone(ctx context.Context, db DBTX, arg CreateMilestoneParams) (Milestone, error) {
230
+	row := db.QueryRow(ctx, createMilestone,
231
+		arg.RepoID,
232
+		arg.Title,
233
+		arg.Description,
234
+		arg.DueOn,
235
+	)
236
+	var i Milestone
237
+	err := row.Scan(
238
+		&i.ID,
239
+		&i.RepoID,
240
+		&i.Title,
241
+		&i.Description,
242
+		&i.State,
243
+		&i.DueOn,
244
+		&i.CreatedAt,
245
+		&i.ClosedAt,
246
+	)
247
+	return i, err
248
+}
249
+
250
+const deleteIssueComment = `-- name: DeleteIssueComment :exec
251
+DELETE FROM issue_comments WHERE id = $1
252
+`
253
+
254
+func (q *Queries) DeleteIssueComment(ctx context.Context, db DBTX, id int64) error {
255
+	_, err := db.Exec(ctx, deleteIssueComment, id)
256
+	return err
257
+}
258
+
259
+const deleteLabel = `-- name: DeleteLabel :exec
260
+DELETE FROM labels WHERE id = $1
261
+`
262
+
263
+func (q *Queries) DeleteLabel(ctx context.Context, db DBTX, id int64) error {
264
+	_, err := db.Exec(ctx, deleteLabel, id)
265
+	return err
266
+}
267
+
268
+const deleteMilestone = `-- name: DeleteMilestone :exec
269
+DELETE FROM milestones WHERE id = $1
270
+`
271
+
272
+func (q *Queries) DeleteMilestone(ctx context.Context, db DBTX, id int64) error {
273
+	_, err := db.Exec(ctx, deleteMilestone, id)
274
+	return err
275
+}
276
+
277
+const ensureRepoIssueCounter = `-- name: EnsureRepoIssueCounter :exec
278
+
279
+
280
+INSERT INTO repo_issue_counter (repo_id, next_number)
281
+VALUES ($1, 1)
282
+ON CONFLICT (repo_id) DO NOTHING
283
+`
284
+
285
+// SPDX-License-Identifier: AGPL-3.0-or-later
286
+// ─── per-repo numbering ───────────────────────────────────────────────
287
+// Lazy-initialize the counter row. Idempotent — invoked from repo
288
+// create AND from the first issue insert (defensive in case someone
289
+// migrates an old repo that predates S21).
290
+func (q *Queries) EnsureRepoIssueCounter(ctx context.Context, db DBTX, repoID int64) error {
291
+	_, err := db.Exec(ctx, ensureRepoIssueCounter, repoID)
292
+	return err
293
+}
294
+
295
+const getIssueByID = `-- name: GetIssueByID :one
296
+SELECT id, repo_id, number, kind, title, body, body_html_cached, md_pipeline_version, author_user_id, state, state_reason, locked, lock_reason, milestone_id, created_at, updated_at, edited_at, closed_at, closed_by_user_id FROM issues WHERE id = $1
297
+`
298
+
299
+func (q *Queries) GetIssueByID(ctx context.Context, db DBTX, id int64) (Issue, error) {
300
+	row := db.QueryRow(ctx, getIssueByID, id)
301
+	var i Issue
302
+	err := row.Scan(
303
+		&i.ID,
304
+		&i.RepoID,
305
+		&i.Number,
306
+		&i.Kind,
307
+		&i.Title,
308
+		&i.Body,
309
+		&i.BodyHtmlCached,
310
+		&i.MdPipelineVersion,
311
+		&i.AuthorUserID,
312
+		&i.State,
313
+		&i.StateReason,
314
+		&i.Locked,
315
+		&i.LockReason,
316
+		&i.MilestoneID,
317
+		&i.CreatedAt,
318
+		&i.UpdatedAt,
319
+		&i.EditedAt,
320
+		&i.ClosedAt,
321
+		&i.ClosedByUserID,
322
+	)
323
+	return i, err
324
+}
325
+
326
+const getIssueByNumber = `-- name: GetIssueByNumber :one
327
+SELECT id, repo_id, number, kind, title, body, body_html_cached, md_pipeline_version, author_user_id, state, state_reason, locked, lock_reason, milestone_id, created_at, updated_at, edited_at, closed_at, closed_by_user_id FROM issues
328
+WHERE repo_id = $1 AND number = $2
329
+`
330
+
331
+type GetIssueByNumberParams struct {
332
+	RepoID int64
333
+	Number int64
334
+}
335
+
336
+func (q *Queries) GetIssueByNumber(ctx context.Context, db DBTX, arg GetIssueByNumberParams) (Issue, error) {
337
+	row := db.QueryRow(ctx, getIssueByNumber, arg.RepoID, arg.Number)
338
+	var i Issue
339
+	err := row.Scan(
340
+		&i.ID,
341
+		&i.RepoID,
342
+		&i.Number,
343
+		&i.Kind,
344
+		&i.Title,
345
+		&i.Body,
346
+		&i.BodyHtmlCached,
347
+		&i.MdPipelineVersion,
348
+		&i.AuthorUserID,
349
+		&i.State,
350
+		&i.StateReason,
351
+		&i.Locked,
352
+		&i.LockReason,
353
+		&i.MilestoneID,
354
+		&i.CreatedAt,
355
+		&i.UpdatedAt,
356
+		&i.EditedAt,
357
+		&i.ClosedAt,
358
+		&i.ClosedByUserID,
359
+	)
360
+	return i, err
361
+}
362
+
363
+const getIssueComment = `-- name: GetIssueComment :one
364
+SELECT id, issue_id, author_user_id, body, body_html_cached, md_pipeline_version, created_at, updated_at, edited_at FROM issue_comments WHERE id = $1
365
+`
366
+
367
+func (q *Queries) GetIssueComment(ctx context.Context, db DBTX, id int64) (IssueComment, error) {
368
+	row := db.QueryRow(ctx, getIssueComment, id)
369
+	var i IssueComment
370
+	err := row.Scan(
371
+		&i.ID,
372
+		&i.IssueID,
373
+		&i.AuthorUserID,
374
+		&i.Body,
375
+		&i.BodyHtmlCached,
376
+		&i.MdPipelineVersion,
377
+		&i.CreatedAt,
378
+		&i.UpdatedAt,
379
+		&i.EditedAt,
380
+	)
381
+	return i, err
382
+}
383
+
384
+const getLabelByName = `-- name: GetLabelByName :one
385
+SELECT id, repo_id, name, color, description, created_at FROM labels WHERE repo_id = $1 AND name = $2
386
+`
387
+
388
+type GetLabelByNameParams struct {
389
+	RepoID int64
390
+	Name   string
391
+}
392
+
393
+func (q *Queries) GetLabelByName(ctx context.Context, db DBTX, arg GetLabelByNameParams) (Label, error) {
394
+	row := db.QueryRow(ctx, getLabelByName, arg.RepoID, arg.Name)
395
+	var i Label
396
+	err := row.Scan(
397
+		&i.ID,
398
+		&i.RepoID,
399
+		&i.Name,
400
+		&i.Color,
401
+		&i.Description,
402
+		&i.CreatedAt,
403
+	)
404
+	return i, err
405
+}
406
+
407
+const getMilestone = `-- name: GetMilestone :one
408
+SELECT id, repo_id, title, description, state, due_on, created_at, closed_at FROM milestones WHERE id = $1
409
+`
410
+
411
+func (q *Queries) GetMilestone(ctx context.Context, db DBTX, id int64) (Milestone, error) {
412
+	row := db.QueryRow(ctx, getMilestone, id)
413
+	var i Milestone
414
+	err := row.Scan(
415
+		&i.ID,
416
+		&i.RepoID,
417
+		&i.Title,
418
+		&i.Description,
419
+		&i.State,
420
+		&i.DueOn,
421
+		&i.CreatedAt,
422
+		&i.ClosedAt,
423
+	)
424
+	return i, err
425
+}
426
+
427
+const insertIssueEvent = `-- name: InsertIssueEvent :one
428
+
429
+INSERT INTO issue_events (issue_id, actor_user_id, kind, meta, ref_target_id)
430
+VALUES ($1, $4::bigint, $2, $3, $5::bigint)
431
+RETURNING id, issue_id, actor_user_id, kind, meta, ref_target_id, created_at
432
+`
433
+
434
+type InsertIssueEventParams struct {
435
+	IssueID     int64
436
+	Kind        string
437
+	Meta        []byte
438
+	ActorUserID pgtype.Int8
439
+	RefTargetID pgtype.Int8
440
+}
441
+
442
+// ─── events + references ─────────────────────────────────────────────
443
+func (q *Queries) InsertIssueEvent(ctx context.Context, db DBTX, arg InsertIssueEventParams) (IssueEvent, error) {
444
+	row := db.QueryRow(ctx, insertIssueEvent,
445
+		arg.IssueID,
446
+		arg.Kind,
447
+		arg.Meta,
448
+		arg.ActorUserID,
449
+		arg.RefTargetID,
450
+	)
451
+	var i IssueEvent
452
+	err := row.Scan(
453
+		&i.ID,
454
+		&i.IssueID,
455
+		&i.ActorUserID,
456
+		&i.Kind,
457
+		&i.Meta,
458
+		&i.RefTargetID,
459
+		&i.CreatedAt,
460
+	)
461
+	return i, err
462
+}
463
+
464
+const insertIssueReference = `-- name: InsertIssueReference :exec
465
+INSERT INTO issue_references (
466
+    source_issue_id, target_issue_id, source_kind, source_object_id
467
+) VALUES (
468
+    $3::bigint, $1, $2, $4::bigint
469
+)
470
+`
471
+
472
+type InsertIssueReferenceParams struct {
473
+	TargetIssueID  int64
474
+	SourceKind     IssueRefSource
475
+	SourceIssueID  pgtype.Int8
476
+	SourceObjectID pgtype.Int8
477
+}
478
+
479
+func (q *Queries) InsertIssueReference(ctx context.Context, db DBTX, arg InsertIssueReferenceParams) error {
480
+	_, err := db.Exec(ctx, insertIssueReference,
481
+		arg.TargetIssueID,
482
+		arg.SourceKind,
483
+		arg.SourceIssueID,
484
+		arg.SourceObjectID,
485
+	)
486
+	return err
487
+}
488
+
489
+const listIssueAssignees = `-- name: ListIssueAssignees :many
490
+SELECT a.issue_id, a.user_id, a.assigned_at, u.username, u.display_name
491
+FROM issue_assignees a
492
+JOIN users u ON u.id = a.user_id
493
+WHERE a.issue_id = $1
494
+ORDER BY a.assigned_at
495
+`
496
+
497
+type ListIssueAssigneesRow struct {
498
+	IssueID     int64
499
+	UserID      int64
500
+	AssignedAt  pgtype.Timestamptz
501
+	Username    string
502
+	DisplayName string
503
+}
504
+
505
+func (q *Queries) ListIssueAssignees(ctx context.Context, db DBTX, issueID int64) ([]ListIssueAssigneesRow, error) {
506
+	rows, err := db.Query(ctx, listIssueAssignees, issueID)
507
+	if err != nil {
508
+		return nil, err
509
+	}
510
+	defer rows.Close()
511
+	items := []ListIssueAssigneesRow{}
512
+	for rows.Next() {
513
+		var i ListIssueAssigneesRow
514
+		if err := rows.Scan(
515
+			&i.IssueID,
516
+			&i.UserID,
517
+			&i.AssignedAt,
518
+			&i.Username,
519
+			&i.DisplayName,
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 listIssueComments = `-- name: ListIssueComments :many
532
+SELECT id, issue_id, author_user_id, body, body_html_cached, md_pipeline_version, created_at, updated_at, edited_at FROM issue_comments
533
+WHERE issue_id = $1
534
+ORDER BY created_at ASC
535
+`
536
+
537
+func (q *Queries) ListIssueComments(ctx context.Context, db DBTX, issueID int64) ([]IssueComment, error) {
538
+	rows, err := db.Query(ctx, listIssueComments, issueID)
539
+	if err != nil {
540
+		return nil, err
541
+	}
542
+	defer rows.Close()
543
+	items := []IssueComment{}
544
+	for rows.Next() {
545
+		var i IssueComment
546
+		if err := rows.Scan(
547
+			&i.ID,
548
+			&i.IssueID,
549
+			&i.AuthorUserID,
550
+			&i.Body,
551
+			&i.BodyHtmlCached,
552
+			&i.MdPipelineVersion,
553
+			&i.CreatedAt,
554
+			&i.UpdatedAt,
555
+			&i.EditedAt,
556
+		); err != nil {
557
+			return nil, err
558
+		}
559
+		items = append(items, i)
560
+	}
561
+	if err := rows.Err(); err != nil {
562
+		return nil, err
563
+	}
564
+	return items, nil
565
+}
566
+
567
+const listIssueEvents = `-- name: ListIssueEvents :many
568
+SELECT id, issue_id, actor_user_id, kind, meta, ref_target_id, created_at FROM issue_events
569
+WHERE issue_id = $1
570
+ORDER BY created_at ASC
571
+`
572
+
573
+func (q *Queries) ListIssueEvents(ctx context.Context, db DBTX, issueID int64) ([]IssueEvent, error) {
574
+	rows, err := db.Query(ctx, listIssueEvents, issueID)
575
+	if err != nil {
576
+		return nil, err
577
+	}
578
+	defer rows.Close()
579
+	items := []IssueEvent{}
580
+	for rows.Next() {
581
+		var i IssueEvent
582
+		if err := rows.Scan(
583
+			&i.ID,
584
+			&i.IssueID,
585
+			&i.ActorUserID,
586
+			&i.Kind,
587
+			&i.Meta,
588
+			&i.RefTargetID,
589
+			&i.CreatedAt,
590
+		); err != nil {
591
+			return nil, err
592
+		}
593
+		items = append(items, i)
594
+	}
595
+	if err := rows.Err(); err != nil {
596
+		return nil, err
597
+	}
598
+	return items, nil
599
+}
600
+
601
+const listIssues = `-- name: ListIssues :many
602
+SELECT id, repo_id, number, kind, title, body, body_html_cached, md_pipeline_version, author_user_id, state, state_reason, locked, lock_reason, milestone_id, created_at, updated_at, edited_at, closed_at, closed_by_user_id FROM issues
603
+WHERE repo_id = $1
604
+  AND ($4::text IS NULL OR state::text = $4::text)
605
+  AND kind = COALESCE($5::issue_kind, 'issue')
606
+ORDER BY updated_at DESC
607
+LIMIT $2 OFFSET $3
608
+`
609
+
610
+type ListIssuesParams struct {
611
+	RepoID      int64
612
+	Limit       int32
613
+	Offset      int32
614
+	StateFilter pgtype.Text
615
+	Kind        NullIssueKind
616
+}
617
+
618
+// Filterable list. Caller passes a state filter (open/closed/all
619
+// where 'all' is encoded as NULL); label/assignee/author/milestone
620
+// filtering happens after this query in Go for v1 — see the
621
+// internal/issues/list.go composer. Per-page hardcoded at 25 in the
622
+// handler; offset is the (page-1)*25.
623
+func (q *Queries) ListIssues(ctx context.Context, db DBTX, arg ListIssuesParams) ([]Issue, error) {
624
+	rows, err := db.Query(ctx, listIssues,
625
+		arg.RepoID,
626
+		arg.Limit,
627
+		arg.Offset,
628
+		arg.StateFilter,
629
+		arg.Kind,
630
+	)
631
+	if err != nil {
632
+		return nil, err
633
+	}
634
+	defer rows.Close()
635
+	items := []Issue{}
636
+	for rows.Next() {
637
+		var i Issue
638
+		if err := rows.Scan(
639
+			&i.ID,
640
+			&i.RepoID,
641
+			&i.Number,
642
+			&i.Kind,
643
+			&i.Title,
644
+			&i.Body,
645
+			&i.BodyHtmlCached,
646
+			&i.MdPipelineVersion,
647
+			&i.AuthorUserID,
648
+			&i.State,
649
+			&i.StateReason,
650
+			&i.Locked,
651
+			&i.LockReason,
652
+			&i.MilestoneID,
653
+			&i.CreatedAt,
654
+			&i.UpdatedAt,
655
+			&i.EditedAt,
656
+			&i.ClosedAt,
657
+			&i.ClosedByUserID,
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 listLabels = `-- name: ListLabels :many
670
+SELECT id, repo_id, name, color, description, created_at FROM labels WHERE repo_id = $1 ORDER BY name
671
+`
672
+
673
+func (q *Queries) ListLabels(ctx context.Context, db DBTX, repoID int64) ([]Label, error) {
674
+	rows, err := db.Query(ctx, listLabels, repoID)
675
+	if err != nil {
676
+		return nil, err
677
+	}
678
+	defer rows.Close()
679
+	items := []Label{}
680
+	for rows.Next() {
681
+		var i Label
682
+		if err := rows.Scan(
683
+			&i.ID,
684
+			&i.RepoID,
685
+			&i.Name,
686
+			&i.Color,
687
+			&i.Description,
688
+			&i.CreatedAt,
689
+		); err != nil {
690
+			return nil, err
691
+		}
692
+		items = append(items, i)
693
+	}
694
+	if err := rows.Err(); err != nil {
695
+		return nil, err
696
+	}
697
+	return items, nil
698
+}
699
+
700
+const listLabelsOnIssue = `-- name: ListLabelsOnIssue :many
701
+SELECT l.id, l.repo_id, l.name, l.color, l.description, l.created_at
702
+FROM issue_labels il
703
+JOIN labels l ON l.id = il.label_id
704
+WHERE il.issue_id = $1
705
+ORDER BY l.name
706
+`
707
+
708
+func (q *Queries) ListLabelsOnIssue(ctx context.Context, db DBTX, issueID int64) ([]Label, error) {
709
+	rows, err := db.Query(ctx, listLabelsOnIssue, issueID)
710
+	if err != nil {
711
+		return nil, err
712
+	}
713
+	defer rows.Close()
714
+	items := []Label{}
715
+	for rows.Next() {
716
+		var i Label
717
+		if err := rows.Scan(
718
+			&i.ID,
719
+			&i.RepoID,
720
+			&i.Name,
721
+			&i.Color,
722
+			&i.Description,
723
+			&i.CreatedAt,
724
+		); err != nil {
725
+			return nil, err
726
+		}
727
+		items = append(items, i)
728
+	}
729
+	if err := rows.Err(); err != nil {
730
+		return nil, err
731
+	}
732
+	return items, nil
733
+}
734
+
735
+const listMilestones = `-- name: ListMilestones :many
736
+SELECT id, repo_id, title, description, state, due_on, created_at, closed_at FROM milestones WHERE repo_id = $1 ORDER BY state, due_on NULLS LAST, title
737
+`
738
+
739
+func (q *Queries) ListMilestones(ctx context.Context, db DBTX, repoID int64) ([]Milestone, error) {
740
+	rows, err := db.Query(ctx, listMilestones, repoID)
741
+	if err != nil {
742
+		return nil, err
743
+	}
744
+	defer rows.Close()
745
+	items := []Milestone{}
746
+	for rows.Next() {
747
+		var i Milestone
748
+		if err := rows.Scan(
749
+			&i.ID,
750
+			&i.RepoID,
751
+			&i.Title,
752
+			&i.Description,
753
+			&i.State,
754
+			&i.DueOn,
755
+			&i.CreatedAt,
756
+			&i.ClosedAt,
757
+		); err != nil {
758
+			return nil, err
759
+		}
760
+		items = append(items, i)
761
+	}
762
+	if err := rows.Err(); err != nil {
763
+		return nil, err
764
+	}
765
+	return items, nil
766
+}
767
+
768
+const milestoneIssueCounts = `-- name: MilestoneIssueCounts :one
769
+SELECT
770
+    count(*) FILTER (WHERE state = 'open')::int   AS open_count,
771
+    count(*) FILTER (WHERE state = 'closed')::int AS closed_count
772
+FROM issues
773
+WHERE milestone_id = $1
774
+`
775
+
776
+type MilestoneIssueCountsRow struct {
777
+	OpenCount   int32
778
+	ClosedCount int32
779
+}
780
+
781
+// Open + closed counts for the milestone progress bar.
782
+func (q *Queries) MilestoneIssueCounts(ctx context.Context, db DBTX, milestoneID pgtype.Int8) (MilestoneIssueCountsRow, error) {
783
+	row := db.QueryRow(ctx, milestoneIssueCounts, milestoneID)
784
+	var i MilestoneIssueCountsRow
785
+	err := row.Scan(&i.OpenCount, &i.ClosedCount)
786
+	return i, err
787
+}
788
+
789
+const removeIssueLabel = `-- name: RemoveIssueLabel :exec
790
+DELETE FROM issue_labels WHERE issue_id = $1 AND label_id = $2
791
+`
792
+
793
+type RemoveIssueLabelParams struct {
794
+	IssueID int64
795
+	LabelID int64
796
+}
797
+
798
+func (q *Queries) RemoveIssueLabel(ctx context.Context, db DBTX, arg RemoveIssueLabelParams) error {
799
+	_, err := db.Exec(ctx, removeIssueLabel, arg.IssueID, arg.LabelID)
800
+	return err
801
+}
802
+
803
+const setIssueLock = `-- name: SetIssueLock :exec
804
+UPDATE issues
805
+SET locked = $2, lock_reason = $3::text, updated_at = now()
806
+WHERE id = $1
807
+`
808
+
809
+type SetIssueLockParams struct {
810
+	ID         int64
811
+	Locked     bool
812
+	LockReason pgtype.Text
813
+}
814
+
815
+func (q *Queries) SetIssueLock(ctx context.Context, db DBTX, arg SetIssueLockParams) error {
816
+	_, err := db.Exec(ctx, setIssueLock, arg.ID, arg.Locked, arg.LockReason)
817
+	return err
818
+}
819
+
820
+const setIssueMilestone = `-- name: SetIssueMilestone :exec
821
+UPDATE issues
822
+SET milestone_id = $2::bigint, updated_at = now()
823
+WHERE id = $1
824
+`
825
+
826
+type SetIssueMilestoneParams struct {
827
+	ID          int64
828
+	MilestoneID pgtype.Int8
829
+}
830
+
831
+func (q *Queries) SetIssueMilestone(ctx context.Context, db DBTX, arg SetIssueMilestoneParams) error {
832
+	_, err := db.Exec(ctx, setIssueMilestone, arg.ID, arg.MilestoneID)
833
+	return err
834
+}
835
+
836
+const setIssueState = `-- name: SetIssueState :exec
837
+UPDATE issues
838
+SET state = $2,
839
+    state_reason = $3::issue_state_reason,
840
+    closed_at = CASE WHEN $2::issue_state = 'closed' THEN now() ELSE NULL END,
841
+    closed_by_user_id = $4::bigint,
842
+    updated_at = now()
843
+WHERE id = $1
844
+`
845
+
846
+type SetIssueStateParams struct {
847
+	ID             int64
848
+	State          IssueState
849
+	StateReason    NullIssueStateReason
850
+	ClosedByUserID pgtype.Int8
851
+}
852
+
853
+func (q *Queries) SetIssueState(ctx context.Context, db DBTX, arg SetIssueStateParams) error {
854
+	_, err := db.Exec(ctx, setIssueState,
855
+		arg.ID,
856
+		arg.State,
857
+		arg.StateReason,
858
+		arg.ClosedByUserID,
859
+	)
860
+	return err
861
+}
862
+
863
+const setMilestoneState = `-- name: SetMilestoneState :exec
864
+UPDATE milestones SET state = $2, closed_at = CASE WHEN $2::milestone_state = 'closed' THEN now() ELSE NULL END WHERE id = $1
865
+`
866
+
867
+type SetMilestoneStateParams struct {
868
+	ID    int64
869
+	State MilestoneState
870
+}
871
+
872
+func (q *Queries) SetMilestoneState(ctx context.Context, db DBTX, arg SetMilestoneStateParams) error {
873
+	_, err := db.Exec(ctx, setMilestoneState, arg.ID, arg.State)
874
+	return err
875
+}
876
+
877
+const unassignUserFromIssue = `-- name: UnassignUserFromIssue :exec
878
+DELETE FROM issue_assignees WHERE issue_id = $1 AND user_id = $2
879
+`
880
+
881
+type UnassignUserFromIssueParams struct {
882
+	IssueID int64
883
+	UserID  int64
884
+}
885
+
886
+func (q *Queries) UnassignUserFromIssue(ctx context.Context, db DBTX, arg UnassignUserFromIssueParams) error {
887
+	_, err := db.Exec(ctx, unassignUserFromIssue, arg.IssueID, arg.UserID)
888
+	return err
889
+}
890
+
891
+const updateIssueCommentBody = `-- name: UpdateIssueCommentBody :exec
892
+UPDATE issue_comments
893
+SET body = $2, body_html_cached = $3, edited_at = now(), updated_at = now()
894
+WHERE id = $1
895
+`
896
+
897
+type UpdateIssueCommentBodyParams struct {
898
+	ID             int64
899
+	Body           string
900
+	BodyHtmlCached pgtype.Text
901
+}
902
+
903
+func (q *Queries) UpdateIssueCommentBody(ctx context.Context, db DBTX, arg UpdateIssueCommentBodyParams) error {
904
+	_, err := db.Exec(ctx, updateIssueCommentBody, arg.ID, arg.Body, arg.BodyHtmlCached)
905
+	return err
906
+}
907
+
908
+const updateIssueTitleBody = `-- name: UpdateIssueTitleBody :exec
909
+UPDATE issues
910
+SET title = $2, body = $3, body_html_cached = $4, edited_at = now(), updated_at = now()
911
+WHERE id = $1
912
+`
913
+
914
+type UpdateIssueTitleBodyParams struct {
915
+	ID             int64
916
+	Title          string
917
+	Body           string
918
+	BodyHtmlCached pgtype.Text
919
+}
920
+
921
+func (q *Queries) UpdateIssueTitleBody(ctx context.Context, db DBTX, arg UpdateIssueTitleBodyParams) error {
922
+	_, err := db.Exec(ctx, updateIssueTitleBody,
923
+		arg.ID,
924
+		arg.Title,
925
+		arg.Body,
926
+		arg.BodyHtmlCached,
927
+	)
928
+	return err
929
+}
930
+
931
+const updateLabel = `-- name: UpdateLabel :exec
932
+UPDATE labels
933
+SET name = $2, color = $3, description = $4
934
+WHERE id = $1
935
+`
936
+
937
+type UpdateLabelParams struct {
938
+	ID          int64
939
+	Name        string
940
+	Color       string
941
+	Description string
942
+}
943
+
944
+func (q *Queries) UpdateLabel(ctx context.Context, db DBTX, arg UpdateLabelParams) error {
945
+	_, err := db.Exec(ctx, updateLabel,
946
+		arg.ID,
947
+		arg.Name,
948
+		arg.Color,
949
+		arg.Description,
950
+	)
951
+	return err
952
+}
953
+
954
+const updateMilestone = `-- name: UpdateMilestone :exec
955
+UPDATE milestones SET title = $2, description = $3, due_on = $4::timestamptz WHERE id = $1
956
+`
957
+
958
+type UpdateMilestoneParams struct {
959
+	ID          int64
960
+	Title       string
961
+	Description string
962
+	DueOn       pgtype.Timestamptz
963
+}
964
+
965
+func (q *Queries) UpdateMilestone(ctx context.Context, db DBTX, arg UpdateMilestoneParams) error {
966
+	_, err := db.Exec(ctx, updateMilestone,
967
+		arg.ID,
968
+		arg.Title,
969
+		arg.Description,
970
+		arg.DueOn,
971
+	)
972
+	return err
973
+}
internal/auth/policy/sqlc/models.go → internal/issues/sqlc/models.gocopied (58% similarity)
@@ -2,7 +2,7 @@
22
 // versions:
33
 //   sqlc v1.31.1
44
 
5
-package policydb
5
+package issuesdb
66
 
77
 import (
88
 	"database/sql/driver"
@@ -57,6 +57,219 @@ func (ns NullCollabRole) Value() (driver.Value, error) {
5757
 	return string(ns.CollabRole), nil
5858
 }
5959
 
60
+type IssueKind string
61
+
62
+const (
63
+	IssueKindIssue IssueKind = "issue"
64
+	IssueKindPr    IssueKind = "pr"
65
+)
66
+
67
+func (e *IssueKind) Scan(src interface{}) error {
68
+	switch s := src.(type) {
69
+	case []byte:
70
+		*e = IssueKind(s)
71
+	case string:
72
+		*e = IssueKind(s)
73
+	default:
74
+		return fmt.Errorf("unsupported scan type for IssueKind: %T", src)
75
+	}
76
+	return nil
77
+}
78
+
79
+type NullIssueKind struct {
80
+	IssueKind IssueKind
81
+	Valid     bool // Valid is true if IssueKind is not NULL
82
+}
83
+
84
+// Scan implements the Scanner interface.
85
+func (ns *NullIssueKind) Scan(value interface{}) error {
86
+	if value == nil {
87
+		ns.IssueKind, ns.Valid = "", false
88
+		return nil
89
+	}
90
+	ns.Valid = true
91
+	return ns.IssueKind.Scan(value)
92
+}
93
+
94
+// Value implements the driver Valuer interface.
95
+func (ns NullIssueKind) Value() (driver.Value, error) {
96
+	if !ns.Valid {
97
+		return nil, nil
98
+	}
99
+	return string(ns.IssueKind), nil
100
+}
101
+
102
+type IssueRefSource string
103
+
104
+const (
105
+	IssueRefSourceCommentBody   IssueRefSource = "comment_body"
106
+	IssueRefSourceIssueBody     IssueRefSource = "issue_body"
107
+	IssueRefSourceCommitMessage IssueRefSource = "commit_message"
108
+)
109
+
110
+func (e *IssueRefSource) Scan(src interface{}) error {
111
+	switch s := src.(type) {
112
+	case []byte:
113
+		*e = IssueRefSource(s)
114
+	case string:
115
+		*e = IssueRefSource(s)
116
+	default:
117
+		return fmt.Errorf("unsupported scan type for IssueRefSource: %T", src)
118
+	}
119
+	return nil
120
+}
121
+
122
+type NullIssueRefSource struct {
123
+	IssueRefSource IssueRefSource
124
+	Valid          bool // Valid is true if IssueRefSource is not NULL
125
+}
126
+
127
+// Scan implements the Scanner interface.
128
+func (ns *NullIssueRefSource) Scan(value interface{}) error {
129
+	if value == nil {
130
+		ns.IssueRefSource, ns.Valid = "", false
131
+		return nil
132
+	}
133
+	ns.Valid = true
134
+	return ns.IssueRefSource.Scan(value)
135
+}
136
+
137
+// Value implements the driver Valuer interface.
138
+func (ns NullIssueRefSource) Value() (driver.Value, error) {
139
+	if !ns.Valid {
140
+		return nil, nil
141
+	}
142
+	return string(ns.IssueRefSource), nil
143
+}
144
+
145
+type IssueState string
146
+
147
+const (
148
+	IssueStateOpen   IssueState = "open"
149
+	IssueStateClosed IssueState = "closed"
150
+)
151
+
152
+func (e *IssueState) Scan(src interface{}) error {
153
+	switch s := src.(type) {
154
+	case []byte:
155
+		*e = IssueState(s)
156
+	case string:
157
+		*e = IssueState(s)
158
+	default:
159
+		return fmt.Errorf("unsupported scan type for IssueState: %T", src)
160
+	}
161
+	return nil
162
+}
163
+
164
+type NullIssueState struct {
165
+	IssueState IssueState
166
+	Valid      bool // Valid is true if IssueState is not NULL
167
+}
168
+
169
+// Scan implements the Scanner interface.
170
+func (ns *NullIssueState) Scan(value interface{}) error {
171
+	if value == nil {
172
+		ns.IssueState, ns.Valid = "", false
173
+		return nil
174
+	}
175
+	ns.Valid = true
176
+	return ns.IssueState.Scan(value)
177
+}
178
+
179
+// Value implements the driver Valuer interface.
180
+func (ns NullIssueState) Value() (driver.Value, error) {
181
+	if !ns.Valid {
182
+		return nil, nil
183
+	}
184
+	return string(ns.IssueState), nil
185
+}
186
+
187
+type IssueStateReason string
188
+
189
+const (
190
+	IssueStateReasonCompleted  IssueStateReason = "completed"
191
+	IssueStateReasonNotPlanned IssueStateReason = "not_planned"
192
+	IssueStateReasonReopened   IssueStateReason = "reopened"
193
+	IssueStateReasonDuplicate  IssueStateReason = "duplicate"
194
+)
195
+
196
+func (e *IssueStateReason) Scan(src interface{}) error {
197
+	switch s := src.(type) {
198
+	case []byte:
199
+		*e = IssueStateReason(s)
200
+	case string:
201
+		*e = IssueStateReason(s)
202
+	default:
203
+		return fmt.Errorf("unsupported scan type for IssueStateReason: %T", src)
204
+	}
205
+	return nil
206
+}
207
+
208
+type NullIssueStateReason struct {
209
+	IssueStateReason IssueStateReason
210
+	Valid            bool // Valid is true if IssueStateReason is not NULL
211
+}
212
+
213
+// Scan implements the Scanner interface.
214
+func (ns *NullIssueStateReason) Scan(value interface{}) error {
215
+	if value == nil {
216
+		ns.IssueStateReason, ns.Valid = "", false
217
+		return nil
218
+	}
219
+	ns.Valid = true
220
+	return ns.IssueStateReason.Scan(value)
221
+}
222
+
223
+// Value implements the driver Valuer interface.
224
+func (ns NullIssueStateReason) Value() (driver.Value, error) {
225
+	if !ns.Valid {
226
+		return nil, nil
227
+	}
228
+	return string(ns.IssueStateReason), nil
229
+}
230
+
231
+type MilestoneState string
232
+
233
+const (
234
+	MilestoneStateOpen   MilestoneState = "open"
235
+	MilestoneStateClosed MilestoneState = "closed"
236
+)
237
+
238
+func (e *MilestoneState) Scan(src interface{}) error {
239
+	switch s := src.(type) {
240
+	case []byte:
241
+		*e = MilestoneState(s)
242
+	case string:
243
+		*e = MilestoneState(s)
244
+	default:
245
+		return fmt.Errorf("unsupported scan type for MilestoneState: %T", src)
246
+	}
247
+	return nil
248
+}
249
+
250
+type NullMilestoneState struct {
251
+	MilestoneState MilestoneState
252
+	Valid          bool // Valid is true if MilestoneState is not NULL
253
+}
254
+
255
+// Scan implements the Scanner interface.
256
+func (ns *NullMilestoneState) Scan(value interface{}) error {
257
+	if value == nil {
258
+		ns.MilestoneState, ns.Valid = "", false
259
+		return nil
260
+	}
261
+	ns.Valid = true
262
+	return ns.MilestoneState.Scan(value)
263
+}
264
+
265
+// Value implements the driver Valuer interface.
266
+func (ns NullMilestoneState) Value() (driver.Value, error) {
267
+	if !ns.Valid {
268
+		return nil, nil
269
+	}
270
+	return string(ns.MilestoneState), nil
271
+}
272
+
60273
 type RepoVisibility string
61274
 
62275
 const (
@@ -228,6 +441,73 @@ type EmailVerification struct {
228441
 	CreatedAt   pgtype.Timestamptz
229442
 }
230443
 
444
+type Issue struct {
445
+	ID                int64
446
+	RepoID            int64
447
+	Number            int64
448
+	Kind              IssueKind
449
+	Title             string
450
+	Body              string
451
+	BodyHtmlCached    pgtype.Text
452
+	MdPipelineVersion int32
453
+	AuthorUserID      pgtype.Int8
454
+	State             IssueState
455
+	StateReason       NullIssueStateReason
456
+	Locked            bool
457
+	LockReason        pgtype.Text
458
+	MilestoneID       pgtype.Int8
459
+	CreatedAt         pgtype.Timestamptz
460
+	UpdatedAt         pgtype.Timestamptz
461
+	EditedAt          pgtype.Timestamptz
462
+	ClosedAt          pgtype.Timestamptz
463
+	ClosedByUserID    pgtype.Int8
464
+}
465
+
466
+type IssueAssignee struct {
467
+	IssueID          int64
468
+	UserID           int64
469
+	AssignedAt       pgtype.Timestamptz
470
+	AssignedByUserID pgtype.Int8
471
+}
472
+
473
+type IssueComment struct {
474
+	ID                int64
475
+	IssueID           int64
476
+	AuthorUserID      pgtype.Int8
477
+	Body              string
478
+	BodyHtmlCached    pgtype.Text
479
+	MdPipelineVersion int32
480
+	CreatedAt         pgtype.Timestamptz
481
+	UpdatedAt         pgtype.Timestamptz
482
+	EditedAt          pgtype.Timestamptz
483
+}
484
+
485
+type IssueEvent struct {
486
+	ID          int64
487
+	IssueID     int64
488
+	ActorUserID pgtype.Int8
489
+	Kind        string
490
+	Meta        []byte
491
+	RefTargetID pgtype.Int8
492
+	CreatedAt   pgtype.Timestamptz
493
+}
494
+
495
+type IssueLabel struct {
496
+	IssueID         int64
497
+	LabelID         int64
498
+	AppliedAt       pgtype.Timestamptz
499
+	AppliedByUserID pgtype.Int8
500
+}
501
+
502
+type IssueReference struct {
503
+	ID             int64
504
+	SourceIssueID  pgtype.Int8
505
+	TargetIssueID  int64
506
+	SourceKind     IssueRefSource
507
+	SourceObjectID pgtype.Int8
508
+	CreatedAt      pgtype.Timestamptz
509
+}
510
+
231511
 type Job struct {
232512
 	ID          int64
233513
 	Kind        string
@@ -243,12 +523,32 @@ type Job struct {
243523
 	CreatedAt   pgtype.Timestamptz
244524
 }
245525
 
526
+type Label struct {
527
+	ID          int64
528
+	RepoID      int64
529
+	Name        string
530
+	Color       string
531
+	Description string
532
+	CreatedAt   pgtype.Timestamptz
533
+}
534
+
246535
 type Meta struct {
247536
 	Key       string
248537
 	Value     []byte
249538
 	UpdatedAt pgtype.Timestamptz
250539
 }
251540
 
541
+type Milestone struct {
542
+	ID          int64
543
+	RepoID      int64
544
+	Title       string
545
+	Description string
546
+	State       MilestoneState
547
+	DueOn       pgtype.Timestamptz
548
+	CreatedAt   pgtype.Timestamptz
549
+	ClosedAt    pgtype.Timestamptz
550
+}
551
+
252552
 type PasswordReset struct {
253553
 	ID        int64
254554
 	UserID    int64
@@ -301,6 +601,11 @@ type RepoCollaborator struct {
301601
 	AddedByUserID pgtype.Int8
302602
 }
303603
 
604
+type RepoIssueCounter struct {
605
+	RepoID     int64
606
+	NextNumber int64
607
+}
608
+
304609
 type RepoRedirect struct {
305610
 	OldOwnerUserID pgtype.Int8
306611
 	OldOwnerOrgID  pgtype.Int8
internal/issues/sqlc/querier.goadded
@@ -0,0 +1,74 @@
1
+// Code generated by sqlc. DO NOT EDIT.
2
+// versions:
3
+//   sqlc v1.31.1
4
+
5
+package issuesdb
6
+
7
+import (
8
+	"context"
9
+
10
+	"github.com/jackc/pgx/v5/pgtype"
11
+)
12
+
13
+type Querier interface {
14
+	// ─── issue ↔ label ───────────────────────────────────────────────────
15
+	AddIssueLabel(ctx context.Context, db DBTX, arg AddIssueLabelParams) error
16
+	// UPDATE … RETURNING is concurrency-safe: each row update is
17
+	// serialized by the row lock; concurrent transactions see different
18
+	// values. The caller wraps this in the same tx as the issue insert.
19
+	AllocateIssueNumber(ctx context.Context, db DBTX, repoID int64) (int64, error)
20
+	// ─── assignees ───────────────────────────────────────────────────────
21
+	AssignUserToIssue(ctx context.Context, db DBTX, arg AssignUserToIssueParams) error
22
+	CountIssues(ctx context.Context, db DBTX, arg CountIssuesParams) (int64, error)
23
+	// ─── issues ──────────────────────────────────────────────────────────
24
+	CreateIssue(ctx context.Context, db DBTX, arg CreateIssueParams) (Issue, error)
25
+	// ─── comments ────────────────────────────────────────────────────────
26
+	CreateIssueComment(ctx context.Context, db DBTX, arg CreateIssueCommentParams) (IssueComment, error)
27
+	// ─── labels ──────────────────────────────────────────────────────────
28
+	CreateLabel(ctx context.Context, db DBTX, arg CreateLabelParams) (Label, error)
29
+	// ─── milestones ──────────────────────────────────────────────────────
30
+	CreateMilestone(ctx context.Context, db DBTX, arg CreateMilestoneParams) (Milestone, error)
31
+	DeleteIssueComment(ctx context.Context, db DBTX, id int64) error
32
+	DeleteLabel(ctx context.Context, db DBTX, id int64) error
33
+	DeleteMilestone(ctx context.Context, db DBTX, id int64) error
34
+	// SPDX-License-Identifier: AGPL-3.0-or-later
35
+	// ─── per-repo numbering ───────────────────────────────────────────────
36
+	// Lazy-initialize the counter row. Idempotent — invoked from repo
37
+	// create AND from the first issue insert (defensive in case someone
38
+	// migrates an old repo that predates S21).
39
+	EnsureRepoIssueCounter(ctx context.Context, db DBTX, repoID int64) error
40
+	GetIssueByID(ctx context.Context, db DBTX, id int64) (Issue, error)
41
+	GetIssueByNumber(ctx context.Context, db DBTX, arg GetIssueByNumberParams) (Issue, error)
42
+	GetIssueComment(ctx context.Context, db DBTX, id int64) (IssueComment, error)
43
+	GetLabelByName(ctx context.Context, db DBTX, arg GetLabelByNameParams) (Label, error)
44
+	GetMilestone(ctx context.Context, db DBTX, id int64) (Milestone, error)
45
+	// ─── events + references ─────────────────────────────────────────────
46
+	InsertIssueEvent(ctx context.Context, db DBTX, arg InsertIssueEventParams) (IssueEvent, error)
47
+	InsertIssueReference(ctx context.Context, db DBTX, arg InsertIssueReferenceParams) error
48
+	ListIssueAssignees(ctx context.Context, db DBTX, issueID int64) ([]ListIssueAssigneesRow, error)
49
+	ListIssueComments(ctx context.Context, db DBTX, issueID int64) ([]IssueComment, error)
50
+	ListIssueEvents(ctx context.Context, db DBTX, issueID int64) ([]IssueEvent, error)
51
+	// Filterable list. Caller passes a state filter (open/closed/all
52
+	// where 'all' is encoded as NULL); label/assignee/author/milestone
53
+	// filtering happens after this query in Go for v1 — see the
54
+	// internal/issues/list.go composer. Per-page hardcoded at 25 in the
55
+	// handler; offset is the (page-1)*25.
56
+	ListIssues(ctx context.Context, db DBTX, arg ListIssuesParams) ([]Issue, error)
57
+	ListLabels(ctx context.Context, db DBTX, repoID int64) ([]Label, error)
58
+	ListLabelsOnIssue(ctx context.Context, db DBTX, issueID int64) ([]Label, error)
59
+	ListMilestones(ctx context.Context, db DBTX, repoID int64) ([]Milestone, error)
60
+	// Open + closed counts for the milestone progress bar.
61
+	MilestoneIssueCounts(ctx context.Context, db DBTX, milestoneID pgtype.Int8) (MilestoneIssueCountsRow, error)
62
+	RemoveIssueLabel(ctx context.Context, db DBTX, arg RemoveIssueLabelParams) error
63
+	SetIssueLock(ctx context.Context, db DBTX, arg SetIssueLockParams) error
64
+	SetIssueMilestone(ctx context.Context, db DBTX, arg SetIssueMilestoneParams) error
65
+	SetIssueState(ctx context.Context, db DBTX, arg SetIssueStateParams) error
66
+	SetMilestoneState(ctx context.Context, db DBTX, arg SetMilestoneStateParams) error
67
+	UnassignUserFromIssue(ctx context.Context, db DBTX, arg UnassignUserFromIssueParams) error
68
+	UpdateIssueCommentBody(ctx context.Context, db DBTX, arg UpdateIssueCommentBodyParams) error
69
+	UpdateIssueTitleBody(ctx context.Context, db DBTX, arg UpdateIssueTitleBodyParams) error
70
+	UpdateLabel(ctx context.Context, db DBTX, arg UpdateLabelParams) error
71
+	UpdateMilestone(ctx context.Context, db DBTX, arg UpdateMilestoneParams) error
72
+}
73
+
74
+var _ Querier = (*Queries)(nil)
internal/meta/sqlc/models.gomodified
@@ -57,6 +57,219 @@ func (ns NullCollabRole) Value() (driver.Value, error) {
5757
 	return string(ns.CollabRole), nil
5858
 }
5959
 
60
+type IssueKind string
61
+
62
+const (
63
+	IssueKindIssue IssueKind = "issue"
64
+	IssueKindPr    IssueKind = "pr"
65
+)
66
+
67
+func (e *IssueKind) Scan(src interface{}) error {
68
+	switch s := src.(type) {
69
+	case []byte:
70
+		*e = IssueKind(s)
71
+	case string:
72
+		*e = IssueKind(s)
73
+	default:
74
+		return fmt.Errorf("unsupported scan type for IssueKind: %T", src)
75
+	}
76
+	return nil
77
+}
78
+
79
+type NullIssueKind struct {
80
+	IssueKind IssueKind
81
+	Valid     bool // Valid is true if IssueKind is not NULL
82
+}
83
+
84
+// Scan implements the Scanner interface.
85
+func (ns *NullIssueKind) Scan(value interface{}) error {
86
+	if value == nil {
87
+		ns.IssueKind, ns.Valid = "", false
88
+		return nil
89
+	}
90
+	ns.Valid = true
91
+	return ns.IssueKind.Scan(value)
92
+}
93
+
94
+// Value implements the driver Valuer interface.
95
+func (ns NullIssueKind) Value() (driver.Value, error) {
96
+	if !ns.Valid {
97
+		return nil, nil
98
+	}
99
+	return string(ns.IssueKind), nil
100
+}
101
+
102
+type IssueRefSource string
103
+
104
+const (
105
+	IssueRefSourceCommentBody   IssueRefSource = "comment_body"
106
+	IssueRefSourceIssueBody     IssueRefSource = "issue_body"
107
+	IssueRefSourceCommitMessage IssueRefSource = "commit_message"
108
+)
109
+
110
+func (e *IssueRefSource) Scan(src interface{}) error {
111
+	switch s := src.(type) {
112
+	case []byte:
113
+		*e = IssueRefSource(s)
114
+	case string:
115
+		*e = IssueRefSource(s)
116
+	default:
117
+		return fmt.Errorf("unsupported scan type for IssueRefSource: %T", src)
118
+	}
119
+	return nil
120
+}
121
+
122
+type NullIssueRefSource struct {
123
+	IssueRefSource IssueRefSource
124
+	Valid          bool // Valid is true if IssueRefSource is not NULL
125
+}
126
+
127
+// Scan implements the Scanner interface.
128
+func (ns *NullIssueRefSource) Scan(value interface{}) error {
129
+	if value == nil {
130
+		ns.IssueRefSource, ns.Valid = "", false
131
+		return nil
132
+	}
133
+	ns.Valid = true
134
+	return ns.IssueRefSource.Scan(value)
135
+}
136
+
137
+// Value implements the driver Valuer interface.
138
+func (ns NullIssueRefSource) Value() (driver.Value, error) {
139
+	if !ns.Valid {
140
+		return nil, nil
141
+	}
142
+	return string(ns.IssueRefSource), nil
143
+}
144
+
145
+type IssueState string
146
+
147
+const (
148
+	IssueStateOpen   IssueState = "open"
149
+	IssueStateClosed IssueState = "closed"
150
+)
151
+
152
+func (e *IssueState) Scan(src interface{}) error {
153
+	switch s := src.(type) {
154
+	case []byte:
155
+		*e = IssueState(s)
156
+	case string:
157
+		*e = IssueState(s)
158
+	default:
159
+		return fmt.Errorf("unsupported scan type for IssueState: %T", src)
160
+	}
161
+	return nil
162
+}
163
+
164
+type NullIssueState struct {
165
+	IssueState IssueState
166
+	Valid      bool // Valid is true if IssueState is not NULL
167
+}
168
+
169
+// Scan implements the Scanner interface.
170
+func (ns *NullIssueState) Scan(value interface{}) error {
171
+	if value == nil {
172
+		ns.IssueState, ns.Valid = "", false
173
+		return nil
174
+	}
175
+	ns.Valid = true
176
+	return ns.IssueState.Scan(value)
177
+}
178
+
179
+// Value implements the driver Valuer interface.
180
+func (ns NullIssueState) Value() (driver.Value, error) {
181
+	if !ns.Valid {
182
+		return nil, nil
183
+	}
184
+	return string(ns.IssueState), nil
185
+}
186
+
187
+type IssueStateReason string
188
+
189
+const (
190
+	IssueStateReasonCompleted  IssueStateReason = "completed"
191
+	IssueStateReasonNotPlanned IssueStateReason = "not_planned"
192
+	IssueStateReasonReopened   IssueStateReason = "reopened"
193
+	IssueStateReasonDuplicate  IssueStateReason = "duplicate"
194
+)
195
+
196
+func (e *IssueStateReason) Scan(src interface{}) error {
197
+	switch s := src.(type) {
198
+	case []byte:
199
+		*e = IssueStateReason(s)
200
+	case string:
201
+		*e = IssueStateReason(s)
202
+	default:
203
+		return fmt.Errorf("unsupported scan type for IssueStateReason: %T", src)
204
+	}
205
+	return nil
206
+}
207
+
208
+type NullIssueStateReason struct {
209
+	IssueStateReason IssueStateReason
210
+	Valid            bool // Valid is true if IssueStateReason is not NULL
211
+}
212
+
213
+// Scan implements the Scanner interface.
214
+func (ns *NullIssueStateReason) Scan(value interface{}) error {
215
+	if value == nil {
216
+		ns.IssueStateReason, ns.Valid = "", false
217
+		return nil
218
+	}
219
+	ns.Valid = true
220
+	return ns.IssueStateReason.Scan(value)
221
+}
222
+
223
+// Value implements the driver Valuer interface.
224
+func (ns NullIssueStateReason) Value() (driver.Value, error) {
225
+	if !ns.Valid {
226
+		return nil, nil
227
+	}
228
+	return string(ns.IssueStateReason), nil
229
+}
230
+
231
+type MilestoneState string
232
+
233
+const (
234
+	MilestoneStateOpen   MilestoneState = "open"
235
+	MilestoneStateClosed MilestoneState = "closed"
236
+)
237
+
238
+func (e *MilestoneState) Scan(src interface{}) error {
239
+	switch s := src.(type) {
240
+	case []byte:
241
+		*e = MilestoneState(s)
242
+	case string:
243
+		*e = MilestoneState(s)
244
+	default:
245
+		return fmt.Errorf("unsupported scan type for MilestoneState: %T", src)
246
+	}
247
+	return nil
248
+}
249
+
250
+type NullMilestoneState struct {
251
+	MilestoneState MilestoneState
252
+	Valid          bool // Valid is true if MilestoneState is not NULL
253
+}
254
+
255
+// Scan implements the Scanner interface.
256
+func (ns *NullMilestoneState) Scan(value interface{}) error {
257
+	if value == nil {
258
+		ns.MilestoneState, ns.Valid = "", false
259
+		return nil
260
+	}
261
+	ns.Valid = true
262
+	return ns.MilestoneState.Scan(value)
263
+}
264
+
265
+// Value implements the driver Valuer interface.
266
+func (ns NullMilestoneState) Value() (driver.Value, error) {
267
+	if !ns.Valid {
268
+		return nil, nil
269
+	}
270
+	return string(ns.MilestoneState), nil
271
+}
272
+
60273
 type RepoVisibility string
61274
 
62275
 const (
@@ -228,6 +441,73 @@ type EmailVerification struct {
228441
 	CreatedAt   pgtype.Timestamptz
229442
 }
230443
 
444
+type Issue struct {
445
+	ID                int64
446
+	RepoID            int64
447
+	Number            int64
448
+	Kind              IssueKind
449
+	Title             string
450
+	Body              string
451
+	BodyHtmlCached    pgtype.Text
452
+	MdPipelineVersion int32
453
+	AuthorUserID      pgtype.Int8
454
+	State             IssueState
455
+	StateReason       NullIssueStateReason
456
+	Locked            bool
457
+	LockReason        pgtype.Text
458
+	MilestoneID       pgtype.Int8
459
+	CreatedAt         pgtype.Timestamptz
460
+	UpdatedAt         pgtype.Timestamptz
461
+	EditedAt          pgtype.Timestamptz
462
+	ClosedAt          pgtype.Timestamptz
463
+	ClosedByUserID    pgtype.Int8
464
+}
465
+
466
+type IssueAssignee struct {
467
+	IssueID          int64
468
+	UserID           int64
469
+	AssignedAt       pgtype.Timestamptz
470
+	AssignedByUserID pgtype.Int8
471
+}
472
+
473
+type IssueComment struct {
474
+	ID                int64
475
+	IssueID           int64
476
+	AuthorUserID      pgtype.Int8
477
+	Body              string
478
+	BodyHtmlCached    pgtype.Text
479
+	MdPipelineVersion int32
480
+	CreatedAt         pgtype.Timestamptz
481
+	UpdatedAt         pgtype.Timestamptz
482
+	EditedAt          pgtype.Timestamptz
483
+}
484
+
485
+type IssueEvent struct {
486
+	ID          int64
487
+	IssueID     int64
488
+	ActorUserID pgtype.Int8
489
+	Kind        string
490
+	Meta        []byte
491
+	RefTargetID pgtype.Int8
492
+	CreatedAt   pgtype.Timestamptz
493
+}
494
+
495
+type IssueLabel struct {
496
+	IssueID         int64
497
+	LabelID         int64
498
+	AppliedAt       pgtype.Timestamptz
499
+	AppliedByUserID pgtype.Int8
500
+}
501
+
502
+type IssueReference struct {
503
+	ID             int64
504
+	SourceIssueID  pgtype.Int8
505
+	TargetIssueID  int64
506
+	SourceKind     IssueRefSource
507
+	SourceObjectID pgtype.Int8
508
+	CreatedAt      pgtype.Timestamptz
509
+}
510
+
231511
 type Job struct {
232512
 	ID          int64
233513
 	Kind        string
@@ -243,12 +523,32 @@ type Job struct {
243523
 	CreatedAt   pgtype.Timestamptz
244524
 }
245525
 
526
+type Label struct {
527
+	ID          int64
528
+	RepoID      int64
529
+	Name        string
530
+	Color       string
531
+	Description string
532
+	CreatedAt   pgtype.Timestamptz
533
+}
534
+
246535
 type Meta struct {
247536
 	Key       string
248537
 	Value     []byte
249538
 	UpdatedAt pgtype.Timestamptz
250539
 }
251540
 
541
+type Milestone struct {
542
+	ID          int64
543
+	RepoID      int64
544
+	Title       string
545
+	Description string
546
+	State       MilestoneState
547
+	DueOn       pgtype.Timestamptz
548
+	CreatedAt   pgtype.Timestamptz
549
+	ClosedAt    pgtype.Timestamptz
550
+}
551
+
252552
 type PasswordReset struct {
253553
 	ID        int64
254554
 	UserID    int64
@@ -301,6 +601,11 @@ type RepoCollaborator struct {
301601
 	AddedByUserID pgtype.Int8
302602
 }
303603
 
604
+type RepoIssueCounter struct {
605
+	RepoID     int64
606
+	NextNumber int64
607
+}
608
+
304609
 type RepoRedirect struct {
305610
 	OldOwnerUserID pgtype.Int8
306611
 	OldOwnerOrgID  pgtype.Int8
internal/migrationsfs/migrations/0022_issues.sqladded
@@ -0,0 +1,192 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+--
3
+-- S21 issues subsystem. Eight tables plus the per-repo counter:
4
+--
5
+--   repo_issue_counter   — per-repo monotonic numbering for issues+PRs
6
+--   issues               — the row; `kind` discriminator covers PRs (S22)
7
+--   issue_comments       — per-issue threaded comments
8
+--   issue_assignees      — many-to-many issue ↔ user
9
+--   labels               — repo-scoped labels (default set seeded on create)
10
+--   issue_labels         — many-to-many issue ↔ label
11
+--   milestones           — repo-scoped milestones
12
+--   issue_events         — generic polymorphic timeline (label/assign/etc.)
13
+--   issue_references     — cross-reference index (comment/body/commit → issue)
14
+--
15
+-- The `kind` discriminator on `issues` ('issue'|'pr') is in from day 1
16
+-- so PR rows in S22 are first-class and don't require a schema split.
17
+-- PR-specific fields live in a `pull_requests` table keyed on issue_id
18
+-- (built in S22).
19
+
20
+-- +goose Up
21
+
22
+CREATE TYPE issue_kind         AS ENUM ('issue', 'pr');
23
+CREATE TYPE issue_state        AS ENUM ('open', 'closed');
24
+CREATE TYPE issue_state_reason AS ENUM ('completed', 'not_planned', 'reopened', 'duplicate');
25
+CREATE TYPE milestone_state    AS ENUM ('open', 'closed');
26
+CREATE TYPE issue_ref_source   AS ENUM ('comment_body', 'issue_body', 'commit_message');
27
+
28
+-- Per-repo monotonic counter. Allocation is one row UPDATE inside the
29
+-- creating transaction so concurrent inserts can't collide. Issues +
30
+-- PRs share the counter (matches GitHub's #N space).
31
+CREATE TABLE repo_issue_counter (
32
+    repo_id     bigint  PRIMARY KEY REFERENCES repos(id) ON DELETE CASCADE,
33
+    next_number bigint  NOT NULL DEFAULT 1
34
+);
35
+
36
+CREATE TABLE issues (
37
+    id                  bigserial         PRIMARY KEY,
38
+    repo_id             bigint            NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
39
+    number              bigint            NOT NULL,
40
+    kind                issue_kind        NOT NULL DEFAULT 'issue',
41
+    title               text              NOT NULL,
42
+    body                text              NOT NULL DEFAULT '',
43
+    body_html_cached    text,
44
+    md_pipeline_version int               NOT NULL DEFAULT 1,
45
+    author_user_id      bigint            REFERENCES users(id) ON DELETE SET NULL,
46
+    state               issue_state       NOT NULL DEFAULT 'open',
47
+    state_reason        issue_state_reason,
48
+    locked              boolean           NOT NULL DEFAULT false,
49
+    lock_reason         text,
50
+    milestone_id        bigint, -- FK added below after milestones exists
51
+    created_at          timestamptz       NOT NULL DEFAULT now(),
52
+    updated_at          timestamptz       NOT NULL DEFAULT now(),
53
+    edited_at           timestamptz,
54
+    closed_at           timestamptz,
55
+    closed_by_user_id   bigint            REFERENCES users(id) ON DELETE SET NULL,
56
+
57
+    CONSTRAINT issues_title_length CHECK (char_length(title) BETWEEN 1 AND 256),
58
+    CONSTRAINT issues_body_length  CHECK (char_length(body)  <= 65535),
59
+
60
+    UNIQUE (repo_id, number)
61
+);
62
+
63
+CREATE INDEX issues_repo_state_updated_idx
64
+    ON issues (repo_id, state, updated_at DESC);
65
+CREATE INDEX issues_repo_kind_state_idx
66
+    ON issues (repo_id, kind, state);
67
+CREATE INDEX issues_author_idx ON issues (author_user_id);
68
+
69
+CREATE TRIGGER set_updated_at BEFORE UPDATE ON issues
70
+    FOR EACH ROW EXECUTE FUNCTION tg_set_updated_at();
71
+
72
+
73
+CREATE TABLE issue_comments (
74
+    id                  bigserial   PRIMARY KEY,
75
+    issue_id            bigint      NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
76
+    author_user_id      bigint      REFERENCES users(id) ON DELETE SET NULL,
77
+    body                text        NOT NULL,
78
+    body_html_cached    text,
79
+    md_pipeline_version int         NOT NULL DEFAULT 1,
80
+    created_at          timestamptz NOT NULL DEFAULT now(),
81
+    updated_at          timestamptz NOT NULL DEFAULT now(),
82
+    edited_at           timestamptz,
83
+
84
+    CONSTRAINT issue_comments_body_length CHECK (char_length(body) BETWEEN 1 AND 65535)
85
+);
86
+
87
+CREATE INDEX issue_comments_issue_idx ON issue_comments (issue_id, created_at);
88
+
89
+CREATE TRIGGER set_updated_at BEFORE UPDATE ON issue_comments
90
+    FOR EACH ROW EXECUTE FUNCTION tg_set_updated_at();
91
+
92
+
93
+CREATE TABLE issue_assignees (
94
+    issue_id            bigint      NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
95
+    user_id             bigint      NOT NULL REFERENCES users(id) ON DELETE CASCADE,
96
+    assigned_at         timestamptz NOT NULL DEFAULT now(),
97
+    assigned_by_user_id bigint      REFERENCES users(id) ON DELETE SET NULL,
98
+
99
+    PRIMARY KEY (issue_id, user_id)
100
+);
101
+
102
+CREATE INDEX issue_assignees_user_idx ON issue_assignees (user_id);
103
+
104
+
105
+CREATE TABLE labels (
106
+    id          bigserial   PRIMARY KEY,
107
+    repo_id     bigint      NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
108
+    name        citext      NOT NULL,
109
+    color       text        NOT NULL DEFAULT 'd0d7de',
110
+    description text        NOT NULL DEFAULT '',
111
+    created_at  timestamptz NOT NULL DEFAULT now(),
112
+
113
+    CONSTRAINT labels_name_length CHECK (char_length(name::text) BETWEEN 1 AND 50),
114
+    CONSTRAINT labels_color_format CHECK (color ~ '^[0-9a-fA-F]{6}$'),
115
+
116
+    UNIQUE (repo_id, name)
117
+);
118
+
119
+
120
+CREATE TABLE issue_labels (
121
+    issue_id           bigint      NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
122
+    label_id           bigint      NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
123
+    applied_at         timestamptz NOT NULL DEFAULT now(),
124
+    applied_by_user_id bigint      REFERENCES users(id) ON DELETE SET NULL,
125
+
126
+    PRIMARY KEY (issue_id, label_id)
127
+);
128
+
129
+CREATE INDEX issue_labels_label_idx ON issue_labels (label_id);
130
+
131
+
132
+CREATE TABLE milestones (
133
+    id          bigserial         PRIMARY KEY,
134
+    repo_id     bigint            NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
135
+    title       text              NOT NULL,
136
+    description text              NOT NULL DEFAULT '',
137
+    state       milestone_state   NOT NULL DEFAULT 'open',
138
+    due_on      timestamptz,
139
+    created_at  timestamptz       NOT NULL DEFAULT now(),
140
+    closed_at   timestamptz,
141
+
142
+    CONSTRAINT milestones_title_length CHECK (char_length(title) BETWEEN 1 AND 200),
143
+
144
+    UNIQUE (repo_id, title)
145
+);
146
+
147
+ALTER TABLE issues ADD CONSTRAINT issues_milestone_fk
148
+    FOREIGN KEY (milestone_id) REFERENCES milestones(id) ON DELETE SET NULL;
149
+
150
+
151
+CREATE TABLE issue_events (
152
+    id            bigserial   PRIMARY KEY,
153
+    issue_id      bigint      NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
154
+    actor_user_id bigint      REFERENCES users(id) ON DELETE SET NULL,
155
+    kind          text        NOT NULL,
156
+    meta          jsonb       NOT NULL DEFAULT '{}'::jsonb,
157
+    ref_target_id bigint, -- denormalized when the event references another issue
158
+    created_at    timestamptz NOT NULL DEFAULT now()
159
+);
160
+
161
+CREATE INDEX issue_events_issue_idx ON issue_events (issue_id, created_at);
162
+
163
+
164
+CREATE TABLE issue_references (
165
+    id                bigserial   PRIMARY KEY,
166
+    source_issue_id   bigint      REFERENCES issues(id) ON DELETE SET NULL,
167
+    target_issue_id   bigint      NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
168
+    source_kind       issue_ref_source NOT NULL,
169
+    source_object_id  bigint, -- comment_id when source_kind=comment_body; issue_id when issue_body; push_event_id when commit_message
170
+    created_at        timestamptz NOT NULL DEFAULT now()
171
+);
172
+
173
+CREATE INDEX issue_references_target_idx ON issue_references (target_issue_id, created_at);
174
+CREATE INDEX issue_references_source_idx ON issue_references (source_issue_id, source_kind);
175
+
176
+
177
+-- +goose Down
178
+DROP TABLE IF EXISTS issue_references;
179
+DROP TABLE IF EXISTS issue_events;
180
+ALTER TABLE issues DROP CONSTRAINT IF EXISTS issues_milestone_fk;
181
+DROP TABLE IF EXISTS milestones;
182
+DROP TABLE IF EXISTS issue_labels;
183
+DROP TABLE IF EXISTS labels;
184
+DROP TABLE IF EXISTS issue_assignees;
185
+DROP TABLE IF EXISTS issue_comments;
186
+DROP TABLE IF EXISTS issues;
187
+DROP TABLE IF EXISTS repo_issue_counter;
188
+DROP TYPE IF EXISTS issue_ref_source;
189
+DROP TYPE IF EXISTS milestone_state;
190
+DROP TYPE IF EXISTS issue_state_reason;
191
+DROP TYPE IF EXISTS issue_state;
192
+DROP TYPE IF EXISTS issue_kind;
internal/repos/sqlc/models.gomodified
@@ -57,6 +57,219 @@ func (ns NullCollabRole) Value() (driver.Value, error) {
5757
 	return string(ns.CollabRole), nil
5858
 }
5959
 
60
+type IssueKind string
61
+
62
+const (
63
+	IssueKindIssue IssueKind = "issue"
64
+	IssueKindPr    IssueKind = "pr"
65
+)
66
+
67
+func (e *IssueKind) Scan(src interface{}) error {
68
+	switch s := src.(type) {
69
+	case []byte:
70
+		*e = IssueKind(s)
71
+	case string:
72
+		*e = IssueKind(s)
73
+	default:
74
+		return fmt.Errorf("unsupported scan type for IssueKind: %T", src)
75
+	}
76
+	return nil
77
+}
78
+
79
+type NullIssueKind struct {
80
+	IssueKind IssueKind
81
+	Valid     bool // Valid is true if IssueKind is not NULL
82
+}
83
+
84
+// Scan implements the Scanner interface.
85
+func (ns *NullIssueKind) Scan(value interface{}) error {
86
+	if value == nil {
87
+		ns.IssueKind, ns.Valid = "", false
88
+		return nil
89
+	}
90
+	ns.Valid = true
91
+	return ns.IssueKind.Scan(value)
92
+}
93
+
94
+// Value implements the driver Valuer interface.
95
+func (ns NullIssueKind) Value() (driver.Value, error) {
96
+	if !ns.Valid {
97
+		return nil, nil
98
+	}
99
+	return string(ns.IssueKind), nil
100
+}
101
+
102
+type IssueRefSource string
103
+
104
+const (
105
+	IssueRefSourceCommentBody   IssueRefSource = "comment_body"
106
+	IssueRefSourceIssueBody     IssueRefSource = "issue_body"
107
+	IssueRefSourceCommitMessage IssueRefSource = "commit_message"
108
+)
109
+
110
+func (e *IssueRefSource) Scan(src interface{}) error {
111
+	switch s := src.(type) {
112
+	case []byte:
113
+		*e = IssueRefSource(s)
114
+	case string:
115
+		*e = IssueRefSource(s)
116
+	default:
117
+		return fmt.Errorf("unsupported scan type for IssueRefSource: %T", src)
118
+	}
119
+	return nil
120
+}
121
+
122
+type NullIssueRefSource struct {
123
+	IssueRefSource IssueRefSource
124
+	Valid          bool // Valid is true if IssueRefSource is not NULL
125
+}
126
+
127
+// Scan implements the Scanner interface.
128
+func (ns *NullIssueRefSource) Scan(value interface{}) error {
129
+	if value == nil {
130
+		ns.IssueRefSource, ns.Valid = "", false
131
+		return nil
132
+	}
133
+	ns.Valid = true
134
+	return ns.IssueRefSource.Scan(value)
135
+}
136
+
137
+// Value implements the driver Valuer interface.
138
+func (ns NullIssueRefSource) Value() (driver.Value, error) {
139
+	if !ns.Valid {
140
+		return nil, nil
141
+	}
142
+	return string(ns.IssueRefSource), nil
143
+}
144
+
145
+type IssueState string
146
+
147
+const (
148
+	IssueStateOpen   IssueState = "open"
149
+	IssueStateClosed IssueState = "closed"
150
+)
151
+
152
+func (e *IssueState) Scan(src interface{}) error {
153
+	switch s := src.(type) {
154
+	case []byte:
155
+		*e = IssueState(s)
156
+	case string:
157
+		*e = IssueState(s)
158
+	default:
159
+		return fmt.Errorf("unsupported scan type for IssueState: %T", src)
160
+	}
161
+	return nil
162
+}
163
+
164
+type NullIssueState struct {
165
+	IssueState IssueState
166
+	Valid      bool // Valid is true if IssueState is not NULL
167
+}
168
+
169
+// Scan implements the Scanner interface.
170
+func (ns *NullIssueState) Scan(value interface{}) error {
171
+	if value == nil {
172
+		ns.IssueState, ns.Valid = "", false
173
+		return nil
174
+	}
175
+	ns.Valid = true
176
+	return ns.IssueState.Scan(value)
177
+}
178
+
179
+// Value implements the driver Valuer interface.
180
+func (ns NullIssueState) Value() (driver.Value, error) {
181
+	if !ns.Valid {
182
+		return nil, nil
183
+	}
184
+	return string(ns.IssueState), nil
185
+}
186
+
187
+type IssueStateReason string
188
+
189
+const (
190
+	IssueStateReasonCompleted  IssueStateReason = "completed"
191
+	IssueStateReasonNotPlanned IssueStateReason = "not_planned"
192
+	IssueStateReasonReopened   IssueStateReason = "reopened"
193
+	IssueStateReasonDuplicate  IssueStateReason = "duplicate"
194
+)
195
+
196
+func (e *IssueStateReason) Scan(src interface{}) error {
197
+	switch s := src.(type) {
198
+	case []byte:
199
+		*e = IssueStateReason(s)
200
+	case string:
201
+		*e = IssueStateReason(s)
202
+	default:
203
+		return fmt.Errorf("unsupported scan type for IssueStateReason: %T", src)
204
+	}
205
+	return nil
206
+}
207
+
208
+type NullIssueStateReason struct {
209
+	IssueStateReason IssueStateReason
210
+	Valid            bool // Valid is true if IssueStateReason is not NULL
211
+}
212
+
213
+// Scan implements the Scanner interface.
214
+func (ns *NullIssueStateReason) Scan(value interface{}) error {
215
+	if value == nil {
216
+		ns.IssueStateReason, ns.Valid = "", false
217
+		return nil
218
+	}
219
+	ns.Valid = true
220
+	return ns.IssueStateReason.Scan(value)
221
+}
222
+
223
+// Value implements the driver Valuer interface.
224
+func (ns NullIssueStateReason) Value() (driver.Value, error) {
225
+	if !ns.Valid {
226
+		return nil, nil
227
+	}
228
+	return string(ns.IssueStateReason), nil
229
+}
230
+
231
+type MilestoneState string
232
+
233
+const (
234
+	MilestoneStateOpen   MilestoneState = "open"
235
+	MilestoneStateClosed MilestoneState = "closed"
236
+)
237
+
238
+func (e *MilestoneState) Scan(src interface{}) error {
239
+	switch s := src.(type) {
240
+	case []byte:
241
+		*e = MilestoneState(s)
242
+	case string:
243
+		*e = MilestoneState(s)
244
+	default:
245
+		return fmt.Errorf("unsupported scan type for MilestoneState: %T", src)
246
+	}
247
+	return nil
248
+}
249
+
250
+type NullMilestoneState struct {
251
+	MilestoneState MilestoneState
252
+	Valid          bool // Valid is true if MilestoneState is not NULL
253
+}
254
+
255
+// Scan implements the Scanner interface.
256
+func (ns *NullMilestoneState) Scan(value interface{}) error {
257
+	if value == nil {
258
+		ns.MilestoneState, ns.Valid = "", false
259
+		return nil
260
+	}
261
+	ns.Valid = true
262
+	return ns.MilestoneState.Scan(value)
263
+}
264
+
265
+// Value implements the driver Valuer interface.
266
+func (ns NullMilestoneState) Value() (driver.Value, error) {
267
+	if !ns.Valid {
268
+		return nil, nil
269
+	}
270
+	return string(ns.MilestoneState), nil
271
+}
272
+
60273
 type RepoVisibility string
61274
 
62275
 const (
@@ -228,6 +441,73 @@ type EmailVerification struct {
228441
 	CreatedAt   pgtype.Timestamptz
229442
 }
230443
 
444
+type Issue struct {
445
+	ID                int64
446
+	RepoID            int64
447
+	Number            int64
448
+	Kind              IssueKind
449
+	Title             string
450
+	Body              string
451
+	BodyHtmlCached    pgtype.Text
452
+	MdPipelineVersion int32
453
+	AuthorUserID      pgtype.Int8
454
+	State             IssueState
455
+	StateReason       NullIssueStateReason
456
+	Locked            bool
457
+	LockReason        pgtype.Text
458
+	MilestoneID       pgtype.Int8
459
+	CreatedAt         pgtype.Timestamptz
460
+	UpdatedAt         pgtype.Timestamptz
461
+	EditedAt          pgtype.Timestamptz
462
+	ClosedAt          pgtype.Timestamptz
463
+	ClosedByUserID    pgtype.Int8
464
+}
465
+
466
+type IssueAssignee struct {
467
+	IssueID          int64
468
+	UserID           int64
469
+	AssignedAt       pgtype.Timestamptz
470
+	AssignedByUserID pgtype.Int8
471
+}
472
+
473
+type IssueComment struct {
474
+	ID                int64
475
+	IssueID           int64
476
+	AuthorUserID      pgtype.Int8
477
+	Body              string
478
+	BodyHtmlCached    pgtype.Text
479
+	MdPipelineVersion int32
480
+	CreatedAt         pgtype.Timestamptz
481
+	UpdatedAt         pgtype.Timestamptz
482
+	EditedAt          pgtype.Timestamptz
483
+}
484
+
485
+type IssueEvent struct {
486
+	ID          int64
487
+	IssueID     int64
488
+	ActorUserID pgtype.Int8
489
+	Kind        string
490
+	Meta        []byte
491
+	RefTargetID pgtype.Int8
492
+	CreatedAt   pgtype.Timestamptz
493
+}
494
+
495
+type IssueLabel struct {
496
+	IssueID         int64
497
+	LabelID         int64
498
+	AppliedAt       pgtype.Timestamptz
499
+	AppliedByUserID pgtype.Int8
500
+}
501
+
502
+type IssueReference struct {
503
+	ID             int64
504
+	SourceIssueID  pgtype.Int8
505
+	TargetIssueID  int64
506
+	SourceKind     IssueRefSource
507
+	SourceObjectID pgtype.Int8
508
+	CreatedAt      pgtype.Timestamptz
509
+}
510
+
231511
 type Job struct {
232512
 	ID          int64
233513
 	Kind        string
@@ -243,12 +523,32 @@ type Job struct {
243523
 	CreatedAt   pgtype.Timestamptz
244524
 }
245525
 
526
+type Label struct {
527
+	ID          int64
528
+	RepoID      int64
529
+	Name        string
530
+	Color       string
531
+	Description string
532
+	CreatedAt   pgtype.Timestamptz
533
+}
534
+
246535
 type Meta struct {
247536
 	Key       string
248537
 	Value     []byte
249538
 	UpdatedAt pgtype.Timestamptz
250539
 }
251540
 
541
+type Milestone struct {
542
+	ID          int64
543
+	RepoID      int64
544
+	Title       string
545
+	Description string
546
+	State       MilestoneState
547
+	DueOn       pgtype.Timestamptz
548
+	CreatedAt   pgtype.Timestamptz
549
+	ClosedAt    pgtype.Timestamptz
550
+}
551
+
252552
 type PasswordReset struct {
253553
 	ID        int64
254554
 	UserID    int64
@@ -301,6 +601,11 @@ type RepoCollaborator struct {
301601
 	AddedByUserID pgtype.Int8
302602
 }
303603
 
604
+type RepoIssueCounter struct {
605
+	RepoID     int64
606
+	NextNumber int64
607
+}
608
+
304609
 type RepoRedirect struct {
305610
 	OldOwnerUserID pgtype.Int8
306611
 	OldOwnerOrgID  pgtype.Int8
internal/users/sqlc/models.gomodified
@@ -57,6 +57,219 @@ func (ns NullCollabRole) Value() (driver.Value, error) {
5757
 	return string(ns.CollabRole), nil
5858
 }
5959
 
60
+type IssueKind string
61
+
62
+const (
63
+	IssueKindIssue IssueKind = "issue"
64
+	IssueKindPr    IssueKind = "pr"
65
+)
66
+
67
+func (e *IssueKind) Scan(src interface{}) error {
68
+	switch s := src.(type) {
69
+	case []byte:
70
+		*e = IssueKind(s)
71
+	case string:
72
+		*e = IssueKind(s)
73
+	default:
74
+		return fmt.Errorf("unsupported scan type for IssueKind: %T", src)
75
+	}
76
+	return nil
77
+}
78
+
79
+type NullIssueKind struct {
80
+	IssueKind IssueKind
81
+	Valid     bool // Valid is true if IssueKind is not NULL
82
+}
83
+
84
+// Scan implements the Scanner interface.
85
+func (ns *NullIssueKind) Scan(value interface{}) error {
86
+	if value == nil {
87
+		ns.IssueKind, ns.Valid = "", false
88
+		return nil
89
+	}
90
+	ns.Valid = true
91
+	return ns.IssueKind.Scan(value)
92
+}
93
+
94
+// Value implements the driver Valuer interface.
95
+func (ns NullIssueKind) Value() (driver.Value, error) {
96
+	if !ns.Valid {
97
+		return nil, nil
98
+	}
99
+	return string(ns.IssueKind), nil
100
+}
101
+
102
+type IssueRefSource string
103
+
104
+const (
105
+	IssueRefSourceCommentBody   IssueRefSource = "comment_body"
106
+	IssueRefSourceIssueBody     IssueRefSource = "issue_body"
107
+	IssueRefSourceCommitMessage IssueRefSource = "commit_message"
108
+)
109
+
110
+func (e *IssueRefSource) Scan(src interface{}) error {
111
+	switch s := src.(type) {
112
+	case []byte:
113
+		*e = IssueRefSource(s)
114
+	case string:
115
+		*e = IssueRefSource(s)
116
+	default:
117
+		return fmt.Errorf("unsupported scan type for IssueRefSource: %T", src)
118
+	}
119
+	return nil
120
+}
121
+
122
+type NullIssueRefSource struct {
123
+	IssueRefSource IssueRefSource
124
+	Valid          bool // Valid is true if IssueRefSource is not NULL
125
+}
126
+
127
+// Scan implements the Scanner interface.
128
+func (ns *NullIssueRefSource) Scan(value interface{}) error {
129
+	if value == nil {
130
+		ns.IssueRefSource, ns.Valid = "", false
131
+		return nil
132
+	}
133
+	ns.Valid = true
134
+	return ns.IssueRefSource.Scan(value)
135
+}
136
+
137
+// Value implements the driver Valuer interface.
138
+func (ns NullIssueRefSource) Value() (driver.Value, error) {
139
+	if !ns.Valid {
140
+		return nil, nil
141
+	}
142
+	return string(ns.IssueRefSource), nil
143
+}
144
+
145
+type IssueState string
146
+
147
+const (
148
+	IssueStateOpen   IssueState = "open"
149
+	IssueStateClosed IssueState = "closed"
150
+)
151
+
152
+func (e *IssueState) Scan(src interface{}) error {
153
+	switch s := src.(type) {
154
+	case []byte:
155
+		*e = IssueState(s)
156
+	case string:
157
+		*e = IssueState(s)
158
+	default:
159
+		return fmt.Errorf("unsupported scan type for IssueState: %T", src)
160
+	}
161
+	return nil
162
+}
163
+
164
+type NullIssueState struct {
165
+	IssueState IssueState
166
+	Valid      bool // Valid is true if IssueState is not NULL
167
+}
168
+
169
+// Scan implements the Scanner interface.
170
+func (ns *NullIssueState) Scan(value interface{}) error {
171
+	if value == nil {
172
+		ns.IssueState, ns.Valid = "", false
173
+		return nil
174
+	}
175
+	ns.Valid = true
176
+	return ns.IssueState.Scan(value)
177
+}
178
+
179
+// Value implements the driver Valuer interface.
180
+func (ns NullIssueState) Value() (driver.Value, error) {
181
+	if !ns.Valid {
182
+		return nil, nil
183
+	}
184
+	return string(ns.IssueState), nil
185
+}
186
+
187
+type IssueStateReason string
188
+
189
+const (
190
+	IssueStateReasonCompleted  IssueStateReason = "completed"
191
+	IssueStateReasonNotPlanned IssueStateReason = "not_planned"
192
+	IssueStateReasonReopened   IssueStateReason = "reopened"
193
+	IssueStateReasonDuplicate  IssueStateReason = "duplicate"
194
+)
195
+
196
+func (e *IssueStateReason) Scan(src interface{}) error {
197
+	switch s := src.(type) {
198
+	case []byte:
199
+		*e = IssueStateReason(s)
200
+	case string:
201
+		*e = IssueStateReason(s)
202
+	default:
203
+		return fmt.Errorf("unsupported scan type for IssueStateReason: %T", src)
204
+	}
205
+	return nil
206
+}
207
+
208
+type NullIssueStateReason struct {
209
+	IssueStateReason IssueStateReason
210
+	Valid            bool // Valid is true if IssueStateReason is not NULL
211
+}
212
+
213
+// Scan implements the Scanner interface.
214
+func (ns *NullIssueStateReason) Scan(value interface{}) error {
215
+	if value == nil {
216
+		ns.IssueStateReason, ns.Valid = "", false
217
+		return nil
218
+	}
219
+	ns.Valid = true
220
+	return ns.IssueStateReason.Scan(value)
221
+}
222
+
223
+// Value implements the driver Valuer interface.
224
+func (ns NullIssueStateReason) Value() (driver.Value, error) {
225
+	if !ns.Valid {
226
+		return nil, nil
227
+	}
228
+	return string(ns.IssueStateReason), nil
229
+}
230
+
231
+type MilestoneState string
232
+
233
+const (
234
+	MilestoneStateOpen   MilestoneState = "open"
235
+	MilestoneStateClosed MilestoneState = "closed"
236
+)
237
+
238
+func (e *MilestoneState) Scan(src interface{}) error {
239
+	switch s := src.(type) {
240
+	case []byte:
241
+		*e = MilestoneState(s)
242
+	case string:
243
+		*e = MilestoneState(s)
244
+	default:
245
+		return fmt.Errorf("unsupported scan type for MilestoneState: %T", src)
246
+	}
247
+	return nil
248
+}
249
+
250
+type NullMilestoneState struct {
251
+	MilestoneState MilestoneState
252
+	Valid          bool // Valid is true if MilestoneState is not NULL
253
+}
254
+
255
+// Scan implements the Scanner interface.
256
+func (ns *NullMilestoneState) Scan(value interface{}) error {
257
+	if value == nil {
258
+		ns.MilestoneState, ns.Valid = "", false
259
+		return nil
260
+	}
261
+	ns.Valid = true
262
+	return ns.MilestoneState.Scan(value)
263
+}
264
+
265
+// Value implements the driver Valuer interface.
266
+func (ns NullMilestoneState) Value() (driver.Value, error) {
267
+	if !ns.Valid {
268
+		return nil, nil
269
+	}
270
+	return string(ns.MilestoneState), nil
271
+}
272
+
60273
 type RepoVisibility string
61274
 
62275
 const (
@@ -228,6 +441,73 @@ type EmailVerification struct {
228441
 	CreatedAt   pgtype.Timestamptz
229442
 }
230443
 
444
+type Issue struct {
445
+	ID                int64
446
+	RepoID            int64
447
+	Number            int64
448
+	Kind              IssueKind
449
+	Title             string
450
+	Body              string
451
+	BodyHtmlCached    pgtype.Text
452
+	MdPipelineVersion int32
453
+	AuthorUserID      pgtype.Int8
454
+	State             IssueState
455
+	StateReason       NullIssueStateReason
456
+	Locked            bool
457
+	LockReason        pgtype.Text
458
+	MilestoneID       pgtype.Int8
459
+	CreatedAt         pgtype.Timestamptz
460
+	UpdatedAt         pgtype.Timestamptz
461
+	EditedAt          pgtype.Timestamptz
462
+	ClosedAt          pgtype.Timestamptz
463
+	ClosedByUserID    pgtype.Int8
464
+}
465
+
466
+type IssueAssignee struct {
467
+	IssueID          int64
468
+	UserID           int64
469
+	AssignedAt       pgtype.Timestamptz
470
+	AssignedByUserID pgtype.Int8
471
+}
472
+
473
+type IssueComment struct {
474
+	ID                int64
475
+	IssueID           int64
476
+	AuthorUserID      pgtype.Int8
477
+	Body              string
478
+	BodyHtmlCached    pgtype.Text
479
+	MdPipelineVersion int32
480
+	CreatedAt         pgtype.Timestamptz
481
+	UpdatedAt         pgtype.Timestamptz
482
+	EditedAt          pgtype.Timestamptz
483
+}
484
+
485
+type IssueEvent struct {
486
+	ID          int64
487
+	IssueID     int64
488
+	ActorUserID pgtype.Int8
489
+	Kind        string
490
+	Meta        []byte
491
+	RefTargetID pgtype.Int8
492
+	CreatedAt   pgtype.Timestamptz
493
+}
494
+
495
+type IssueLabel struct {
496
+	IssueID         int64
497
+	LabelID         int64
498
+	AppliedAt       pgtype.Timestamptz
499
+	AppliedByUserID pgtype.Int8
500
+}
501
+
502
+type IssueReference struct {
503
+	ID             int64
504
+	SourceIssueID  pgtype.Int8
505
+	TargetIssueID  int64
506
+	SourceKind     IssueRefSource
507
+	SourceObjectID pgtype.Int8
508
+	CreatedAt      pgtype.Timestamptz
509
+}
510
+
231511
 type Job struct {
232512
 	ID          int64
233513
 	Kind        string
@@ -243,12 +523,32 @@ type Job struct {
243523
 	CreatedAt   pgtype.Timestamptz
244524
 }
245525
 
526
+type Label struct {
527
+	ID          int64
528
+	RepoID      int64
529
+	Name        string
530
+	Color       string
531
+	Description string
532
+	CreatedAt   pgtype.Timestamptz
533
+}
534
+
246535
 type Meta struct {
247536
 	Key       string
248537
 	Value     []byte
249538
 	UpdatedAt pgtype.Timestamptz
250539
 }
251540
 
541
+type Milestone struct {
542
+	ID          int64
543
+	RepoID      int64
544
+	Title       string
545
+	Description string
546
+	State       MilestoneState
547
+	DueOn       pgtype.Timestamptz
548
+	CreatedAt   pgtype.Timestamptz
549
+	ClosedAt    pgtype.Timestamptz
550
+}
551
+
252552
 type PasswordReset struct {
253553
 	ID        int64
254554
 	UserID    int64
@@ -301,6 +601,11 @@ type RepoCollaborator struct {
301601
 	AddedByUserID pgtype.Int8
302602
 }
303603
 
604
+type RepoIssueCounter struct {
605
+	RepoID     int64
606
+	NextNumber int64
607
+}
608
+
304609
 type RepoRedirect struct {
305610
 	OldOwnerUserID pgtype.Int8
306611
 	OldOwnerOrgID  pgtype.Int8
internal/worker/sqlc/models.gomodified
@@ -57,6 +57,219 @@ func (ns NullCollabRole) Value() (driver.Value, error) {
5757
 	return string(ns.CollabRole), nil
5858
 }
5959
 
60
+type IssueKind string
61
+
62
+const (
63
+	IssueKindIssue IssueKind = "issue"
64
+	IssueKindPr    IssueKind = "pr"
65
+)
66
+
67
+func (e *IssueKind) Scan(src interface{}) error {
68
+	switch s := src.(type) {
69
+	case []byte:
70
+		*e = IssueKind(s)
71
+	case string:
72
+		*e = IssueKind(s)
73
+	default:
74
+		return fmt.Errorf("unsupported scan type for IssueKind: %T", src)
75
+	}
76
+	return nil
77
+}
78
+
79
+type NullIssueKind struct {
80
+	IssueKind IssueKind
81
+	Valid     bool // Valid is true if IssueKind is not NULL
82
+}
83
+
84
+// Scan implements the Scanner interface.
85
+func (ns *NullIssueKind) Scan(value interface{}) error {
86
+	if value == nil {
87
+		ns.IssueKind, ns.Valid = "", false
88
+		return nil
89
+	}
90
+	ns.Valid = true
91
+	return ns.IssueKind.Scan(value)
92
+}
93
+
94
+// Value implements the driver Valuer interface.
95
+func (ns NullIssueKind) Value() (driver.Value, error) {
96
+	if !ns.Valid {
97
+		return nil, nil
98
+	}
99
+	return string(ns.IssueKind), nil
100
+}
101
+
102
+type IssueRefSource string
103
+
104
+const (
105
+	IssueRefSourceCommentBody   IssueRefSource = "comment_body"
106
+	IssueRefSourceIssueBody     IssueRefSource = "issue_body"
107
+	IssueRefSourceCommitMessage IssueRefSource = "commit_message"
108
+)
109
+
110
+func (e *IssueRefSource) Scan(src interface{}) error {
111
+	switch s := src.(type) {
112
+	case []byte:
113
+		*e = IssueRefSource(s)
114
+	case string:
115
+		*e = IssueRefSource(s)
116
+	default:
117
+		return fmt.Errorf("unsupported scan type for IssueRefSource: %T", src)
118
+	}
119
+	return nil
120
+}
121
+
122
+type NullIssueRefSource struct {
123
+	IssueRefSource IssueRefSource
124
+	Valid          bool // Valid is true if IssueRefSource is not NULL
125
+}
126
+
127
+// Scan implements the Scanner interface.
128
+func (ns *NullIssueRefSource) Scan(value interface{}) error {
129
+	if value == nil {
130
+		ns.IssueRefSource, ns.Valid = "", false
131
+		return nil
132
+	}
133
+	ns.Valid = true
134
+	return ns.IssueRefSource.Scan(value)
135
+}
136
+
137
+// Value implements the driver Valuer interface.
138
+func (ns NullIssueRefSource) Value() (driver.Value, error) {
139
+	if !ns.Valid {
140
+		return nil, nil
141
+	}
142
+	return string(ns.IssueRefSource), nil
143
+}
144
+
145
+type IssueState string
146
+
147
+const (
148
+	IssueStateOpen   IssueState = "open"
149
+	IssueStateClosed IssueState = "closed"
150
+)
151
+
152
+func (e *IssueState) Scan(src interface{}) error {
153
+	switch s := src.(type) {
154
+	case []byte:
155
+		*e = IssueState(s)
156
+	case string:
157
+		*e = IssueState(s)
158
+	default:
159
+		return fmt.Errorf("unsupported scan type for IssueState: %T", src)
160
+	}
161
+	return nil
162
+}
163
+
164
+type NullIssueState struct {
165
+	IssueState IssueState
166
+	Valid      bool // Valid is true if IssueState is not NULL
167
+}
168
+
169
+// Scan implements the Scanner interface.
170
+func (ns *NullIssueState) Scan(value interface{}) error {
171
+	if value == nil {
172
+		ns.IssueState, ns.Valid = "", false
173
+		return nil
174
+	}
175
+	ns.Valid = true
176
+	return ns.IssueState.Scan(value)
177
+}
178
+
179
+// Value implements the driver Valuer interface.
180
+func (ns NullIssueState) Value() (driver.Value, error) {
181
+	if !ns.Valid {
182
+		return nil, nil
183
+	}
184
+	return string(ns.IssueState), nil
185
+}
186
+
187
+type IssueStateReason string
188
+
189
+const (
190
+	IssueStateReasonCompleted  IssueStateReason = "completed"
191
+	IssueStateReasonNotPlanned IssueStateReason = "not_planned"
192
+	IssueStateReasonReopened   IssueStateReason = "reopened"
193
+	IssueStateReasonDuplicate  IssueStateReason = "duplicate"
194
+)
195
+
196
+func (e *IssueStateReason) Scan(src interface{}) error {
197
+	switch s := src.(type) {
198
+	case []byte:
199
+		*e = IssueStateReason(s)
200
+	case string:
201
+		*e = IssueStateReason(s)
202
+	default:
203
+		return fmt.Errorf("unsupported scan type for IssueStateReason: %T", src)
204
+	}
205
+	return nil
206
+}
207
+
208
+type NullIssueStateReason struct {
209
+	IssueStateReason IssueStateReason
210
+	Valid            bool // Valid is true if IssueStateReason is not NULL
211
+}
212
+
213
+// Scan implements the Scanner interface.
214
+func (ns *NullIssueStateReason) Scan(value interface{}) error {
215
+	if value == nil {
216
+		ns.IssueStateReason, ns.Valid = "", false
217
+		return nil
218
+	}
219
+	ns.Valid = true
220
+	return ns.IssueStateReason.Scan(value)
221
+}
222
+
223
+// Value implements the driver Valuer interface.
224
+func (ns NullIssueStateReason) Value() (driver.Value, error) {
225
+	if !ns.Valid {
226
+		return nil, nil
227
+	}
228
+	return string(ns.IssueStateReason), nil
229
+}
230
+
231
+type MilestoneState string
232
+
233
+const (
234
+	MilestoneStateOpen   MilestoneState = "open"
235
+	MilestoneStateClosed MilestoneState = "closed"
236
+)
237
+
238
+func (e *MilestoneState) Scan(src interface{}) error {
239
+	switch s := src.(type) {
240
+	case []byte:
241
+		*e = MilestoneState(s)
242
+	case string:
243
+		*e = MilestoneState(s)
244
+	default:
245
+		return fmt.Errorf("unsupported scan type for MilestoneState: %T", src)
246
+	}
247
+	return nil
248
+}
249
+
250
+type NullMilestoneState struct {
251
+	MilestoneState MilestoneState
252
+	Valid          bool // Valid is true if MilestoneState is not NULL
253
+}
254
+
255
+// Scan implements the Scanner interface.
256
+func (ns *NullMilestoneState) Scan(value interface{}) error {
257
+	if value == nil {
258
+		ns.MilestoneState, ns.Valid = "", false
259
+		return nil
260
+	}
261
+	ns.Valid = true
262
+	return ns.MilestoneState.Scan(value)
263
+}
264
+
265
+// Value implements the driver Valuer interface.
266
+func (ns NullMilestoneState) Value() (driver.Value, error) {
267
+	if !ns.Valid {
268
+		return nil, nil
269
+	}
270
+	return string(ns.MilestoneState), nil
271
+}
272
+
60273
 type RepoVisibility string
61274
 
62275
 const (
@@ -228,6 +441,73 @@ type EmailVerification struct {
228441
 	CreatedAt   pgtype.Timestamptz
229442
 }
230443
 
444
+type Issue struct {
445
+	ID                int64
446
+	RepoID            int64
447
+	Number            int64
448
+	Kind              IssueKind
449
+	Title             string
450
+	Body              string
451
+	BodyHtmlCached    pgtype.Text
452
+	MdPipelineVersion int32
453
+	AuthorUserID      pgtype.Int8
454
+	State             IssueState
455
+	StateReason       NullIssueStateReason
456
+	Locked            bool
457
+	LockReason        pgtype.Text
458
+	MilestoneID       pgtype.Int8
459
+	CreatedAt         pgtype.Timestamptz
460
+	UpdatedAt         pgtype.Timestamptz
461
+	EditedAt          pgtype.Timestamptz
462
+	ClosedAt          pgtype.Timestamptz
463
+	ClosedByUserID    pgtype.Int8
464
+}
465
+
466
+type IssueAssignee struct {
467
+	IssueID          int64
468
+	UserID           int64
469
+	AssignedAt       pgtype.Timestamptz
470
+	AssignedByUserID pgtype.Int8
471
+}
472
+
473
+type IssueComment struct {
474
+	ID                int64
475
+	IssueID           int64
476
+	AuthorUserID      pgtype.Int8
477
+	Body              string
478
+	BodyHtmlCached    pgtype.Text
479
+	MdPipelineVersion int32
480
+	CreatedAt         pgtype.Timestamptz
481
+	UpdatedAt         pgtype.Timestamptz
482
+	EditedAt          pgtype.Timestamptz
483
+}
484
+
485
+type IssueEvent struct {
486
+	ID          int64
487
+	IssueID     int64
488
+	ActorUserID pgtype.Int8
489
+	Kind        string
490
+	Meta        []byte
491
+	RefTargetID pgtype.Int8
492
+	CreatedAt   pgtype.Timestamptz
493
+}
494
+
495
+type IssueLabel struct {
496
+	IssueID         int64
497
+	LabelID         int64
498
+	AppliedAt       pgtype.Timestamptz
499
+	AppliedByUserID pgtype.Int8
500
+}
501
+
502
+type IssueReference struct {
503
+	ID             int64
504
+	SourceIssueID  pgtype.Int8
505
+	TargetIssueID  int64
506
+	SourceKind     IssueRefSource
507
+	SourceObjectID pgtype.Int8
508
+	CreatedAt      pgtype.Timestamptz
509
+}
510
+
231511
 type Job struct {
232512
 	ID          int64
233513
 	Kind        string
@@ -243,12 +523,32 @@ type Job struct {
243523
 	CreatedAt   pgtype.Timestamptz
244524
 }
245525
 
526
+type Label struct {
527
+	ID          int64
528
+	RepoID      int64
529
+	Name        string
530
+	Color       string
531
+	Description string
532
+	CreatedAt   pgtype.Timestamptz
533
+}
534
+
246535
 type Meta struct {
247536
 	Key       string
248537
 	Value     []byte
249538
 	UpdatedAt pgtype.Timestamptz
250539
 }
251540
 
541
+type Milestone struct {
542
+	ID          int64
543
+	RepoID      int64
544
+	Title       string
545
+	Description string
546
+	State       MilestoneState
547
+	DueOn       pgtype.Timestamptz
548
+	CreatedAt   pgtype.Timestamptz
549
+	ClosedAt    pgtype.Timestamptz
550
+}
551
+
252552
 type PasswordReset struct {
253553
 	ID        int64
254554
 	UserID    int64
@@ -301,6 +601,11 @@ type RepoCollaborator struct {
301601
 	AddedByUserID pgtype.Int8
302602
 }
303603
 
604
+type RepoIssueCounter struct {
605
+	RepoID     int64
606
+	NextNumber int64
607
+}
608
+
304609
 type RepoRedirect struct {
305610
 	OldOwnerUserID pgtype.Int8
306611
 	OldOwnerOrgID  pgtype.Int8
sqlc.yamlmodified
@@ -50,6 +50,22 @@ sql:
5050
         emit_empty_slices: true
5151
         emit_methods_with_db_argument: true
5252
 
53
+  - engine: postgresql
54
+    schema: internal/migrationsfs/migrations
55
+    queries: internal/issues/queries
56
+    gen:
57
+      go:
58
+        package: issuesdb
59
+        out: internal/issues/sqlc
60
+        sql_package: pgx/v5
61
+        emit_json_tags: false
62
+        emit_pointers_for_null_types: false
63
+        emit_prepared_queries: false
64
+        emit_interface: true
65
+        emit_exact_table_names: false
66
+        emit_empty_slices: true
67
+        emit_methods_with_db_argument: true
68
+
5369
   - engine: postgresql
5470
     schema: internal/migrationsfs/migrations
5571
     queries: internal/auth/policy/queries