Monorepo for Tangled
tangled.org
1package db
2
3import (
4 "database/sql"
5 "fmt"
6 "strings"
7
8 "github.com/bluesky-social/indigo/atproto/syntax"
9 "tangled.org/core/api/tangled"
10 "tangled.org/core/appview/models"
11 "tangled.org/core/orm"
12)
13
14// ValidateReferenceLinks resolves refLinks to Issue/PR/Comment ATURIs.
15// It will ignore missing refLinks.
16func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
17 var (
18 issueRefs []models.ReferenceLink
19 pullRefs []models.ReferenceLink
20 )
21 for _, ref := range refLinks {
22 switch ref.Kind {
23 case models.RefKindIssue:
24 issueRefs = append(issueRefs, ref)
25 case models.RefKindPull:
26 pullRefs = append(pullRefs, ref)
27 }
28 }
29 issueUris, err := findIssueReferences(e, issueRefs)
30 if err != nil {
31 return nil, fmt.Errorf("find issue references: %w", err)
32 }
33 pullUris, err := findPullReferences(e, pullRefs)
34 if err != nil {
35 return nil, fmt.Errorf("find pull references: %w", err)
36 }
37
38 return append(issueUris, pullUris...), nil
39}
40
41func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
42 if len(refLinks) == 0 {
43 return nil, nil
44 }
45 vals := make([]string, len(refLinks))
46 args := make([]any, 0, len(refLinks)*4)
47 for i, ref := range refLinks {
48 vals[i] = "(?, ?, ?, ?)"
49 args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId)
50 }
51 query := fmt.Sprintf(
52 `with input(owner_did, name, issue_id, comment_id) as (
53 values %s
54 )
55 select
56 i.at_uri, c.at_uri
57 from input inp
58 join repos r
59 on r.did = inp.owner_did
60 and r.name = inp.name
61 join issues i
62 on i.repo_did = r.repo_did
63 and i.issue_id = inp.issue_id
64 left join comments c
65 on inp.comment_id is not null
66 and c.subject_uri = i.at_uri
67 and c.id = inp.comment_id
68 `,
69 strings.Join(vals, ","),
70 )
71 rows, err := e.Query(query, args...)
72 if err != nil {
73 return nil, err
74 }
75 defer rows.Close()
76
77 var uris []syntax.ATURI
78
79 for rows.Next() {
80 // Scan rows
81 var issueUri string
82 var commentUri sql.NullString
83 var uri syntax.ATURI
84 if err := rows.Scan(&issueUri, &commentUri); err != nil {
85 return nil, err
86 }
87 if commentUri.Valid {
88 uri = syntax.ATURI(commentUri.String)
89 } else {
90 uri = syntax.ATURI(issueUri)
91 }
92 uris = append(uris, uri)
93 }
94 if err := rows.Err(); err != nil {
95 return nil, fmt.Errorf("iterate rows: %w", err)
96 }
97
98 return uris, nil
99}
100
101func findPullReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
102 if len(refLinks) == 0 {
103 return nil, nil
104 }
105 vals := make([]string, len(refLinks))
106 args := make([]any, 0, len(refLinks)*4)
107 for i, ref := range refLinks {
108 vals[i] = "(?, ?, ?, ?)"
109 args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId)
110 }
111 query := fmt.Sprintf(
112 `with input(owner_did, name, pull_id, comment_id) as (
113 values %s
114 )
115 select
116 p.owner_did, p.rkey, c.at_uri
117 from input inp
118 join repos r
119 on r.did = inp.owner_did
120 and r.name = inp.name
121 join pulls p
122 on p.repo_did = r.repo_did
123 and p.pull_id = inp.pull_id
124 left join comments c
125 on inp.comment_id is not null
126 and c.subject_uri = ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey)
127 and c.id = inp.comment_id
128 `,
129 strings.Join(vals, ","),
130 )
131 rows, err := e.Query(query, args...)
132 if err != nil {
133 return nil, err
134 }
135 defer rows.Close()
136
137 var uris []syntax.ATURI
138
139 for rows.Next() {
140 // Scan rows
141 var pullOwner, pullRkey string
142 var commentUri sql.NullString
143 var uri syntax.ATURI
144 if err := rows.Scan(&pullOwner, &pullRkey, &commentUri); err != nil {
145 return nil, err
146 }
147 if commentUri.Valid {
148 // no-op
149 uri = syntax.ATURI(commentUri.String)
150 } else {
151 uri = syntax.ATURI(fmt.Sprintf(
152 "at://%s/%s/%s",
153 pullOwner,
154 tangled.RepoPullNSID,
155 pullRkey,
156 ))
157 }
158 uris = append(uris, uri)
159 }
160 return uris, nil
161}
162
163func putReferences(tx *sql.Tx, fromAt syntax.ATURI, references []syntax.ATURI) error {
164 err := deleteReferences(tx, fromAt)
165 if err != nil {
166 return fmt.Errorf("delete old reference_links: %w", err)
167 }
168 if len(references) == 0 {
169 return nil
170 }
171
172 values := make([]string, 0, len(references))
173 args := make([]any, 0, len(references)*2)
174 for _, ref := range references {
175 values = append(values, "(?, ?)")
176 args = append(args, fromAt, ref)
177 }
178 _, err = tx.Exec(
179 fmt.Sprintf(
180 `insert into reference_links (from_at, to_at)
181 values %s`,
182 strings.Join(values, ","),
183 ),
184 args...,
185 )
186 if err != nil {
187 return fmt.Errorf("insert new reference_links: %w", err)
188 }
189 return nil
190}
191
192func deleteReferences(tx *sql.Tx, fromAt syntax.ATURI) error {
193 _, err := tx.Exec(`delete from reference_links where from_at = ?`, fromAt)
194 return err
195}
196
197func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) {
198 var (
199 conditions []string
200 args []any
201 )
202 for _, filter := range filters {
203 conditions = append(conditions, filter.Condition())
204 args = append(args, filter.Arg()...)
205 }
206
207 whereClause := ""
208 if conditions != nil {
209 whereClause = " where " + strings.Join(conditions, " and ")
210 }
211
212 rows, err := e.Query(
213 fmt.Sprintf(
214 `select from_at, to_at from reference_links %s`,
215 whereClause,
216 ),
217 args...,
218 )
219 if err != nil {
220 return nil, fmt.Errorf("query reference_links: %w", err)
221 }
222 defer rows.Close()
223
224 result := make(map[syntax.ATURI][]syntax.ATURI)
225
226 for rows.Next() {
227 var from, to syntax.ATURI
228 if err := rows.Scan(&from, &to); err != nil {
229 return nil, fmt.Errorf("scan row: %w", err)
230 }
231
232 result[from] = append(result[from], to)
233 }
234 if err := rows.Err(); err != nil {
235 return nil, fmt.Errorf("iterate rows: %w", err)
236 }
237
238 return result, nil
239}
240
241func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) {
242 rows, err := e.Query(
243 `select from_at from reference_links
244 where to_at = ? and from_at <> to_at`,
245 target,
246 )
247 if err != nil {
248 return nil, fmt.Errorf("query backlinks: %w", err)
249 }
250 defer rows.Close()
251
252 var (
253 backlinks []models.RichReferenceLink
254 backlinksMap = make(map[string][]syntax.ATURI)
255 )
256 for rows.Next() {
257 var from syntax.ATURI
258 if err := rows.Scan(&from); err != nil {
259 return nil, fmt.Errorf("scan row: %w", err)
260 }
261 nsid := from.Collection().String()
262 backlinksMap[nsid] = append(backlinksMap[nsid], from)
263 }
264 if err := rows.Err(); err != nil {
265 return nil, fmt.Errorf("iterate rows: %w", err)
266 }
267
268 var ls []models.RichReferenceLink
269 ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID])
270 if err != nil {
271 return nil, fmt.Errorf("get issue backlinks: %w", err)
272 }
273 backlinks = append(backlinks, ls...)
274 ls, err = getIssueCommentBacklinks(e, target, backlinksMap[tangled.FeedCommentNSID])
275 if err != nil {
276 return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
277 }
278 backlinks = append(backlinks, ls...)
279 ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID])
280 if err != nil {
281 return nil, fmt.Errorf("get pull backlinks: %w", err)
282 }
283 backlinks = append(backlinks, ls...)
284 ls, err = getPullCommentBacklinks(e, target, backlinksMap[tangled.FeedCommentNSID])
285 if err != nil {
286 return nil, fmt.Errorf("get pull_comment backlinks: %w", err)
287 }
288 backlinks = append(backlinks, ls...)
289
290 return backlinks, nil
291}
292
293func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
294 if len(aturis) == 0 {
295 return nil, nil
296 }
297 vals := make([]string, len(aturis))
298 args := make([]any, 0, len(aturis)*2)
299 for i, aturi := range aturis {
300 vals[i] = "(?, ?)"
301 did := aturi.Authority().String()
302 rkey := aturi.RecordKey().String()
303 args = append(args, did, rkey)
304 }
305 rows, err := e.Query(
306 fmt.Sprintf(
307 `select r.did, r.name, i.issue_id, i.title, i.open
308 from issues i
309 join repos r
310 on r.repo_did = i.repo_did
311 where (i.did, i.rkey) in (%s)`,
312 strings.Join(vals, ","),
313 ),
314 args...,
315 )
316 if err != nil {
317 return nil, err
318 }
319 defer rows.Close()
320 var refLinks []models.RichReferenceLink
321 for rows.Next() {
322 var l models.RichReferenceLink
323 l.Kind = models.RefKindIssue
324 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
325 return nil, err
326 }
327 refLinks = append(refLinks, l)
328 }
329 if err := rows.Err(); err != nil {
330 return nil, fmt.Errorf("iterate rows: %w", err)
331 }
332 return refLinks, nil
333}
334
335func getIssueCommentBacklinks(e Execer, target syntax.ATURI, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
336 if len(aturis) == 0 {
337 return nil, nil
338 }
339 filter := orm.FilterIn("c.at_uri", aturis)
340 exclude := orm.FilterNotEq("i.at_uri", target)
341 rows, err := e.Query(
342 fmt.Sprintf(
343 `select r.did, r.name, i.issue_id, c.id, i.title, i.open
344 from comments c
345 join issues i
346 on i.at_uri = c.subject_uri
347 join repos r
348 on r.repo_did = i.repo_did
349 where %s and %s`,
350 filter.Condition(),
351 exclude.Condition(),
352 ),
353 append(filter.Arg(), exclude.Arg()...)...,
354 )
355 if err != nil {
356 return nil, err
357 }
358 defer rows.Close()
359 var refLinks []models.RichReferenceLink
360 for rows.Next() {
361 var l models.RichReferenceLink
362 l.Kind = models.RefKindIssue
363 l.CommentId = new(int)
364 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
365 return nil, err
366 }
367 refLinks = append(refLinks, l)
368 }
369 if err := rows.Err(); err != nil {
370 return nil, fmt.Errorf("iterate rows: %w", err)
371 }
372 return refLinks, nil
373}
374
375func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
376 if len(aturis) == 0 {
377 return nil, nil
378 }
379 vals := make([]string, len(aturis))
380 args := make([]any, 0, len(aturis)*2)
381 for i, aturi := range aturis {
382 vals[i] = "(?, ?)"
383 did := aturi.Authority().String()
384 rkey := aturi.RecordKey().String()
385 args = append(args, did, rkey)
386 }
387 rows, err := e.Query(
388 fmt.Sprintf(
389 `select r.did, r.name, p.pull_id, p.title, p.state
390 from pulls p
391 join repos r
392 on r.repo_did = p.repo_did
393 where (p.owner_did, p.rkey) in (%s)`,
394 strings.Join(vals, ","),
395 ),
396 args...,
397 )
398 if err != nil {
399 return nil, err
400 }
401 defer rows.Close()
402 var refLinks []models.RichReferenceLink
403 for rows.Next() {
404 var l models.RichReferenceLink
405 l.Kind = models.RefKindPull
406 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
407 return nil, err
408 }
409 refLinks = append(refLinks, l)
410 }
411 if err := rows.Err(); err != nil {
412 return nil, fmt.Errorf("iterate rows: %w", err)
413 }
414 return refLinks, nil
415}
416
417func getPullCommentBacklinks(e Execer, target syntax.ATURI, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
418 if len(aturis) == 0 {
419 return nil, nil
420 }
421 filter := orm.FilterIn("c.at_uri", aturis)
422 exclude := orm.FilterNotEq("p.at_uri", target)
423 rows, err := e.Query(
424 fmt.Sprintf(
425 `select r.did, r.name, p.pull_id, c.id, p.title, p.state
426 from repos r
427 join pulls p
428 on r.repo_did = p.repo_did
429 join comments c
430 on ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) = c.subject_uri
431 where %s and %s`,
432 filter.Condition(),
433 exclude.Condition(),
434 ),
435 append(filter.Arg(), exclude.Arg()...)...,
436 )
437 if err != nil {
438 return nil, err
439 }
440 defer rows.Close()
441 var refLinks []models.RichReferenceLink
442 for rows.Next() {
443 var l models.RichReferenceLink
444 l.Kind = models.RefKindPull
445 l.CommentId = new(int)
446 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
447 return nil, err
448 }
449 refLinks = append(refLinks, l)
450 }
451 if err := rows.Err(); err != nil {
452 return nil, fmt.Errorf("iterate rows: %w", err)
453 }
454 return refLinks, nil
455}