···23722372 return err
23732373 })
2374237423752375+ orm.RunMigration(conn, logger, "add-issue-pull-state-tables", func(tx *sql.Tx) error {
23762376+ _, err := tx.Exec(`
23772377+ create table if not exists issue_states (
23782378+ id integer primary key autoincrement,
23792379+ did text not null,
23802380+ rkey text not null,
23812381+ at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue.state' || '/' || rkey) stored,
23822382+23832383+ subject text not null,
23842384+ state text not null check (state in ('open', 'closed')),
23852385+ created_micros integer not null,
23862386+23872387+ unique(did, rkey),
23882388+ foreign key (subject) references issues(at_uri) on delete cascade
23892389+ );
23902390+ create index if not exists idx_issue_states_subject on issue_states(subject);
23912391+23922392+ create table if not exists pull_states (
23932393+ id integer primary key autoincrement,
23942394+ did text not null,
23952395+ rkey text not null,
23962396+ at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.pull.status' || '/' || rkey) stored,
23972397+23982398+ subject text not null,
23992399+ status text not null check (status in ('open', 'closed', 'merged')),
24002400+ created_micros integer not null,
24012401+24022402+ unique(did, rkey),
24032403+ foreign key (subject) references pulls(at_uri) on delete cascade
24042404+ );
24052405+ create index if not exists idx_pull_states_subject on pull_states(subject);
24062406+24072407+ create table if not exists pending_state_records (
24082408+ id integer primary key autoincrement,
24092409+ did text not null,
24102410+ rkey text not null,
24112411+ nsid text not null,
24122412+24132413+ subject text not null,
24142414+ record blob not null,
24152415+ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
24162416+24172417+ unique(did, rkey, nsid)
24182418+ );
24192419+ create index if not exists idx_pending_state_subject on pending_state_records(subject);
24202420+ `)
24212421+ return err
24222422+ })
24232423+23752424 return &DB{
23762425 db,
23772426 logger,
+259
appview/db/entity_state.go
···11+package db
22+33+import (
44+ "database/sql"
55+ "errors"
66+ "fmt"
77+88+ "github.com/bluesky-social/indigo/atproto/syntax"
99+ "tangled.org/core/appview/models"
1010+)
1111+1212+type stateTable struct {
1313+ name string
1414+ valueCol string
1515+}
1616+1717+var (
1818+ issueStateTable = stateTable{name: "issue_states", valueCol: "state"}
1919+ pullStateTable = stateTable{name: "pull_states", valueCol: "status"}
2020+)
2121+2222+func putStateRecord(tx *sql.Tx, t stateTable, rec models.StateRecord) (syntax.ATURI, error) {
2323+ var priorSubject string
2424+ err := tx.QueryRow(
2525+ fmt.Sprintf(`select subject from %s where did = ? and rkey = ?`, t.name),
2626+ rec.Did, rec.Rkey,
2727+ ).Scan(&priorSubject)
2828+ switch {
2929+ case errors.Is(err, sql.ErrNoRows):
3030+ priorSubject = ""
3131+ case err != nil:
3232+ return "", err
3333+ }
3434+3535+ if _, err := tx.Exec(fmt.Sprintf(`
3636+ insert into %s (did, rkey, subject, %s, created_micros)
3737+ values (?, ?, ?, ?, ?)
3838+ on conflict(did, rkey) do update set
3939+ subject = excluded.subject,
4040+ %s = excluded.%s,
4141+ created_micros = excluded.created_micros
4242+ `, t.name, t.valueCol, t.valueCol, t.valueCol),
4343+ rec.Did, rec.Rkey, rec.Subject, string(rec.Value), rec.SortMicros); err != nil {
4444+ return "", err
4545+ }
4646+4747+ if priorSubject != "" && syntax.ATURI(priorSubject) != rec.Subject {
4848+ return syntax.ATURI(priorSubject), nil
4949+ }
5050+ return "", nil
5151+}
5252+5353+func deleteStateRecord(tx *sql.Tx, t stateTable, did, rkey string) (syntax.ATURI, error) {
5454+ var subject string
5555+ err := tx.QueryRow(
5656+ fmt.Sprintf(`select subject from %s where did = ? and rkey = ?`, t.name),
5757+ did, rkey,
5858+ ).Scan(&subject)
5959+ switch {
6060+ case errors.Is(err, sql.ErrNoRows):
6161+ return "", nil
6262+ case err != nil:
6363+ return "", err
6464+ }
6565+6666+ if _, err := tx.Exec(
6767+ fmt.Sprintf(`delete from %s where did = ? and rkey = ?`, t.name),
6868+ did, rkey,
6969+ ); err != nil {
7070+ return "", err
7171+ }
7272+ return syntax.ATURI(subject), nil
7373+}
7474+7575+func stateWinner(e Execer, t stateTable, subject syntax.ATURI) (models.StateValue, bool, error) {
7676+ var v string
7777+ err := e.QueryRow(fmt.Sprintf(`
7878+ select %s from %s
7979+ where subject = ?
8080+ order by created_micros desc, at_uri desc
8181+ limit 1
8282+ `, t.valueCol, t.name), subject).Scan(&v)
8383+ switch {
8484+ case errors.Is(err, sql.ErrNoRows):
8585+ return "", false, nil
8686+ case err != nil:
8787+ return "", false, err
8888+ }
8989+ return models.StateValue(v), true, nil
9090+}
9191+9292+func PutIssueState(tx *sql.Tx, rec models.StateRecord) (syntax.ATURI, error) {
9393+ return putStateRecord(tx, issueStateTable, rec)
9494+}
9595+9696+func PutPullStatus(tx *sql.Tx, rec models.StateRecord) (syntax.ATURI, error) {
9797+ return putStateRecord(tx, pullStateTable, rec)
9898+}
9999+100100+type PendingStateRecord struct {
101101+ Did string
102102+ Rkey string
103103+ Nsid string
104104+ Subject syntax.ATURI
105105+ Record []byte
106106+}
107107+108108+func ParkStateRecord(tx *sql.Tx, p PendingStateRecord) error {
109109+ _, err := tx.Exec(`
110110+ insert into pending_state_records (did, rkey, nsid, subject, record)
111111+ values (?, ?, ?, ?, ?)
112112+ on conflict(did, rkey, nsid) do update set
113113+ subject = excluded.subject,
114114+ record = excluded.record
115115+ `, p.Did, p.Rkey, p.Nsid, p.Subject, p.Record)
116116+ return err
117117+}
118118+119119+func UnparkStateRecord(tx *sql.Tx, did, rkey, nsid string) error {
120120+ _, err := tx.Exec(
121121+ `delete from pending_state_records where did = ? and rkey = ? and nsid = ?`,
122122+ did, rkey, nsid,
123123+ )
124124+ return err
125125+}
126126+127127+func DistinctPendingStateSubjects(e Execer) ([]syntax.ATURI, error) {
128128+ rows, err := e.Query(`select distinct subject from pending_state_records order by subject asc`)
129129+ if err != nil {
130130+ return nil, err
131131+ }
132132+ defer rows.Close()
133133+134134+ var subjects []syntax.ATURI
135135+ for rows.Next() {
136136+ var s string
137137+ if err := rows.Scan(&s); err != nil {
138138+ return nil, err
139139+ }
140140+ subjects = append(subjects, syntax.ATURI(s))
141141+ }
142142+ return subjects, rows.Err()
143143+}
144144+145145+func EvictStalePendingStateRecords(e Execer, before string) (int64, error) {
146146+ res, err := e.Exec(`
147147+ delete from pending_state_records
148148+ where created < ?
149149+ and not exists (select 1 from issues where at_uri = pending_state_records.subject)
150150+ and not exists (select 1 from pulls where at_uri = pending_state_records.subject)
151151+ `, before)
152152+ if err != nil {
153153+ return 0, err
154154+ }
155155+ return res.RowsAffected()
156156+}
157157+158158+func PendingStateRecordsForSubject(e Execer, subject syntax.ATURI) ([]PendingStateRecord, error) {
159159+ rows, err := e.Query(
160160+ `select did, rkey, nsid, subject, record from pending_state_records where subject = ? order by id asc`,
161161+ subject,
162162+ )
163163+ if err != nil {
164164+ return nil, err
165165+ }
166166+ defer rows.Close()
167167+168168+ var pending []PendingStateRecord
169169+ for rows.Next() {
170170+ var p PendingStateRecord
171171+ var subj string
172172+ if err := rows.Scan(&p.Did, &p.Rkey, &p.Nsid, &subj, &p.Record); err != nil {
173173+ return nil, err
174174+ }
175175+ p.Subject = syntax.ATURI(subj)
176176+ pending = append(pending, p)
177177+ }
178178+ return pending, rows.Err()
179179+}
180180+181181+func DeleteIssueState(tx *sql.Tx, did, rkey string) (syntax.ATURI, error) {
182182+ return deleteStateRecord(tx, issueStateTable, did, rkey)
183183+}
184184+185185+func DeletePullStatus(tx *sql.Tx, did, rkey string) (syntax.ATURI, error) {
186186+ return deleteStateRecord(tx, pullStateTable, did, rkey)
187187+}
188188+189189+func setIssueOpen(tx *sql.Tx, subject syntax.ATURI, open bool) error {
190190+ v := 0
191191+ if open {
192192+ v = 1
193193+ }
194194+ _, err := tx.Exec(`update issues set open = ? where at_uri = ?`, v, subject)
195195+ return err
196196+}
197197+198198+func applyIssueState(tx *sql.Tx, subject syntax.ATURI, resetWhenEmpty bool) error {
199199+ winner, ok, err := stateWinner(tx, issueStateTable, subject)
200200+ if err != nil {
201201+ return err
202202+ }
203203+ if !ok {
204204+ if resetWhenEmpty {
205205+ return setIssueOpen(tx, subject, true)
206206+ }
207207+ return nil
208208+ }
209209+ return setIssueOpen(tx, subject, winner == models.StateOpen)
210210+}
211211+212212+func ResolveIssueState(tx *sql.Tx, subject syntax.ATURI) error {
213213+ return applyIssueState(tx, subject, false)
214214+}
215215+216216+func RecomputeIssueState(tx *sql.Tx, subject syntax.ATURI) error {
217217+ return applyIssueState(tx, subject, true)
218218+}
219219+220220+func pullStateFromValue(v models.StateValue) models.PullState {
221221+ switch v {
222222+ case models.StateMerged:
223223+ return models.PullMerged
224224+ case models.StateClosed:
225225+ return models.PullClosed
226226+ default:
227227+ return models.PullOpen
228228+ }
229229+}
230230+231231+func setPullState(tx *sql.Tx, subject syntax.ATURI, st models.PullState) error {
232232+ _, err := tx.Exec(
233233+ `update pulls set state = ? where at_uri = ? and state <> ?`,
234234+ st, subject, models.PullAbandoned,
235235+ )
236236+ return err
237237+}
238238+239239+func applyPullStatus(tx *sql.Tx, subject syntax.ATURI, resetWhenEmpty bool) error {
240240+ winner, ok, err := stateWinner(tx, pullStateTable, subject)
241241+ if err != nil {
242242+ return err
243243+ }
244244+ if !ok {
245245+ if resetWhenEmpty {
246246+ return setPullState(tx, subject, models.PullOpen)
247247+ }
248248+ return nil
249249+ }
250250+ return setPullState(tx, subject, pullStateFromValue(winner))
251251+}
252252+253253+func ResolvePullStatus(tx *sql.Tx, subject syntax.ATURI) error {
254254+ return applyPullStatus(tx, subject, false)
255255+}
256256+257257+func RecomputePullStatus(tx *sql.Tx, subject syntax.ATURI) error {
258258+ return applyPullStatus(tx, subject, true)
259259+}