this repo has no description
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, 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 = ?`,
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, backlinksMap[tangled.CommentNSID])
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, backlinksMap[tangled.CommentNSID])
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.at_uri = i.repo_at
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, 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 rows, err := e.Query(
341 fmt.Sprintf(
342 `select r.did, r.name, i.issue_id, c.id, i.title, i.open
343 from comments c
344 join issues i
345 on i.at_uri = c.subject_at
346 join repos r
347 on r.at_uri = i.repo_at
348 where %s`,
349 filter.Condition(),
350 ),
351 filter.Arg()...,
352 )
353 if err != nil {
354 return nil, err
355 }
356 defer rows.Close()
357 var refLinks []models.RichReferenceLink
358 for rows.Next() {
359 var l models.RichReferenceLink
360 l.Kind = models.RefKindIssue
361 l.CommentId = new(int)
362 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
363 return nil, err
364 }
365 refLinks = append(refLinks, l)
366 }
367 if err := rows.Err(); err != nil {
368 return nil, fmt.Errorf("iterate rows: %w", err)
369 }
370 return refLinks, nil
371}
372
373func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
374 if len(aturis) == 0 {
375 return nil, nil
376 }
377 vals := make([]string, len(aturis))
378 args := make([]any, 0, len(aturis)*2)
379 for i, aturi := range aturis {
380 vals[i] = "(?, ?)"
381 did := aturi.Authority().String()
382 rkey := aturi.RecordKey().String()
383 args = append(args, did, rkey)
384 }
385 rows, err := e.Query(
386 fmt.Sprintf(
387 `select r.did, r.name, p.pull_id, p.title, p.state
388 from pulls p
389 join repos r
390 on r.at_uri = p.repo_at
391 where (p.owner_did, p.rkey) in (%s)`,
392 strings.Join(vals, ","),
393 ),
394 args...,
395 )
396 if err != nil {
397 return nil, err
398 }
399 defer rows.Close()
400 var refLinks []models.RichReferenceLink
401 for rows.Next() {
402 var l models.RichReferenceLink
403 l.Kind = models.RefKindPull
404 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
405 return nil, err
406 }
407 refLinks = append(refLinks, l)
408 }
409 if err := rows.Err(); err != nil {
410 return nil, fmt.Errorf("iterate rows: %w", err)
411 }
412 return refLinks, nil
413}
414
415func getPullCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
416 if len(aturis) == 0 {
417 return nil, nil
418 }
419 filter := orm.FilterIn("c.at_uri", aturis)
420 rows, err := e.Query(
421 fmt.Sprintf(
422 `select r.did, r.name, p.pull_id, c.id, p.title, p.state
423 from repos r
424 join pulls p
425 on r.at_uri = p.repo_at
426 join comments c
427 on ('at://' || p.owner_did || '/' || 'sh.tangled.repo.pull' || '/' || p.rkey) = c.subject_at
428 where %s`,
429 filter.Condition(),
430 ),
431 filter.Arg()...,
432 )
433 if err != nil {
434 return nil, err
435 }
436 defer rows.Close()
437 var refLinks []models.RichReferenceLink
438 for rows.Next() {
439 var l models.RichReferenceLink
440 l.Kind = models.RefKindPull
441 l.CommentId = new(int)
442 if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
443 return nil, err
444 }
445 refLinks = append(refLinks, l)
446 }
447 if err := rows.Err(); err != nil {
448 return nil, fmt.Errorf("iterate rows: %w", err)
449 }
450 return refLinks, nil
451}