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