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_at = r.at_uri
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_at = 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_at = r.at_uri
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_at = ('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, fromCid *syntax.CID, references []syntax.ATURI) error {
164 // clear existing referneces when cid isn't provided
165 if fromCid == nil {
166 err := deleteReferences(tx, fromAt)
167 if err != nil {
168 return fmt.Errorf("delete old reference_links: %w", err)
169 }
170 if len(references) == 0 {
171 return nil
172 }
173 }
174
175 values := make([]string, 0, len(references))
176 args := make([]any, 0, len(references)*2)
177 for _, ref := range references {
178 values = append(values, "(?, ?, ?)")
179 args = append(args, fromAt, fromCid, ref)
180 }
181 _, err := tx.Exec(
182 fmt.Sprintf(
183 `insert into reference_links (from_at, from_cid, to_at)
184 values %s`,
185 strings.Join(values, ","),
186 ),
187 args...,
188 )
189 if err != nil {
190 return fmt.Errorf("insert new reference_links: %w", err)
191 }
192 return nil
193}
194
195func deleteReferences(tx *sql.Tx, fromAt syntax.ATURI) error {
196 _, err := tx.Exec(`delete from reference_links where from_at = ?`, fromAt)
197 return err
198}
199
200func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) {
201 var (
202 conditions []string
203 args []any
204 )
205 for _, filter := range filters {
206 conditions = append(conditions, filter.Condition())
207 args = append(args, filter.Arg()...)
208 }
209
210 whereClause := ""
211 if conditions != nil {
212 whereClause = " where " + strings.Join(conditions, " and ")
213 }
214
215 rows, err := e.Query(
216 fmt.Sprintf(
217 `select from_at, to_at from reference_links %s`,
218 whereClause,
219 ),
220 args...,
221 )
222 if err != nil {
223 return nil, fmt.Errorf("query reference_links: %w", err)
224 }
225 defer rows.Close()
226
227 result := make(map[syntax.ATURI][]syntax.ATURI)
228
229 for rows.Next() {
230 var from, to syntax.ATURI
231 if err := rows.Scan(&from, &to); err != nil {
232 return nil, fmt.Errorf("scan row: %w", err)
233 }
234
235 result[from] = append(result[from], to)
236 }
237 if err := rows.Err(); err != nil {
238 return nil, fmt.Errorf("iterate rows: %w", err)
239 }
240
241 return result, nil
242}
243
244func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) {
245 rows, err := e.Query(
246 `select from_at from reference_links
247 where to_at = ?`,
248 target,
249 )
250 if err != nil {
251 return nil, fmt.Errorf("query backlinks: %w", err)
252 }
253 defer rows.Close()
254
255 var (
256 backlinks []models.RichReferenceLink
257 backlinksMap = make(map[string][]syntax.ATURI)
258 )
259 for rows.Next() {
260 var from syntax.ATURI
261 if err := rows.Scan(&from); err != nil {
262 return nil, fmt.Errorf("scan row: %w", err)
263 }
264 nsid := from.Collection().String()
265 backlinksMap[nsid] = append(backlinksMap[nsid], from)
266 }
267 if err := rows.Err(); err != nil {
268 return nil, fmt.Errorf("iterate rows: %w", err)
269 }
270
271 var ls []models.RichReferenceLink
272 ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID])
273 if err != nil {
274 return nil, fmt.Errorf("get issue backlinks: %w", err)
275 }
276 backlinks = append(backlinks, ls...)
277 ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.CommentNSID])
278 if err != nil {
279 return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
280 }
281 backlinks = append(backlinks, ls...)
282 ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID])
283 if err != nil {
284 return nil, fmt.Errorf("get pull backlinks: %w", err)
285 }
286 backlinks = append(backlinks, ls...)
287 ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.CommentNSID])
288 if err != nil {
289 return nil, fmt.Errorf("get pull_comment backlinks: %w", err)
290 }
291 backlinks = append(backlinks, ls...)
292
293 return backlinks, nil
294}
295
296func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
297 if len(aturis) == 0 {
298 return nil, nil
299 }
300 vals := make([]string, len(aturis))
301 args := make([]any, 0, len(aturis)*2)
302 for i, aturi := range aturis {
303 vals[i] = "(?, ?)"
304 did := aturi.Authority().String()
305 rkey := aturi.RecordKey().String()
306 args = append(args, did, rkey)
307 }
308 rows, err := e.Query(
309 fmt.Sprintf(
310 `select r.did, r.name, i.issue_id, i.title, i.open
311 from issues i
312 join repos r
313 on r.at_uri = i.repo_at
314 where (i.did, i.rkey) in (%s)`,
315 strings.Join(vals, ","),
316 ),
317 args...,
318 )
319 if err != nil {
320 return nil, err
321 }
322 defer rows.Close()
323 var refLinks []models.RichReferenceLink
324 for rows.Next() {
325 var l models.RichReferenceLink
326 l.Kind = models.RefKindIssue
327 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
328 return nil, err
329 }
330 refLinks = append(refLinks, l)
331 }
332 if err := rows.Err(); err != nil {
333 return nil, fmt.Errorf("iterate rows: %w", err)
334 }
335 return refLinks, nil
336}
337
338func getIssueCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
339 if len(aturis) == 0 {
340 return nil, nil
341 }
342 filter := orm.FilterIn("c.at_uri", aturis)
343 rows, err := e.Query(
344 fmt.Sprintf(
345 `select r.did, r.name, i.issue_id, c.id, i.title, i.open
346 from comments c
347 join issues i
348 on i.at_uri = c.subject_at
349 join repos r
350 on r.at_uri = i.repo_at
351 where %s`,
352 filter.Condition(),
353 ),
354 filter.Arg()...,
355 )
356 if err != nil {
357 return nil, err
358 }
359 defer rows.Close()
360 var refLinks []models.RichReferenceLink
361 for rows.Next() {
362 var l models.RichReferenceLink
363 l.Kind = models.RefKindIssue
364 l.CommentId = new(int)
365 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
366 return nil, err
367 }
368 refLinks = append(refLinks, l)
369 }
370 if err := rows.Err(); err != nil {
371 return nil, fmt.Errorf("iterate rows: %w", err)
372 }
373 return refLinks, nil
374}
375
376func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
377 if len(aturis) == 0 {
378 return nil, nil
379 }
380 vals := make([]string, len(aturis))
381 args := make([]any, 0, len(aturis)*2)
382 for i, aturi := range aturis {
383 vals[i] = "(?, ?)"
384 did := aturi.Authority().String()
385 rkey := aturi.RecordKey().String()
386 args = append(args, did, rkey)
387 }
388 rows, err := e.Query(
389 fmt.Sprintf(
390 `select r.did, r.name, p.pull_id, p.title, p.state
391 from pulls p
392 join repos r
393 on r.at_uri = p.repo_at
394 where (p.owner_did, p.rkey) in (%s)`,
395 strings.Join(vals, ","),
396 ),
397 args...,
398 )
399 if err != nil {
400 return nil, err
401 }
402 defer rows.Close()
403 var refLinks []models.RichReferenceLink
404 for rows.Next() {
405 var l models.RichReferenceLink
406 l.Kind = models.RefKindPull
407 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
408 return nil, err
409 }
410 refLinks = append(refLinks, l)
411 }
412 if err := rows.Err(); err != nil {
413 return nil, fmt.Errorf("iterate rows: %w", err)
414 }
415 return refLinks, nil
416}
417
418func getPullCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
419 if len(aturis) == 0 {
420 return nil, nil
421 }
422 filter := orm.FilterIn("c.at_uri", aturis)
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.at_uri = p.repo_at
429 join comments c
430 on ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) = c.subject_at
431 where %s`,
432 filter.Condition(),
433 ),
434 filter.Arg()...,
435 )
436 if err != nil {
437 return nil, err
438 }
439 defer rows.Close()
440 var refLinks []models.RichReferenceLink
441 for rows.Next() {
442 var l models.RichReferenceLink
443 l.Kind = models.RefKindPull
444 l.CommentId = new(int)
445 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
446 return nil, err
447 }
448 refLinks = append(refLinks, l)
449 }
450 if err := rows.Err(); err != nil {
451 return nil, fmt.Errorf("iterate rows: %w", err)
452 }
453 return refLinks, nil
454}