···103103 unique(issue_id, comment_id),
104104 foreign key (repo_at, issue_id) references issues(repo_at, issue_id) on delete cascade
105105 );
106106+ create table if not exists pulls (
107107+ id integer primary key autoincrement,
108108+ owner_did text not null,
109109+ repo_at text not null,
110110+ pull_id integer not null,
111111+ title text not null,
112112+ patch text,
113113+ patch_at text not null,
114114+ open integer not null default 1,
115115+ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
116116+ unique(repo_at, pull_id),
117117+ foreign key (repo_at) references repos(at_uri) on delete cascade
118118+ );
119119+ create table if not exists pull_comments (
120120+ id integer primary key autoincrement,
121121+ owner_did text not null,
122122+ pull_id integer not null,
123123+ repo_at text not null,
124124+ comment_id integer not null,
125125+ comment_at text not null,
126126+ body text not null,
127127+ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
128128+ unique(pull_id, comment_id),
129129+ foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade
130130+ );
106131 create table if not exists _jetstream (
107132 id integer primary key autoincrement,
108133 last_time_us integer not null
···111136 create table if not exists repo_issue_seqs (
112137 repo_at text primary key,
113138 next_issue_id integer not null default 1
139139+ );
140140+141141+ create table if not exists repo_pull_seqs (
142142+ repo_at text primary key,
143143+ next_pull_id integer not null default 1
114144 );
115145116146 create table if not exists stars (
+254
appview/db/pulls.go
···11+package db
22+33+import (
44+ "database/sql"
55+ "time"
66+77+ "github.com/bluesky-social/indigo/atproto/syntax"
88+)
99+1010+type Pulls struct {
1111+ ID int `json:"id"`
1212+ OwnerDid string `json:"owner_did"`
1313+ RepoAt string `json:"repo_at"`
1414+ PullId int `json:"pull_id"`
1515+ Title string `json:"title"`
1616+ Patch string `json:"patch,omitempty"`
1717+ PatchAt string `json:"patch_at"`
1818+ Open int `json:"open"`
1919+ Created time.Time `json:"created"`
2020+}
2121+2222+type PullComments struct {
2323+ ID int `json:"id"`
2424+ OwnerDid string `json:"owner_did"`
2525+ PullId int `json:"pull_id"`
2626+ RepoAt string `json:"repo_at"`
2727+ CommentId int `json:"comment_id"`
2828+ CommentAt string `json:"comment_at"`
2929+ Body string `json:"body"`
3030+ Created time.Time `json:"created"`
3131+}
3232+3333+func NewPull(tx *sql.Tx, pull *Pulls) error {
3434+ defer tx.Rollback()
3535+3636+ _, err := tx.Exec(`
3737+ insert or ignore into repo_pull_seqs (repo_at, next_pull_id)
3838+ values (?, 1)
3939+ `, pull.RepoAt)
4040+ if err != nil {
4141+ return err
4242+ }
4343+4444+ var nextId int
4545+ err = tx.QueryRow(`
4646+ update repo_pull_seqs
4747+ set next_pull_id = next_pull_id + 1
4848+ where repo_at = ?
4949+ returning next_pull_id - 1
5050+ `, pull.RepoAt).Scan(&nextId)
5151+ if err != nil {
5252+ return err
5353+ }
5454+5555+ pull.PullId = nextId
5656+5757+ _, err = tx.Exec(`
5858+ insert into pulls (repo_at, owner_did, pull_id, title, patch)
5959+ values (?, ?, ?, ?, ?)
6060+ `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.Patch)
6161+ if err != nil {
6262+ return err
6363+ }
6464+6565+ if err := tx.Commit(); err != nil {
6666+ return err
6767+ }
6868+6969+ return nil
7070+}
7171+7272+func SetPullAt(e Execer, repoAt syntax.ATURI, pullId int, pullAt string) error {
7373+ _, err := e.Exec(`update pulls set patch_at = ? where repo_at = ? and pull_id = ?`, pullAt, repoAt, pullId)
7474+ return err
7575+}
7676+7777+func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (string, error) {
7878+ var pullAt string
7979+ err := e.QueryRow(`select patch_at from pulls where repo_at = ? and pull_id = ?`, repoAt, pullId).Scan(&pullAt)
8080+ return pullAt, err
8181+}
8282+8383+func GetPullId(e Execer, repoAt syntax.ATURI) (int, error) {
8484+ var pullId int
8585+ err := e.QueryRow(`select next_pull_id from repo_pull_seqs where repo_at = ?`, repoAt).Scan(&pullId)
8686+ return pullId - 1, err
8787+}
8888+8989+func GetPullOwnerDid(e Execer, repoAt syntax.ATURI, pullId int) (string, error) {
9090+ var ownerDid string
9191+ err := e.QueryRow(`select owner_did from pulls where repo_at = ? and pull_id = ?`, repoAt, pullId).Scan(&ownerDid)
9292+ return ownerDid, err
9393+}
9494+9595+func GetPulls(e Execer, repoAt syntax.ATURI) ([]Pulls, error) {
9696+ var pulls []Pulls
9797+9898+ rows, err := e.Query(`select owner_did, pull_id, created, title, patch, open from pulls where repo_at = ? order by created desc`, repoAt)
9999+ if err != nil {
100100+ return nil, err
101101+ }
102102+ defer rows.Close()
103103+104104+ for rows.Next() {
105105+ var pull Pulls
106106+ var createdAt string
107107+ err := rows.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.Patch, &pull.Open)
108108+ if err != nil {
109109+ return nil, err
110110+ }
111111+112112+ createdTime, err := time.Parse(time.RFC3339, createdAt)
113113+ if err != nil {
114114+ return nil, err
115115+ }
116116+ pull.Created = createdTime
117117+118118+ pulls = append(pulls, pull)
119119+ }
120120+121121+ if err := rows.Err(); err != nil {
122122+ return nil, err
123123+ }
124124+125125+ return pulls, nil
126126+}
127127+128128+func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pulls, error) {
129129+ query := `select owner_did, created, title, patch, open from pulls where repo_at = ? and pull_id = ?`
130130+ row := e.QueryRow(query, repoAt, pullId)
131131+132132+ var pull Pulls
133133+ var createdAt string
134134+ err := row.Scan(&pull.OwnerDid, &createdAt, &pull.Title, &pull.Patch, &pull.Open)
135135+ if err != nil {
136136+ return nil, err
137137+ }
138138+139139+ createdTime, err := time.Parse(time.RFC3339, createdAt)
140140+ if err != nil {
141141+ return nil, err
142142+ }
143143+ pull.Created = createdTime
144144+145145+ return &pull, nil
146146+}
147147+148148+func GetPullWithComments(e Execer, repoAt syntax.ATURI, pullId int) (*Pulls, []PullComments, error) {
149149+ query := `select owner_did, pull_id, created, title, patch, open from pulls where repo_at = ? and pull_id = ?`
150150+ row := e.QueryRow(query, repoAt, pullId)
151151+152152+ var pull Pulls
153153+ var createdAt string
154154+ err := row.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.Patch, &pull.Open)
155155+ if err != nil {
156156+ return nil, nil, err
157157+ }
158158+159159+ createdTime, err := time.Parse(time.RFC3339, createdAt)
160160+ if err != nil {
161161+ return nil, nil, err
162162+ }
163163+ pull.Created = createdTime
164164+165165+ comments, err := GetPullComments(e, repoAt, pullId)
166166+ if err != nil {
167167+ return nil, nil, err
168168+ }
169169+170170+ return &pull, comments, nil
171171+}
172172+173173+func NewPullComment(e Execer, comment *PullComments) error {
174174+ query := `insert into pull_comments (owner_did, repo_at, comment_at, pull_id, comment_id, body) values (?, ?, ?, ?, ?, ?)`
175175+ _, err := e.Exec(
176176+ query,
177177+ comment.OwnerDid,
178178+ comment.RepoAt,
179179+ comment.CommentAt,
180180+ comment.PullId,
181181+ comment.CommentId,
182182+ comment.Body,
183183+ )
184184+ return err
185185+}
186186+187187+func GetPullComments(e Execer, repoAt syntax.ATURI, pullId int) ([]PullComments, error) {
188188+ var comments []PullComments
189189+190190+ rows, err := e.Query(`select owner_did, pull_id, comment_id, comment_at, body, created from pull_comments where repo_at = ? and pull_id = ? order by created asc`, repoAt, pullId)
191191+ if err == sql.ErrNoRows {
192192+ return []PullComments{}, nil
193193+ }
194194+ if err != nil {
195195+ return nil, err
196196+ }
197197+ defer rows.Close()
198198+199199+ for rows.Next() {
200200+ var comment PullComments
201201+ var createdAt string
202202+ err := rows.Scan(&comment.OwnerDid, &comment.PullId, &comment.CommentId, &comment.CommentAt, &comment.Body, &createdAt)
203203+ if err != nil {
204204+ return nil, err
205205+ }
206206+207207+ createdAtTime, err := time.Parse(time.RFC3339, createdAt)
208208+ if err != nil {
209209+ return nil, err
210210+ }
211211+ comment.Created = createdAtTime
212212+213213+ comments = append(comments, comment)
214214+ }
215215+216216+ if err := rows.Err(); err != nil {
217217+ return nil, err
218218+ }
219219+220220+ return comments, nil
221221+}
222222+223223+func ClosePull(e Execer, repoAt syntax.ATURI, pullId int) error {
224224+ _, err := e.Exec(`update pulls set open = 0 where repo_at = ? and pull_id = ?`, repoAt, pullId)
225225+ return err
226226+}
227227+228228+func ReopenPull(e Execer, repoAt syntax.ATURI, pullId int) error {
229229+ _, err := e.Exec(`update pulls set open = 1 where repo_at = ? and pull_id = ?`, repoAt, pullId)
230230+ return err
231231+}
232232+233233+type PullCount struct {
234234+ Open int
235235+ Closed int
236236+}
237237+238238+func GetPullCount(e Execer, repoAt syntax.ATURI) (PullCount, error) {
239239+ row := e.QueryRow(`
240240+ select
241241+ count(case when open = 1 then 1 end) as open_count,
242242+ count(case when open = 0 then 1 end) as closed_count
243243+ from pulls
244244+ where repo_at = ?`,
245245+ repoAt,
246246+ )
247247+248248+ var count PullCount
249249+ if err := row.Scan(&count.Open, &count.Closed); err != nil {
250250+ return PullCount{0, 0}, err
251251+ }
252252+253253+ return count, nil
254254+}